2016-12-17 17:52:36 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* AbuseFilterCachingParser is the version of AbuseFilterParser which parses
|
|
|
|
* the code into an abstract syntax tree before evaluating it, and caches that
|
|
|
|
* tree.
|
|
|
|
*
|
|
|
|
* It currently inherits AbuseFilterParser in order to avoid code duplication.
|
|
|
|
* In future, this code will replace current AbuseFilterParser entirely.
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
*
|
|
|
|
* @todo Override checkSyntax and make it only try to build the AST. That would mean faster results,
|
|
|
|
* and no need to mess with DUNDEFINED and the like. However, we must first try to reduce the
|
|
|
|
* amount of runtime-only exceptions, and try to detect them in the AFPTreeParser instead.
|
|
|
|
* Otherwise, people may be able to save a broken filter without the syntax check reporting that.
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
|
|
|
class AbuseFilterCachingParser extends AbuseFilterParser {
|
2019-08-24 09:48:20 +00:00
|
|
|
const CACHE_VERSION = 1;
|
|
|
|
|
2016-12-17 17:52:36 +00:00
|
|
|
/**
|
|
|
|
* Return the generated version of the parser for cache invalidation
|
|
|
|
* purposes. Automatically tracks list of all functions and invalidates the
|
|
|
|
* cache if it is changed.
|
2017-10-06 18:52:31 +00:00
|
|
|
* @return string
|
2016-12-17 17:52:36 +00:00
|
|
|
*/
|
|
|
|
public static function getCacheVersion() {
|
|
|
|
static $version = null;
|
|
|
|
if ( $version !== null ) {
|
|
|
|
return $version;
|
|
|
|
}
|
|
|
|
|
|
|
|
$versionKey = [
|
2019-08-24 09:48:20 +00:00
|
|
|
self::CACHE_VERSION,
|
2016-12-17 17:52:36 +00:00
|
|
|
AFPTreeParser::CACHE_VERSION,
|
|
|
|
AbuseFilterTokenizer::CACHE_VERSION,
|
|
|
|
array_keys( AbuseFilterParser::$mFunctions ),
|
|
|
|
array_keys( AbuseFilterParser::$mKeywords ),
|
|
|
|
];
|
|
|
|
$version = hash( 'sha256', serialize( $versionKey ) );
|
|
|
|
|
|
|
|
return $version;
|
|
|
|
}
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
/**
|
|
|
|
* Resets the state of the parser
|
|
|
|
*/
|
2016-12-17 17:52:36 +00:00
|
|
|
public function resetState() {
|
2018-12-27 17:06:56 +00:00
|
|
|
$this->mVariables = new AbuseFilterVariableHolder;
|
2016-12-17 17:52:36 +00:00
|
|
|
$this->mCur = new AFPToken();
|
2019-01-24 10:33:01 +00:00
|
|
|
$this->mCondCount = 0;
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
/**
|
|
|
|
* @param string $code
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
2019-08-20 18:54:19 +00:00
|
|
|
public function intEval( $code ) : AFPData {
|
2019-08-24 09:48:20 +00:00
|
|
|
$tree = $this->getTree( $code );
|
|
|
|
$res = $this->evalTree( $tree );
|
|
|
|
|
|
|
|
if ( $res->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
$res = new AFPData( AFPData::DBOOL, false );
|
|
|
|
}
|
|
|
|
return $res;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $code
|
|
|
|
* @return AFPSyntaxTree
|
|
|
|
*/
|
|
|
|
private function getTree( $code ) : AFPSyntaxTree {
|
2016-12-17 17:52:36 +00:00
|
|
|
static $cache = null;
|
|
|
|
if ( !$cache ) {
|
|
|
|
$cache = ObjectCache::getLocalServerInstance( 'hash' );
|
|
|
|
}
|
|
|
|
|
2019-08-24 09:48:20 +00:00
|
|
|
return $cache->getWithSetCallback(
|
2016-12-17 17:52:36 +00:00
|
|
|
$cache->makeGlobalKey(
|
|
|
|
__CLASS__,
|
|
|
|
self::getCacheVersion(),
|
|
|
|
hash( 'sha256', $code )
|
|
|
|
),
|
|
|
|
$cache::TTL_DAY,
|
|
|
|
function () use ( $code ) {
|
|
|
|
$parser = new AFPTreeParser();
|
2019-08-13 16:03:13 +00:00
|
|
|
return $parser->parse( $code );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
);
|
2019-08-24 09:48:20 +00:00
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-08-24 09:48:20 +00:00
|
|
|
/**
|
|
|
|
* @param AFPSyntaxTree $tree
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
private function evalTree( AFPSyntaxTree $tree ) : AFPData {
|
|
|
|
$root = $tree->getRoot();
|
2019-08-12 09:18:15 +00:00
|
|
|
|
2019-08-24 09:48:20 +00:00
|
|
|
if ( !$root ) {
|
|
|
|
return new AFPData( AFPData::DNULL );
|
2019-08-12 09:18:15 +00:00
|
|
|
}
|
2019-08-24 09:48:20 +00:00
|
|
|
|
|
|
|
return $this->evalNode( $root );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Evaluate the value of the specified AST node.
|
|
|
|
*
|
|
|
|
* @param AFPTreeNode $node The node to evaluate.
|
2018-04-29 17:52:45 +00:00
|
|
|
* @return AFPData|AFPTreeNode|string
|
2016-12-17 17:52:36 +00:00
|
|
|
* @throws AFPException
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
* @throws MWException
|
|
|
|
*/
|
2019-08-24 09:48:20 +00:00
|
|
|
private function evalNode( AFPTreeNode $node ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
// A lot of AbuseFilterParser features rely on $this->mCur->pos or
|
|
|
|
// $this->mPos for error reporting.
|
2018-07-17 15:17:44 +00:00
|
|
|
// FIXME: this is a hack which needs to be removed when the parsers are merged.
|
2016-12-17 17:52:36 +00:00
|
|
|
$this->mPos = $node->position;
|
|
|
|
$this->mCur->pos = $node->position;
|
|
|
|
|
|
|
|
switch ( $node->type ) {
|
|
|
|
case AFPTreeNode::ATOM:
|
|
|
|
$tok = $node->children;
|
|
|
|
switch ( $tok->type ) {
|
|
|
|
case AFPToken::TID:
|
|
|
|
return $this->getVarValue( strtolower( $tok->value ) );
|
|
|
|
case AFPToken::TSTRING:
|
|
|
|
return new AFPData( AFPData::DSTRING, $tok->value );
|
|
|
|
case AFPToken::TFLOAT:
|
|
|
|
return new AFPData( AFPData::DFLOAT, $tok->value );
|
|
|
|
case AFPToken::TINT:
|
|
|
|
return new AFPData( AFPData::DINT, $tok->value );
|
|
|
|
/** @noinspection PhpMissingBreakStatementInspection */
|
|
|
|
case AFPToken::TKEYWORD:
|
|
|
|
switch ( $tok->value ) {
|
|
|
|
case "true":
|
|
|
|
return new AFPData( AFPData::DBOOL, true );
|
|
|
|
case "false":
|
|
|
|
return new AFPData( AFPData::DBOOL, false );
|
|
|
|
case "null":
|
2019-05-23 10:55:20 +00:00
|
|
|
return new AFPData( AFPData::DNULL );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
// Fallthrough intended
|
|
|
|
default:
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreStart
|
2016-12-17 17:52:36 +00:00
|
|
|
throw new AFPException( "Unknown token provided in the ATOM node" );
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreEnd
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
2018-04-16 15:37:10 +00:00
|
|
|
case AFPTreeNode::ARRAY_DEFINITION:
|
2016-12-17 17:52:36 +00:00
|
|
|
$items = array_map( [ $this, 'evalNode' ], $node->children );
|
2018-04-16 15:37:10 +00:00
|
|
|
return new AFPData( AFPData::DARRAY, $items );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::FUNCTION_CALL:
|
|
|
|
$functionName = $node->children[0];
|
|
|
|
$args = array_slice( $node->children, 1 );
|
|
|
|
|
|
|
|
$dataArgs = array_map( [ $this, 'evalNode' ], $args );
|
|
|
|
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
return $this->callFunc( $functionName, $dataArgs );
|
2018-04-16 15:37:10 +00:00
|
|
|
case AFPTreeNode::ARRAY_INDEX:
|
|
|
|
list( $array, $offset ) = $node->children;
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = $this->evalNode( $array );
|
2019-08-02 11:49:34 +00:00
|
|
|
|
2019-08-03 15:52:14 +00:00
|
|
|
if ( $array->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
return new AFPData( AFPData::DUNDEFINED );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
|
|
|
|
2019-01-24 10:10:22 +00:00
|
|
|
if ( $array->getType() !== AFPData::DARRAY ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
throw new AFPUserVisibleException( 'notarray', $node->position, [] );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$offset = $this->evalNode( $offset )->toInt();
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = $array->toArray();
|
|
|
|
if ( count( $array ) <= $offset ) {
|
2016-12-17 17:52:36 +00:00
|
|
|
throw new AFPUserVisibleException( 'outofbounds', $node->position,
|
2018-04-16 15:37:10 +00:00
|
|
|
[ $offset, count( $array ) ] );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
return $array[$offset];
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::UNARY:
|
|
|
|
list( $operation, $argument ) = $node->children;
|
|
|
|
$argument = $this->evalNode( $argument );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $operation === '-' ) {
|
2019-08-12 11:53:38 +00:00
|
|
|
return $argument->unaryMinus();
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
return $argument;
|
|
|
|
|
|
|
|
case AFPTreeNode::KEYWORD_OPERATOR:
|
|
|
|
list( $keyword, $leftOperand, $rightOperand ) = $node->children;
|
|
|
|
$func = self::$mKeywords[$keyword];
|
|
|
|
$leftOperand = $this->evalNode( $leftOperand );
|
|
|
|
$rightOperand = $this->evalNode( $rightOperand );
|
|
|
|
|
2019-08-02 11:49:34 +00:00
|
|
|
if (
|
2019-08-03 15:52:14 +00:00
|
|
|
$leftOperand->getType() === AFPData::DUNDEFINED ||
|
|
|
|
$rightOperand->getType() === AFPData::DUNDEFINED
|
2019-08-02 11:49:34 +00:00
|
|
|
) {
|
2019-08-03 15:52:14 +00:00
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
2019-08-02 11:49:34 +00:00
|
|
|
} else {
|
|
|
|
$this->raiseCondCount();
|
2019-01-24 10:33:01 +00:00
|
|
|
|
2019-08-02 11:49:34 +00:00
|
|
|
// @phan-suppress-next-line PhanParamTooMany Not every function needs the position
|
2019-08-12 12:23:46 +00:00
|
|
|
$result = $this->$func( $leftOperand, $rightOperand, $node->position );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
return $result;
|
|
|
|
case AFPTreeNode::BOOL_INVERT:
|
|
|
|
list( $argument ) = $node->children;
|
|
|
|
$argument = $this->evalNode( $argument );
|
2019-08-12 11:53:38 +00:00
|
|
|
return $argument->boolInvert();
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::POW:
|
|
|
|
list( $base, $exponent ) = $node->children;
|
|
|
|
$base = $this->evalNode( $base );
|
|
|
|
$exponent = $this->evalNode( $exponent );
|
2019-08-12 11:53:38 +00:00
|
|
|
return $base->pow( $exponent );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::MUL_REL:
|
|
|
|
list( $op, $leftOperand, $rightOperand ) = $node->children;
|
|
|
|
$leftOperand = $this->evalNode( $leftOperand );
|
|
|
|
$rightOperand = $this->evalNode( $rightOperand );
|
2019-08-12 12:40:51 +00:00
|
|
|
return $leftOperand->mulRel( $rightOperand, $op, $node->position );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::SUM_REL:
|
|
|
|
list( $op, $leftOperand, $rightOperand ) = $node->children;
|
|
|
|
$leftOperand = $this->evalNode( $leftOperand );
|
|
|
|
$rightOperand = $this->evalNode( $rightOperand );
|
|
|
|
switch ( $op ) {
|
|
|
|
case '+':
|
2019-08-12 11:53:38 +00:00
|
|
|
return $leftOperand->sum( $rightOperand );
|
2016-12-17 17:52:36 +00:00
|
|
|
case '-':
|
2019-08-12 11:53:38 +00:00
|
|
|
return $leftOperand->sub( $rightOperand );
|
2016-12-17 17:52:36 +00:00
|
|
|
default:
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreStart
|
2016-12-17 17:52:36 +00:00
|
|
|
throw new AFPException( "Unknown sum-related operator: {$op}" );
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreEnd
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case AFPTreeNode::COMPARE:
|
|
|
|
list( $op, $leftOperand, $rightOperand ) = $node->children;
|
|
|
|
$leftOperand = $this->evalNode( $leftOperand );
|
|
|
|
$rightOperand = $this->evalNode( $rightOperand );
|
2019-01-24 10:33:01 +00:00
|
|
|
$this->raiseCondCount();
|
2019-08-12 12:40:51 +00:00
|
|
|
return $leftOperand->compareOp( $rightOperand, $op );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::LOGIC:
|
|
|
|
list( $op, $leftOperand, $rightOperand ) = $node->children;
|
|
|
|
$leftOperand = $this->evalNode( $leftOperand );
|
2019-08-12 09:18:15 +00:00
|
|
|
$value = $leftOperand->getType() === AFPData::DUNDEFINED ? false : $leftOperand->toBool();
|
2016-12-17 17:52:36 +00:00
|
|
|
// Short-circuit.
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( ( !$value && $op === '&' ) || ( $value && $op === '|' ) ) {
|
2019-08-02 11:49:34 +00:00
|
|
|
if ( $rightOperand instanceof AFPTreeNode ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$this->discardWithHoisting( $rightOperand );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
$rightOperand = $this->evalNode( $rightOperand );
|
2019-08-12 12:40:51 +00:00
|
|
|
return $leftOperand->boolOp( $rightOperand, $op );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
|
|
|
case AFPTreeNode::CONDITIONAL:
|
|
|
|
list( $condition, $valueIfTrue, $valueIfFalse ) = $node->children;
|
|
|
|
$condition = $this->evalNode( $condition );
|
|
|
|
if ( $condition->toBool() ) {
|
2019-08-20 16:19:31 +00:00
|
|
|
if ( $valueIfFalse !== null ) {
|
|
|
|
$this->discardWithHoisting( $valueIfFalse );
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
return $this->evalNode( $valueIfTrue );
|
|
|
|
} else {
|
2019-08-19 16:28:57 +00:00
|
|
|
$this->discardWithHoisting( $valueIfTrue );
|
2019-08-20 16:19:31 +00:00
|
|
|
return $valueIfFalse !== null
|
|
|
|
? $this->evalNode( $valueIfFalse )
|
|
|
|
// We assume null as default if the else is missing
|
|
|
|
: new AFPData( AFPData::DNULL );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case AFPTreeNode::ASSIGNMENT:
|
|
|
|
list( $varName, $value ) = $node->children;
|
|
|
|
$value = $this->evalNode( $value );
|
|
|
|
$this->setUserVariable( $varName, $value );
|
|
|
|
return $value;
|
|
|
|
|
|
|
|
case AFPTreeNode::INDEX_ASSIGNMENT:
|
|
|
|
list( $varName, $offset, $value ) = $node->children;
|
|
|
|
|
2019-08-03 13:21:53 +00:00
|
|
|
if ( $this->isBuiltinVar( $varName ) ) {
|
|
|
|
throw new AFPUserVisibleException( 'overridebuiltin', $node->position, [ $varName ] );
|
|
|
|
} elseif ( !$this->mVariables->varIsSet( $varName ) ) {
|
2018-08-22 14:33:35 +00:00
|
|
|
throw new AFPUserVisibleException( 'unrecognisedvar', $node->position, [ $varName ] );
|
|
|
|
}
|
2018-12-27 17:06:56 +00:00
|
|
|
$array = $this->mVariables->getVar( $varName );
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-08-20 18:54:19 +00:00
|
|
|
$value = $this->evalNode( $value );
|
2019-08-03 15:52:14 +00:00
|
|
|
if ( $array->getType() !== AFPData::DUNDEFINED ) {
|
|
|
|
// If it's a DUNDEFINED, leave it as is
|
2019-08-02 11:49:34 +00:00
|
|
|
if ( $array->getType() !== AFPData::DARRAY ) {
|
|
|
|
throw new AFPUserVisibleException( 'notarray', $node->position, [] );
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-08-02 11:49:34 +00:00
|
|
|
$offset = $this->evalNode( $offset )->toInt();
|
|
|
|
|
|
|
|
$array = $array->toArray();
|
|
|
|
if ( count( $array ) <= $offset ) {
|
|
|
|
throw new AFPUserVisibleException( 'outofbounds', $node->position,
|
|
|
|
[ $offset, count( $array ) ] );
|
|
|
|
}
|
|
|
|
|
2019-08-20 18:54:19 +00:00
|
|
|
$array[$offset] = $value;
|
2019-08-02 11:49:34 +00:00
|
|
|
$this->setUserVariable( $varName, new AFPData( AFPData::DARRAY, $array ) );
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
case AFPTreeNode::ARRAY_APPEND:
|
2016-12-17 17:52:36 +00:00
|
|
|
list( $varName, $value ) = $node->children;
|
|
|
|
|
2019-08-03 13:21:53 +00:00
|
|
|
if ( $this->isBuiltinVar( $varName ) ) {
|
|
|
|
throw new AFPUserVisibleException( 'overridebuiltin', $node->position, [ $varName ] );
|
|
|
|
} elseif ( !$this->mVariables->varIsSet( $varName ) ) {
|
|
|
|
throw new AFPUserVisibleException( 'unrecognisedvar', $node->position, [ $varName ] );
|
|
|
|
}
|
|
|
|
|
2018-12-27 17:06:56 +00:00
|
|
|
$array = $this->mVariables->getVar( $varName );
|
2019-08-03 15:52:14 +00:00
|
|
|
if ( $array->getType() !== AFPData::DUNDEFINED ) {
|
|
|
|
// If it's a DUNDEFINED, leave it as is
|
2019-08-02 11:49:34 +00:00
|
|
|
if ( $array->getType() !== AFPData::DARRAY ) {
|
|
|
|
throw new AFPUserVisibleException( 'notarray', $node->position, [] );
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
|
2019-08-02 11:49:34 +00:00
|
|
|
$array = $array->toArray();
|
|
|
|
$array[] = $this->evalNode( $value );
|
|
|
|
$this->setUserVariable( $varName, new AFPData( AFPData::DARRAY, $array ) );
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
return $value;
|
|
|
|
|
|
|
|
case AFPTreeNode::SEMICOLON:
|
|
|
|
$lastValue = null;
|
2019-03-02 09:26:14 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach children is array here
|
2016-12-17 17:52:36 +00:00
|
|
|
foreach ( $node->children as $statement ) {
|
|
|
|
$lastValue = $this->evalNode( $statement );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $lastValue;
|
|
|
|
default:
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreStart
|
2016-12-17 17:52:36 +00:00
|
|
|
throw new AFPException( "Unknown node type passed: {$node->type}" );
|
2018-08-22 14:33:35 +00:00
|
|
|
// @codeCoverageIgnoreEnd
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-02 11:49:34 +00:00
|
|
|
|
|
|
|
/**
|
2019-08-19 16:28:57 +00:00
|
|
|
* Intended to be used for short-circuit as a solution for T214674.
|
|
|
|
* Given a node, check it and its children; if there are assignments of non-existing variables,
|
|
|
|
* hoist them. In case of index assignment or array append, the old value is always erased and
|
|
|
|
* overwritten with a DUNDEFINED. This is used to allow stuff like:
|
|
|
|
* false & ( var := 'foo' ); var == 2
|
|
|
|
* or
|
|
|
|
* if ( false ) then ( var := 'foo' ) else ( 1 ) end; var == 2
|
|
|
|
* where `false` is something evaluated as false at runtime.
|
|
|
|
* In the future, we may decide to always throw in those cases (for stability and parser speed),
|
|
|
|
* but that's not good, at least as long as people don't have an on-wiki way to see if filters
|
|
|
|
* are failing at runtime.
|
|
|
|
* @todo Decide about that.
|
2019-08-02 11:49:34 +00:00
|
|
|
*
|
|
|
|
* @param AFPTreeNode $node
|
|
|
|
*/
|
2019-08-19 16:28:57 +00:00
|
|
|
private function discardWithHoisting( AFPTreeNode $node ) {
|
|
|
|
if (
|
|
|
|
$node->type === AFPTreeNode::ASSIGNMENT &&
|
|
|
|
!$this->mVariables->varIsSet( $node->children[0] )
|
|
|
|
) {
|
2019-08-03 15:52:14 +00:00
|
|
|
$this->setUserVariable( $node->children[0], new AFPData( AFPData::DUNDEFINED ) );
|
2019-08-02 11:49:34 +00:00
|
|
|
} elseif (
|
|
|
|
$node->type === AFPTreeNode::INDEX_ASSIGNMENT ||
|
|
|
|
$node->type === AFPTreeNode::ARRAY_APPEND
|
|
|
|
) {
|
|
|
|
$varName = $node->children[0];
|
|
|
|
if ( !$this->mVariables->varIsSet( $varName ) ) {
|
|
|
|
throw new AFPUserVisibleException( 'unrecognisedvar', $node->position, [ $varName ] );
|
|
|
|
}
|
2019-08-03 15:52:14 +00:00
|
|
|
$this->setUserVariable( $varName, new AFPData( AFPData::DUNDEFINED ) );
|
2019-08-06 12:14:55 +00:00
|
|
|
} elseif (
|
|
|
|
$node->type === AFPTreeNode::FUNCTION_CALL &&
|
|
|
|
in_array( $node->children[0], [ 'set', 'set_var' ] ) &&
|
|
|
|
isset( $node->children[1] )
|
|
|
|
) {
|
|
|
|
$varnameNode = $node->children[1];
|
2019-08-19 16:28:57 +00:00
|
|
|
if ( $varnameNode->type !== AFPTreeNode::ATOM ) {
|
|
|
|
// Shouldn't happen since variable variables are not allowed
|
|
|
|
throw new AFPException( "Got non-atom type {$varnameNode->type} for set_var" );
|
|
|
|
}
|
|
|
|
$varname = $varnameNode->children->value;
|
|
|
|
if ( !$this->mVariables->varIsSet( $varname ) ) {
|
|
|
|
$this->setUserVariable( $varname, new AFPData( AFPData::DUNDEFINED ) );
|
2019-08-06 12:14:55 +00:00
|
|
|
}
|
2019-08-02 11:49:34 +00:00
|
|
|
} elseif ( $node->type === AFPTreeNode::ATOM ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach ATOM case excluded above
|
|
|
|
foreach ( $node->children as $child ) {
|
|
|
|
if ( $child instanceof AFPTreeNode ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$this->discardWithHoisting( $child );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-12-17 17:52:36 +00:00
|
|
|
}
|