mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-12-01 09:06:22 +00:00
004ccfdb5c
This implements T230982#5475400, and it should speed up the CachingParser by roughly 40%. Bug: T230982 Change-Id: I803cc58637d50eb90e57decf243f5ca78075d63d
196 lines
6.7 KiB
PHP
196 lines
6.7 KiB
PHP
<?php
|
|
/**
|
|
* Represents a node of a parser tree.
|
|
*/
|
|
class AFPTreeNode {
|
|
// Each of the constants below represents a node corresponding to a level
|
|
// of the parser, from the top of the tree to the bottom.
|
|
|
|
// ENTRY is always one-element and thus does not have its own node.
|
|
|
|
// SEMICOLON is a many-children node, denoting that the nodes have to be
|
|
// evaluated in order and the last value has to be returned.
|
|
const SEMICOLON = 'SEMICOLON';
|
|
|
|
// ASSIGNMENT (formerly known as SET) is a node which is responsible for
|
|
// assigning values to variables. ASSIGNMENT is a (variable name [string],
|
|
// value [tree node]) tuple, INDEX_ASSIGNMENT (which is used to assign
|
|
// values at array offsets) is a (variable name [string], index [tree node],
|
|
// value [tree node]) tuple, and ARRAY_APPEND has the form of (variable name
|
|
// [string], value [tree node]).
|
|
const ASSIGNMENT = 'ASSIGNMENT';
|
|
const INDEX_ASSIGNMENT = 'INDEX_ASSIGNMENT';
|
|
const ARRAY_APPEND = 'ARRAY_APPEND';
|
|
|
|
// CONDITIONAL represents both a ternary operator and an if-then-else-end
|
|
// construct. The format is (condition, evaluated-if-true, evaluated-in-false).
|
|
// The first two are tree nodes, the last one can be a node, or null if there's no else.
|
|
const CONDITIONAL = 'CONDITIONAL';
|
|
|
|
// LOGIC is a logic operator accepted by AFPData::boolOp. The format is
|
|
// (operation, left operand, right operand).
|
|
const LOGIC = 'LOGIC';
|
|
|
|
// COMPARE is a comparison operator accepted by AFPData::boolOp. The format is
|
|
// (operation, left operand, right operand).
|
|
const COMPARE = 'COMPARE';
|
|
|
|
// SUM_REL is either '+' or '-'. The format is (operation, left operand,
|
|
// right operand).
|
|
const SUM_REL = 'SUM_REL';
|
|
|
|
// MUL_REL is a multiplication-related operation accepted by AFPData::mulRel.
|
|
// The format is (operation, left operand, right operand).
|
|
const MUL_REL = 'MUL_REL';
|
|
|
|
// POW is an exponentiation operator. The format is (base, exponent).
|
|
const POW = 'POW';
|
|
|
|
// BOOL_INVERT is a boolean inversion operator. The format is (operand).
|
|
const BOOL_INVERT = 'BOOL_INVERT';
|
|
|
|
// KEYWORD_OPERATOR is one of the binary keyword operators supported by the
|
|
// filter language. The format is (keyword, left operand, right operand).
|
|
const KEYWORD_OPERATOR = 'KEYWORD_OPERATOR';
|
|
|
|
// UNARY is either unary minus or unary plus. The format is (operator, operand).
|
|
const UNARY = 'UNARY';
|
|
|
|
// ARRAY_INDEX is an operation of accessing an array by an offset. The format
|
|
// is (array, offset).
|
|
const ARRAY_INDEX = 'ARRAY_INDEX';
|
|
|
|
// Since parenthesis only manipulate precedence of the operators, they are
|
|
// not explicitly represented in the tree.
|
|
|
|
// FUNCTION_CALL is an invocation of built-in function. The format is a
|
|
// tuple where the first element is a function name, and all subsequent
|
|
// elements are the arguments.
|
|
const FUNCTION_CALL = 'FUNCTION_CALL';
|
|
|
|
// ARRAY_DEFINITION is an array literal. The $children field contains tree
|
|
// nodes for the values of each of the array element used.
|
|
const ARRAY_DEFINITION = 'ARRAY_DEFINITION';
|
|
|
|
// ATOM is a node representing a literal. The only element of $children is a
|
|
// token corresponding to the literal.
|
|
const ATOM = 'ATOM';
|
|
|
|
/** @var string Type of the node, one of the constants above */
|
|
public $type;
|
|
/**
|
|
* Parameters of the value. Typically it is an array of children nodes,
|
|
* which might be either strings (for parametrization of the node) or another
|
|
* node. In case of ATOM it's a parser token.
|
|
* @var AFPTreeNode[]|string[]|AFPToken
|
|
*/
|
|
public $children;
|
|
|
|
/** @var int Position used for error reporting. */
|
|
public $position;
|
|
|
|
/**
|
|
* @var string[] Names of the variables assigned in this node or any of its descendants
|
|
* @todo We could change this to be an instance of a new AFPScope class (holding a var map)
|
|
* if we'll have the need to store other scope-specific data,
|
|
* see <https://phabricator.wikimedia.org/T230982#5475400>.
|
|
*/
|
|
private $innerAssignments = [];
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param AFPTreeNode[]|string[]|AFPToken $children
|
|
* @param int $position
|
|
*/
|
|
public function __construct( $type, $children, $position ) {
|
|
$this->type = $type;
|
|
$this->children = $children;
|
|
$this->position = $position;
|
|
$this->populateInnerAssignments();
|
|
}
|
|
|
|
/**
|
|
* Save in this node all the variable names used in the children, and in this node if it's an
|
|
* assignment-related node. Note that this doesn't check whether the variable is custom or builtin:
|
|
* this is already checked when calling setUserVariable.
|
|
* In case we'll ever need to store other data in a node, or maybe even a Scope object, this could
|
|
* be moved to a different class which could also re-visit the whole AST.
|
|
*/
|
|
private function populateInnerAssignments() {
|
|
if ( $this->type === self::ATOM ) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
$this->type === self::ASSIGNMENT ||
|
|
$this->type === self::INDEX_ASSIGNMENT ||
|
|
$this->type === self::ARRAY_APPEND
|
|
) {
|
|
$this->innerAssignments = [ $this->children[0] ];
|
|
} elseif (
|
|
$this->type === self::FUNCTION_CALL &&
|
|
in_array( $this->children[0], [ 'set', 'set_var' ] ) &&
|
|
// If unset, parsing will fail when checking arguments
|
|
isset( $this->children[1] )
|
|
) {
|
|
$varnameNode = $this->children[1];
|
|
if ( $varnameNode->type !== self::ATOM ) {
|
|
// Shouldn't happen since variable variables are not allowed
|
|
throw new AFPException( "Got non-atom type {$varnameNode->type} for set_var" );
|
|
}
|
|
$this->innerAssignments = [ $varnameNode->children->value ];
|
|
}
|
|
|
|
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach ATOM excluded above
|
|
foreach ( $this->children as $child ) {
|
|
if ( $child instanceof self ) {
|
|
$this->innerAssignments = array_merge( $this->innerAssignments, $child->getInnerAssignments() );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getInnerAssignments() : array {
|
|
return $this->innerAssignments;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function toDebugString() {
|
|
return implode( "\n", $this->toDebugStringInner() );
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
* @codeCoverageIgnore
|
|
*/
|
|
private function toDebugStringInner() {
|
|
if ( $this->type === self::ATOM ) {
|
|
return [ "ATOM({$this->children->type} {$this->children->value})" ];
|
|
}
|
|
|
|
$align = function ( $line ) {
|
|
return ' ' . $line;
|
|
};
|
|
|
|
$lines = [ "{$this->type}" ];
|
|
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach children is array here
|
|
foreach ( $this->children as $subnode ) {
|
|
if ( $subnode instanceof AFPTreeNode ) {
|
|
$sublines = array_map( $align, $subnode->toDebugStringInner() );
|
|
} elseif ( is_string( $subnode ) ) {
|
|
$sublines = [ " {$subnode}" ];
|
|
} else {
|
|
throw new AFPException( "Each node parameter has to be either a node or a string" );
|
|
}
|
|
|
|
$lines = array_merge( $lines, $sublines );
|
|
}
|
|
return $lines;
|
|
}
|
|
}
|