php-parser
Version:
Parse PHP code from JS and returns its AST
606 lines (568 loc) • 17.2 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 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);
},
};