2016-08-24 04:52:58 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A version of the abuse filter parser that separates parsing the filter and
|
|
|
|
* evaluating it into different passes, allowing the parse tree to be cached.
|
|
|
|
*
|
|
|
|
* @file
|
2019-10-09 10:37:38 +00:00
|
|
|
* @phan-file-suppress PhanPossiblyInfiniteRecursionSameParams Recursion controlled by class props
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
|
2020-12-03 22:22:43 +00:00
|
|
|
namespace MediaWiki\Extension\AbuseFilter\Parser;
|
|
|
|
|
|
|
|
use IBufferingStatsdDataFactory;
|
2020-10-03 13:01:53 +00:00
|
|
|
use InvalidArgumentException;
|
2020-01-15 16:08:53 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\KeywordsManager;
|
2021-08-29 22:58:17 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException;
|
2019-08-21 10:04:10 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2019-08-21 09:01:50 +00:00
|
|
|
|
2016-08-24 04:52:58 +00:00
|
|
|
/**
|
|
|
|
* A parser that transforms the text of the filter into a parse tree.
|
|
|
|
*/
|
2020-10-03 13:01:53 +00:00
|
|
|
class AFPTreeParser {
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @var array[] Contains the AFPTokens for the code being parsed
|
2021-09-01 18:29:19 +00:00
|
|
|
* @phan-var array<int,array{0:AFPToken,1:int}>
|
2019-08-21 09:01:50 +00:00
|
|
|
*/
|
2016-08-24 04:52:58 +00:00
|
|
|
public $mTokens;
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @var AFPToken The current token
|
|
|
|
*/
|
|
|
|
public $mCur;
|
2020-10-03 13:01:53 +00:00
|
|
|
/** @var int The position of the current token */
|
|
|
|
private $mPos;
|
|
|
|
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @var string|null The ID of the filter being parsed, if available. Can also be "global-$ID"
|
|
|
|
*/
|
|
|
|
protected $mFilter;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2020-01-21 11:13:11 +00:00
|
|
|
public const CACHE_VERSION = 2;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
/**
|
|
|
|
* @var LoggerInterface Used for debugging
|
|
|
|
*/
|
|
|
|
protected $logger;
|
|
|
|
|
2019-09-16 16:53:36 +00:00
|
|
|
/**
|
|
|
|
* @var IBufferingStatsdDataFactory
|
|
|
|
*/
|
|
|
|
protected $statsd;
|
|
|
|
|
2020-01-15 16:08:53 +00:00
|
|
|
/** @var KeywordsManager */
|
|
|
|
protected $keywordsManager;
|
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
/**
|
|
|
|
* @param LoggerInterface $logger Used for debugging
|
2019-09-16 16:53:36 +00:00
|
|
|
* @param IBufferingStatsdDataFactory $statsd
|
2020-01-15 16:08:53 +00:00
|
|
|
* @param KeywordsManager $keywordsManager
|
2019-08-21 10:04:10 +00:00
|
|
|
*/
|
2019-09-16 16:53:36 +00:00
|
|
|
public function __construct(
|
|
|
|
LoggerInterface $logger,
|
2020-01-15 16:08:53 +00:00
|
|
|
IBufferingStatsdDataFactory $statsd,
|
|
|
|
KeywordsManager $keywordsManager
|
2019-09-16 16:53:36 +00:00
|
|
|
) {
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->logger = $logger;
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd = $statsd;
|
2020-01-15 16:08:53 +00:00
|
|
|
$this->keywordsManager = $keywordsManager;
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->resetState();
|
|
|
|
}
|
|
|
|
|
2019-08-21 09:01:50 +00:00
|
|
|
/**
|
|
|
|
* @param string $filter
|
|
|
|
*/
|
|
|
|
public function setFilter( $filter ) {
|
|
|
|
$this->mFilter = $filter;
|
|
|
|
}
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
/**
|
|
|
|
* Resets the state
|
|
|
|
*/
|
2021-09-01 18:29:19 +00:00
|
|
|
private function resetState() {
|
2017-06-15 14:23:34 +00:00
|
|
|
$this->mTokens = [];
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos = 0;
|
2019-08-21 09:01:50 +00:00
|
|
|
$this->mFilter = null;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Advances the parser to the next token in the filter code.
|
|
|
|
*/
|
|
|
|
protected function move() {
|
|
|
|
list( $this->mCur, $this->mPos ) = $this->mTokens[$this->mPos];
|
|
|
|
}
|
|
|
|
|
2019-08-06 18:59:45 +00:00
|
|
|
/**
|
|
|
|
* Get the next token. This is similar to move() but doesn't change class members,
|
|
|
|
* allowing to look ahead without rolling back the state.
|
|
|
|
*
|
|
|
|
* @return AFPToken
|
|
|
|
*/
|
|
|
|
protected function getNextToken() {
|
|
|
|
return $this->mTokens[$this->mPos][0];
|
|
|
|
}
|
|
|
|
|
2016-08-24 04:52:58 +00:00
|
|
|
/**
|
|
|
|
* getState() function allows parser state to be rollbacked to several tokens
|
|
|
|
* back.
|
|
|
|
*
|
|
|
|
* @return AFPParserState
|
|
|
|
*/
|
|
|
|
protected function getState() {
|
|
|
|
return new AFPParserState( $this->mCur, $this->mPos );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* setState() function allows parser state to be rollbacked to several tokens
|
|
|
|
* back.
|
|
|
|
*
|
|
|
|
* @param AFPParserState $state
|
|
|
|
*/
|
|
|
|
protected function setState( AFPParserState $state ) {
|
|
|
|
$this->mCur = $state->token;
|
|
|
|
$this->mPos = $state->pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse the supplied filter source code into a tree.
|
|
|
|
*
|
2021-09-01 18:29:19 +00:00
|
|
|
* @param array[] $tokens
|
|
|
|
* @phan-param array<int,array{0:AFPToken,1:int}> $tokens
|
2019-08-24 09:48:20 +00:00
|
|
|
* @return AFPSyntaxTree
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
2021-09-01 18:29:19 +00:00
|
|
|
public function parse( array $tokens ): AFPSyntaxTree {
|
|
|
|
$this->mTokens = $tokens;
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos = 0;
|
|
|
|
|
2019-08-24 09:48:20 +00:00
|
|
|
return $this->buildSyntaxTree();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return AFPSyntaxTree
|
|
|
|
*/
|
2021-09-01 18:29:19 +00:00
|
|
|
private function buildSyntaxTree(): AFPSyntaxTree {
|
2019-09-16 16:53:36 +00:00
|
|
|
$startTime = microtime( true );
|
2019-08-24 09:48:20 +00:00
|
|
|
$root = $this->doLevelEntry();
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd->timing( 'abusefilter_cachingParser_buildtree', microtime( true ) - $startTime );
|
2019-09-09 17:20:47 +00:00
|
|
|
return new AFPSyntaxTree( $root );
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Levels */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles unexpected characters after the expression.
|
2019-08-24 09:48:20 +00:00
|
|
|
* @return AFPTreeNode|null Null only if no statements
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelEntry() {
|
|
|
|
$result = $this->doLevelSemicolon();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TNONE ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2016-08-24 04:52:58 +00:00
|
|
|
'unexpectedatend',
|
|
|
|
$this->mPos, [ $this->mCur->type ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles the semicolon operator.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode|null
|
|
|
|
*/
|
|
|
|
protected function doLevelSemicolon() {
|
|
|
|
$statements = [];
|
|
|
|
|
|
|
|
do {
|
|
|
|
$this->move();
|
|
|
|
$position = $this->mPos;
|
|
|
|
|
2019-08-12 16:16:32 +00:00
|
|
|
if (
|
|
|
|
$this->mCur->type === AFPToken::TNONE ||
|
2018-08-22 14:33:35 +00:00
|
|
|
( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value == ')' )
|
|
|
|
) {
|
2019-08-12 16:16:32 +00:00
|
|
|
// Handle special cases which the other parser handled in doLevelAtom
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allow empty statements.
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$statements[] = $this->doLevelSet();
|
|
|
|
$position = $this->mPos;
|
2018-08-26 08:34:42 +00:00
|
|
|
} while ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR );
|
2016-08-24 04:52:58 +00:00
|
|
|
|
|
|
|
// Flatten the tree if possible.
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( count( $statements ) === 0 ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
return null;
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( count( $statements ) === 1 ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
return $statements[0];
|
|
|
|
} else {
|
|
|
|
return new AFPTreeNode( AFPTreeNode::SEMICOLON, $statements, $position );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles variable assignment.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelSet() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID ) {
|
2019-12-07 17:20:10 +00:00
|
|
|
$varname = (string)$this->mCur->value;
|
2016-08-24 04:52:58 +00:00
|
|
|
|
|
|
|
// Speculatively parse the assignment statement assuming it can
|
|
|
|
// potentially be an assignment, but roll back if it isn't.
|
2019-09-09 17:20:47 +00:00
|
|
|
// @todo Use $this->getNextToken for clearer code
|
2016-08-24 04:52:58 +00:00
|
|
|
$initialState = $this->getState();
|
|
|
|
$this->move();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$value = $this->doLevelSet();
|
|
|
|
|
|
|
|
return new AFPTreeNode( AFPTreeNode::ASSIGNMENT, [ $varname, $value ], $position );
|
|
|
|
}
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->move();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$index = 'append';
|
|
|
|
} else {
|
|
|
|
// Parse index offset.
|
|
|
|
$this->setState( $initialState );
|
|
|
|
$this->move();
|
|
|
|
$index = $this->doLevelSemicolon();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound', $this->mPos,
|
2016-08-24 04:52:58 +00:00
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$value = $this->doLevelSet();
|
|
|
|
if ( $index === 'append' ) {
|
|
|
|
return new AFPTreeNode(
|
2018-04-16 15:37:10 +00:00
|
|
|
AFPTreeNode::ARRAY_APPEND, [ $varname, $value ], $position );
|
2016-08-24 04:52:58 +00:00
|
|
|
} else {
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::INDEX_ASSIGNMENT,
|
|
|
|
[ $varname, $index, $value ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we reached this point, we did not find an assignment. Roll back
|
|
|
|
// and assume this was just a literal.
|
|
|
|
$this->setState( $initialState );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelConditions();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles ternary operator and if-then-else-end.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelConditions() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'if' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$condition = $this->doLevelBoolOps();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'then' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound',
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
'then',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfTrue = $this->doLevelConditions();
|
|
|
|
|
2019-08-20 16:19:31 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'else' ) {
|
|
|
|
$this->move();
|
|
|
|
$valueIfFalse = $this->doLevelConditions();
|
|
|
|
} else {
|
|
|
|
$valueIfFalse = null;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'end' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound',
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
'end',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::CONDITIONAL,
|
|
|
|
[ $condition, $valueIfTrue, $valueIfFalse ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$condition = $this->doLevelBoolOps();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '?' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfTrue = $this->doLevelConditions();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound',
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
':',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
$valueIfFalse = $this->doLevelConditions();
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::CONDITIONAL,
|
|
|
|
[ $condition, $valueIfTrue, $valueIfFalse ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $condition;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles logic operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelBoolOps() {
|
|
|
|
$leftOperand = $this->doLevelCompares();
|
|
|
|
$ops = [ '&', '|', '^' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
|
2016-11-07 01:28:49 +00:00
|
|
|
$rightOperand = $this->doLevelCompares();
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2016-11-07 01:28:49 +00:00
|
|
|
$leftOperand = new AFPTreeNode(
|
2016-08-24 04:52:58 +00:00
|
|
|
AFPTreeNode::LOGIC,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles comparison operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelCompares() {
|
|
|
|
$leftOperand = $this->doLevelSumRels();
|
2019-03-21 15:48:35 +00:00
|
|
|
$equalityOps = [ '==', '===', '!=', '!==', '=' ];
|
|
|
|
$orderOps = [ '<', '>', '<=', '>=' ];
|
|
|
|
// Only allow either a single operation, or a combination of a single equalityOps and a single
|
|
|
|
// orderOps. This resembles what PHP does, and allows `a < b == c` while rejecting `a < b < c`
|
|
|
|
$allowedOps = array_merge( $equalityOps, $orderOps );
|
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $allowedOps ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
2019-03-21 15:48:35 +00:00
|
|
|
$allowedOps = in_array( $op, $equalityOps ) ?
|
|
|
|
array_diff( $allowedOps, $equalityOps ) :
|
|
|
|
array_diff( $allowedOps, $orderOps );
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelSumRels();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::COMPARE,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle addition and subtraction.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelSumRels() {
|
|
|
|
$leftOperand = $this->doLevelMulRels();
|
|
|
|
$ops = [ '+', '-' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelMulRels();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::SUM_REL,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles multiplication and division.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelMulRels() {
|
|
|
|
$leftOperand = $this->doLevelPow();
|
|
|
|
$ops = [ '*', '/', '%' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelPow();
|
|
|
|
$leftOperand = new AFPTreeNode(
|
|
|
|
AFPTreeNode::MUL_REL,
|
|
|
|
[ $op, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles exponentiation.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelPow() {
|
|
|
|
$base = $this->doLevelBoolInvert();
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '**' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$exponent = $this->doLevelBoolInvert();
|
|
|
|
$base = new AFPTreeNode( AFPTreeNode::POW, [ $base, $exponent ], $position );
|
|
|
|
}
|
|
|
|
return $base;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles boolean inversion.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelBoolInvert() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '!' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$argument = $this->doLevelKeywordOperators();
|
|
|
|
return new AFPTreeNode( AFPTreeNode::BOOL_INVERT, [ $argument ], $position );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelKeywordOperators();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles keyword operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelKeywordOperators() {
|
|
|
|
$leftOperand = $this->doLevelUnarys();
|
|
|
|
$keyword = strtolower( $this->mCur->value );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD &&
|
2021-08-31 22:26:13 +00:00
|
|
|
isset( FilterEvaluator::KEYWORDS[$keyword] )
|
2016-08-24 04:52:58 +00:00
|
|
|
) {
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
$rightOperand = $this->doLevelUnarys();
|
|
|
|
|
|
|
|
return new AFPTreeNode(
|
|
|
|
AFPTreeNode::KEYWORD_OPERATOR,
|
|
|
|
[ $keyword, $leftOperand, $rightOperand ],
|
|
|
|
$position
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $leftOperand;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles unary operators.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
|
|
|
*/
|
|
|
|
protected function doLevelUnarys() {
|
|
|
|
$op = $this->mCur->value;
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && ( $op === "+" || $op === "-" ) ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
2018-04-16 15:37:10 +00:00
|
|
|
$argument = $this->doLevelArrayElements();
|
2016-08-24 04:52:58 +00:00
|
|
|
return new AFPTreeNode( AFPTreeNode::UNARY, [ $op, $argument ], $position );
|
|
|
|
}
|
2018-04-16 15:37:10 +00:00
|
|
|
return $this->doLevelArrayElements();
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-16 15:37:10 +00:00
|
|
|
* Handles accessing an array element by an offset.
|
2016-08-24 04:52:58 +00:00
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
2018-04-16 15:37:10 +00:00
|
|
|
protected function doLevelArrayElements() {
|
|
|
|
$array = $this->doLevelParenthesis();
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
$position = $this->mPos;
|
|
|
|
$index = $this->doLevelSemicolon();
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = new AFPTreeNode( AFPTreeNode::ARRAY_INDEX, [ $array, $index ], $position );
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound', $this->mPos,
|
2016-08-24 04:52:58 +00:00
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
return $array;
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles parenthesis.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelParenthesis() {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === '(' ) {
|
2019-09-09 12:12:39 +00:00
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type === AFPToken::TBRACE && $next->value === ')' ) {
|
|
|
|
// Empty parentheses are never allowed
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2019-09-09 12:12:39 +00:00
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
2016-08-24 04:52:58 +00:00
|
|
|
$result = $this->doLevelSemicolon();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === ')' ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2016-08-24 04:52:58 +00:00
|
|
|
'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[ ')', $this->mCur->type, $this->mCur->value ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelFunction();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles function calls.
|
|
|
|
*
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelFunction() {
|
Add a static analyzer for the filter language
This commit adds a class AFPSyntaxChecker which can statically analyze
a filter code to detect the following errors:
- unbound variables (which comes in two modes: conservative and liberal,
default to conservative)
- unused variables (disabled by default for compatibilty)
- assignment on built-in identifiers
- function application's arity mismatch
- function application's invalid function name
- non-string literal in the first argument of set / set_var
The existing parser and evaluator are modified as follows:
- The new (caching) evaluator no longer needs to perform variable
hoisting at runtime.
- Note that for array assignment, this changes the semantics.
- The new parser is more lenient, reducing parsing errors.
The static analyzer will catch these errors instead, allowing us
to give a much better error message and reduces the complexity of
the parser.
* The parser now allows function name to be any identifier.
* The parser now allows arity mismatch to occur.
* The parser now allows the first argument of set to be any expression.
Concretely, obvious changes that users will see are:
1. a := [1]; false & (a[] := 2); a[0] === 1
would evaluate to true, while it used to evaluate to the undefined value
due to hoisting
2. f(1)
will now error with 'f is not a valid function' as opposed to
'Unexpected "T_BRACE"'
3. length
will now error with 'Illegal use of built-in identifier "length"'
as opposed to 'Expected a ('
Appendix: conservative and liberal mode
The conservative mode is completely compatible with the current evaluator.
That is,
false & (a := 1); a
will not deem `a` as unbound, though this is actually undesirable because
`a` would then be bound to the troublesome undefined value.
The liberal mode rejects the above pattern by deeming `a` as unbound.
However, it also rejects
true & (a := 1); a
even though (a := 1) is always executed. Since there are several filters
in Wikimedia projects that rely on this behavior, we default the mode
to conservative for now.
Note that even the liberal mode doesn't really respect lexical scope
appeared in some other programming languages (see also T234690).
For instance:
(if true then (a := 1) else (a := 2) end); a
would be accepted by the liberal checker, even though under lexical scope,
`a` would be unbound. However, it is unlikely that lexical scope
will be suitable for the filter language, as most filters in
Wikimedia projects that have user-defined variable do violate lexical scope.
Bug: T260903
Bug: T238709
Bug: T237610
Bug: T234690
Bug: T231536
Change-Id: Ic6d030503e554933f8d220c6f87b680505918ae2
2019-11-20 07:09:41 +00:00
|
|
|
$next = $this->getNextToken();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID &&
|
Add a static analyzer for the filter language
This commit adds a class AFPSyntaxChecker which can statically analyze
a filter code to detect the following errors:
- unbound variables (which comes in two modes: conservative and liberal,
default to conservative)
- unused variables (disabled by default for compatibilty)
- assignment on built-in identifiers
- function application's arity mismatch
- function application's invalid function name
- non-string literal in the first argument of set / set_var
The existing parser and evaluator are modified as follows:
- The new (caching) evaluator no longer needs to perform variable
hoisting at runtime.
- Note that for array assignment, this changes the semantics.
- The new parser is more lenient, reducing parsing errors.
The static analyzer will catch these errors instead, allowing us
to give a much better error message and reduces the complexity of
the parser.
* The parser now allows function name to be any identifier.
* The parser now allows arity mismatch to occur.
* The parser now allows the first argument of set to be any expression.
Concretely, obvious changes that users will see are:
1. a := [1]; false & (a[] := 2); a[0] === 1
would evaluate to true, while it used to evaluate to the undefined value
due to hoisting
2. f(1)
will now error with 'f is not a valid function' as opposed to
'Unexpected "T_BRACE"'
3. length
will now error with 'Illegal use of built-in identifier "length"'
as opposed to 'Expected a ('
Appendix: conservative and liberal mode
The conservative mode is completely compatible with the current evaluator.
That is,
false & (a := 1); a
will not deem `a` as unbound, though this is actually undesirable because
`a` would then be bound to the troublesome undefined value.
The liberal mode rejects the above pattern by deeming `a` as unbound.
However, it also rejects
true & (a := 1); a
even though (a := 1) is always executed. Since there are several filters
in Wikimedia projects that rely on this behavior, we default the mode
to conservative for now.
Note that even the liberal mode doesn't really respect lexical scope
appeared in some other programming languages (see also T234690).
For instance:
(if true then (a := 1) else (a := 2) end); a
would be accepted by the liberal checker, even though under lexical scope,
`a` would be unbound. However, it is unlikely that lexical scope
will be suitable for the filter language, as most filters in
Wikimedia projects that have user-defined variable do violate lexical scope.
Bug: T260903
Bug: T238709
Bug: T237610
Bug: T234690
Bug: T231536
Change-Id: Ic6d030503e554933f8d220c6f87b680505918ae2
2019-11-20 07:09:41 +00:00
|
|
|
$next->type === AFPToken::TBRACE &&
|
|
|
|
$next->value === '('
|
2016-08-24 04:52:58 +00:00
|
|
|
) {
|
|
|
|
$func = $this->mCur->value;
|
|
|
|
$position = $this->mPos;
|
|
|
|
$this->move();
|
|
|
|
|
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
|
|
|
$args = [];
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type !== AFPToken::TBRACE || $next->value !== ')' ) {
|
|
|
|
do {
|
2019-09-06 16:28:53 +00:00
|
|
|
$thisArg = $this->doLevelSemicolon();
|
2019-10-05 17:22:28 +00:00
|
|
|
if ( $thisArg !== null ) {
|
|
|
|
$args[] = $thisArg;
|
|
|
|
} elseif ( !$this->functionIsVariadic( $func ) ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2019-09-06 16:28:53 +00:00
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
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
|
|
|
} while ( $this->mCur->type === AFPToken::TCOMMA );
|
|
|
|
} else {
|
|
|
|
$this->move();
|
|
|
|
}
|
2019-09-06 16:28:53 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== ')' ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException( 'expectednotfound',
|
2016-08-24 04:52:58 +00:00
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
')',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
|
|
|
|
array_unshift( $args, $func );
|
|
|
|
return new AFPTreeNode( AFPTreeNode::FUNCTION_CALL, $args, $position );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->doLevelAtom();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle literals.
|
|
|
|
* @return AFPTreeNode
|
2021-08-29 22:58:17 +00:00
|
|
|
* @throws UserVisibleException
|
2016-08-24 04:52:58 +00:00
|
|
|
*/
|
|
|
|
protected function doLevelAtom() {
|
|
|
|
$tok = $this->mCur->value;
|
|
|
|
switch ( $this->mCur->type ) {
|
|
|
|
case AFPToken::TID:
|
2019-10-02 11:24:48 +00:00
|
|
|
$this->checkLogDeprecatedVar( strtolower( $tok ) );
|
|
|
|
// Fallthrough intended
|
2016-08-24 04:52:58 +00:00
|
|
|
case AFPToken::TSTRING:
|
|
|
|
case AFPToken::TFLOAT:
|
|
|
|
case AFPToken::TINT:
|
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos );
|
|
|
|
break;
|
|
|
|
case AFPToken::TKEYWORD:
|
|
|
|
if ( in_array( $tok, [ "true", "false", "null" ] ) ) {
|
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2016-08-24 04:52:58 +00:00
|
|
|
'unrecognisedkeyword',
|
|
|
|
$this->mPos,
|
|
|
|
[ $tok ]
|
|
|
|
);
|
|
|
|
/** @noinspection PhpMissingBreakStatementInspection */
|
|
|
|
case AFPToken::TSQUAREBRACKET:
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->value === '[' ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = [];
|
2016-08-24 04:52:58 +00:00
|
|
|
while ( true ) {
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$array[] = $this->doLevelSet();
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TCOMMA ) {
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2016-08-24 04:52:58 +00:00
|
|
|
'expectednotfound',
|
|
|
|
$this->mPos,
|
|
|
|
[ ', or ]', $this->mCur->type, $this->mCur->value ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-16 15:37:10 +00:00
|
|
|
$result = new AFPTreeNode( AFPTreeNode::ARRAY_DEFINITION, $array, $this->mPos );
|
2016-08-24 04:52:58 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallthrough expected
|
|
|
|
default:
|
2021-08-29 22:58:17 +00:00
|
|
|
throw new UserVisibleException(
|
2016-08-24 04:52:58 +00:00
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mPos,
|
|
|
|
[
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->move();
|
2020-01-28 17:43:26 +00:00
|
|
|
// @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
|
2019-12-07 17:20:10 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeMismatchReturnNullable Until phan can understand the switch
|
2016-08-24 04:52:58 +00:00
|
|
|
return $result;
|
|
|
|
}
|
2019-10-02 11:24:48 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a variable name, check if the variable is deprecated. If it is, log the use.
|
|
|
|
* Do that here, and not every time the AST is eval'ed. This means less logging, but more
|
|
|
|
* performance.
|
|
|
|
* @param string $varname
|
|
|
|
*/
|
|
|
|
protected function checkLogDeprecatedVar( $varname ) {
|
2020-01-15 16:08:53 +00:00
|
|
|
if ( $this->keywordsManager->isVarDeprecated( $varname ) ) {
|
2019-10-02 11:24:48 +00:00
|
|
|
$this->logger->debug( "Deprecated variable $varname used in filter {$this->mFilter}." );
|
|
|
|
}
|
|
|
|
}
|
2020-10-03 13:01:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $fname
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function functionIsVariadic( string $fname ): bool {
|
2021-08-31 22:26:13 +00:00
|
|
|
if ( !array_key_exists( $fname, FilterEvaluator::FUNC_ARG_COUNT ) ) {
|
2020-10-03 13:01:53 +00:00
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
throw new InvalidArgumentException( "Function $fname is not valid" );
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
}
|
2021-08-31 22:26:13 +00:00
|
|
|
return FilterEvaluator::FUNC_ARG_COUNT[$fname][1] === INF;
|
2020-10-03 13:01:53 +00:00
|
|
|
}
|
2016-08-24 04:52:58 +00:00
|
|
|
}
|