UNPKG

php-parser

Version:

Parse PHP code from JS and returns its AST

806 lines (746 loc) 22.6 kB
/** * Copyright (C) 2018 Glayzzle (BSD3 License) * @authors https://github.com/glayzzle/php-parser/graphs/contributors * @url http://glayzzle.com */ "use strict"; module.exports = { /* * reading a class * ```ebnf * class ::= class_scope? T_CLASS T_STRING (T_EXTENDS NAMESPACE_NAME)? (T_IMPLEMENTS (NAMESPACE_NAME ',')* NAMESPACE_NAME)? '{' CLASS_BODY '}' * ``` */ read_class_declaration_statement(attrs) { const result = this.node("class"); const flag = this.read_class_modifiers(); // graceful mode : ignore token & go next if (this.token !== this.tok.T_CLASS) { this.error(this.tok.T_CLASS); this.next(); return null; } this.next().expect(this.tok.T_STRING); let propName = this.node("identifier"); const name = this.text(); this.next(); propName = propName(name); const propExtends = this.read_extends_from(); const propImplements = this.read_implements_list(); this.expect("{"); const body = this.next().read_class_body(true, false); const node = result(propName, propExtends, propImplements, body, flag); if (attrs) node.attrGroups = attrs; return node; }, read_class_modifiers() { const modifier = this.read_class_modifier({ readonly: 0, final_or_abstract: 0, }); return [0, 0, modifier.final_or_abstract, modifier.readonly]; }, read_class_modifier(memo) { 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 ) { 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 ) { this.next(); memo.final_or_abstract = 2; memo = this.read_class_modifier(memo); } return memo; }, /* * Reads a class body * ```ebnf * class_body ::= (member_flags? (T_VAR | T_STRING | T_FUNCTION))* * ``` */ read_class_body(allow_variables, allow_enum_cases) { let result = []; let attrs = []; while (this.token !== this.EOF && this.token !== "}") { if (this.token === this.tok.T_COMMENT) { result.push(this.read_comment()); continue; } if (this.token === this.tok.T_DOC_COMMENT) { result.push(this.read_doc_comment()); continue; } // check T_USE trait if (this.token === this.tok.T_USE) { result = result.concat(this.read_trait_use_statement()); continue; } const locStart = this.position(); if (this.token === this.tok.T_ATTRIBUTE) { attrs = this.read_attr_list(); } // check enum cases if (allow_enum_cases && this.token === this.tok.T_CASE) { const enumcase = this.read_enum_case(attrs); attrs = []; if (this.expect(";")) { this.next(); } result = result.concat(enumcase); continue; } // read member flags const flags = this.read_member_flags(false); // check constant if (this.token === this.tok.T_CONST) { if (flags[0][1] !== -1) { this.raiseError("Cannot use asymmetric visibility on constants"); } const constants = this.read_constant_list(flags, attrs, locStart); if (this.expect(";")) { this.next(); } result = result.concat(constants); continue; } // jump over T_VAR then land on T_VARIABLE if (allow_variables && this.token === this.tok.T_VAR) { this.next().expect(this.tok.T_VARIABLE); flags[0][0] = null; // public (as null) flags[1] = 0; // non static var } if (this.token === this.tok.T_FUNCTION) { // reads a function result.push(this.read_function(false, flags, attrs, locStart)); attrs = []; } else if ( allow_variables && (this.token === this.tok.T_VARIABLE || (this.version >= 801 && this.token === this.tok.T_READ_ONLY) || // support https://wiki.php.net/rfc/typed_properties_v2 (this.version >= 704 && (this.token === "?" || this.token === this.tok.T_ARRAY || this.token === this.tok.T_CALLABLE || this.token === this.tok.T_NAMESPACE || this.token === this.tok.T_NAME_FULLY_QUALIFIED || this.token === this.tok.T_NAME_QUALIFIED || this.token === this.tok.T_NAME_RELATIVE || this.token === this.tok.T_NS_SEPARATOR || this.token === this.tok.T_STRING))) ) { // reads a variable const variables = this.read_variable_list(flags, attrs, locStart); attrs = []; result = result.concat(variables); } else { // raise an error this.error([ this.tok.T_CONST, ...(allow_variables ? [this.tok.T_VARIABLE] : []), ...(allow_enum_cases ? [this.tok.T_CASE] : []), this.tok.T_FUNCTION, ]); // ignore token this.next(); } } this.expect("}"); this.next(); return result; }, /* * Reads variable list * ```ebnf * variable_list ::= (variable_declaration ',')* variable_declaration * ``` */ read_variable_list(flags, attrs, locStart) { let property_statement = this.node("propertystatement"); const properties = this.read_list( /* * Reads a variable declaration * * ```ebnf * variable_declaration ::= T_VARIABLE '=' scalar * ``` */ function read_variable_declaration() { const result = this.node("property"); let readonly = flags[3] === 1; if (!readonly && this.token === this.tok.T_READ_ONLY) { readonly = true; this.next(); } const [nullable, type] = this.read_optional_type(); this.expect(this.tok.T_VARIABLE); let propName = this.node("identifier"); const name = this.text().substring(1); // ignore $ this.next(); propName = propName(name); let value = null; let property_hooks = []; this.expect([",", ";", "=", "{"]); // Property has a value if (this.token === "=") { // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L815 value = this.next().read_expr(); } // Property is using a hook to define getter/setters if (this.token === "{") { property_hooks = this.read_property_hooks(); } else { this.expect([";", ","]); } return result( propName, value, readonly, nullable, type, attrs || [], property_hooks, ); }, ",", ); property_statement = property_statement(null, properties, flags); if (locStart && property_statement.loc) { property_statement.loc.start = locStart; if (property_statement.loc.source) { property_statement.loc.source = this.lexer._input.substr( property_statement.loc.start.offset, property_statement.loc.end.offset - property_statement.loc.start.offset, ); } } // semicolons are found only for regular properties definitions. // Property hooks are terminated by a closing curly brace, }. // property_statement is instanciated before this check to avoid including the semicolon in the AST end location of the property. if (this.token === ";") { this.next(); } return property_statement; }, /* * Reads property hooks */ read_property_hooks() { if (this.version < 804) { this.raiseError("Parse Error: Property hooks require PHP 8.4+"); } this.expect("{"); this.next(); const hooks = []; while (this.token !== this.EOF && this.token !== "}") { hooks.push(this.read_property_hook()); } this.expect("}"); this.next(); return hooks; }, read_property_hook() { const property_hooks = this.node("propertyhook"); let attrs = []; if (this.token === this.tok.T_ATTRIBUTE) { attrs = this.read_attr_list(); } const is_final = this.token === this.tok.T_FINAL; if (is_final) this.next(); const is_reference = this.token === "&"; if (is_reference) this.next(); const method_name = this.text(); if (method_name !== "get" && method_name !== "set") { this.raiseError( "Parse Error: Property hooks must be either 'get' or 'set'", ); } this.next(); let parameter = null; let body = null; this.expect([this.tok.T_DOUBLE_ARROW, "{", "(", ";"]); // interface or abstract definition if (this.token === ";") { this.next(); return property_hooks( method_name, is_final, is_reference, parameter, body, attrs, ); } if (this.token === "(") { this.next(); parameter = this.read_parameter(false); this.expect(")"); this.next(); } if (this.token === this.tok.T_DOUBLE_ARROW) { this.next(); body = this.read_expr(); this.next(); } else if (this.token === "{") { body = this.read_code_block(); } return property_hooks( method_name, is_final, is_reference, parameter, body, attrs, ); }, /* * Reads constant list * ```ebnf * constant_list ::= T_CONST [type] (constant_declaration ',')* constant_declaration * ``` */ read_constant_list(flags, attrs, locStart) { const result = this.node("classconstant"); if (this.expect(this.tok.T_CONST)) { this.next(); } // static, abstract, and readonly are never valid on class constants if (flags[1] === 1 || flags[2] === 1 || flags[3] === 1) { this.error(); } // final class constants require PHP 8.1+ if (flags[2] === 2 && this.version < 801) { this.raiseError("Final class constants are not allowed before PHP 8.1"); } const [nullable, type] = this.version >= 803 ? this.read_optional_type() : [false, null]; const items = this.read_list( /* * Reads a constant declaration * * ```ebnf * constant_declaration ::= (T_STRING | IDENTIFIER) '=' expr * ``` * @return {Constant} [:link:](AST.md#constant) */ function read_constant_declaration() { const result = this.node("constant"); let constName = null; let value = null; if ( this.token === this.tok.T_STRING || (this.version >= 700 && this.is("IDENTIFIER")) ) { constName = this.node("identifier"); const name = this.text(); this.next(); constName = constName(name); } else { this.expect("IDENTIFIER"); } if (this.expect("=")) { value = this.next().read_expr(); } return result(constName, value); }, ",", ); const node = result(null, items, flags, nullable, type, attrs || []); if (locStart && node.loc) { node.loc.start = locStart; if (node.loc.source) { node.loc.source = this.lexer._input.substr( node.loc.start.offset, node.loc.end.offset - node.loc.start.offset, ); } } return node; }, /* * Read member flags * @return array * 1st index : [get, set] visibility tuple * get/set: -1 => no visibility, 0 => public, 1 => protected, 2 => private * 2nd index : 0 => instance member, 1 => static member * 3rd index : 0 => normal, 1 => abstract member, 2 => final member * 4th index : 0 => no readonly, 1 => readonly */ read_member_flags(asInterface) { const result = [[-1, -1], 0, 0, 0]; const seen = new Set(); while (this.is("T_MEMBER_FLAGS")) { let idx = -1, val = -1; switch (this.token) { case this.tok.T_PUBLIC: case this.tok.T_PROTECTED: case this.tok.T_PRIVATE: { idx = 0; val = this.token === this.tok.T_PUBLIC ? 0 : this.token === this.tok.T_PROTECTED ? 1 : 2; if (asInterface && val === 2) { // an interface can't be private this.expect([this.tok.T_PUBLIC, this.tok.T_PROTECTED]); val = -1; } this.next(); // consume the visibility keyword if (this.version >= 804 && this.token === "(") { // visibility(set) modifier: e.g. private(set) // For shorthand, default get visibility to public if not already set if (result[0][0] === -1) { result[0][0] = 0; } this.next(); // consume '(' if (this.token !== this.tok.T_STRING || this.text() !== "set") { this.error("set"); } else { this.next(); // consume 'set' } if (this.expect(")")) { this.next(); // consume ')' } if (seen.has("set")) { this.error(); // set modifier already defined } else if (val !== -1) { seen.add("set"); result[0][1] = val; } continue; } if (seen.has(idx)) { this.error(); } else if (val !== -1) { seen.add(idx); result[0][0] = val; } continue; } case this.tok.T_STATIC: idx = 1; val = 1; break; case this.tok.T_ABSTRACT: idx = 2; val = 1; break; case this.tok.T_FINAL: idx = 2; val = 2; break; case this.tok.T_READ_ONLY: idx = 3; val = 1; break; } if (asInterface && idx === 2 && val === 1) { // an interface can't be abstract this.error(); val = -1; } if (seen.has(idx)) { // already defined flag this.error(); } else if (val !== -1) { seen.add(idx); result[idx] = val; } this.next(); } return result; }, /* * optional_type: * /- empty -/ { $$ = NULL; } * | type_expr { $$ = $1; } * ; * * type_expr: * type { $$ = $1; } * | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } * | union_type { $$ = $1; } * ; * * type: * T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); } * | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); } * | name { $$ = $1; } * ; * * union_type: * type '|' type { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); } * | union_type '|' type { $$ = zend_ast_list_add($1, $3); } * ; */ read_optional_type() { const nullable = this.token === "?"; if (nullable) { this.next(); } if (this.peekSkipComments() === "=") { return [false, null]; } let type = this.read_types(); if (nullable && !type) { this.raiseError( "Expecting a type definition combined with nullable operator", ); } if (!nullable && !type) { return [false, null]; } if (this.token === "|") { type = [type]; do { this.next(); const variant = this.read_type(); if (!variant) { this.raiseError("Expecting a type definition"); break; } type.push(variant); } while (this.token === "|"); } return [nullable, type]; }, peekSkipComments() { const lexerState = this.lexer.getState(); let nextToken; do { nextToken = this.lexer.lex(); } while ( nextToken === this.tok.T_COMMENT || nextToken === this.tok.T_WHITESPACE ); this.lexer.setState(lexerState); return nextToken; }, /* * reading an interface * ```ebnf * interface ::= T_INTERFACE T_STRING (T_EXTENDS (NAMESPACE_NAME ',')* NAMESPACE_NAME)? '{' INTERFACE_BODY '}' * ``` */ read_interface_declaration_statement(attrs) { const result = this.node("interface"); if (this.token !== this.tok.T_INTERFACE) { this.error(this.tok.T_INTERFACE); this.next(); return null; } this.next().expect(this.tok.T_STRING); let propName = this.node("identifier"); const name = this.text(); this.next(); propName = propName(name); const propExtends = this.read_interface_extends_list(); this.expect("{"); const body = this.next().read_interface_body(); return result(propName, propExtends, body, attrs || []); }, /* * Reads an interface body * ```ebnf * interface_body ::= (member_flags? (T_CONST | T_FUNCTION))* * ``` */ read_interface_body() { let result = []; let attrs; while (this.token !== this.EOF && this.token !== "}") { if (this.token === this.tok.T_COMMENT) { result.push(this.read_comment()); continue; } if (this.token === this.tok.T_DOC_COMMENT) { result.push(this.read_doc_comment()); continue; } const locStart = this.position(); attrs = []; if (this.token === this.tok.T_ATTRIBUTE) { attrs = this.read_attr_list(); } // read member flags const flags = this.read_member_flags(true); // check constant if (this.token === this.tok.T_CONST) { if (flags[0][1] !== -1) { this.raiseError("Cannot use asymmetric visibility on constants"); } const constants = this.read_constant_list(flags, attrs, locStart); if (this.expect(";")) { this.next(); } result = result.concat(constants); } else if (this.token === this.tok.T_FUNCTION) { // reads a function const method = this.read_function_declaration( 2, flags, attrs, locStart, ); method.parseFlags(flags); result.push(method); if (this.expect(";")) { this.next(); } } else if (this.token === this.tok.T_STRING) { result.push(this.read_variable_list(flags, attrs, locStart)); } else { // raise an error this.error([this.tok.T_CONST, this.tok.T_FUNCTION, this.tok.T_STRING]); this.next(); } } if (this.expect("}")) { this.next(); } return result; }, /* * reading a trait * ```ebnf * trait ::= T_TRAIT T_STRING (T_EXTENDS (NAMESPACE_NAME ',')* NAMESPACE_NAME)? '{' FUNCTION* '}' * ``` */ read_trait_declaration_statement(attrs) { const result = this.node("trait"); // graceful mode : ignore token & go next if (this.token !== this.tok.T_TRAIT) { this.error(this.tok.T_TRAIT); this.next(); return null; } this.next().expect(this.tok.T_STRING); let propName = this.node("identifier"); const name = this.text(); this.next(); propName = propName(name); this.expect("{"); const body = this.next().read_class_body(true, false); const node = result(propName, body); if (attrs) node.attrGroups = attrs; return node; }, /* * reading a use statement * ```ebnf * trait_use_statement ::= namespace_name (',' namespace_name)* ('{' trait_use_alias '}')? * ``` */ read_trait_use_statement() { // defines use statements 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()); } 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(";"); } if (this.expect("}")) { this.next(); } } else { if (this.expect(";")) { this.next(); } } return node(traits, adaptations); }, /* * Reading trait alias * ```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() { const node = this.node(); let trait = null; let method; if (this.is("IDENTIFIER")) { method = this.node("identifier"); const methodName = this.text(); this.next(); method = method(methodName); } else { method = this.read_namespace_name(); if (this.token === this.tok.T_DOUBLE_COLON) { this.next(); if ( this.token === this.tok.T_STRING || (this.version >= 700 && this.is("IDENTIFIER")) ) { trait = method; method = this.node("identifier"); const methodName = this.text(); this.next(); method = method(methodName); } else { this.expect(this.tok.T_STRING); } } else { // convert identifier as string method = method.name; } } // handle trait precedence if (this.token === this.tok.T_INSTEADOF) { return node( "traitprecedence", trait, method, this.next().read_name_list(), ); } else if (this.token === this.tok.T_AS) { // handle trait alias let flags = null; let alias = null; if (this.next().is("T_MEMBER_FLAGS")) { flags = this.read_member_flags(); } if ( this.token === this.tok.T_STRING || (this.version >= 700 && this.is("IDENTIFIER")) ) { alias = this.node("identifier"); const name = this.text(); this.next(); alias = alias(name); } else if (flags === null) { // no visibility flags and no name => too bad this.expect(this.tok.T_STRING); } return node("traitalias", trait, method, alias, flags); } // handle errors this.expect([this.tok.T_AS, this.tok.T_INSTEADOF]); return node("traitalias", trait, method, null, null); }, };