Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace PhpParser;
|
Chris@0
|
4
|
Chris@0
|
5 /*
|
Chris@0
|
6 * This parser is based on a skeleton written by Moriyoshi Koizumi, which in
|
Chris@0
|
7 * turn is based on work by Masato Bito.
|
Chris@0
|
8 */
|
Chris@0
|
9 use PhpParser\Node\Name;
|
Chris@0
|
10 use PhpParser\Node\Param;
|
Chris@0
|
11 use PhpParser\Node\Scalar\LNumber;
|
Chris@0
|
12 use PhpParser\Node\Scalar\String_;
|
Chris@0
|
13 use PhpParser\Node\Stmt\Class_;
|
Chris@0
|
14 use PhpParser\Node\Stmt\ClassConst;
|
Chris@0
|
15 use PhpParser\Node\Stmt\ClassMethod;
|
Chris@0
|
16 use PhpParser\Node\Stmt\Interface_;
|
Chris@0
|
17 use PhpParser\Node\Stmt\Namespace_;
|
Chris@0
|
18 use PhpParser\Node\Stmt\Property;
|
Chris@0
|
19 use PhpParser\Node\Stmt\TryCatch;
|
Chris@0
|
20 use PhpParser\Node\Stmt\UseUse;
|
Chris@0
|
21
|
Chris@0
|
22 abstract class ParserAbstract implements Parser
|
Chris@0
|
23 {
|
Chris@0
|
24 const SYMBOL_NONE = -1;
|
Chris@0
|
25
|
Chris@0
|
26 /*
|
Chris@0
|
27 * The following members will be filled with generated parsing data:
|
Chris@0
|
28 */
|
Chris@0
|
29
|
Chris@0
|
30 /** @var int Size of $tokenToSymbol map */
|
Chris@0
|
31 protected $tokenToSymbolMapSize;
|
Chris@0
|
32 /** @var int Size of $action table */
|
Chris@0
|
33 protected $actionTableSize;
|
Chris@0
|
34 /** @var int Size of $goto table */
|
Chris@0
|
35 protected $gotoTableSize;
|
Chris@0
|
36
|
Chris@0
|
37 /** @var int Symbol number signifying an invalid token */
|
Chris@0
|
38 protected $invalidSymbol;
|
Chris@0
|
39 /** @var int Symbol number of error recovery token */
|
Chris@0
|
40 protected $errorSymbol;
|
Chris@0
|
41 /** @var int Action number signifying default action */
|
Chris@0
|
42 protected $defaultAction;
|
Chris@0
|
43 /** @var int Rule number signifying that an unexpected token was encountered */
|
Chris@0
|
44 protected $unexpectedTokenRule;
|
Chris@0
|
45
|
Chris@0
|
46 protected $YY2TBLSTATE;
|
Chris@0
|
47 protected $YYNLSTATES;
|
Chris@0
|
48
|
Chris@0
|
49 /** @var array Map of lexer tokens to internal symbols */
|
Chris@0
|
50 protected $tokenToSymbol;
|
Chris@0
|
51 /** @var array Map of symbols to their names */
|
Chris@0
|
52 protected $symbolToName;
|
Chris@0
|
53 /** @var array Names of the production rules (only necessary for debugging) */
|
Chris@0
|
54 protected $productions;
|
Chris@0
|
55
|
Chris@0
|
56 /** @var array Map of states to a displacement into the $action table. The corresponding action for this
|
Chris@0
|
57 * state/symbol pair is $action[$actionBase[$state] + $symbol]. If $actionBase[$state] is 0, the
|
Chris@0
|
58 action is defaulted, i.e. $actionDefault[$state] should be used instead. */
|
Chris@0
|
59 protected $actionBase;
|
Chris@0
|
60 /** @var array Table of actions. Indexed according to $actionBase comment. */
|
Chris@0
|
61 protected $action;
|
Chris@0
|
62 /** @var array Table indexed analogously to $action. If $actionCheck[$actionBase[$state] + $symbol] != $symbol
|
Chris@0
|
63 * then the action is defaulted, i.e. $actionDefault[$state] should be used instead. */
|
Chris@0
|
64 protected $actionCheck;
|
Chris@0
|
65 /** @var array Map of states to their default action */
|
Chris@0
|
66 protected $actionDefault;
|
Chris@0
|
67
|
Chris@0
|
68 /** @var array Map of non-terminals to a displacement into the $goto table. The corresponding goto state for this
|
Chris@0
|
69 * non-terminal/state pair is $goto[$gotoBase[$nonTerminal] + $state] (unless defaulted) */
|
Chris@0
|
70 protected $gotoBase;
|
Chris@0
|
71 /** @var array Table of states to goto after reduction. Indexed according to $gotoBase comment. */
|
Chris@0
|
72 protected $goto;
|
Chris@0
|
73 /** @var array Table indexed analogously to $goto. If $gotoCheck[$gotoBase[$nonTerminal] + $state] != $nonTerminal
|
Chris@0
|
74 * then the goto state is defaulted, i.e. $gotoDefault[$nonTerminal] should be used. */
|
Chris@0
|
75 protected $gotoCheck;
|
Chris@0
|
76 /** @var array Map of non-terminals to the default state to goto after their reduction */
|
Chris@0
|
77 protected $gotoDefault;
|
Chris@0
|
78
|
Chris@0
|
79 /** @var array Map of rules to the non-terminal on their left-hand side, i.e. the non-terminal to use for
|
Chris@0
|
80 * determining the state to goto after reduction. */
|
Chris@0
|
81 protected $ruleToNonTerminal;
|
Chris@0
|
82 /** @var array Map of rules to the length of their right-hand side, which is the number of elements that have to
|
Chris@0
|
83 * be popped from the stack(s) on reduction. */
|
Chris@0
|
84 protected $ruleToLength;
|
Chris@0
|
85
|
Chris@0
|
86 /*
|
Chris@0
|
87 * The following members are part of the parser state:
|
Chris@0
|
88 */
|
Chris@0
|
89
|
Chris@0
|
90 /** @var Lexer Lexer that is used when parsing */
|
Chris@0
|
91 protected $lexer;
|
Chris@0
|
92 /** @var mixed Temporary value containing the result of last semantic action (reduction) */
|
Chris@0
|
93 protected $semValue;
|
Chris@0
|
94 /** @var int Position in stacks (state stack, semantic value stack, attribute stack) */
|
Chris@0
|
95 protected $stackPos;
|
Chris@0
|
96 /** @var array Semantic value stack (contains values of tokens and semantic action results) */
|
Chris@0
|
97 protected $semStack;
|
Chris@0
|
98 /** @var array[] Start attribute stack */
|
Chris@0
|
99 protected $startAttributeStack;
|
Chris@0
|
100 /** @var array[] End attribute stack */
|
Chris@0
|
101 protected $endAttributeStack;
|
Chris@0
|
102 /** @var array End attributes of last *shifted* token */
|
Chris@0
|
103 protected $endAttributes;
|
Chris@0
|
104 /** @var array Start attributes of last *read* token */
|
Chris@0
|
105 protected $lookaheadStartAttributes;
|
Chris@0
|
106
|
Chris@0
|
107 /** @var ErrorHandler Error handler */
|
Chris@0
|
108 protected $errorHandler;
|
Chris@0
|
109 /** @var Error[] Errors collected during last parse */
|
Chris@0
|
110 protected $errors;
|
Chris@0
|
111 /** @var int Error state, used to avoid error floods */
|
Chris@0
|
112 protected $errorState;
|
Chris@0
|
113
|
Chris@0
|
114 /**
|
Chris@0
|
115 * Creates a parser instance.
|
Chris@0
|
116 *
|
Chris@0
|
117 * @param Lexer $lexer A lexer
|
Chris@0
|
118 * @param array $options Options array. Currently no options are supported.
|
Chris@0
|
119 */
|
Chris@0
|
120 public function __construct(Lexer $lexer, array $options = array()) {
|
Chris@0
|
121 $this->lexer = $lexer;
|
Chris@0
|
122 $this->errors = array();
|
Chris@0
|
123
|
Chris@0
|
124 if (isset($options['throwOnError'])) {
|
Chris@0
|
125 throw new \LogicException(
|
Chris@0
|
126 '"throwOnError" is no longer supported, use "errorHandler" instead');
|
Chris@0
|
127 }
|
Chris@0
|
128 }
|
Chris@0
|
129
|
Chris@0
|
130 /**
|
Chris@0
|
131 * Parses PHP code into a node tree.
|
Chris@0
|
132 *
|
Chris@0
|
133 * If a non-throwing error handler is used, the parser will continue parsing after an error
|
Chris@0
|
134 * occurred and attempt to build a partial AST.
|
Chris@0
|
135 *
|
Chris@0
|
136 * @param string $code The source code to parse
|
Chris@0
|
137 * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults
|
Chris@0
|
138 * to ErrorHandler\Throwing.
|
Chris@0
|
139 *
|
Chris@0
|
140 * @return Node[]|null Array of statements (or null if the 'throwOnError' option is disabled and the parser was
|
Chris@0
|
141 * unable to recover from an error).
|
Chris@0
|
142 */
|
Chris@0
|
143 public function parse($code, ErrorHandler $errorHandler = null) {
|
Chris@0
|
144 $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing;
|
Chris@0
|
145
|
Chris@0
|
146 // Initialize the lexer
|
Chris@0
|
147 $this->lexer->startLexing($code, $this->errorHandler);
|
Chris@0
|
148
|
Chris@0
|
149 // We start off with no lookahead-token
|
Chris@0
|
150 $symbol = self::SYMBOL_NONE;
|
Chris@0
|
151
|
Chris@0
|
152 // The attributes for a node are taken from the first and last token of the node.
|
Chris@0
|
153 // From the first token only the startAttributes are taken and from the last only
|
Chris@0
|
154 // the endAttributes. Both are merged using the array union operator (+).
|
Chris@0
|
155 $startAttributes = '*POISON';
|
Chris@0
|
156 $endAttributes = '*POISON';
|
Chris@0
|
157 $this->endAttributes = $endAttributes;
|
Chris@0
|
158
|
Chris@0
|
159 // Keep stack of start and end attributes
|
Chris@0
|
160 $this->startAttributeStack = array();
|
Chris@0
|
161 $this->endAttributeStack = array($endAttributes);
|
Chris@0
|
162
|
Chris@0
|
163 // Start off in the initial state and keep a stack of previous states
|
Chris@0
|
164 $state = 0;
|
Chris@0
|
165 $stateStack = array($state);
|
Chris@0
|
166
|
Chris@0
|
167 // Semantic value stack (contains values of tokens and semantic action results)
|
Chris@0
|
168 $this->semStack = array();
|
Chris@0
|
169
|
Chris@0
|
170 // Current position in the stack(s)
|
Chris@0
|
171 $this->stackPos = 0;
|
Chris@0
|
172
|
Chris@0
|
173 $this->errorState = 0;
|
Chris@0
|
174
|
Chris@0
|
175 for (;;) {
|
Chris@0
|
176 //$this->traceNewState($state, $symbol);
|
Chris@0
|
177
|
Chris@0
|
178 if ($this->actionBase[$state] == 0) {
|
Chris@0
|
179 $rule = $this->actionDefault[$state];
|
Chris@0
|
180 } else {
|
Chris@0
|
181 if ($symbol === self::SYMBOL_NONE) {
|
Chris@0
|
182 // Fetch the next token id from the lexer and fetch additional info by-ref.
|
Chris@0
|
183 // The end attributes are fetched into a temporary variable and only set once the token is really
|
Chris@0
|
184 // shifted (not during read). Otherwise you would sometimes get off-by-one errors, when a rule is
|
Chris@0
|
185 // reduced after a token was read but not yet shifted.
|
Chris@0
|
186 $tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes);
|
Chris@0
|
187
|
Chris@0
|
188 // map the lexer token id to the internally used symbols
|
Chris@0
|
189 $symbol = $tokenId >= 0 && $tokenId < $this->tokenToSymbolMapSize
|
Chris@0
|
190 ? $this->tokenToSymbol[$tokenId]
|
Chris@0
|
191 : $this->invalidSymbol;
|
Chris@0
|
192
|
Chris@0
|
193 if ($symbol === $this->invalidSymbol) {
|
Chris@0
|
194 throw new \RangeException(sprintf(
|
Chris@0
|
195 'The lexer returned an invalid token (id=%d, value=%s)',
|
Chris@0
|
196 $tokenId, $tokenValue
|
Chris@0
|
197 ));
|
Chris@0
|
198 }
|
Chris@0
|
199
|
Chris@0
|
200 // This is necessary to assign some meaningful attributes to /* empty */ productions. They'll get
|
Chris@0
|
201 // the attributes of the next token, even though they don't contain it themselves.
|
Chris@0
|
202 $this->startAttributeStack[$this->stackPos+1] = $startAttributes;
|
Chris@0
|
203 $this->endAttributeStack[$this->stackPos+1] = $endAttributes;
|
Chris@0
|
204 $this->lookaheadStartAttributes = $startAttributes;
|
Chris@0
|
205
|
Chris@0
|
206 //$this->traceRead($symbol);
|
Chris@0
|
207 }
|
Chris@0
|
208
|
Chris@0
|
209 $idx = $this->actionBase[$state] + $symbol;
|
Chris@0
|
210 if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $symbol)
|
Chris@0
|
211 || ($state < $this->YY2TBLSTATE
|
Chris@0
|
212 && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $symbol) >= 0
|
Chris@0
|
213 && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $symbol))
|
Chris@0
|
214 && ($action = $this->action[$idx]) != $this->defaultAction) {
|
Chris@0
|
215 /*
|
Chris@0
|
216 * >= YYNLSTATES: shift and reduce
|
Chris@0
|
217 * > 0: shift
|
Chris@0
|
218 * = 0: accept
|
Chris@0
|
219 * < 0: reduce
|
Chris@0
|
220 * = -YYUNEXPECTED: error
|
Chris@0
|
221 */
|
Chris@0
|
222 if ($action > 0) {
|
Chris@0
|
223 /* shift */
|
Chris@0
|
224 //$this->traceShift($symbol);
|
Chris@0
|
225
|
Chris@0
|
226 ++$this->stackPos;
|
Chris@0
|
227 $stateStack[$this->stackPos] = $state = $action;
|
Chris@0
|
228 $this->semStack[$this->stackPos] = $tokenValue;
|
Chris@0
|
229 $this->startAttributeStack[$this->stackPos] = $startAttributes;
|
Chris@0
|
230 $this->endAttributeStack[$this->stackPos] = $endAttributes;
|
Chris@0
|
231 $this->endAttributes = $endAttributes;
|
Chris@0
|
232 $symbol = self::SYMBOL_NONE;
|
Chris@0
|
233
|
Chris@0
|
234 if ($this->errorState) {
|
Chris@0
|
235 --$this->errorState;
|
Chris@0
|
236 }
|
Chris@0
|
237
|
Chris@0
|
238 if ($action < $this->YYNLSTATES) {
|
Chris@0
|
239 continue;
|
Chris@0
|
240 }
|
Chris@0
|
241
|
Chris@0
|
242 /* $yyn >= YYNLSTATES means shift-and-reduce */
|
Chris@0
|
243 $rule = $action - $this->YYNLSTATES;
|
Chris@0
|
244 } else {
|
Chris@0
|
245 $rule = -$action;
|
Chris@0
|
246 }
|
Chris@0
|
247 } else {
|
Chris@0
|
248 $rule = $this->actionDefault[$state];
|
Chris@0
|
249 }
|
Chris@0
|
250 }
|
Chris@0
|
251
|
Chris@0
|
252 for (;;) {
|
Chris@0
|
253 if ($rule === 0) {
|
Chris@0
|
254 /* accept */
|
Chris@0
|
255 //$this->traceAccept();
|
Chris@0
|
256 return $this->semValue;
|
Chris@0
|
257 } elseif ($rule !== $this->unexpectedTokenRule) {
|
Chris@0
|
258 /* reduce */
|
Chris@0
|
259 //$this->traceReduce($rule);
|
Chris@0
|
260
|
Chris@0
|
261 try {
|
Chris@0
|
262 $this->{'reduceRule' . $rule}();
|
Chris@0
|
263 } catch (Error $e) {
|
Chris@0
|
264 if (-1 === $e->getStartLine() && isset($startAttributes['startLine'])) {
|
Chris@0
|
265 $e->setStartLine($startAttributes['startLine']);
|
Chris@0
|
266 }
|
Chris@0
|
267
|
Chris@0
|
268 $this->emitError($e);
|
Chris@0
|
269 // Can't recover from this type of error
|
Chris@0
|
270 return null;
|
Chris@0
|
271 }
|
Chris@0
|
272
|
Chris@0
|
273 /* Goto - shift nonterminal */
|
Chris@0
|
274 $lastEndAttributes = $this->endAttributeStack[$this->stackPos];
|
Chris@0
|
275 $this->stackPos -= $this->ruleToLength[$rule];
|
Chris@0
|
276 $nonTerminal = $this->ruleToNonTerminal[$rule];
|
Chris@0
|
277 $idx = $this->gotoBase[$nonTerminal] + $stateStack[$this->stackPos];
|
Chris@0
|
278 if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] == $nonTerminal) {
|
Chris@0
|
279 $state = $this->goto[$idx];
|
Chris@0
|
280 } else {
|
Chris@0
|
281 $state = $this->gotoDefault[$nonTerminal];
|
Chris@0
|
282 }
|
Chris@0
|
283
|
Chris@0
|
284 ++$this->stackPos;
|
Chris@0
|
285 $stateStack[$this->stackPos] = $state;
|
Chris@0
|
286 $this->semStack[$this->stackPos] = $this->semValue;
|
Chris@0
|
287 $this->endAttributeStack[$this->stackPos] = $lastEndAttributes;
|
Chris@0
|
288 } else {
|
Chris@0
|
289 /* error */
|
Chris@0
|
290 switch ($this->errorState) {
|
Chris@0
|
291 case 0:
|
Chris@0
|
292 $msg = $this->getErrorMessage($symbol, $state);
|
Chris@0
|
293 $this->emitError(new Error($msg, $startAttributes + $endAttributes));
|
Chris@0
|
294 // Break missing intentionally
|
Chris@0
|
295 case 1:
|
Chris@0
|
296 case 2:
|
Chris@0
|
297 $this->errorState = 3;
|
Chris@0
|
298
|
Chris@0
|
299 // Pop until error-expecting state uncovered
|
Chris@0
|
300 while (!(
|
Chris@0
|
301 (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0
|
Chris@0
|
302 && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
|
Chris@0
|
303 || ($state < $this->YY2TBLSTATE
|
Chris@0
|
304 && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $this->errorSymbol) >= 0
|
Chris@0
|
305 && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
|
Chris@0
|
306 ) || ($action = $this->action[$idx]) == $this->defaultAction) { // Not totally sure about this
|
Chris@0
|
307 if ($this->stackPos <= 0) {
|
Chris@0
|
308 // Could not recover from error
|
Chris@0
|
309 return null;
|
Chris@0
|
310 }
|
Chris@0
|
311 $state = $stateStack[--$this->stackPos];
|
Chris@0
|
312 //$this->tracePop($state);
|
Chris@0
|
313 }
|
Chris@0
|
314
|
Chris@0
|
315 //$this->traceShift($this->errorSymbol);
|
Chris@0
|
316 ++$this->stackPos;
|
Chris@0
|
317 $stateStack[$this->stackPos] = $state = $action;
|
Chris@0
|
318
|
Chris@0
|
319 // We treat the error symbol as being empty, so we reset the end attributes
|
Chris@0
|
320 // to the end attributes of the last non-error symbol
|
Chris@0
|
321 $this->endAttributeStack[$this->stackPos] = $this->endAttributeStack[$this->stackPos - 1];
|
Chris@0
|
322 $this->endAttributes = $this->endAttributeStack[$this->stackPos - 1];
|
Chris@0
|
323 break;
|
Chris@0
|
324
|
Chris@0
|
325 case 3:
|
Chris@0
|
326 if ($symbol === 0) {
|
Chris@0
|
327 // Reached EOF without recovering from error
|
Chris@0
|
328 return null;
|
Chris@0
|
329 }
|
Chris@0
|
330
|
Chris@0
|
331 //$this->traceDiscard($symbol);
|
Chris@0
|
332 $symbol = self::SYMBOL_NONE;
|
Chris@0
|
333 break 2;
|
Chris@0
|
334 }
|
Chris@0
|
335 }
|
Chris@0
|
336
|
Chris@0
|
337 if ($state < $this->YYNLSTATES) {
|
Chris@0
|
338 break;
|
Chris@0
|
339 }
|
Chris@0
|
340
|
Chris@0
|
341 /* >= YYNLSTATES means shift-and-reduce */
|
Chris@0
|
342 $rule = $state - $this->YYNLSTATES;
|
Chris@0
|
343 }
|
Chris@0
|
344 }
|
Chris@0
|
345
|
Chris@0
|
346 throw new \RuntimeException('Reached end of parser loop');
|
Chris@0
|
347 }
|
Chris@0
|
348
|
Chris@0
|
349 protected function emitError(Error $error) {
|
Chris@0
|
350 $this->errorHandler->handleError($error);
|
Chris@0
|
351 }
|
Chris@0
|
352
|
Chris@0
|
353 protected function getErrorMessage($symbol, $state) {
|
Chris@0
|
354 $expectedString = '';
|
Chris@0
|
355 if ($expected = $this->getExpectedTokens($state)) {
|
Chris@0
|
356 $expectedString = ', expecting ' . implode(' or ', $expected);
|
Chris@0
|
357 }
|
Chris@0
|
358
|
Chris@0
|
359 return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString;
|
Chris@0
|
360 }
|
Chris@0
|
361
|
Chris@0
|
362 protected function getExpectedTokens($state) {
|
Chris@0
|
363 $expected = array();
|
Chris@0
|
364
|
Chris@0
|
365 $base = $this->actionBase[$state];
|
Chris@0
|
366 foreach ($this->symbolToName as $symbol => $name) {
|
Chris@0
|
367 $idx = $base + $symbol;
|
Chris@0
|
368 if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
|
Chris@0
|
369 || $state < $this->YY2TBLSTATE
|
Chris@0
|
370 && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $symbol) >= 0
|
Chris@0
|
371 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
|
Chris@0
|
372 ) {
|
Chris@0
|
373 if ($this->action[$idx] != $this->unexpectedTokenRule
|
Chris@0
|
374 && $this->action[$idx] != $this->defaultAction
|
Chris@0
|
375 && $symbol != $this->errorSymbol
|
Chris@0
|
376 ) {
|
Chris@0
|
377 if (count($expected) == 4) {
|
Chris@0
|
378 /* Too many expected tokens */
|
Chris@0
|
379 return array();
|
Chris@0
|
380 }
|
Chris@0
|
381
|
Chris@0
|
382 $expected[] = $name;
|
Chris@0
|
383 }
|
Chris@0
|
384 }
|
Chris@0
|
385 }
|
Chris@0
|
386
|
Chris@0
|
387 return $expected;
|
Chris@0
|
388 }
|
Chris@0
|
389
|
Chris@0
|
390 /*
|
Chris@0
|
391 * Tracing functions used for debugging the parser.
|
Chris@0
|
392 */
|
Chris@0
|
393
|
Chris@0
|
394 /*
|
Chris@0
|
395 protected function traceNewState($state, $symbol) {
|
Chris@0
|
396 echo '% State ' . $state
|
Chris@0
|
397 . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
|
Chris@0
|
398 }
|
Chris@0
|
399
|
Chris@0
|
400 protected function traceRead($symbol) {
|
Chris@0
|
401 echo '% Reading ' . $this->symbolToName[$symbol] . "\n";
|
Chris@0
|
402 }
|
Chris@0
|
403
|
Chris@0
|
404 protected function traceShift($symbol) {
|
Chris@0
|
405 echo '% Shift ' . $this->symbolToName[$symbol] . "\n";
|
Chris@0
|
406 }
|
Chris@0
|
407
|
Chris@0
|
408 protected function traceAccept() {
|
Chris@0
|
409 echo "% Accepted.\n";
|
Chris@0
|
410 }
|
Chris@0
|
411
|
Chris@0
|
412 protected function traceReduce($n) {
|
Chris@0
|
413 echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
|
Chris@0
|
414 }
|
Chris@0
|
415
|
Chris@0
|
416 protected function tracePop($state) {
|
Chris@0
|
417 echo '% Recovering, uncovered state ' . $state . "\n";
|
Chris@0
|
418 }
|
Chris@0
|
419
|
Chris@0
|
420 protected function traceDiscard($symbol) {
|
Chris@0
|
421 echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
|
Chris@0
|
422 }
|
Chris@0
|
423 */
|
Chris@0
|
424
|
Chris@0
|
425 /*
|
Chris@0
|
426 * Helper functions invoked by semantic actions
|
Chris@0
|
427 */
|
Chris@0
|
428
|
Chris@0
|
429 /**
|
Chris@0
|
430 * Moves statements of semicolon-style namespaces into $ns->stmts and checks various error conditions.
|
Chris@0
|
431 *
|
Chris@0
|
432 * @param Node[] $stmts
|
Chris@0
|
433 * @return Node[]
|
Chris@0
|
434 */
|
Chris@0
|
435 protected function handleNamespaces(array $stmts) {
|
Chris@0
|
436 $hasErrored = false;
|
Chris@0
|
437 $style = $this->getNamespacingStyle($stmts);
|
Chris@0
|
438 if (null === $style) {
|
Chris@0
|
439 // not namespaced, nothing to do
|
Chris@0
|
440 return $stmts;
|
Chris@0
|
441 } elseif ('brace' === $style) {
|
Chris@0
|
442 // For braced namespaces we only have to check that there are no invalid statements between the namespaces
|
Chris@0
|
443 $afterFirstNamespace = false;
|
Chris@0
|
444 foreach ($stmts as $stmt) {
|
Chris@0
|
445 if ($stmt instanceof Node\Stmt\Namespace_) {
|
Chris@0
|
446 $afterFirstNamespace = true;
|
Chris@0
|
447 } elseif (!$stmt instanceof Node\Stmt\HaltCompiler
|
Chris@0
|
448 && !$stmt instanceof Node\Stmt\Nop
|
Chris@0
|
449 && $afterFirstNamespace && !$hasErrored) {
|
Chris@0
|
450 $this->emitError(new Error(
|
Chris@0
|
451 'No code may exist outside of namespace {}', $stmt->getAttributes()));
|
Chris@0
|
452 $hasErrored = true; // Avoid one error for every statement
|
Chris@0
|
453 }
|
Chris@0
|
454 }
|
Chris@0
|
455 return $stmts;
|
Chris@0
|
456 } else {
|
Chris@0
|
457 // For semicolon namespaces we have to move the statements after a namespace declaration into ->stmts
|
Chris@0
|
458 $resultStmts = array();
|
Chris@0
|
459 $targetStmts =& $resultStmts;
|
Chris@0
|
460 foreach ($stmts as $stmt) {
|
Chris@0
|
461 if ($stmt instanceof Node\Stmt\Namespace_) {
|
Chris@0
|
462 if ($stmt->stmts === null) {
|
Chris@0
|
463 $stmt->stmts = array();
|
Chris@0
|
464 $targetStmts =& $stmt->stmts;
|
Chris@0
|
465 $resultStmts[] = $stmt;
|
Chris@0
|
466 } else {
|
Chris@0
|
467 // This handles the invalid case of mixed style namespaces
|
Chris@0
|
468 $resultStmts[] = $stmt;
|
Chris@0
|
469 $targetStmts =& $resultStmts;
|
Chris@0
|
470 }
|
Chris@0
|
471 } elseif ($stmt instanceof Node\Stmt\HaltCompiler) {
|
Chris@0
|
472 // __halt_compiler() is not moved into the namespace
|
Chris@0
|
473 $resultStmts[] = $stmt;
|
Chris@0
|
474 } else {
|
Chris@0
|
475 $targetStmts[] = $stmt;
|
Chris@0
|
476 }
|
Chris@0
|
477 }
|
Chris@0
|
478 return $resultStmts;
|
Chris@0
|
479 }
|
Chris@0
|
480 }
|
Chris@0
|
481
|
Chris@0
|
482 private function getNamespacingStyle(array $stmts) {
|
Chris@0
|
483 $style = null;
|
Chris@0
|
484 $hasNotAllowedStmts = false;
|
Chris@0
|
485 foreach ($stmts as $i => $stmt) {
|
Chris@0
|
486 if ($stmt instanceof Node\Stmt\Namespace_) {
|
Chris@0
|
487 $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace';
|
Chris@0
|
488 if (null === $style) {
|
Chris@0
|
489 $style = $currentStyle;
|
Chris@0
|
490 if ($hasNotAllowedStmts) {
|
Chris@0
|
491 $this->emitError(new Error(
|
Chris@0
|
492 'Namespace declaration statement has to be the very first statement in the script',
|
Chris@0
|
493 $stmt->getLine() // Avoid marking the entire namespace as an error
|
Chris@0
|
494 ));
|
Chris@0
|
495 }
|
Chris@0
|
496 } elseif ($style !== $currentStyle) {
|
Chris@0
|
497 $this->emitError(new Error(
|
Chris@0
|
498 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations',
|
Chris@0
|
499 $stmt->getLine() // Avoid marking the entire namespace as an error
|
Chris@0
|
500 ));
|
Chris@0
|
501 // Treat like semicolon style for namespace normalization
|
Chris@0
|
502 return 'semicolon';
|
Chris@0
|
503 }
|
Chris@0
|
504 continue;
|
Chris@0
|
505 }
|
Chris@0
|
506
|
Chris@0
|
507 /* declare(), __halt_compiler() and nops can be used before a namespace declaration */
|
Chris@0
|
508 if ($stmt instanceof Node\Stmt\Declare_
|
Chris@0
|
509 || $stmt instanceof Node\Stmt\HaltCompiler
|
Chris@0
|
510 || $stmt instanceof Node\Stmt\Nop) {
|
Chris@0
|
511 continue;
|
Chris@0
|
512 }
|
Chris@0
|
513
|
Chris@0
|
514 /* There may be a hashbang line at the very start of the file */
|
Chris@0
|
515 if ($i == 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) {
|
Chris@0
|
516 continue;
|
Chris@0
|
517 }
|
Chris@0
|
518
|
Chris@0
|
519 /* Everything else if forbidden before namespace declarations */
|
Chris@0
|
520 $hasNotAllowedStmts = true;
|
Chris@0
|
521 }
|
Chris@0
|
522 return $style;
|
Chris@0
|
523 }
|
Chris@0
|
524
|
Chris@0
|
525 protected function handleBuiltinTypes(Name $name) {
|
Chris@0
|
526 $scalarTypes = [
|
Chris@0
|
527 'bool' => true,
|
Chris@0
|
528 'int' => true,
|
Chris@0
|
529 'float' => true,
|
Chris@0
|
530 'string' => true,
|
Chris@0
|
531 'iterable' => true,
|
Chris@0
|
532 'void' => true,
|
Chris@0
|
533 'object' => true,
|
Chris@0
|
534 ];
|
Chris@0
|
535
|
Chris@0
|
536 if (!$name->isUnqualified()) {
|
Chris@0
|
537 return $name;
|
Chris@0
|
538 }
|
Chris@0
|
539
|
Chris@0
|
540 $lowerName = strtolower($name->toString());
|
Chris@0
|
541 return isset($scalarTypes[$lowerName]) ? $lowerName : $name;
|
Chris@0
|
542 }
|
Chris@0
|
543
|
Chris@0
|
544 protected static $specialNames = array(
|
Chris@0
|
545 'self' => true,
|
Chris@0
|
546 'parent' => true,
|
Chris@0
|
547 'static' => true,
|
Chris@0
|
548 );
|
Chris@0
|
549
|
Chris@0
|
550 protected function getAttributesAt($pos) {
|
Chris@0
|
551 return $this->startAttributeStack[$pos] + $this->endAttributeStack[$pos];
|
Chris@0
|
552 }
|
Chris@0
|
553
|
Chris@0
|
554 protected function parseLNumber($str, $attributes, $allowInvalidOctal = false) {
|
Chris@0
|
555 try {
|
Chris@0
|
556 return LNumber::fromString($str, $attributes, $allowInvalidOctal);
|
Chris@0
|
557 } catch (Error $error) {
|
Chris@0
|
558 $this->emitError($error);
|
Chris@0
|
559 // Use dummy value
|
Chris@0
|
560 return new LNumber(0, $attributes);
|
Chris@0
|
561 }
|
Chris@0
|
562 }
|
Chris@0
|
563
|
Chris@0
|
564 protected function parseNumString($str, $attributes) {
|
Chris@0
|
565 if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) {
|
Chris@0
|
566 return new String_($str, $attributes);
|
Chris@0
|
567 }
|
Chris@0
|
568
|
Chris@0
|
569 $num = +$str;
|
Chris@0
|
570 if (!is_int($num)) {
|
Chris@0
|
571 return new String_($str, $attributes);
|
Chris@0
|
572 }
|
Chris@0
|
573
|
Chris@0
|
574 return new LNumber($num, $attributes);
|
Chris@0
|
575 }
|
Chris@0
|
576
|
Chris@0
|
577 protected function checkModifier($a, $b, $modifierPos) {
|
Chris@0
|
578 // Jumping through some hoops here because verifyModifier() is also used elsewhere
|
Chris@0
|
579 try {
|
Chris@0
|
580 Class_::verifyModifier($a, $b);
|
Chris@0
|
581 } catch (Error $error) {
|
Chris@0
|
582 $error->setAttributes($this->getAttributesAt($modifierPos));
|
Chris@0
|
583 $this->emitError($error);
|
Chris@0
|
584 }
|
Chris@0
|
585 }
|
Chris@0
|
586
|
Chris@0
|
587 protected function checkParam(Param $node) {
|
Chris@0
|
588 if ($node->variadic && null !== $node->default) {
|
Chris@0
|
589 $this->emitError(new Error(
|
Chris@0
|
590 'Variadic parameter cannot have a default value',
|
Chris@0
|
591 $node->default->getAttributes()
|
Chris@0
|
592 ));
|
Chris@0
|
593 }
|
Chris@0
|
594 }
|
Chris@0
|
595
|
Chris@0
|
596 protected function checkTryCatch(TryCatch $node) {
|
Chris@0
|
597 if (empty($node->catches) && null === $node->finally) {
|
Chris@0
|
598 $this->emitError(new Error(
|
Chris@0
|
599 'Cannot use try without catch or finally', $node->getAttributes()
|
Chris@0
|
600 ));
|
Chris@0
|
601 }
|
Chris@0
|
602 }
|
Chris@0
|
603
|
Chris@0
|
604 protected function checkNamespace(Namespace_ $node) {
|
Chris@0
|
605 if (isset(self::$specialNames[strtolower($node->name)])) {
|
Chris@0
|
606 $this->emitError(new Error(
|
Chris@0
|
607 sprintf('Cannot use \'%s\' as namespace name', $node->name),
|
Chris@0
|
608 $node->name->getAttributes()
|
Chris@0
|
609 ));
|
Chris@0
|
610 }
|
Chris@0
|
611
|
Chris@0
|
612 if (null !== $node->stmts) {
|
Chris@0
|
613 foreach ($node->stmts as $stmt) {
|
Chris@0
|
614 if ($stmt instanceof Namespace_) {
|
Chris@0
|
615 $this->emitError(new Error(
|
Chris@0
|
616 'Namespace declarations cannot be nested', $stmt->getAttributes()
|
Chris@0
|
617 ));
|
Chris@0
|
618 }
|
Chris@0
|
619 }
|
Chris@0
|
620 }
|
Chris@0
|
621 }
|
Chris@0
|
622
|
Chris@0
|
623 protected function checkClass(Class_ $node, $namePos) {
|
Chris@0
|
624 if (null !== $node->name && isset(self::$specialNames[strtolower($node->name)])) {
|
Chris@0
|
625 $this->emitError(new Error(
|
Chris@0
|
626 sprintf('Cannot use \'%s\' as class name as it is reserved', $node->name),
|
Chris@0
|
627 $this->getAttributesAt($namePos)
|
Chris@0
|
628 ));
|
Chris@0
|
629 }
|
Chris@0
|
630
|
Chris@0
|
631 if (isset(self::$specialNames[strtolower($node->extends)])) {
|
Chris@0
|
632 $this->emitError(new Error(
|
Chris@0
|
633 sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
|
Chris@0
|
634 $node->extends->getAttributes()
|
Chris@0
|
635 ));
|
Chris@0
|
636 }
|
Chris@0
|
637
|
Chris@0
|
638 foreach ($node->implements as $interface) {
|
Chris@0
|
639 if (isset(self::$specialNames[strtolower($interface)])) {
|
Chris@0
|
640 $this->emitError(new Error(
|
Chris@0
|
641 sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
|
Chris@0
|
642 $interface->getAttributes()
|
Chris@0
|
643 ));
|
Chris@0
|
644 }
|
Chris@0
|
645 }
|
Chris@0
|
646 }
|
Chris@0
|
647
|
Chris@0
|
648 protected function checkInterface(Interface_ $node, $namePos) {
|
Chris@0
|
649 if (null !== $node->name && isset(self::$specialNames[strtolower($node->name)])) {
|
Chris@0
|
650 $this->emitError(new Error(
|
Chris@0
|
651 sprintf('Cannot use \'%s\' as class name as it is reserved', $node->name),
|
Chris@0
|
652 $this->getAttributesAt($namePos)
|
Chris@0
|
653 ));
|
Chris@0
|
654 }
|
Chris@0
|
655
|
Chris@0
|
656 foreach ($node->extends as $interface) {
|
Chris@0
|
657 if (isset(self::$specialNames[strtolower($interface)])) {
|
Chris@0
|
658 $this->emitError(new Error(
|
Chris@0
|
659 sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
|
Chris@0
|
660 $interface->getAttributes()
|
Chris@0
|
661 ));
|
Chris@0
|
662 }
|
Chris@0
|
663 }
|
Chris@0
|
664 }
|
Chris@0
|
665
|
Chris@0
|
666 protected function checkClassMethod(ClassMethod $node, $modifierPos) {
|
Chris@0
|
667 if ($node->flags & Class_::MODIFIER_STATIC) {
|
Chris@0
|
668 switch (strtolower($node->name)) {
|
Chris@0
|
669 case '__construct':
|
Chris@0
|
670 $this->emitError(new Error(
|
Chris@0
|
671 sprintf('Constructor %s() cannot be static', $node->name),
|
Chris@0
|
672 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
673 break;
|
Chris@0
|
674 case '__destruct':
|
Chris@0
|
675 $this->emitError(new Error(
|
Chris@0
|
676 sprintf('Destructor %s() cannot be static', $node->name),
|
Chris@0
|
677 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
678 break;
|
Chris@0
|
679 case '__clone':
|
Chris@0
|
680 $this->emitError(new Error(
|
Chris@0
|
681 sprintf('Clone method %s() cannot be static', $node->name),
|
Chris@0
|
682 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
683 break;
|
Chris@0
|
684 }
|
Chris@0
|
685 }
|
Chris@0
|
686 }
|
Chris@0
|
687
|
Chris@0
|
688 protected function checkClassConst(ClassConst $node, $modifierPos) {
|
Chris@0
|
689 if ($node->flags & Class_::MODIFIER_STATIC) {
|
Chris@0
|
690 $this->emitError(new Error(
|
Chris@0
|
691 "Cannot use 'static' as constant modifier",
|
Chris@0
|
692 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
693 }
|
Chris@0
|
694 if ($node->flags & Class_::MODIFIER_ABSTRACT) {
|
Chris@0
|
695 $this->emitError(new Error(
|
Chris@0
|
696 "Cannot use 'abstract' as constant modifier",
|
Chris@0
|
697 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
698 }
|
Chris@0
|
699 if ($node->flags & Class_::MODIFIER_FINAL) {
|
Chris@0
|
700 $this->emitError(new Error(
|
Chris@0
|
701 "Cannot use 'final' as constant modifier",
|
Chris@0
|
702 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
703 }
|
Chris@0
|
704 }
|
Chris@0
|
705
|
Chris@0
|
706 protected function checkProperty(Property $node, $modifierPos) {
|
Chris@0
|
707 if ($node->flags & Class_::MODIFIER_ABSTRACT) {
|
Chris@0
|
708 $this->emitError(new Error('Properties cannot be declared abstract',
|
Chris@0
|
709 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
710 }
|
Chris@0
|
711
|
Chris@0
|
712 if ($node->flags & Class_::MODIFIER_FINAL) {
|
Chris@0
|
713 $this->emitError(new Error('Properties cannot be declared final',
|
Chris@0
|
714 $this->getAttributesAt($modifierPos)));
|
Chris@0
|
715 }
|
Chris@0
|
716 }
|
Chris@0
|
717
|
Chris@0
|
718 protected function checkUseUse(UseUse $node, $namePos) {
|
Chris@0
|
719 if ('self' == strtolower($node->alias) || 'parent' == strtolower($node->alias)) {
|
Chris@0
|
720 $this->emitError(new Error(
|
Chris@0
|
721 sprintf(
|
Chris@0
|
722 'Cannot use %s as %s because \'%2$s\' is a special class name',
|
Chris@0
|
723 $node->name, $node->alias
|
Chris@0
|
724 ),
|
Chris@0
|
725 $this->getAttributesAt($namePos)
|
Chris@0
|
726 ));
|
Chris@0
|
727 }
|
Chris@0
|
728 }
|
Chris@0
|
729 }
|