From 8741d13612b371a7a7b903c0e3ff2315876a85d7 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Wed, 11 Sep 2019 17:07:53 +0300 Subject: [PATCH] fix: invalid parsing of traitalias --- src/ast/traitalias.js | 18 +- src/parser/class.js | 107 +++-- .../__snapshots__/graceful.test.js.snap | 4 +- .../__snapshots__/traitalias.test.js.snap | 388 ++++++++++++++++++ test/snapshot/traitalias.test.js | 123 ++++++ 5 files changed, 605 insertions(+), 35 deletions(-) create mode 100644 test/snapshot/__snapshots__/traitalias.test.js.snap create mode 100644 test/snapshot/traitalias.test.js diff --git a/src/ast/traitalias.js b/src/ast/traitalias.js index 32214375e..f85c3c393 100644 --- a/src/ast/traitalias.js +++ b/src/ast/traitalias.js @@ -26,7 +26,7 @@ module.exports = Node.extends(KIND, function TraitAlias( trait, method, as, - flags, + visibility, docs, location ) { @@ -34,14 +34,18 @@ module.exports = Node.extends(KIND, function TraitAlias( this.trait = trait; this.method = method; this.as = as; - this.visibility = IS_UNDEFINED; - if (flags) { - if (flags[0] === 0) { + + switch (visibility) { + case 0: this.visibility = IS_PUBLIC; - } else if (flags[0] === 1) { + break; + case 1: this.visibility = IS_PROTECTED; - } else if (flags[0] === 2) { + break; + case 2: this.visibility = IS_PRIVATE; - } + break; + default: + this.visibility = IS_UNDEFINED; } }); diff --git a/src/parser/class.js b/src/parser/class.js index c86d455bf..ce30a9d33 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -203,6 +203,40 @@ module.exports = { return result(null, items, flags); }, + + read_member_modifier: function() { + let modifier; + + switch (this.token) { + case this.tok.T_PUBLIC: + modifier = 0; + break; + case this.tok.T_PROTECTED: + modifier = 1; + break; + case this.tok.T_PRIVATE: + modifier = 2; + break; + case this.tok.T_STATIC: + modifier = 3; + break; + case this.tok.T_ABSTRACT: + modifier = 4; + break; + case this.tok.T_FINAL: + modifier = 5; + break; + default: { + const err = this.error("T_MEMBER_FLAGS"); + this.next(); + return err; + } + } + + this.next(); + return modifier; + }, + /** * Read member flags * @return array @@ -213,31 +247,34 @@ module.exports = { read_member_flags: function(asInterface) { const result = [-1, -1, -1]; if (this.is("T_MEMBER_FLAGS")) { - let idx = 0, - val = 0; do { - switch (this.token) { - case this.tok.T_PUBLIC: + let idx = 0; + let val = 0; + + const visibility = this.read_member_modifier(); + + switch (visibility) { + case 0: idx = 0; val = 0; break; - case this.tok.T_PROTECTED: + case 1: idx = 0; val = 1; break; - case this.tok.T_PRIVATE: + case 2: idx = 0; val = 2; break; - case this.tok.T_STATIC: + case 3: idx = 1; val = 1; break; - case this.tok.T_ABSTRACT: + case 4: idx = 2; val = 1; break; - case this.tok.T_FINAL: + case 5: idx = 2; val = 2; break; @@ -259,7 +296,7 @@ module.exports = { } else if (val !== -1) { result[idx] = val; } - } while (this.next().is("T_MEMBER_FLAGS")); + } while (this.is("T_MEMBER_FLAGS")); } if (result[1] == -1) result[1] = 0; @@ -372,18 +409,18 @@ module.exports = { const node = this.node("traituse"); this.expect(this.tok.T_USE) && this.next(); const traits = [this.read_namespace_name()]; - let adaptations = null; while (this.token === ",") { traits.push(this.next().read_namespace_name()); } + const adaptations = this.read_trait_adaptations(); + return node(traits, adaptations); + }, + + read_trait_adaptations: function() { + let adaptations = null; + if (this.token === "{") { - adaptations = []; - // defines alias statements - while (this.next().token !== this.EOF) { - if (this.token === "}") break; - adaptations.push(this.read_trait_use_alias()); - this.expect(";"); - } + adaptations = this.read_trait_adaptation_list(); if (this.expect("}")) { this.next(); } @@ -392,17 +429,34 @@ module.exports = { this.next(); } } - return node(traits, adaptations); + + return adaptations; }, + + /* + * Reads trait adaptation list + */ + read_trait_adaptation_list: function() { + let adaptations = []; + // defines alias statements + while (this.next().token !== this.EOF) { + if (this.token === "}") break; + adaptations.push(this.read_trait_adaptation()); + this.expect(";"); + } + + return adaptations; + }, + /** - * Reading trait alias + * Reading trait adaptation * ```ebnf * trait_use_alias ::= namespace_name ( T_DOUBLE_COLON T_STRING )? (T_INSTEADOF namespace_name) | (T_AS member_flags? T_STRING) * ``` * name list : https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L303 * trait adaptation : https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L742 */ - read_trait_use_alias: function() { + read_trait_adaptation: function() { const node = this.node(); let trait = null; let method; @@ -445,10 +499,11 @@ module.exports = { ); } else if (this.token === this.tok.T_AS) { // handle trait alias - let flags = null; + let visibility = null; let alias = null; - if (this.next().is("T_MEMBER_FLAGS")) { - flags = this.read_member_flags(); + this.next(); + if (this.is("T_MEMBER_FLAGS")) { + visibility = this.read_member_modifier(); } if ( @@ -459,12 +514,12 @@ module.exports = { const name = this.text(); this.next(); alias = alias(name); - } else if (flags === false) { + } else if (visibility === false) { // no visibility flags and no name => too bad this.expect(this.tok.T_STRING); } - return node("traitalias", trait, method, alias, flags); + return node("traitalias", trait, method, alias, visibility); } // handle errors diff --git a/test/snapshot/__snapshots__/graceful.test.js.snap b/test/snapshot/__snapshots__/graceful.test.js.snap index d6ae2966e..caaaf5f61 100644 --- a/test/snapshot/__snapshots__/graceful.test.js.snap +++ b/test/snapshot/__snapshots__/graceful.test.js.snap @@ -575,8 +575,8 @@ Program { "expected": undefined, "kind": "error", "line": 3, - "message": "Parse Error : syntax error, unexpected 'abstract' (T_ABSTRACT) on line 3", - "token": "'abstract' (T_ABSTRACT)", + "message": "Parse Error : syntax error, unexpected 'function' (T_FUNCTION) on line 3", + "token": "'function' (T_FUNCTION)", }, Error { "expected": ";", diff --git a/test/snapshot/__snapshots__/traitalias.test.js.snap b/test/snapshot/__snapshots__/traitalias.test.js.snap new file mode 100644 index 000000000..3a2a8adcd --- /dev/null +++ b/test/snapshot/__snapshots__/traitalias.test.js.snap @@ -0,0 +1,388 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`traitalias as abstract 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as final 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as method 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": Identifier { + "kind": "identifier", + "name": "myPrivateHello", + }, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass2", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as private 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "private", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as protected 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "protected", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as protected protected 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": Identifier { + "kind": "identifier", + "name": "protected", + }, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "protected", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as public 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "public", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as public with method 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": Identifier { + "kind": "identifier", + "name": "myPrivateHello", + }, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "public", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass2", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`traitalias as static 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + TraitUse { + "adaptations": Array [ + TraitAlias { + "as": null, + "kind": "traitalias", + "method": "sayHello", + "trait": null, + "visibility": "", + }, + ], + "kind": "traituse", + "traits": Array [ + ClassReference { + "kind": "classreference", + "name": "HelloWorld", + "resolution": "uqn", + }, + ], + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "MyClass1", + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; diff --git a/test/snapshot/traitalias.test.js b/test/snapshot/traitalias.test.js new file mode 100644 index 000000000..a3c30a021 --- /dev/null +++ b/test/snapshot/traitalias.test.js @@ -0,0 +1,123 @@ +const parser = require("../main"); + +describe("traitalias", function() { + it("as public with method", function() { + expect( + parser.parseEval(` +class MyClass2 { + use HelloWorld { sayHello as public myPrivateHello; } +} +`) + ).toMatchSnapshot(); + }); + + it("as method", function() { + expect( + parser.parseEval(` +class MyClass2 { + use HelloWorld { sayHello as myPrivateHello; } +} +`) + ).toMatchSnapshot(); + }); + + it("as public", function() { + expect( + parser.parseEval(` +class MyClass1 { + use HelloWorld { sayHello as public; } +} +`) + ).toMatchSnapshot(); + }); + + it("as protected", function() { + expect( + parser.parseEval(` +class MyClass1 { + use HelloWorld { sayHello as protected; } +} +`) + ).toMatchSnapshot(); + }); + + it("as private", function() { + expect( + parser.parseEval(` +class MyClass1 { + use HelloWorld { sayHello as private; } +} +`) + ).toMatchSnapshot(); + }); + + // PHP Fatal error: Cannot use 'static' as method modifier + // but should be parsable, because allowed by grammar + it("as static", function() { + const astErr = parser.parseEval( + ` +class MyClass1 { + use HelloWorld { sayHello as static; } +} +`, + { + parser: { + suppressErrors: true + } + } + ); + expect(astErr).toMatchSnapshot(); + }); + + // PHP Fatal error: Cannot use 'abstract' as method modifier + // but should be parsable, because allowed by grammar + it("as abstract", function() { + const astErr = parser.parseEval( + ` +class MyClass1 { + use HelloWorld { sayHello as abstract; } +} +`, + { + parser: { + suppressErrors: true + } + } + ); + expect(astErr).toMatchSnapshot(); + }); + + // PHP Fatal error: Cannot use 'final' as method modifier + // but should be parsable, because allowed by grammar + it("as final", function() { + const astErr = parser.parseEval( + ` +class MyClass1 { + use HelloWorld { sayHello as final; } +} +`, + { + parser: { + suppressErrors: true + } + } + ); + expect(astErr).toMatchSnapshot(); + }); + + it("as protected protected", function() { + const astErr = parser.parseEval( + ` +class MyClass1 { + use HelloWorld { sayHello as protected protected; } +} +`, + { + parser: { + suppressErrors: true + } + } + ); + expect(astErr).toMatchSnapshot(); + }); +});