diff --git a/README.md b/README.md index e1e605e4..54e514c3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ const engine = require("php-parser"); // initialize a new parser instance const parser = new engine({ // some options : + version: "8.4", // specify the PHP version to parse parser: { extractDoc: true, php7: true, diff --git a/src/index.js b/src/index.js index 8e9b734d..c94191cc 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,8 @@ const parser = require("./parser"); const tokens = require("./tokens"); const AST = require("./ast"); +const DEFAULT_PHP_VERSION = "8.4"; + /** * @private */ @@ -43,10 +45,10 @@ function combine(src, to) { * @example * var parser = require('php-parser'); * var instance = new parser({ + * version: 704 // or '7.4' * parser: { * extractDoc: true, * suppressErrors: true, - * version: 704 // or '7.4' * }, * ast: { * withPositions: true @@ -81,30 +83,40 @@ const Engine = function (options) { if (!options.lexer) { options.lexer = {}; } - if (options.parser.version) { - if (typeof options.parser.version === "string") { - let version = options.parser.version.split("."); - version = parseInt(version[0]) * 100 + parseInt(version[1]); - if (isNaN(version)) { - throw new Error("Bad version number : " + options.parser.version); - } else { - options.parser.version = version; - } - } else if (typeof options.parser.version !== "number") { - throw new Error("Expecting a number for version"); - } - if (options.parser.version < 500 || options.parser.version > 900) { - throw new Error("Can only handle versions between 5.x to 8.x"); - } - } } combine(options, this); - - // same version flags based on parser options - this.lexer.version = this.parser.version; } + + // options.parser.version is deprecated, use options.version instead + const versionString = options?.version ?? options?.parser?.version; + this.version = normalizeVersion(versionString ?? DEFAULT_PHP_VERSION); }; +/** + * Validate and normalize a version (string or number) to a version number + * @private + * @param {String|Number} versionString - The version string or number to + * validate and normalize, e.g., "7.4", or 704 + * @return {Number} - The normalized version number, e.g. 704 + * @throws {Error} - If the version is not a valid number or out of range + */ +function normalizeVersion(versionString) { + let version = versionString; + if (typeof version === "string") { + const versionParts = version.split("."); + version = parseInt(versionParts[0]) * 100 + parseInt(versionParts[1]); + if (isNaN(version)) { + throw new Error("Bad version number : " + versionString); + } + } else if (typeof version !== "number") { + throw new Error("Expecting a string or number for version"); + } + if (version < 500 || version > 900) { + throw new Error("Can only handle versions between 5.x to 8.x"); + } + return version; +} + /** * Check if the inpyt is a buffer or a string * @private diff --git a/src/lexer.js b/src/lexer.js index e3e387ac..df55c027 100644 --- a/src/lexer.js +++ b/src/lexer.js @@ -30,7 +30,6 @@ const Lexer = function (engine) { this.mode_eval = false; this.asp_tags = false; this.short_tags = false; - this.version = 803; this.yyprevcol = 0; this.keywords = { __class__: this.tok.T_CLASS_C, @@ -150,7 +149,7 @@ Lexer.prototype.setInput = function (input) { last_column: 0, }; this.tokens = []; - if (this.version > 703) { + if (this.engine.version > 703) { this.keywords.fn = this.tok.T_FN; } else { delete this.keywords.fn; diff --git a/src/lexer/scripting.js b/src/lexer/scripting.js index 1da761af..6ae0b266 100644 --- a/src/lexer/scripting.js +++ b/src/lexer/scripting.js @@ -16,7 +16,7 @@ module.exports = { case "\r\n": return this.T_WHITESPACE(); case "#": - if (this.version >= 800 && this._input[this.offset] === "[") { + if (this.engine.version >= 800 && this._input[this.offset] === "[") { this.input(); this.attributeListDepth[++this.attributeIndex] = 0; this.begin("ST_ATTRIBUTE"); diff --git a/src/lexer/strings.js b/src/lexer/strings.js index 6bba00bf..c3794a10 100644 --- a/src/lexer/strings.js +++ b/src/lexer/strings.js @@ -159,7 +159,7 @@ module.exports = { let indentation = 0; let leading_ch = this._input[offset - 1]; - if (this.version >= 703) { + if (this.engine.version >= 703) { while (leading_ch === "\t" || leading_ch === " ") { if (leading_ch === " ") { indentation_uses_spaces = true; @@ -188,7 +188,7 @@ module.exports = { ) { const ch = this._input[offset - 1 + this.heredoc_label.length]; if ( - (this.version >= 703 + (this.engine.version >= 703 ? valid_after_heredoc_73 : valid_after_heredoc ).includes(ch) diff --git a/src/lexer/tokens.js b/src/lexer/tokens.js index 638834da..397a4485 100644 --- a/src/lexer/tokens.js +++ b/src/lexer/tokens.js @@ -11,7 +11,7 @@ module.exports = { let id = this.keywords[token]; if (typeof id !== "number") { if (token === "yield") { - if (this.version >= 700 && this.tryMatch(" from")) { + if (this.engine.version >= 700 && this.tryMatch(" from")) { this.consume(5); id = this.tok.T_YIELD_FROM; } else { @@ -34,7 +34,7 @@ module.exports = { // https://github.com/php/php-src/blob/master/Zend/zend_language_scanner.l#L1546 if (id === this.tok.T_ENUM) { - if (this.version < 801) { + if (this.engine.version < 801) { return this.tok.T_STRING; } const initial = this.offset; @@ -224,8 +224,11 @@ module.exports = { return "!"; }, "?"() { - if (this.version >= 700 && this._input[this.offset] === "?") { - if (this.version >= 704 && this._input[this.offset + 1] === "=") { + if (this.engine.version >= 700 && this._input[this.offset] === "?") { + if ( + this.engine.version >= 704 && + this._input[this.offset + 1] === "=" + ) { this.consume(2); return this.tok.T_COALESCE_EQUAL; } else { @@ -234,7 +237,7 @@ module.exports = { } } if ( - this.version >= 800 && + this.engine.version >= 800 && this._input[this.offset] === "-" && this._input[this.offset + 1] === ">" ) { @@ -260,7 +263,7 @@ module.exports = { return this.tok.T_SL; } else if (nchar === "=") { this.input(); - if (this.version >= 700 && this._input[this.offset] === ">") { + if (this.engine.version >= 700 && this._input[this.offset] === ">") { this.input(); return this.tok.T_SPACESHIP; } else { diff --git a/src/parser.js b/src/parser.js index 5e5ef1fa..6231e774 100644 --- a/src/parser.js +++ b/src/parser.js @@ -30,13 +30,13 @@ function isNumber(n) { */ const Parser = function (lexer, ast) { this.lexer = lexer; + this.engine = lexer.engine; this.ast = ast; this.tok = lexer.tok; this.EOF = lexer.EOF; this.token = null; this.prev = null; this.debug = false; - this.version = 803; this.extractDoc = false; this.extractTokens = false; this.suppressErrors = false; diff --git a/src/parser/array.js b/src/parser/array.js index 7499a6c9..acf21b40 100644 --- a/src/parser/array.js +++ b/src/parser/array.js @@ -82,7 +82,10 @@ module.exports = { this.next(); byRef = true; value = this.read_variable(true, false); - } else if (this.token === this.tok.T_ELLIPSIS && this.version >= 704) { + } else if ( + this.token === this.tok.T_ELLIPSIS && + this.engine.version >= 704 + ) { this.next(); if (this.token === "&") { this.error(); diff --git a/src/parser/class.js b/src/parser/class.js index 8e2311c4..c7b91e7b 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -136,9 +136,9 @@ module.exports = { } else if ( allow_variables && (this.token === this.tok.T_VARIABLE || - (this.version >= 801 && this.token === this.tok.T_READ_ONLY) || + (this.engine.version >= 801 && this.token === this.tok.T_READ_ONLY) || // support https://wiki.php.net/rfc/typed_properties_v2 - (this.version >= 704 && + (this.engine.version >= 704 && (this.token === "?" || this.token === this.tok.T_ARRAY || this.token === this.tok.T_CALLABLE || @@ -228,7 +228,7 @@ module.exports = { } const [nullable, type] = - this.version >= 803 ? this.read_optional_type() : [false, null]; + this.engine.version >= 803 ? this.read_optional_type() : [false, null]; const result = this.node("classconstant"); const items = this.read_list( @@ -246,7 +246,7 @@ module.exports = { let value = null; if ( this.token === this.tok.T_STRING || - (this.version >= 700 && this.is("IDENTIFIER")) + (this.engine.version >= 700 && this.is("IDENTIFIER")) ) { constName = this.node("identifier"); const name = this.text(); @@ -565,7 +565,7 @@ module.exports = { this.next(); if ( this.token === this.tok.T_STRING || - (this.version >= 700 && this.is("IDENTIFIER")) + (this.engine.version >= 700 && this.is("IDENTIFIER")) ) { trait = method; method = this.node("identifier"); @@ -599,7 +599,7 @@ module.exports = { if ( this.token === this.tok.T_STRING || - (this.version >= 700 && this.is("IDENTIFIER")) + (this.engine.version >= 700 && this.is("IDENTIFIER")) ) { alias = this.node("identifier"); const name = this.text(); diff --git a/src/parser/expr.js b/src/parser/expr.js index 3d73a15f..d3695569 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -352,7 +352,10 @@ module.exports = { case this.tok.T_NEW: expr = this.read_new_expr(); - if (this.token === this.tok.T_OBJECT_OPERATOR && this.version < 804) { + if ( + this.token === this.tok.T_OBJECT_OPERATOR && + this.engine.version < 804 + ) { this.raiseError( "New without parenthesis is not allowed before PHP 8.4", ); @@ -394,7 +397,7 @@ module.exports = { return this.read_expr_cast("unset"); case this.tok.T_THROW: { - if (this.version < 800) { + if (this.engine.version < 800) { this.raiseError("PHP 8+ is required to use throw as an expression"); } const result = this.node("throw"); @@ -444,7 +447,7 @@ module.exports = { this.next(); if ( this.token === this.tok.T_FUNCTION || - (this.version >= 704 && this.token === this.tok.T_FN) + (this.engine.version >= 704 && this.token === this.tok.T_FN) ) { // handles static function return this.read_inline_function([0, 1, 0], attrs); @@ -595,7 +598,7 @@ module.exports = { this.next(); let right; if (this.token === this.tok.T_NEW) { - if (this.version >= 700) { + if (this.engine.version >= 700) { this.error(); } right = this.read_new_expr(); @@ -628,7 +631,7 @@ module.exports = { return result; } // introduced in PHP 7.4 - if (!this.version >= 704) { + if (!this.engine.version >= 704) { this.raiseError("Arrow Functions are not allowed"); } // as an arrowfunc @@ -667,7 +670,7 @@ module.exports = { read_match_expression() { const node = this.node("match"); this.expect(this.tok.T_MATCH) && this.next(); - if (this.version < 800) { + if (this.engine.version < 800) { this.raiseError("Match statements are not allowed before PHP 8"); } let cond = null; diff --git a/src/parser/function.js b/src/parser/function.js index 2cde2d60..35b2727b 100644 --- a/src/parser/function.js +++ b/src/parser/function.js @@ -84,11 +84,11 @@ module.exports = { if (type !== 1) { const nameNode = this.node("identifier"); if (type === 2) { - if (this.version >= 700) { + if (this.engine.version >= 700) { if (this.token === this.tok.T_STRING || this.is("IDENTIFIER")) { name = this.text(); this.next(); - } else if (this.version < 704) { + } else if (this.engine.version < 704) { this.error("IDENTIFIER"); } } else if (this.token === this.tok.T_STRING) { @@ -98,11 +98,11 @@ module.exports = { this.error("IDENTIFIER"); } } else { - if (this.version >= 700) { + if (this.engine.version >= 700) { if (this.token === this.tok.T_STRING) { name = this.text(); this.next(); - } else if (this.version >= 704) { + } else if (this.engine.version >= 704) { if (!this.expect("(")) { this.next(); } @@ -178,7 +178,7 @@ module.exports = { result.push(item()); if (this.token == ",") { this.next(); - if (this.version >= 800 && this.token === ")") { + if (this.engine.version >= 800 && this.token === ")") { return result; } } else if (this.token == ")") { @@ -258,7 +258,7 @@ module.exports = { let attrs = []; if (this.token === this.tok.T_ATTRIBUTE) attrs = this.read_attr_list(); - if (this.version >= 801 && this.token === this.tok.T_READ_ONLY) { + if (this.engine.version >= 801 && this.token === this.tok.T_READ_ONLY) { if (is_class_constructor) { this.next(); readonly = true; @@ -273,7 +273,7 @@ module.exports = { if ( !readonly && - this.version >= 801 && + this.engine.version >= 801 && this.token === this.tok.T_READ_ONLY ) { if (is_class_constructor) { @@ -336,7 +336,10 @@ module.exports = { // is the current token a: // - | for union type // - & for intersection type (> php 8.1) - while (this.token === "|" || (this.version >= 801 && this.token === "&")) { + while ( + this.token === "|" || + (this.engine.version >= 801 && this.token === "&") + ) { const nextToken = this.peek(); if ( @@ -400,7 +403,7 @@ module.exports = { let result = []; this.expect("(") && this.next(); if ( - this.version >= 801 && + this.engine.version >= 801 && this.token === this.tok.T_ELLIPSIS && this.peek() === ")" ) { @@ -453,7 +456,7 @@ module.exports = { ) { const nextToken = this.peek(); if (nextToken === ":") { - if (this.version < 800) { + if (this.engine.version < 800) { this.raiseError("PHP 8+ is required to use named arguments"); } return this.node("namedargument")( diff --git a/src/parser/scalar.js b/src/parser/scalar.js index 39364473..e2c679cf 100644 --- a/src/parser/scalar.js +++ b/src/parser/scalar.js @@ -341,7 +341,7 @@ module.exports = { result = result( "string", false, - this.version >= 703 && !this.lexer.heredoc_label.finished + this.engine.version >= 703 && !this.lexer.heredoc_label.finished ? this.remove_heredoc_leading_whitespace_chars( this.resolve_special_chars(text, isDoubleQuote), this.lexer.heredoc_label.indentation, diff --git a/src/parser/try.js b/src/parser/try.js index 7cccdad8..e32f8119 100644 --- a/src/parser/try.js +++ b/src/parser/try.js @@ -29,7 +29,7 @@ module.exports = { this.next().expect("(") && this.next(); const what = this.read_list(this.read_namespace_name, "|", false); let variable = null; - if (this.version < 800 || this.token === this.tok.T_VARIABLE) { + if (this.engine.version < 800 || this.token === this.tok.T_VARIABLE) { variable = this.read_variable(true, false); } this.expect(")"); diff --git a/src/parser/utils.js b/src/parser/utils.js index a62ef5d9..ccbc55bc 100644 --- a/src/parser/utils.js +++ b/src/parser/utils.js @@ -40,7 +40,11 @@ module.exports = { read_function_list(item, separator) { const result = []; do { - if (this.token == separator && this.version >= 703 && result.length > 0) { + if ( + this.token == separator && + this.engine.version >= 703 && + result.length > 0 + ) { result.push(this.node("noop")()); break; } @@ -48,7 +52,7 @@ module.exports = { if (this.token != separator) { break; } - if (this.next().token == ")" && this.version >= 703) { + if (this.next().token == ")" && this.engine.version >= 703) { break; } } while (this.token != this.EOF); diff --git a/src/parser/variable.js b/src/parser/variable.js index 3036c7b3..27b580ed 100644 --- a/src/parser/variable.js +++ b/src/parser/variable.js @@ -94,7 +94,7 @@ module.exports = { } else if ( this.token === this.tok.T_STRING || this.token === this.tok.T_CLASS || - (this.version >= 700 && this.is("IDENTIFIER")) + (this.engine.version >= 700 && this.is("IDENTIFIER")) ) { offset = this.node("identifier"); name = this.text(); diff --git a/test/version.test.js b/test/version.test.js index 9d82d1e4..cd90738b 100644 --- a/test/version.test.js +++ b/test/version.test.js @@ -1,54 +1,22 @@ const parser = require("./main"); describe("Test versions", function () { - it("unserialize a version string", function () { - const test = parser.create({ - parser: { - version: "7.3", - }, - }); - expect(test.parser.version).toEqual(703); + it.each([ + { version: "7.3", expected: 703 }, + { version: "7.3.5", expected: 703 }, + ])("parse version number $version", ({ version, expected }) => { + const test = parser.create({ version }); + expect(test.version).toEqual(expected); }); - it("unserialize a version string - with bugfix ignored", function () { - const test = parser.create({ - parser: { - version: "7.3.5", - }, - }); - expect(test.parser.version).toEqual(703); - }); - it("fail to parse array", function () { - expect( - parser.create.bind(null, { - parser: { - version: [701], - }, - }), - ).toThrow(new Error("Expecting a number for version")); - }); - it("fail to parse bad version numbers", function () { - expect( - parser.create.bind(null, { - parser: { - version: "x.y.z", - }, - }), - ).toThrow(new Error("Bad version number : x.y.z")); - }); - it("unhandled version", function () { - expect( - parser.create.bind(null, { - parser: { - version: "4.9", - }, - }), - ).toThrow(new Error("Can only handle versions between 5.x to 8.x")); - expect( - parser.create.bind(null, { - parser: { - version: "9.9", - }, - }), - ).toThrow(new Error("Can only handle versions between 5.x to 8.x")); + + it.each([ + { version: [701], expected: "Expecting a string or number for version" }, + { version: "x.y.z", expected: "Bad version number : x.y.z" }, + { version: "4.9", expected: "Can only handle versions between 5.x to 8.x" }, + { version: "9.9", expected: "Can only handle versions between 5.x to 8.x" }, + ])("fail to parse version $version", ({ version, expected }) => { + expect(() => { + parser.create({ version }); + }).toThrow(new Error(expected)); }); }); diff --git a/types.d.ts b/types.d.ts index a5ad9c93..d057e32e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1037,10 +1037,10 @@ declare module "php-parser" { * @example * var parser = require('php-parser'); * var instance = new parser({ + * version: 704 // or '7.4' * parser: { * extractDoc: true, * suppressErrors: true, - * version: 704 // or '7.4' * }, * ast: { * withPositions: true