UNPKG

php-parser

Version:

Parse PHP code from JS and returns its AST

508 lines (483 loc) 13.5 kB
/** * Copyright (C) 2018 Glayzzle (BSD3 License) * @authors https://github.com/glayzzle/php-parser/graphs/contributors * @url http://glayzzle.com */ "use strict"; module.exports = { /* * checks if current token is a reference keyword */ is_reference: function () { if (this.token === "&") { this.next(); return true; } return false; }, /* * checks if current token is a variadic keyword */ is_variadic: function () { if (this.token === this.tok.T_ELLIPSIS) { this.next(); return true; } return false; }, /* * reading a function * ```ebnf * function ::= function_declaration code_block * ``` */ read_function: function (closure, flag, attrs, locStart) { const result = this.read_function_declaration( closure ? 1 : flag ? 2 : 0, flag && flag[1] === 1, attrs || [], locStart, ); if (flag && flag[2] == 1) { // abstract function : result.parseFlags(flag); if (this.expect(";")) { this.next(); } } else { if (this.expect("{")) { result.body = this.read_code_block(false); if (result.loc && result.body.loc) { result.loc.end = result.body.loc.end; } } if (!closure && flag) { result.parseFlags(flag); } } return result; }, /* * reads a function declaration (without his body) * ```ebnf * function_declaration ::= T_FUNCTION '&'? T_STRING '(' parameter_list ')' * ``` */ read_function_declaration: function (type, isStatic, attrs, locStart) { let nodeName = "function"; if (type === 1) { nodeName = "closure"; } else if (type === 2) { nodeName = "method"; } const result = this.node(nodeName); if (this.expect(this.tok.T_FUNCTION)) { this.next(); } const isRef = this.is_reference(); let name = false, use = [], returnType = null, nullable = false; if (type !== 1) { const nameNode = this.node("identifier"); if (type === 2) { if (this.version >= 700) { if (this.token === this.tok.T_STRING || this.is("IDENTIFIER")) { name = this.text(); this.next(); } else if (this.version < 704) { this.error("IDENTIFIER"); } } else if (this.token === this.tok.T_STRING) { name = this.text(); this.next(); } else { this.error("IDENTIFIER"); } } else { if (this.version >= 700) { if (this.token === this.tok.T_STRING) { name = this.text(); this.next(); } else if (this.version >= 704) { if (!this.expect("(")) { this.next(); } } else { this.error(this.tok.T_STRING); this.next(); } } else { if (this.expect(this.tok.T_STRING)) { name = this.text(); } this.next(); } } name = nameNode(name); } if (this.expect("(")) this.next(); const params = this.read_parameter_list(name.name === "__construct"); if (this.expect(")")) this.next(); if (type === 1) { use = this.read_lexical_vars(); } if (this.token === ":") { if (this.next().token === "?") { nullable = true; this.next(); } returnType = this.read_types(); } const apply_attrgroup_location = (node) => { node.attrGroups = 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; }; if (type === 1) { // closure return apply_attrgroup_location( result(params, isRef, use, returnType, nullable, isStatic), ); } return apply_attrgroup_location( result(name, params, isRef, returnType, nullable), ); }, read_lexical_vars: function () { let result = []; if (this.token === this.tok.T_USE) { this.next(); this.expect("(") && this.next(); result = this.read_lexical_var_list(); this.expect(")") && this.next(); } return result; }, read_list_with_dangling_comma: function (item) { const result = []; while (this.token != this.EOF) { result.push(item()); if (this.token == ",") { this.next(); if (this.version >= 800 && this.token === ")") { return result; } } else if (this.token == ")") { break; } else { this.error([",", ")"]); break; } } return result; }, read_lexical_var_list: function () { return this.read_list_with_dangling_comma(this.read_lexical_var.bind(this)); }, /* * ```ebnf * lexical_var ::= '&'? T_VARIABLE * ``` */ read_lexical_var: function () { if (this.token === "&") { return this.read_byref(this.read_lexical_var.bind(this)); } const result = this.node("variable"); this.expect(this.tok.T_VARIABLE); const name = this.text().substring(1); this.next(); return result(name, false); }, /* * reads a list of parameters * ```ebnf * parameter_list ::= (parameter ',')* parameter? * ``` */ read_parameter_list: function (is_class_constructor) { if (this.token !== ")") { let wasVariadic = false; return this.read_list_with_dangling_comma( function () { const parameter = this.read_parameter(is_class_constructor); if (parameter) { // variadic parameters can only be defined at the end of the parameter list if (wasVariadic) { this.raiseError( "Unexpected parameter after a variadic parameter", ); } if (parameter.variadic) { wasVariadic = true; } } return parameter; }.bind(this), ",", ); } return []; }, /* * ```ebnf * parameter ::= type? '&'? T_ELLIPSIS? T_VARIABLE ('=' expr)? * ``` * @see https://github.com/php/php-src/blob/493524454d66adde84e00d249d607ecd540de99f/Zend/zend_language_parser.y#L640 */ read_parameter: function (is_class_constructor) { const node = this.node("parameter"); let parameterName = null; let value = null; let types = null; let nullable = false; let readonly = false; 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 (is_class_constructor) { this.next(); readonly = true; } else { this.raiseError( "readonly properties can be used only on class constructor", ); } } const flags = this.read_promoted(); if ( !readonly && this.version >= 801 && this.token === this.tok.T_READ_ONLY ) { if (is_class_constructor) { this.next(); readonly = true; } else { this.raiseError( "readonly properties can be used only on class constructor", ); } } if (this.token === "?") { this.next(); nullable = true; } types = this.read_types(); if (nullable && !types) { this.raiseError( "Expecting a type definition combined with nullable operator", ); } const isRef = this.is_reference(); const isVariadic = this.is_variadic(); if (this.expect(this.tok.T_VARIABLE)) { parameterName = this.node("identifier"); const name = this.text().substring(1); this.next(); parameterName = parameterName(name); } if (this.token == "=") { value = this.next().read_expr(); } const result = node( parameterName, types, value, isRef, isVariadic, readonly, nullable, flags, ); if (attrs) result.attrGroups = attrs; return result; }, read_types: function () { const MODE_UNSET = "unset"; const MODE_UNION = "union"; const MODE_INTERSECTION = "intersection"; const types = []; let mode = MODE_UNSET; const type = this.read_type(); if (!type) return null; // we have matched a single type types.push(type); // is the current token a: // - | for union type // - & for intersection type (> php 8.1) while (this.token === "|" || (this.version >= 801 && this.token === "&")) { const nextToken = this.peek(); if ( nextToken === this.tok.T_ELLIPSIS || nextToken === this.tok.T_VARIABLE ) { // the next token is part of the variable (or the variable itself), // we're not gonna match anymore types break; } if (mode === MODE_UNSET) { // are we in union or intersection "mode" mode = this.token === "|" ? MODE_UNION : MODE_INTERSECTION; } else { // it is not possible to mix "modes" if ( (mode === MODE_UNION && this.token !== "|") || (mode === MODE_INTERSECTION && this.token !== "&") ) { this.raiseError( 'Unexpect token "' + this.token + '", "|" and "&" can not be mixed', ); } } this.next(); types.push(this.read_type()); } if (types.length === 1) { return types[0]; } else { return mode === MODE_INTERSECTION ? this.node("intersectiontype")(types) : this.node("uniontype")(types); } }, read_promoted: function () { const MODIFIER_PUBLIC = 1; const MODIFIER_PROTECTED = 2; const MODIFIER_PRIVATE = 4; if (this.token === this.tok.T_PUBLIC) { this.next(); return MODIFIER_PUBLIC; } else if (this.token === this.tok.T_PROTECTED) { this.next(); return MODIFIER_PROTECTED; } else if (this.token === this.tok.T_PRIVATE) { this.next(); return MODIFIER_PRIVATE; } return 0; }, /* * Reads a list of arguments * ```ebnf * function_argument_list ::= '(' (argument_list (',' argument_list)*)? ')' * ``` */ read_argument_list: function () { let result = []; this.expect("(") && this.next(); if ( this.version >= 801 && this.token === this.tok.T_ELLIPSIS && this.peek() === ")" ) { result.push(this.node("variadicplaceholder")()); this.next(); } else if (this.token !== ")") { result = this.read_non_empty_argument_list(); } this.expect(")") && this.next(); return result; }, /* * Reads non empty argument list */ read_non_empty_argument_list: function () { let wasVariadic = false; return this.read_function_list( function () { const argument = this.read_argument(); if (argument) { const isVariadic = argument.kind === "variadic"; // variadic arguments can only be followed by other variadic arguments if (wasVariadic && !isVariadic) { this.raiseError( "Unexpected non-variadic argument after a variadic argument", ); } if (isVariadic) { wasVariadic = true; } } return argument; }.bind(this), ",", ); }, /* * ```ebnf * argument_list ::= T_STRING ':' expr | T_ELLIPSIS? expr * ``` */ read_argument: function () { if (this.token === this.tok.T_ELLIPSIS) { return this.node("variadic")(this.next().read_expr()); } if ( this.token === this.tok.T_STRING || Object.values(this.lexer.keywords).includes(this.token) ) { const nextToken = this.peek(); if (nextToken === ":") { if (this.version < 800) { this.raiseError("PHP 8+ is required to use named arguments"); } return this.node("namedargument")( this.text(), this.next().next().read_expr(), ); } } return this.read_expr(); }, /* * read type hinting * ```ebnf * type ::= T_ARRAY | T_CALLABLE | namespace_name * ``` */ read_type: function () { const result = this.node(); if (this.token === this.tok.T_ARRAY || this.token === this.tok.T_CALLABLE) { const type = this.text(); this.next(); return result("typereference", type.toLowerCase(), type); } else if ( this.token === this.tok.T_NAME_RELATIVE || this.token === this.tok.T_NAME_QUALIFIED || this.token === this.tok.T_NAME_FULLY_QUALIFIED || this.token === this.tok.T_STRING || this.token === this.tok.T_STATIC ) { const type = this.text(); const backup = [this.token, this.lexer.getState()]; this.next(); if ( this.token !== this.tok.T_NS_SEPARATOR && this.ast.typereference.types.indexOf(type.toLowerCase()) > -1 ) { return result("typereference", type.toLowerCase(), type); } else { // rollback a classic namespace this.lexer.tokens.push(backup); this.next(); // fix : destroy not consumed node (release comments) result.destroy(); return this.read_namespace_name(); } } // fix : destroy not consumed node (release comments) result.destroy(); return null; }, };