Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
285 / 285 |
|
100.00% |
17 / 17 |
CRAP | |
100.00% |
1 / 1 |
SyntaxChecker | |
100.00% |
285 / 285 |
|
100.00% |
17 / 17 |
89 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
start | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
desugar | |
100.00% |
62 / 62 |
|
100.00% |
1 / 1 |
23 | |||
desugarAndOr | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
3 | |||
newNode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newNodeReplaceType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newNodeMapAll | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newNodeMapExceptFirst | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
newNodeBinop | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
newNodeNamedBinop | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
check | |
100.00% |
81 / 81 |
|
100.00% |
1 / 1 |
28 | |||
mapUnion | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
mapIntersect | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
assignVar | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
lookupVar | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
5 | |||
checkArgCount | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
isReservedIdentifier | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\Parser; |
4 | |
5 | use InvalidArgumentException; |
6 | use LogicException; |
7 | use MediaWiki\Extension\AbuseFilter\KeywordsManager; |
8 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException; |
9 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException; |
10 | use 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 | */ |
30 | class 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 | |