php-parser
Version:
Parse PHP code from JS and returns its AST
356 lines (344 loc) • 10.9 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 = {
/*
* Reads a variable
*
* ```ebnf
* variable ::= &? ...complex @todo
* ```
*
* Some samples of parsed code :
* ```php
* &$var // simple var
* $var // simple var
* classname::CONST_NAME // dynamic class name with const retrieval
* foo() // function call
* $var->func()->property // chained calls
* ```
*/
read_variable: function (read_only, encapsed) {
let result;
// check the byref flag
if (this.token === "&") {
return this.read_byref(
this.read_variable.bind(this, read_only, encapsed),
);
}
// reads the entry point
if (this.is([this.tok.T_VARIABLE, "$"])) {
result = this.read_reference_variable(encapsed);
} else if (
this.is([
this.tok.T_NS_SEPARATOR,
this.tok.T_STRING,
this.tok.T_NAME_RELATIVE,
this.tok.T_NAME_QUALIFIED,
this.tok.T_NAME_FULLY_QUALIFIED,
this.tok.T_NAMESPACE,
])
) {
result = this.node();
const name = this.read_namespace_name();
if (
this.token != this.tok.T_DOUBLE_COLON &&
this.token != "(" &&
["parentreference", "selfreference"].indexOf(name.kind) === -1
) {
// @see parser.js line 130 : resolves a conflict with scalar
const literal = name.name.toLowerCase();
if (literal === "true") {
result = name.destroy(result("boolean", true, name.name));
} else if (literal === "false") {
result = name.destroy(result("boolean", false, name.name));
} else if (literal === "null") {
result = name.destroy(result("nullkeyword", name.name));
} else {
result.destroy(name);
result = name;
}
} else {
// @fixme possible #193 bug
result.destroy(name);
result = name;
}
} else if (this.token === this.tok.T_STATIC) {
result = this.node("staticreference");
const raw = this.text();
this.next();
result = result(raw);
} else {
this.expect("VARIABLE");
}
// static mode
if (this.token === this.tok.T_DOUBLE_COLON) {
result = this.read_static_getter(result, encapsed);
}
return this.recursive_variable_chain_scan(result, read_only, encapsed);
},
// resolves a static call
read_static_getter: function (what, encapsed) {
const result = this.node("staticlookup");
let offset, name;
if (this.next().is([this.tok.T_VARIABLE, "$"])) {
offset = this.read_reference_variable(encapsed);
} else if (
this.token === this.tok.T_STRING ||
this.token === this.tok.T_CLASS ||
(this.version >= 700 && this.is("IDENTIFIER"))
) {
offset = this.node("identifier");
name = this.text();
this.next();
offset = offset(name);
} else if (this.token === "{") {
offset = this.node("literal");
name = this.next().read_expr();
this.expect("}") && this.next();
offset = offset("literal", name, null);
this.expect("(");
} else {
this.error([this.tok.T_VARIABLE, this.tok.T_STRING]);
// graceful mode : set getter as error node and continue
offset = this.node("identifier");
name = this.text();
this.next();
offset = offset(name);
}
return result(what, offset);
},
read_what: function (is_static_lookup = false) {
let what = null;
let name = null;
switch (this.next().token) {
case this.tok.T_STRING:
what = this.node("identifier");
name = this.text();
this.next();
what = what(name);
if (is_static_lookup && this.token === this.tok.T_OBJECT_OPERATOR) {
this.error();
}
break;
case this.tok.T_VARIABLE:
what = this.node("variable");
name = this.text().substring(1);
this.next();
what = what(name, false);
break;
case "$":
what = this.node();
this.next().expect(["$", "{", this.tok.T_VARIABLE]);
if (this.token === "{") {
// $obj->${$varname}
name = this.next().read_expr();
this.expect("}") && this.next();
what = what("variable", name, true);
} else {
// $obj->$$varname
name = this.read_expr();
what = what("variable", name, false);
}
break;
case "{":
what = this.node("encapsedpart");
name = this.next().read_expr();
this.expect("}") && this.next();
what = what(name, "complex", false);
break;
default:
this.error([this.tok.T_STRING, this.tok.T_VARIABLE, "$", "{"]);
// graceful mode : set what as error mode & continue
what = this.node("identifier");
name = this.text();
this.next();
what = what(name);
break;
}
return what;
},
recursive_variable_chain_scan: function (result, read_only, encapsed) {
let node, offset;
recursive_scan_loop: while (this.token != this.EOF) {
switch (this.token) {
case "(":
if (read_only) {
// @fixme : add more informations & test
return result;
} else {
result = this.node("call")(result, this.read_argument_list());
}
break;
case "[":
case "{": {
const backet = this.token;
const isSquareBracket = backet === "[";
node = this.node("offsetlookup");
this.next();
offset = false;
if (encapsed) {
offset = this.read_encaps_var_offset();
this.expect(isSquareBracket ? "]" : "}") && this.next();
} else {
const isCallableVariable = isSquareBracket
? this.token !== "]"
: this.token !== "}";
// callable_variable : https://github.com/php/php-src/blob/493524454d66adde84e00d249d607ecd540de99f/Zend/zend_language_parser.y#L1122
if (isCallableVariable) {
offset = this.read_expr();
this.expect(isSquareBracket ? "]" : "}") && this.next();
} else {
this.next();
}
}
result = node(result, offset);
break;
}
case this.tok.T_DOUBLE_COLON:
// @see https://github.com/glayzzle/php-parser/issues/107#issuecomment-354104574
if (
result.kind === "staticlookup" &&
result.offset.kind === "identifier"
) {
this.error();
}
node = this.node("staticlookup");
result = node(result, this.read_what(true));
// fix 185
// static lookup dereferencables are limited to staticlookup over functions
/*if (dereferencable && this.token !== "(") {
this.error("(");
}*/
break;
case this.tok.T_OBJECT_OPERATOR: {
node = this.node("propertylookup");
result = node(result, this.read_what());
break;
}
case this.tok.T_NULLSAFE_OBJECT_OPERATOR: {
node = this.node("nullsafepropertylookup");
result = node(result, this.read_what());
break;
}
default:
break recursive_scan_loop;
}
}
return result;
},
/*
* https://github.com/php/php-src/blob/493524454d66adde84e00d249d607ecd540de99f/Zend/zend_language_parser.y#L1231
*/
read_encaps_var_offset: function () {
let offset = this.node();
if (this.token === this.tok.T_STRING) {
const text = this.text();
this.next();
offset = offset("identifier", text);
} else if (this.token === this.tok.T_NUM_STRING) {
const num = this.text();
this.next();
offset = offset("number", num, null);
} else if (this.token === "-") {
this.next();
const num = -1 * this.text();
this.expect(this.tok.T_NUM_STRING) && this.next();
offset = offset("number", num, null);
} else if (this.token === this.tok.T_VARIABLE) {
const name = this.text().substring(1);
this.next();
offset = offset("variable", name, false);
} else {
this.expect([
this.tok.T_STRING,
this.tok.T_NUM_STRING,
"-",
this.tok.T_VARIABLE,
]);
// fallback : consider as identifier
const text = this.text();
this.next();
offset = offset("identifier", text);
}
return offset;
},
/*
* ```ebnf
* reference_variable ::= simple_variable ('[' OFFSET ']')* | '{' EXPR '}'
* ```
* <code>
* $foo[123]; // foo is an array ==> gets its entry
* $foo{1}; // foo is a string ==> get the 2nd char offset
* ${'foo'}[123]; // get the dynamic var $foo
* $foo[123]{1}; // gets the 2nd char from the 123 array entry
* </code>
*/
read_reference_variable: function (encapsed) {
let result = this.read_simple_variable();
let offset;
while (this.token != this.EOF) {
const node = this.node();
if (this.token == "{" && !encapsed) {
// @fixme check coverage, not sure thats working
offset = this.next().read_expr();
this.expect("}") && this.next();
result = node("offsetlookup", result, offset);
} else {
node.destroy();
break;
}
}
return result;
},
/*
* ```ebnf
* simple_variable ::= T_VARIABLE | '$' '{' expr '}' | '$' simple_variable
* ```
*/
read_simple_variable: function () {
let result = this.node("variable");
let name;
if (
this.expect([this.tok.T_VARIABLE, "$"]) &&
this.token === this.tok.T_VARIABLE
) {
// plain variable name
name = this.text().substring(1);
this.next();
result = result(name, false);
} else {
if (this.token === "$") this.next();
// dynamic variable name
switch (this.token) {
case "{": {
const expr = this.next().read_expr();
this.expect("}") && this.next();
result = result(expr, true);
break;
}
case "$": // $$$var
result = result(this.read_simple_variable(), false);
break;
case this.tok.T_VARIABLE: {
// $$var
name = this.text().substring(1);
const node = this.node("variable");
this.next();
result = result(node(name, false), false);
break;
}
default:
this.error(["{", "$", this.tok.T_VARIABLE]);
// graceful mode
name = this.text();
this.next();
result = result(name, false);
}
}
return result;
},
};