Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
285 / 285
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
SyntaxChecker
100.00% covered (success)
100.00%
285 / 285
100.00% covered (success)
100.00%
17 / 17
89
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 start
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 desugar
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
1 / 1
23
 desugarAndOr
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
3
 newNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newNodeReplaceType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newNodeMapAll
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newNodeMapExceptFirst
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 newNodeBinop
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 newNodeNamedBinop
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 check
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
1 / 1
28
 mapUnion
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 mapIntersect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 assignVar
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 lookupVar
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 checkArgCount
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 isReservedIdentifier
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Parser;
4
5use InvalidArgumentException;
6use LogicException;
7use MediaWiki\Extension\AbuseFilter\KeywordsManager;
8use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException;
9use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException;
10use Message;
11
12/**
13 * SyntaxChecker statically analyzes the code without actually running it.
14 * Currently, it only checks for
15 *
16 * - unbound variables
17 * - unused variables: note that a := 1; a := 1; a
18 *     is considered OK even though the first `a` seems unused
19 *     because the pattern "a := null; if ... then (a := ...) end; ..."
20 *     should not count first `a` as unused.
21 * - assignment to built-in identifiers
22 * - invalid function call (arity mismatch, non-valid function)
23 * - first-order information of `set_var` and `set`
24 *
25 * Because it doesn't cover all checks that the current Check Syntax does,
26 * it is currently complementary to the current Check Syntax.
27 * In the future, it could subsume the current Check Syntax, and could be
28 * extended to perform type checking or type inference.
29 */
30class SyntaxChecker {
31    /**
32     * @var AFPTreeNode|null Root of the AST to check
33     */
34    private $treeRoot;
35
36    /** @var KeywordsManager */
37    private $keywordsManager;
38
39    public const MCONSERVATIVE = 'MODE_CONSERVATIVE';
40    public const MLIBERAL = 'MODE_LIBERAL';
41    public const DUMMYPOS = 0;
42    public const CACHE_VERSION = 1;
43
44    /**
45     * @var string The mode of checking. The value should be either
46     *
47     *     - MLIBERAL: which guarantees that all user-defined variables
48     *       will be bound, but incompatible with what the evaluator currently
49     *       permits. E.g.,
50     *
51     *       if true then (a := 1) else null end; a
52     *
53     *       is rejected in this mode, even though `a` is in fact always bound.
54     *
55     *     - MCONSERVATIVE which is compatible with what the evaluator
56     *       currently permits, but could allow undefined variables to occur.
57     *       E.g.,
58     *
59     *       if false then (a := 1) else null end; a
60     *
61     *       is accepted in this mode, even though `a` is in fact always unbound.
62     */
63    private $mode;
64
65    /**
66     * @var bool Whether we want to check for unused variables
67     */
68    private $checkUnusedVars;
69
70    /**
71     * @param AFPSyntaxTree $tree
72     * @param KeywordsManager $keywordsManager
73     * @param string $mode
74     * @param bool $checkUnusedVars
75     */
76    public function __construct(
77        AFPSyntaxTree $tree,
78        KeywordsManager $keywordsManager,
79        string $mode = self::MCONSERVATIVE,
80        bool $checkUnusedVars = false
81    ) {
82        $this->treeRoot = $tree->getRoot();
83        $this->keywordsManager = $keywordsManager;
84        $this->mode = $mode;
85        $this->checkUnusedVars = $checkUnusedVars;
86    }
87
88    /**
89     * Start the static analysis
90     *
91     * @throws UserVisibleException
92     */
93    public function start(): void {
94        if ( !$this->treeRoot ) {
95            return;
96        }
97        $bound = $this->check( $this->desugar( $this->treeRoot ), [] );
98        $unused = array_keys( array_filter( $bound, static function ( $v ) {
99            return !$v;
100        } ) );
101        if ( $this->checkUnusedVars && $unused ) {
102            throw new UserVisibleException(
103                'unusedvars',
104                self::DUMMYPOS,
105                [ Message::listParam( $unused, 'comma' ) ]
106            );
107        }
108    }
109
110    /**
111     * Remove syntactic sugar so that we don't need to deal with
112     * too many cases.
113     *
114     * This could benefit the evaluator as well, but for now, this is
115     * only used for static analysis.
116     *
117     * Postcondition:
118     *     - The tree will not contain nodes of
119     *       type ASSIGNMENT, LOGIC, COMPARE, SUM_REL, MUL_REL, POW,
120     *       KEYWORD_OPERATOR, and ARRAY_INDEX
121     *     - The tree may additionally contain a node of type BINOP.
122     *     - The tree should not have set_var function application.
123     *     - Conditionals will have both branches.
124     *
125     * @param AFPTreeNode $node
126     * @return AFPTreeNode
127     * @throws InternalException
128     */
129    private function desugar( AFPTreeNode $node ): AFPTreeNode {
130        switch ( $node->type ) {
131            case AFPTreeNode::ATOM:
132                return $node;
133
134            case AFPTreeNode::FUNCTION_CALL:
135                if ( $node->children[0] === 'set_var' ) {
136                    $node->children[0] = 'set';
137                }
138                return $this->newNodeMapExceptFirst( $node );
139
140            case AFPTreeNode::ARRAY_INDEX:
141                return $this->newNodeNamedBinop( $node, '[]' );
142
143            case AFPTreeNode::POW:
144                return $this->newNodeNamedBinop( $node, '**' );
145