UNPKG

php-parser

Version:

Parse PHP code from JS and returns its AST

606 lines (568 loc) 17.2 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: function (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: function () { const modifier = this.read_class_modifier({ readonly: 0, final_or_abstract: 0, }); return [0, 0, modifier.final_or_abstract, modifier.readonly]; }, read_class_modifier: function (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: function (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; } // check enum cases if (allow_enum_cases && this.token === this.tok.T_CASE) { const enumcase = this.read_enum_case(); if (this.expect(";")) { this.next(); } result = result.concat(enumcase); continue; } if (this.token === this.tok.T_ATTRIBUTE) { attrs = this.read_attr_list(); } const locStart = this.position(); // read member flags const flags = this.read_member_flags(false); // check constant if (this.token === this.tok.T_CONST) { const constants = this.read_constant_list(flags, attrs); 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] = 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); attrs = []; this.expect(";"); this.next(); 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: function (flags, attrs) { const result = 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 = false; if (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; this.expect([",", ";", "="]); if (this.token === "=") { // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L815 value = this.next().read_expr(); } return result(propName, value, readonly, nullable, type, attrs || []); }, ",", ); return result(null, properties, flags); }, /* * Reads constant list * ```ebnf * constant_list ::= T_CONST [type] (constant_declaration ',')* constant_declaration * ``` */ read_constant_list: function (flags, attrs) { if (this.expect(this.tok.T_CONST)) { this.next(); } const [nullable, type] = this.version >= 803 ? this.read_optional_type() : [false, null]; const result = this.node("classconstant"); 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); }, ",", ); return result(null, items, flags, nullable, type, attrs || []); }, /* * 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 */ 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: 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; break; case this.tok.T_ABSTRACT: idx = 2; val = 1; break; case this.tok.T_FINAL: idx = 2; val = 2; break; } 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) { // an interface cant be abstract this.error(); val = -1; } } 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; }, /* * 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: function () { const nullable = this.token === "?"; if (nullable) { this.next(); } if (this.peek() === "=") { 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]; }, /* * reading an interface * ```ebnf * interface ::= T_INTERFACE T_STRING (T_EXTENDS (NAMESPACE_NAME ',')* NAMESPACE_NAME)? '{' INTERFACE_BODY '}' * ``` */ read_interface_declaration_statement: function (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: function () { let result = [], 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 = this.read_attr_list(); // read member flags const flags = this.read_member_flags(true); // check constant if (this.token === this.tok.T_CONST) { const constants = this.read_constant_list(flags, attrs); if (this.expect(";")) { this.next(); } result = result.concat(constants); attrs = []; } 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(); } attrs = []; } else { // raise an error this.error([this.tok.T_CONST, this.tok.T_FUNCTION]); 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: function () { 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); return result(propName, body); }, /* * reading a use statement * ```ebnf * trait_use_statement ::= namespace_name (',' namespace_name)* ('{' trait_use_alias '}')? * ``` */ read_trait_use_statement: function () { // 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: function () { 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 === false) { // 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); }, };