php-parser
Version:
Parse PHP code from JS and returns its AST
445 lines (420 loc) • 13.4 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 = {
/*
* reading a list of top statements (helper for top_statement*)
* ```ebnf
* top_statements ::= top_statement*
* ```
*/
read_top_statements: function () {
let result = [];
while (this.token !== this.EOF && this.token !== "}") {
const statement = this.read_top_statement();
if (statement) {
if (Array.isArray(statement)) {
result = result.concat(statement);
} else {
result.push(statement);
}
}
}
return result;
},
/*
* reading a top statement
* ```ebnf
* top_statement ::=
* namespace | function | class
* | interface | trait
* | use_statements | const_list
* | statement
* ```
*/
read_top_statement: function () {
let attrs = [];
if (this.token === this.tok.T_ATTRIBUTE) {
attrs = this.read_attr_list();
}
switch (this.token) {
case this.tok.T_FUNCTION:
return this.read_function(false, false, attrs);
// optional flags
case this.tok.T_ABSTRACT:
case this.tok.T_FINAL:
case this.tok.T_READ_ONLY:
case this.tok.T_CLASS:
return this.read_class_declaration_statement(attrs);
case this.tok.T_INTERFACE:
return this.read_interface_declaration_statement(attrs);
case this.tok.T_TRAIT:
return this.read_trait_declaration_statement();
case this.tok.T_ENUM:
return this.read_enum_declaration_statement(attrs);
case this.tok.T_USE:
return this.read_use_statement();
case this.tok.T_CONST: {
const result = this.node("constantstatement");
const items = this.next().read_const_list();
this.expectEndOfStatement();
return result(null, items);
}
case this.tok.T_NAMESPACE:
return this.read_namespace();
case this.tok.T_HALT_COMPILER: {
const result = this.node("halt");
if (this.next().expect("(")) this.next();
if (this.expect(")")) this.next();
this.expect(";");
this.lexer.done = true;
return result(this.lexer._input.substring(this.lexer.offset));
}
default:
return this.read_statement();
}
},
/*
* reads a list of simple inner statements (helper for inner_statement*)
* ```ebnf
* inner_statements ::= inner_statement*
* ```
*/
read_inner_statements: function () {
let result = [];
while (this.token != this.EOF && this.token !== "}") {
const statement = this.read_inner_statement();
if (statement) {
if (Array.isArray(statement)) {
result = result.concat(statement);
} else {
result.push(statement);
}
}
}
return result;
},
/*
* Reads a list of constants declaration
* ```ebnf
* const_list ::= T_CONST T_STRING '=' expr (',' T_STRING '=' expr)* ';'
* ```
*/
read_const_list: function () {
return this.read_list(
function () {
this.expect(this.tok.T_STRING);
const result = this.node("constant");
let constName = this.node("identifier");
const name = this.text();
this.next();
constName = constName(name);
if (this.expect("=")) {
return result(constName, this.next().read_expr());
} else {
// fallback
return result(constName, null);
}
},
",",
false,
);
},
/*
* Reads a list of constants declaration
* ```ebnf
* declare_list ::= IDENTIFIER '=' expr (',' IDENTIFIER '=' expr)*
* ```
* @retrurn {Array}
*/
read_declare_list: function () {
const result = [];
while (this.token != this.EOF && this.token !== ")") {
this.expect(this.tok.T_STRING);
const directive = this.node("declaredirective");
let key = this.node("identifier");
const name = this.text();
this.next();
key = key(name);
let value = null;
if (this.expect("=")) {
value = this.next().read_expr();
}
result.push(directive(key, value));
if (this.token !== ",") break;
this.next();
}
return result;
},
/*
* reads a simple inner statement
* ```ebnf
* inner_statement ::= '{' inner_statements '}' | token
* ```
*/
read_inner_statement: function () {
let attrs = [];
if (this.token === this.tok.T_ATTRIBUTE) {
attrs = this.read_attr_list();
}
switch (this.token) {
case this.tok.T_FUNCTION: {
const result = this.read_function(false, false);
result.attrGroups = attrs;
return result;
}
// optional flags
case this.tok.T_ABSTRACT:
case this.tok.T_FINAL:
case this.tok.T_CLASS:
return this.read_class_declaration_statement();
case this.tok.T_INTERFACE:
return this.read_interface_declaration_statement();
case this.tok.T_TRAIT:
return this.read_trait_declaration_statement();
case this.tok.T_ENUM:
return this.read_enum_declaration_statement();
case this.tok.T_HALT_COMPILER: {
this.raiseError(
"__HALT_COMPILER() can only be used from the outermost scope",
);
// fallback : returns a node but does not stop the parsing
let node = this.node("halt");
this.next().expect("(") && this.next();
this.expect(")") && this.next();
node = node(this.lexer._input.substring(this.lexer.offset));
this.expect(";") && this.next();
return node;
}
default:
return this.read_statement();
}
},
/*
* Reads statements
*/
read_statement: function () {
switch (this.token) {
case "{":
return this.read_code_block(false);
case this.tok.T_IF:
return this.read_if();
case this.tok.T_SWITCH:
return this.read_switch();
case this.tok.T_FOR:
return this.read_for();
case this.tok.T_FOREACH:
return this.read_foreach();
case this.tok.T_WHILE:
return this.read_while();
case this.tok.T_DO:
return this.read_do();
case this.tok.T_COMMENT:
return this.read_comment();
case this.tok.T_DOC_COMMENT:
return this.read_doc_comment();
case this.tok.T_RETURN: {
const result = this.node("return");
this.next();
const expr = this.read_optional_expr(";");
this.expectEndOfStatement();
return result(expr);
}
// https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L429
case this.tok.T_BREAK:
case this.tok.T_CONTINUE: {
const result = this.node(
this.token === this.tok.T_CONTINUE ? "continue" : "break",
);
this.next();
const level = this.read_optional_expr(";");
this.expectEndOfStatement();
return result(level);
}
case this.tok.T_GLOBAL: {
const result = this.node("global");
const items = this.next().read_list(this.read_simple_variable, ",");
this.expectEndOfStatement();
return result(items);
}
case this.tok.T_STATIC: {
const current = [this.token, this.lexer.getState()];
const result = this.node();
if (this.next().token === this.tok.T_DOUBLE_COLON) {
// static keyword for a class
this.lexer.tokens.push(current);
const expr = this.next().read_expr();
this.expectEndOfStatement(expr);
return result("expressionstatement", expr);
}
if (this.token === this.tok.T_FUNCTION) {
return this.read_function(true, [0, 1, 0]);
}
const items = this.read_variable_declarations();
this.expectEndOfStatement();
return result("static", items);
}
case this.tok.T_ECHO: {
const result = this.node("echo");
const text = this.text();
const shortForm = text === "<?=" || text === "<%=";
const expressions = this.next().read_function_list(this.read_expr, ",");
this.expectEndOfStatement();
return result(expressions, shortForm);
}
case this.tok.T_INLINE_HTML: {
const value = this.text();
let prevChar =
this.lexer.yylloc.first_offset > 0
? this.lexer._input[this.lexer.yylloc.first_offset - 1]
: null;
const fixFirstLine = prevChar === "\r" || prevChar === "\n";
// revert back the first stripped line
if (fixFirstLine) {
if (
prevChar === "\n" &&
this.lexer.yylloc.first_offset > 1 &&
this.lexer._input[this.lexer.yylloc.first_offset - 2] === "\r"
) {
prevChar = "\r\n";
}
}
const result = this.node("inline");
this.next();
return result(value, fixFirstLine ? prevChar + value : value);
}
case this.tok.T_UNSET: {
const result = this.node("unset");
this.next().expect("(") && this.next();
const variables = this.read_function_list(this.read_variable, ",");
this.expect(")") && this.next();
this.expect(";") && this.next();
return result(variables);
}
case this.tok.T_DECLARE: {
const result = this.node("declare");
const body = [];
let mode;
this.next().expect("(") && this.next();
const directives = this.read_declare_list();
this.expect(")") && this.next();
if (this.token === ":") {
this.next();
while (
this.token != this.EOF &&
this.token !== this.tok.T_ENDDECLARE
) {
// @todo : check declare_statement from php / not valid
body.push(this.read_top_statement());
}
if (
body.length === 0 &&
this.extractDoc &&
this._docs.length > this._docIndex
) {
body.push(this.node("noop")());
}
this.expect(this.tok.T_ENDDECLARE) && this.next();
this.expectEndOfStatement();
mode = this.ast.declare.MODE_SHORT;
} else if (this.token === "{") {
this.next();
while (this.token != this.EOF && this.token !== "}") {
// @todo : check declare_statement from php / not valid
body.push(this.read_top_statement());
}
if (
body.length === 0 &&
this.extractDoc &&
this._docs.length > this._docIndex
) {
body.push(this.node("noop")());
}
this.expect("}") && this.next();
mode = this.ast.declare.MODE_BLOCK;
} else {
this.expect(";") && this.next();
mode = this.ast.declare.MODE_NONE;
}
return result(directives, body, mode);
}
case this.tok.T_TRY:
return this.read_try();
case this.tok.T_THROW: {
const result = this.node("throw");
const expr = this.next().read_expr();
this.expectEndOfStatement();
return result(expr);
}
// ignore this (extra ponctuation)
case ";": {
this.next();
return null;
}
case this.tok.T_STRING: {
const result = this.node();
const current = [this.token, this.lexer.getState()];
const labelNameText = this.text();
let labelName = this.node("identifier");
// AST : https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L457
if (this.next().token === ":") {
labelName = labelName(labelNameText);
this.next();
return result("label", labelName);
} else {
labelName.destroy();
}
// default fallback expr / T_STRING '::' (etc...)
result.destroy();
this.lexer.tokens.push(current);
const statement = this.node("expressionstatement");
const expr = this.next().read_expr();
this.expectEndOfStatement(expr);
return statement(expr);
}
case this.tok.T_GOTO: {
const result = this.node("goto");
let labelName = null;
if (this.next().expect(this.tok.T_STRING)) {
labelName = this.node("identifier");
const name = this.text();
this.next();
labelName = labelName(name);
this.expectEndOfStatement();
}
return result(labelName);
}
default: {
// default fallback expr
const statement = this.node("expressionstatement");
const expr = this.read_expr();
this.expectEndOfStatement(expr);
return statement(expr);
}
}
},
/*
* ```ebnf
* code_block ::= '{' (inner_statements | top_statements) '}'
* ```
*/
read_code_block: function (top) {
const result = this.node("block");
this.expect("{") && this.next();
const body = top
? this.read_top_statements()
: this.read_inner_statements();
if (
body.length === 0 &&
this.extractDoc &&
this._docs.length > this._docIndex
) {
body.push(this.node("noop")());
}
this.expect("}") && this.next();
return result(null, body);
},
};