From 789d5edb9d491c7cc334e6ac4072fcf45bc2b0b1 Mon Sep 17 00:00:00 2001 From: Thomas Genin Date: Wed, 4 Dec 2024 00:37:34 -0800 Subject: [PATCH] proof of concept --- src/ast/classconstant.js | 32 +---- src/ast/declaration.js | 28 +---- src/ast/propertystatement.js | 31 +---- src/parser.js | 24 +++- src/parser/class.js | 119 +++++++++--------- src/parser/expr.js | 10 +- src/parser/utils.js | 51 ++++++++ .../propertystatement.test.js.snap | 54 ++++++++ test/snapshot/propertystatement.test.js | 10 ++ 9 files changed, 211 insertions(+), 148 deletions(-) diff --git a/src/ast/classconstant.js b/src/ast/classconstant.js index bea667188..926fc4896 100644 --- a/src/ast/classconstant.js +++ b/src/ast/classconstant.js @@ -8,11 +8,6 @@ const ConstantStatement = require("./constantstatement"); const KIND = "classconstant"; -const IS_UNDEFINED = ""; -const IS_PUBLIC = "public"; -const IS_PROTECTED = "protected"; -const IS_PRIVATE = "private"; - /** * Defines a class/interface/trait constant * @constructor ClassConstant @@ -37,35 +32,12 @@ const ClassConstant = ConstantStatement.extends( location, ) { ConstantStatement.apply(this, [kind || KIND, constants, docs, location]); - this.parseFlags(flags); this.nullable = nullable; this.type = type; this.attrGroups = attrGroups; + this.visibility = flags.compute_visibility; + this.final = flags.isFinal; }, ); -/** - * Generic flags parser - * @function - * @name ClassConstant#parseFlags - * @memberOf module:php-parser - * @param {Array} flags - * @return {void} - */ -ClassConstant.prototype.parseFlags = function (flags) { - if (flags[0] === -1) { - this.visibility = IS_UNDEFINED; - } else if (flags[0] === null) { - /* istanbul ignore next */ - this.visibility = null; - } else if (flags[0] === 0) { - this.visibility = IS_PUBLIC; - } else if (flags[0] === 1) { - this.visibility = IS_PROTECTED; - } else if (flags[0] === 2) { - this.visibility = IS_PRIVATE; - } - this.final = flags[2] === 2; -}; - module.exports = ClassConstant; diff --git a/src/ast/declaration.js b/src/ast/declaration.js index 33ffbf5e0..b57161f9e 100644 --- a/src/ast/declaration.js +++ b/src/ast/declaration.js @@ -8,11 +8,6 @@ const Statement = require("./statement"); const KIND = "declaration"; -const IS_UNDEFINED = ""; -const IS_PUBLIC = "public"; -const IS_PROTECTED = "protected"; -const IS_PRIVATE = "private"; - /** * A declaration statement (function, class, interface...) * @constructor Declaration @@ -33,27 +28,16 @@ const Declaration = Statement.extends( * @function * @name Declaration#parseFlags * @memberOf module:php-parser - * @param {Array} flags + * @param {MemberFlags} flags * @return {void} */ Declaration.prototype.parseFlags = function (flags) { - this.isAbstract = flags[2] === 1; - this.isFinal = flags[2] === 2; - this.isReadonly = flags[3] === 1; + this.isAbstract = flags.isAbstract; + this.isFinal = flags.isFinal; + this.isReadonly = flags.isReadonly; if (this.kind !== "class") { - if (flags[0] === -1) { - this.visibility = IS_UNDEFINED; - } else if (flags[0] === null) { - /* istanbul ignore next */ - this.visibility = null; - } else if (flags[0] === 0) { - this.visibility = IS_PUBLIC; - } else if (flags[0] === 1) { - this.visibility = IS_PROTECTED; - } else if (flags[0] === 2) { - this.visibility = IS_PRIVATE; - } - this.isStatic = flags[1] === 1; + this.visibility = flags.compute_visibility; + this.isStatic = flags.isStatic; } }; diff --git a/src/ast/propertystatement.js b/src/ast/propertystatement.js index 271c8e6a8..c6fddc9d0 100644 --- a/src/ast/propertystatement.js +++ b/src/ast/propertystatement.js @@ -8,11 +8,6 @@ const Statement = require("./statement"); const KIND = "propertystatement"; -const IS_UNDEFINED = ""; -const IS_PUBLIC = "public"; -const IS_PROTECTED = "protected"; -const IS_PRIVATE = "private"; - /** * Declares a properties into the current scope * @constructor PropertyStatement @@ -27,31 +22,9 @@ const PropertyStatement = Statement.extends( function PropertyStatement(kind, properties, flags, docs, location) { Statement.apply(this, [KIND, docs, location]); this.properties = properties; - this.parseFlags(flags); + this.visibility = flags.compute_visibility; + this.isStatic = flags.isStatic; }, ); -/** - * Generic flags parser - * @function PropertyStatement#parseFlags - * @memberOf module:php-parser - * @param {Array} flags - * @return {void} - */ -PropertyStatement.prototype.parseFlags = function (flags) { - if (flags[0] === -1) { - this.visibility = IS_UNDEFINED; - } else if (flags[0] === null) { - this.visibility = null; - } else if (flags[0] === 0) { - this.visibility = IS_PUBLIC; - } else if (flags[0] === 1) { - this.visibility = IS_PROTECTED; - } else if (flags[0] === 2) { - this.visibility = IS_PRIVATE; - } - - this.isStatic = flags[1] === 1; -}; - module.exports = PropertyStatement; diff --git a/src/parser.js b/src/parser.js index 35d714c2a..6226831d0 100644 --- a/src/parser.js +++ b/src/parser.js @@ -183,6 +183,9 @@ const Parser = function (lexer, ast) { this.tok.T_FINAL, ].map(mapIt), ), + T_VISIBILITY_FLAGS: new Map( + [this.tok.T_PUBLIC, this.tok.T_PRIVATE, this.tok.T_PROTECTED].map(mapIt), + ), EOS: new Map([";", this.EOF, this.tok.T_INLINE_HTML].map(mapIt)), EXPR: new Map( [ @@ -706,14 +709,27 @@ Parser.prototype.lex = function () { /** * Check if token is of specified type - * @function Parser#is + * @function Parser#_is * @memberOf module:php-parser */ -Parser.prototype.is = function (type) { +Parser.prototype._is = function (token, type) { if (Array.isArray(type)) { - return type.indexOf(this.token) !== -1; + return type.indexOf(token) !== -1; } - return this.entries[type].has(this.token); + return this.entries[type].has(token); +}; + +/** + * Check if token is of specified type + * @function Parser#is + * @memberOf module:php-parser + */ +Parser.prototype.is = function (type) { + return this._is(this.token, type); +}; + +Parser.prototype.peek_is = function (type) { + return this._is(this.peek(), type); }; // extends the parser with syntax files diff --git a/src/parser/class.js b/src/parser/class.js index 996f5acf3..8130b1bbc 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -5,6 +5,8 @@ */ "use strict"; +const MemberFlags = require("./utils").MemberFlags; + module.exports = { /* * reading a class @@ -36,35 +38,28 @@ module.exports = { }, read_class_modifiers: function () { - const modifier = this.read_class_modifier({ - readonly: 0, - final_or_abstract: 0, - }); - return [0, 0, modifier.final_or_abstract, modifier.readonly]; + const flags = new MemberFlags(); + this.read_class_modifier(flags); + if (flags.isFinal && flags.isAbstract) { + this.raiseError("A class can not be final and abstract"); + } + return flags; }, - read_class_modifier: function (memo) { + read_class_modifier: function (flags) { if (this.token === this.tok.T_READ_ONLY) { this.next(); - memo.readonly = 1; - memo = this.read_class_modifier(memo); - } else if ( - memo.final_or_abstract === 0 && - this.token === this.tok.T_ABSTRACT - ) { + flags.isReadonly = true; + this.read_class_modifier(flags); + } else if (this.token === this.tok.T_ABSTRACT) { this.next(); - memo.final_or_abstract = 1; - memo = this.read_class_modifier(memo); - } else if ( - memo.final_or_abstract === 0 && - this.token === this.tok.T_FINAL - ) { + flags.isAbstract = true; + this.read_class_modifier(flags); + } else if (this.token === this.tok.T_FINAL) { this.next(); - memo.final_or_abstract = 2; - memo = this.read_class_modifier(memo); + flags.isFinal = true; + this.read_class_modifier(flags); } - - return memo; }, /* @@ -274,66 +269,66 @@ module.exports = { }, /* * Read member flags - * @return array - * 1st index : 0 => public, 1 => protected, 2 => private - * 2nd index : 0 => instance member, 1 => static member - * 3rd index : 0 => normal, 1 => abstract member, 2 => final member + * @return MemberFlags */ read_member_flags: function (asInterface) { - const result = [-1, -1, -1]; + const flags = new MemberFlags(); + if (this.is("T_MEMBER_FLAGS")) { - let idx = 0, - val = 0; do { switch (this.token) { - case this.tok.T_PUBLIC: - idx = 0; - val = 0; - break; - case this.tok.T_PROTECTED: - idx = 0; - val = 1; - break; - case this.tok.T_PRIVATE: - idx = 0; - val = 2; - break; case this.tok.T_STATIC: - idx = 1; - val = 1; + flags.isStatic = true; break; case this.tok.T_ABSTRACT: - idx = 2; - val = 1; + flags.isAbstract = true; break; case this.tok.T_FINAL: - idx = 2; - val = 2; + flags.isFinal = true; break; } + + if (this.is("T_VISIBILITY_FLAGS")) { + if (this.peek_is("T_VISIBILITY_FLAGS")) { + flags.read_visibility = this.token; + this.next(); + } + if (this.version >= 804 && this.peek() === "(") { + const visibility_token = this.token; + this.next(); + this.next(); + if (this.text() !== "set") { + this.raiseError("Expecting set keyword"); + } + this.next(); + this.expect(")"); + flags.write_visibility = visibility_token; + } else { + flags.visibility = this.token; + // this.next(); + } + } + + if (flags.isAbstract && flags.isFinal) { + this.error(); + } + if (asInterface) { - if (idx === 0 && val === 2) { - // an interface can't be private - this.expect([this.tok.T_PUBLIC, this.tok.T_PROTECTED]); - val = -1; - } else if (idx === 2 && val === 1) { + if (flags.isAbstract) { // an interface cant be abstract this.error(); - val = -1; + flags.isAbstract = false; + } + // an interface can't be private + if (flags.isPrivate) { + this.expect([this.tok.T_PUBLIC, this.tok.T_PROTECTED]); + flags.visibility = null; } - } - if (result[idx] !== -1) { - // already defined flag - this.error(); - } else if (val !== -1) { - result[idx] = val; } } while (this.next().is("T_MEMBER_FLAGS")); } - if (result[1] === -1) result[1] = 0; - if (result[2] === -1) result[2] = 0; - return result; + return flags; }, /* diff --git a/src/parser/expr.js b/src/parser/expr.js index 1e280d5b9..baf87de7a 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -5,6 +5,7 @@ */ "use strict"; +const { MemberFlags } = require("./utils"); module.exports = { read_expr: function (expr) { const result = this.node(); @@ -768,7 +769,14 @@ module.exports = { if (this.expect("{")) { body = this.next().read_class_body(true, false); } - const whatNode = what(null, propExtends, propImplements, body, [0, 0, 0]); + + const whatNode = what( + null, + propExtends, + propImplements, + body, + new MemberFlags(), + ); whatNode.attrGroups = attrs; return result(whatNode, args); } diff --git a/src/parser/utils.js b/src/parser/utils.js index 73734fc54..9f44395b7 100644 --- a/src/parser/utils.js +++ b/src/parser/utils.js @@ -5,7 +5,58 @@ */ "use strict"; +class MemberFlags { + constructor() { + this.visibility = null; + this.read_visibility = null; + this.write_visibility = null; + this.isStatic = false; + this.isAbstract = false; + this.isFinal = false; + this.isReadonly = false; + } + + get isPrivate() { + return this.visibility === 197; + } + + get compute_visibility() { + if (this.visibility !== null) { + return this.#visibility_token_to_string(this.visibility); + } + if (this.read_visibility === null && this.write_visibility) { + return this.#visibility_token_to_string(this.write_visibility) + "(set)"; + } + if (this.read_visibility && this.write_visibility) { + return ( + this.#visibility_token_to_string(this.read_visibility) + + " " + + this.#visibility_token_to_string(this.write_visibility) + + "(set)" + ); + } + return ""; + } + visibilibty_append(what) { + this.visibility += what; + } + get is_visibility_defined() { + return this.visibility !== ""; + } + + #visibility_token_to_string(token) { + if (token === 195) { + return "public"; + } + if (token === 196) { + return "protected"; + } + return "private"; + } +} + module.exports = { + MemberFlags: MemberFlags, /* * Reads a short form of tokens * @param {Number} token - The ending token diff --git a/test/snapshot/__snapshots__/propertystatement.test.js.snap b/test/snapshot/__snapshots__/propertystatement.test.js.snap index 3e3d14d9c..c5278b48b 100644 --- a/test/snapshot/__snapshots__/propertystatement.test.js.snap +++ b/test/snapshot/__snapshots__/propertystatement.test.js.snap @@ -1,5 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Asymmetric Visibility public private ( set) 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "bar", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'baz'", + "unicode": false, + "value": "baz", + }, + }, + ], + "visibility": "private(set)", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Foo", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + exports[`propertystatement multiple (var) 1`] = ` Program { "children": [ diff --git a/test/snapshot/propertystatement.test.js b/test/snapshot/propertystatement.test.js index 6c9707a51..89a2798a9 100644 --- a/test/snapshot/propertystatement.test.js +++ b/test/snapshot/propertystatement.test.js @@ -4,6 +4,7 @@ describe("propertystatement", () => { it("simple", () => { expect(parser.parseEval("class Foo { public $dsn; }")).toMatchSnapshot(); }); + it("simple (var)", () => { expect(parser.parseEval("class Foo { var $dsn; }")).toMatchSnapshot(); }); @@ -18,3 +19,12 @@ describe("propertystatement", () => { ).toMatchSnapshot(); }); }); + +describe("Asymmetric Visibility", () => { + const parser8_4 = parser.create({ parser: { version: "8.4" } }); + test("public private ( set)", () => { + expect( + parser8_4.parseEval("class Foo { private ( set) string $bar = 'baz';}"), + ).toMatchSnapshot(); + }); +});