php-parser
Version:
Parse PHP code from JS and returns its AST
833 lines (763 loc) • 24.7 kB
JavaScript
/**
* Copyright (C) 2018 Glayzzle (BSD3 License)
* @authors https://github.com/glayzzle/php-parser/graphs/contributors
* @url http://glayzzle.com
*/
"use strict";
module.exports = {
read_expr: function (expr) {
const result = this.node();
if (this.token === "@") {
if (!expr) {
expr = this.next().read_expr();
}
return result("silent", expr);
}
if (!expr) {
expr = this.read_expr_item();
}
// binary operations
if (this.token === "|") {
return result("bin", "|", expr, this.next().read_expr());
}
if (this.token === "&") {
return result("bin", "&", expr, this.next().read_expr());
}
if (this.token === "^") {
return result("bin", "^", expr, this.next().read_expr());
}
if (this.token === ".") {
return result("bin", ".", expr, this.next().read_expr());
}
if (this.token === "+") {
return result("bin", "+", expr, this.next().read_expr());
}
if (this.token === "-") {
return result("bin", "-", expr, this.next().read_expr());
}
if (this.token === "*") {
return result("bin", "*", expr, this.next().read_expr());
}
if (this.token === "/") {
return result("bin", "/", expr, this.next().read_expr());
}
if (this.token === "%") {
return result("bin", "%", expr, this.next().read_expr());
}
if (this.token === this.tok.T_POW) {
return result("bin", "**", expr, this.next().read_expr());
}
if (this.token === this.tok.T_SL) {
return result("bin", "<<", expr, this.next().read_expr());
}
if (this.token === this.tok.T_SR) {
return result("bin", ">>", expr, this.next().read_expr());
}
// more binary operations (formerly bool)
if (this.token === this.tok.T_BOOLEAN_OR) {
return result("bin", "||", expr, this.next().read_expr());
}
if (this.token === this.tok.T_LOGICAL_OR) {
return result("bin", "or", expr, this.next().read_expr());
}
if (this.token === this.tok.T_BOOLEAN_AND) {
return result("bin", "&&", expr, this.next().read_expr());
}
if (this.token === this.tok.T_LOGICAL_AND) {
return result("bin", "and", expr, this.next().read_expr());
}
if (this.token === this.tok.T_LOGICAL_XOR) {
return result("bin", "xor", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_IDENTICAL) {
return result("bin", "===", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_NOT_IDENTICAL) {
return result("bin", "!==", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_EQUAL) {
return result("bin", "==", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_NOT_EQUAL) {
return result("bin", "!=", expr, this.next().read_expr());
}
if (this.token === "<") {
return result("bin", "<", expr, this.next().read_expr());
}
if (this.token === ">") {
return result("bin", ">", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_SMALLER_OR_EQUAL) {
return result("bin", "<=", expr, this.next().read_expr());
}
if (this.token === this.tok.T_IS_GREATER_OR_EQUAL) {
return result("bin", ">=", expr, this.next().read_expr());
}
if (this.token === this.tok.T_SPACESHIP) {
return result("bin", "<=>", expr, this.next().read_expr());
}
if (this.token === this.tok.T_INSTANCEOF) {
expr = result(
"bin",
"instanceof",
expr,
this.next().read_class_name_reference(),
);
if (
this.token !== ";" &&
this.token !== this.tok.T_INLINE_HTML &&
this.token !== this.EOF
) {
expr = this.read_expr(expr);
}
}
// extra operations :
// $username = $_GET['user'] ?? 'nobody';
if (this.token === this.tok.T_COALESCE) {
return result("bin", "??", expr, this.next().read_expr());
}
// extra operations :
// $username = $_GET['user'] ? true : false;
if (this.token === "?") {
let trueArg = null;
if (this.next().token !== ":") {
trueArg = this.read_expr();
}
this.expect(":") && this.next();
return result("retif", expr, trueArg, this.read_expr());
} else {
// see #193
result.destroy(expr);
}
return expr;
},
/*
* Reads a cast expression
*/
read_expr_cast: function (type) {
return this.node("cast")(type, this.text(), this.next().read_expr());
},
/*
* Read a isset variable
*/
read_isset_variable: function () {
return this.read_expr();
},
/*
* Reads isset variables
*/
read_isset_variables: function () {
return this.read_function_list(this.read_isset_variable, ",");
},
/*
* Reads internal PHP functions
*/
read_internal_functions_in_yacc: function () {
let result = null;
switch (this.token) {
case this.tok.T_ISSET:
{
result = this.node("isset");
if (this.next().expect("(")) {
this.next();
}
const variables = this.read_isset_variables();
if (this.expect(")")) {
this.next();
}
result = result(variables);
}
break;
case this.tok.T_EMPTY:
{
result = this.node("empty");
if (this.next().expect("(")) {
this.next();
}
const expression = this.read_expr();
if (this.expect(")")) {
this.next();
}
result = result(expression);
}
break;
case this.tok.T_INCLUDE:
result = this.node("include")(false, false, this.next().read_expr());
break;
case this.tok.T_INCLUDE_ONCE:
result = this.node("include")(true, false, this.next().read_expr());
break;
case this.tok.T_EVAL:
{
result = this.node("eval");
if (this.next().expect("(")) {
this.next();
}
const expr = this.read_expr();
if (this.expect(")")) {
this.next();
}
result = result(expr);
}
break;
case this.tok.T_REQUIRE:
result = this.node("include")(false, true, this.next().read_expr());
break;
case this.tok.T_REQUIRE_ONCE:
result = this.node("include")(true, true, this.next().read_expr());
break;
}
return result;
},
/*
* Reads optional expression
*/
read_optional_expr: function (stopToken) {
if (this.token !== stopToken) {
return this.read_expr();
}
return null;
},
/*
* Reads exit expression
*/
read_exit_expr: function () {
let expression = null;
if (this.token === "(") {
this.next();
expression = this.read_optional_expr(")");
this.expect(")") && this.next();
}
return expression;
},
/*
* ```ebnf
* Reads an expression
* expr ::= @todo
* ```
*/
read_expr_item: function () {
let result,
expr,
attrs = [];
if (this.token === "+") {
return this.node("unary")("+", this.next().read_expr());
}
if (this.token === "-") {
return this.node("unary")("-", this.next().read_expr());
}
if (this.token === "!") {
return this.node("unary")("!", this.next().read_expr());
}
if (this.token === "~") {
return this.node("unary")("~", this.next().read_expr());
}
if (this.token === "(") {
expr = this.next().read_expr();
expr.parenthesizedExpression = true;
this.expect(")") && this.next();
return this.handleDereferencable(expr);
}
if (this.token === "`") {
// https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L1048
return this.read_encapsed_string("`");
}
if (this.token === this.tok.T_LIST) {
let assign = null;
const isInner = this.innerList;
result = this.node("list");
if (!isInner) {
assign = this.node("assign");
}
if (this.next().expect("(")) {
this.next();
}
if (!this.innerList) this.innerList = true;
// reads inner items
const assignList = this.read_array_pair_list(false);
if (this.expect(")")) {
this.next();
}
// check if contains at least one assignment statement
let hasItem = false;
for (let i = 0; i < assignList.length; i++) {
if (assignList[i] !== null && assignList[i].kind !== "noop") {
hasItem = true;
break;
}
}
if (!hasItem) {
/* istanbul ignore next */
this.raiseError(
"Fatal Error : Cannot use empty list on line " +
this.lexer.yylloc.first_line,
);
}
// handles the node resolution
if (!isInner) {
this.innerList = false;
if (this.expect("=")) {
return assign(
result(assignList, false),
this.next().read_expr(),
"=",
);
} else {
// error fallback : list($a, $b);
/* istanbul ignore next */
return result(assignList, false);
}
} else {
return result(assignList, false);
}
}
if (this.token === this.tok.T_ATTRIBUTE) {
attrs = this.read_attr_list();
}
if (this.token === this.tok.T_CLONE) {
return this.node("clone")(this.next().read_expr());
}
switch (this.token) {
case this.tok.T_INC:
return this.node("pre")("+", this.next().read_variable(false, false));
case this.tok.T_DEC:
return this.node("pre")("-", this.next().read_variable(false, false));
case this.tok.T_NEW:
return this.read_new_expr();
case this.tok.T_ISSET:
case this.tok.T_EMPTY:
case this.tok.T_INCLUDE:
case this.tok.T_INCLUDE_ONCE:
case this.tok.T_EVAL:
case this.tok.T_REQUIRE:
case this.tok.T_REQUIRE_ONCE:
return this.read_internal_functions_in_yacc();
case this.tok.T_MATCH:
return this.read_match_expression();
case this.tok.T_INT_CAST:
return this.read_expr_cast("int");
case this.tok.T_DOUBLE_CAST:
return this.read_expr_cast("float");
case this.tok.T_STRING_CAST:
return this.read_expr_cast(
this.text().indexOf("binary") !== -1 ? "binary" : "string",
);
case this.tok.T_ARRAY_CAST:
return this.read_expr_cast("array");
case this.tok.T_OBJECT_CAST:
return this.read_expr_cast("object");
case this.tok.T_BOOL_CAST:
return this.read_expr_cast("bool");
case this.tok.T_UNSET_CAST:
return this.read_expr_cast("unset");
case this.tok.T_THROW: {
if (this.version < 800) {
this.raiseError("PHP 8+ is required to use throw as an expression");
}
const result = this.node("throw");
const expr = this.next().read_expr();
return result(expr);
}
case this.tok.T_EXIT: {
const useDie = this.lexer.yytext.toLowerCase() === "die";
result = this.node("exit");
this.next();
const expression = this.read_exit_expr();
return result(expression, useDie);
}
case this.tok.T_PRINT:
return this.node("print")(this.next().read_expr());
// T_YIELD (expr (T_DOUBLE_ARROW expr)?)?
case this.tok.T_YIELD: {
let value = null;
let key = null;
result = this.node("yield");
if (this.next().is("EXPR")) {
// reads the yield return value
value = this.read_expr();
if (this.token === this.tok.T_DOUBLE_ARROW) {
// reads the yield returned key
key = value;
value = this.next().read_expr();
}
}
return result(value, key);
}
// T_YIELD_FROM expr
case this.tok.T_YIELD_FROM:
result = this.node("yieldfrom");
expr = this.next().read_expr();
return result(expr);
case this.tok.T_FN:
case this.tok.T_FUNCTION:
return this.read_inline_function(undefined, attrs);
case this.tok.T_STATIC: {
const backup = [this.token, this.lexer.getState()];
this.next();
if (
this.token === this.tok.T_FUNCTION ||
(this.version >= 704 && this.token === this.tok.T_FN)
) {
// handles static function
return this.read_inline_function([0, 1, 0], attrs);
} else {
// rollback
this.lexer.tokens.push(backup);
this.next();
}
}
}
// SCALAR | VARIABLE
if (this.is("VARIABLE")) {
result = this.node();
expr = this.read_variable(false, false);
// https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L877
// should accept only a variable
const isConst =
expr.kind === "identifier" ||
(expr.kind === "staticlookup" && expr.offset.kind === "identifier");
// VARIABLES SPECIFIC OPERATIONS
switch (this.token) {
case "=": {
if (isConst) this.error("VARIABLE");
if (this.next().token == "&") {
return this.read_assignref(result, expr);
}
return result("assign", expr, this.read_expr(), "=");
}
// operations :
case this.tok.T_PLUS_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "+=");
case this.tok.T_MINUS_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "-=");
case this.tok.T_MUL_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "*=");
case this.tok.T_POW_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "**=");
case this.tok.T_DIV_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "/=");
case this.tok.T_CONCAT_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), ".=");
case this.tok.T_MOD_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "%=");
case this.tok.T_AND_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "&=");
case this.tok.T_OR_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "|=");
case this.tok.T_XOR_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "^=");
case this.tok.T_SL_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "<<=");
case this.tok.T_SR_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), ">>=");
case this.tok.T_COALESCE_EQUAL:
if (isConst) this.error("VARIABLE");
return result("assign", expr, this.next().read_expr(), "??=");
case this.tok.T_INC:
if (isConst) this.error("VARIABLE");
this.next();
return result("post", "+", expr);
case this.tok.T_DEC:
if (isConst) this.error("VARIABLE");
this.next();
return result("post", "-", expr);
default:
// see #193
result.destroy(expr);
}
} else if (this.is("SCALAR")) {
result = this.node();
expr = this.read_scalar();
if (expr.kind === "array" && expr.shortForm && this.token === "=") {
// list assign
const list = this.convertToList(expr);
if (expr.loc) list.loc = expr.loc;
const right = this.next().read_expr();
return result("assign", list, right, "=");
} else {
// see #189 - swap docs on nodes
result.destroy(expr);
}
// classic array
return this.handleDereferencable(expr);
} else {
this.error("EXPR");
this.next();
}
// returns variable | scalar
return expr;
},
/*
* Recursively convert nested array to nested list.
*/
convertToList: function (array) {
const convertedItems = array.items.map((entry) => {
if (
entry.value &&
entry.value.kind === "array" &&
entry.value.shortForm
) {
entry.value = this.convertToList(entry.value);
}
return entry;
});
const node = this.node("list")(convertedItems, true);
if (array.loc) node.loc = array.loc;
if (array.leadingComments) node.leadingComments = array.leadingComments;
if (array.trailingComments) node.trailingComments = array.trailingComments;
return node;
},
/*
* Reads assignment
* @param {*} left
*/
read_assignref: function (result, left) {
this.next();
let right;
if (this.token === this.tok.T_NEW) {
if (this.version >= 700) {
this.error();
}
right = this.read_new_expr();
} else {
right = this.read_variable(false, false);
}
return result("assignref", left, right);
},
/*
*
* inline_function:
* function returns_ref backup_doc_comment '(' parameter_list ')' lexical_vars return_type
* backup_fn_flags '{' inner_statement_list '}' backup_fn_flags
* { $$ = zend_ast_create_decl(ZEND_AST_CLOSURE, $2 | $13, $1, $3,
* zend_string_init("{closure}", sizeof("{closure}") - 1, 0),
* $5, $7, $11, $8); CG(extra_fn_flags) = $9; }
* | fn returns_ref '(' parameter_list ')' return_type backup_doc_comment T_DOUBLE_ARROW backup_fn_flags backup_lex_pos expr backup_fn_flags
* { $$ = zend_ast_create_decl(ZEND_AST_ARROW_FUNC, $2 | $12, $1, $7,
* zend_string_init("{closure}", sizeof("{closure}") - 1, 0), $4, NULL,
* zend_ast_create(ZEND_AST_RETURN, $11), $6);
* ((zend_ast_decl *) $$)->lex_pos = $10;
* CG(extra_fn_flags) = $9; } *
*/
read_inline_function: function (flags, attrs) {
if (this.token === this.tok.T_FUNCTION) {
const result = this.read_function(true, flags, attrs);
result.attrGroups = attrs;
return result;
}
// introduced in PHP 7.4
if (!this.version >= 704) {
this.raiseError("Arrow Functions are not allowed");
}
// as an arrowfunc
const node = this.node("arrowfunc");
// eat T_FN
if (this.expect(this.tok.T_FN)) this.next();
// check the &
const isRef = this.is_reference();
// ...
if (this.expect("(")) this.next();
const params = this.read_parameter_list();
if (this.expect(")")) this.next();
let nullable = false;
let returnType = null;
if (this.token === ":") {
if (this.next().token === "?") {
nullable = true;
this.next();
}
returnType = this.read_types();
}
if (this.expect(this.tok.T_DOUBLE_ARROW)) this.next();
const body = this.read_expr();
const result = node(
params,
isRef,
body,
returnType,
nullable,
flags ? true : false,
);
result.attrGroups = attrs;
return result;
},
read_match_expression: function () {
const node = this.node("match");
this.expect(this.tok.T_MATCH) && this.next();
if (this.version < 800) {
this.raiseError("Match statements are not allowed before PHP 8");
}
let cond = null;
let arms = [];
if (this.expect("(")) this.next();
cond = this.read_expr();
if (this.expect(")")) this.next();
if (this.expect("{")) this.next();
arms = this.read_match_arms();
if (this.expect("}")) this.next();
return node(cond, arms);
},
read_match_arms: function () {
return this.read_list(() => this.read_match_arm(), ",", true);
},
read_match_arm: function () {
if (this.token === "}") {
return;
}
return this.node("matcharm")(this.read_match_arm_conds(), this.read_expr());
},
read_match_arm_conds: function () {
let conds = [];
if (this.token === this.tok.T_DEFAULT) {
conds = null;
this.next();
} else {
conds.push(this.read_expr());
while (this.token === ",") {
this.next();
if (this.token === this.tok.T_DOUBLE_ARROW) {
this.next();
return conds;
}
conds.push(this.read_expr());
}
}
if (this.expect(this.tok.T_DOUBLE_ARROW)) {
this.next();
}
return conds;
},
read_attribute() {
const name = this.text();
let args = [];
this.next();
if (this.token === "(") {
args = this.read_argument_list();
}
return this.node("attribute")(name, args);
},
read_attr_list() {
const list = [];
if (this.token === this.tok.T_ATTRIBUTE) {
do {
const attrGr = this.node("attrgroup")([]);
this.next();
attrGr.attrs.push(this.read_attribute());
while (this.token === ",") {
this.next();
if (this.token !== "]") attrGr.attrs.push(this.read_attribute());
}
list.push(attrGr);
this.expect("]");
this.next();
} while (this.token === this.tok.T_ATTRIBUTE);
}
return list;
},
/*
* ```ebnf
* new_expr ::= T_NEW (namespace_name function_argument_list) | (T_CLASS ... class declaration)
* ```
* https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L850
*/
read_new_expr: function () {
const result = this.node("new");
this.expect(this.tok.T_NEW) && this.next();
let args = [];
if (this.token === "(") {
this.next();
const newExp = this.read_expr();
this.expect(")");
this.next();
if (this.token === "(") {
args = this.read_argument_list();
}
return result(newExp, args);
}
const attrs = this.read_attr_list();
if (this.token === this.tok.T_CLASS) {
const what = this.node("class");
// Annonymous class declaration
if (this.next().token === "(") {
args = this.read_argument_list();
}
const propExtends = this.read_extends_from();
const propImplements = this.read_implements_list();
let body = null;
if (this.expect("{")) {
body = this.next().read_class_body(true, false);
}
const whatNode = what(null, propExtends, propImplements, body, [0, 0, 0]);
whatNode.attrGroups = attrs;
return result(whatNode, args);
}
// Already existing class
let name = this.read_new_class_name();
while (this.token === "[") {
const offsetNode = this.node("offsetlookup");
const offset = this.next().read_encaps_var_offset();
this.expect("]") && this.next();
name = offsetNode(name, offset);
}
if (this.token === "(") {
args = this.read_argument_list();
}
return result(name, args);
},
/*
* Reads a class name
* ```ebnf
* read_new_class_name ::= namespace_name | variable
* ```
*/
read_new_class_name: function () {
if (
this.token === this.tok.T_NS_SEPARATOR ||
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_NAMESPACE
) {
let result = this.read_namespace_name(true);
if (this.token === this.tok.T_DOUBLE_COLON) {
result = this.read_static_getter(result);
}
return result;
} else if (this.is("VARIABLE")) {
return this.read_variable(true, false);
} else {
this.expect([this.tok.T_STRING, "VARIABLE"]);
}
},
handleDereferencable: function (expr) {
while (this.token !== this.EOF) {
if (
this.token === this.tok.T_OBJECT_OPERATOR ||
this.token === this.tok.T_DOUBLE_COLON
) {
expr = this.recursive_variable_chain_scan(expr, false, false, true);
} else if (this.token === this.tok.T_CURLY_OPEN || this.token === "[") {
expr = this.read_dereferencable(expr);
} else if (this.token === "(") {
// https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L1118
expr = this.node("call")(expr, this.read_argument_list());
} else {
return expr;
}
}
return expr;
},
};