ParserAbstract.php 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294
  1. <?php declare(strict_types=1);
  2. namespace PhpParser;
  3. /*
  4. * This parser is based on a skeleton written by Moriyoshi Koizumi, which in
  5. * turn is based on work by Masato Bito.
  6. */
  7. use PhpParser\Node\Arg;
  8. use PhpParser\Node\Expr;
  9. use PhpParser\Node\Expr\Array_;
  10. use PhpParser\Node\Expr\Cast\Double;
  11. use PhpParser\Node\Identifier;
  12. use PhpParser\Node\InterpolatedStringPart;
  13. use PhpParser\Node\Name;
  14. use PhpParser\Node\Param;
  15. use PhpParser\Node\PropertyHook;
  16. use PhpParser\Node\Scalar\InterpolatedString;
  17. use PhpParser\Node\Scalar\Int_;
  18. use PhpParser\Node\Scalar\String_;
  19. use PhpParser\Node\Stmt;
  20. use PhpParser\Node\Stmt\Class_;
  21. use PhpParser\Node\Stmt\ClassConst;
  22. use PhpParser\Node\Stmt\ClassMethod;
  23. use PhpParser\Node\Stmt\Const_;
  24. use PhpParser\Node\Stmt\Else_;
  25. use PhpParser\Node\Stmt\ElseIf_;
  26. use PhpParser\Node\Stmt\Enum_;
  27. use PhpParser\Node\Stmt\Interface_;
  28. use PhpParser\Node\Stmt\Namespace_;
  29. use PhpParser\Node\Stmt\Nop;
  30. use PhpParser\Node\Stmt\Property;
  31. use PhpParser\Node\Stmt\TryCatch;
  32. use PhpParser\Node\UseItem;
  33. use PhpParser\Node\VarLikeIdentifier;
  34. use PhpParser\NodeVisitor\CommentAnnotatingVisitor;
  35. abstract class ParserAbstract implements Parser {
  36. private const SYMBOL_NONE = -1;
  37. /** @var Lexer Lexer that is used when parsing */
  38. protected Lexer $lexer;
  39. /** @var PhpVersion PHP version to target on a best-effort basis */
  40. protected PhpVersion $phpVersion;
  41. /*
  42. * The following members will be filled with generated parsing data:
  43. */
  44. /** @var int Size of $tokenToSymbol map */
  45. protected int $tokenToSymbolMapSize;
  46. /** @var int Size of $action table */
  47. protected int $actionTableSize;
  48. /** @var int Size of $goto table */
  49. protected int $gotoTableSize;
  50. /** @var int Symbol number signifying an invalid token */
  51. protected int $invalidSymbol;
  52. /** @var int Symbol number of error recovery token */
  53. protected int $errorSymbol;
  54. /** @var int Action number signifying default action */
  55. protected int $defaultAction;
  56. /** @var int Rule number signifying that an unexpected token was encountered */
  57. protected int $unexpectedTokenRule;
  58. protected int $YY2TBLSTATE;
  59. /** @var int Number of non-leaf states */
  60. protected int $numNonLeafStates;
  61. /** @var int[] Map of PHP token IDs to internal symbols */
  62. protected array $phpTokenToSymbol;
  63. /** @var array<int, bool> Map of PHP token IDs to drop */
  64. protected array $dropTokens;
  65. /** @var int[] Map of external symbols (static::T_*) to internal symbols */
  66. protected array $tokenToSymbol;
  67. /** @var string[] Map of symbols to their names */
  68. protected array $symbolToName;
  69. /** @var array<int, string> Names of the production rules (only necessary for debugging) */
  70. protected array $productions;
  71. /** @var int[] Map of states to a displacement into the $action table. The corresponding action for this
  72. * state/symbol pair is $action[$actionBase[$state] + $symbol]. If $actionBase[$state] is 0, the
  73. * action is defaulted, i.e. $actionDefault[$state] should be used instead. */
  74. protected array $actionBase;
  75. /** @var int[] Table of actions. Indexed according to $actionBase comment. */
  76. protected array $action;
  77. /** @var int[] Table indexed analogously to $action. If $actionCheck[$actionBase[$state] + $symbol] != $symbol
  78. * then the action is defaulted, i.e. $actionDefault[$state] should be used instead. */
  79. protected array $actionCheck;
  80. /** @var int[] Map of states to their default action */
  81. protected array $actionDefault;
  82. /** @var callable[] Semantic action callbacks */
  83. protected array $reduceCallbacks;
  84. /** @var int[] Map of non-terminals to a displacement into the $goto table. The corresponding goto state for this
  85. * non-terminal/state pair is $goto[$gotoBase[$nonTerminal] + $state] (unless defaulted) */
  86. protected array $gotoBase;
  87. /** @var int[] Table of states to goto after reduction. Indexed according to $gotoBase comment. */
  88. protected array $goto;
  89. /** @var int[] Table indexed analogously to $goto. If $gotoCheck[$gotoBase[$nonTerminal] + $state] != $nonTerminal
  90. * then the goto state is defaulted, i.e. $gotoDefault[$nonTerminal] should be used. */
  91. protected array $gotoCheck;
  92. /** @var int[] Map of non-terminals to the default state to goto after their reduction */
  93. protected array $gotoDefault;
  94. /** @var int[] Map of rules to the non-terminal on their left-hand side, i.e. the non-terminal to use for
  95. * determining the state to goto after reduction. */
  96. protected array $ruleToNonTerminal;
  97. /** @var int[] Map of rules to the length of their right-hand side, which is the number of elements that have to
  98. * be popped from the stack(s) on reduction. */
  99. protected array $ruleToLength;
  100. /*
  101. * The following members are part of the parser state:
  102. */
  103. /** @var mixed Temporary value containing the result of last semantic action (reduction) */
  104. protected $semValue;
  105. /** @var mixed[] Semantic value stack (contains values of tokens and semantic action results) */
  106. protected array $semStack;
  107. /** @var int[] Token start position stack */
  108. protected array $tokenStartStack;
  109. /** @var int[] Token end position stack */
  110. protected array $tokenEndStack;
  111. /** @var ErrorHandler Error handler */
  112. protected ErrorHandler $errorHandler;
  113. /** @var int Error state, used to avoid error floods */
  114. protected int $errorState;
  115. /** @var \SplObjectStorage<Array_, null>|null Array nodes created during parsing, for postprocessing of empty elements. */
  116. protected ?\SplObjectStorage $createdArrays;
  117. /** @var Token[] Tokens for the current parse */
  118. protected array $tokens;
  119. /** @var int Current position in token array */
  120. protected int $tokenPos;
  121. /**
  122. * Initialize $reduceCallbacks map.
  123. */
  124. abstract protected function initReduceCallbacks(): void;
  125. /**
  126. * Creates a parser instance.
  127. *
  128. * Options:
  129. * * phpVersion: ?PhpVersion,
  130. *
  131. * @param Lexer $lexer A lexer
  132. * @param PhpVersion $phpVersion PHP version to target, defaults to latest supported. This
  133. * option is best-effort: Even if specified, parsing will generally assume the latest
  134. * supported version and only adjust behavior in minor ways, for example by omitting
  135. * errors in older versions and interpreting type hints as a name or identifier depending
  136. * on version.
  137. */
  138. public function __construct(Lexer $lexer, ?PhpVersion $phpVersion = null) {
  139. $this->lexer = $lexer;
  140. $this->phpVersion = $phpVersion ?? PhpVersion::getNewestSupported();
  141. $this->initReduceCallbacks();
  142. $this->phpTokenToSymbol = $this->createTokenMap();
  143. $this->dropTokens = array_fill_keys(
  144. [\T_WHITESPACE, \T_OPEN_TAG, \T_COMMENT, \T_DOC_COMMENT, \T_BAD_CHARACTER], true
  145. );
  146. }
  147. /**
  148. * Parses PHP code into a node tree.
  149. *
  150. * If a non-throwing error handler is used, the parser will continue parsing after an error
  151. * occurred and attempt to build a partial AST.
  152. *
  153. * @param string $code The source code to parse
  154. * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults
  155. * to ErrorHandler\Throwing.
  156. *
  157. * @return Node\Stmt[]|null Array of statements (or null non-throwing error handler is used and
  158. * the parser was unable to recover from an error).
  159. */
  160. public function parse(string $code, ?ErrorHandler $errorHandler = null): ?array {
  161. $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing();
  162. $this->createdArrays = new \SplObjectStorage();
  163. $this->tokens = $this->lexer->tokenize($code, $this->errorHandler);
  164. $result = $this->doParse();
  165. // Report errors for any empty elements used inside arrays. This is delayed until after the main parse,
  166. // because we don't know a priori whether a given array expression will be used in a destructuring context
  167. // or not.
  168. foreach ($this->createdArrays as $node) {
  169. foreach ($node->items as $item) {
  170. if ($item->value instanceof Expr\Error) {
  171. $this->errorHandler->handleError(
  172. new Error('Cannot use empty array elements in arrays', $item->getAttributes()));
  173. }
  174. }
  175. }
  176. // Clear out some of the interior state, so we don't hold onto unnecessary
  177. // memory between uses of the parser
  178. $this->tokenStartStack = [];
  179. $this->tokenEndStack = [];
  180. $this->semStack = [];
  181. $this->semValue = null;
  182. $this->createdArrays = null;
  183. if ($result !== null) {
  184. $traverser = new NodeTraverser(new CommentAnnotatingVisitor($this->tokens));
  185. $traverser->traverse($result);
  186. }
  187. return $result;
  188. }
  189. public function getTokens(): array {
  190. return $this->tokens;
  191. }
  192. /** @return Stmt[]|null */
  193. protected function doParse(): ?array {
  194. // We start off with no lookahead-token
  195. $symbol = self::SYMBOL_NONE;
  196. $tokenValue = null;
  197. $this->tokenPos = -1;
  198. // Keep stack of start and end attributes
  199. $this->tokenStartStack = [];
  200. $this->tokenEndStack = [0];
  201. // Start off in the initial state and keep a stack of previous states
  202. $state = 0;
  203. $stateStack = [$state];
  204. // Semantic value stack (contains values of tokens and semantic action results)
  205. $this->semStack = [];
  206. // Current position in the stack(s)
  207. $stackPos = 0;
  208. $this->errorState = 0;
  209. for (;;) {
  210. //$this->traceNewState($state, $symbol);
  211. if ($this->actionBase[$state] === 0) {
  212. $rule = $this->actionDefault[$state];
  213. } else {
  214. if ($symbol === self::SYMBOL_NONE) {
  215. do {
  216. $token = $this->tokens[++$this->tokenPos];
  217. $tokenId = $token->id;
  218. } while (isset($this->dropTokens[$tokenId]));
  219. // Map the lexer token id to the internally used symbols.
  220. $tokenValue = $token->text;
  221. if (!isset($this->phpTokenToSymbol[$tokenId])) {
  222. throw new \RangeException(sprintf(
  223. 'The lexer returned an invalid token (id=%d, value=%s)',
  224. $tokenId, $tokenValue
  225. ));
  226. }
  227. $symbol = $this->phpTokenToSymbol[$tokenId];
  228. //$this->traceRead($symbol);
  229. }
  230. $idx = $this->actionBase[$state] + $symbol;
  231. if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol)
  232. || ($state < $this->YY2TBLSTATE
  233. && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0
  234. && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol))
  235. && ($action = $this->action[$idx]) !== $this->defaultAction) {
  236. /*
  237. * >= numNonLeafStates: shift and reduce
  238. * > 0: shift
  239. * = 0: accept
  240. * < 0: reduce
  241. * = -YYUNEXPECTED: error
  242. */
  243. if ($action > 0) {
  244. /* shift */
  245. //$this->traceShift($symbol);
  246. ++$stackPos;
  247. $stateStack[$stackPos] = $state = $action;
  248. $this->semStack[$stackPos] = $tokenValue;
  249. $this->tokenStartStack[$stackPos] = $this->tokenPos;
  250. $this->tokenEndStack[$stackPos] = $this->tokenPos;
  251. $symbol = self::SYMBOL_NONE;
  252. if ($this->errorState) {
  253. --$this->errorState;
  254. }
  255. if ($action < $this->numNonLeafStates) {
  256. continue;
  257. }
  258. /* $yyn >= numNonLeafStates means shift-and-reduce */
  259. $rule = $action - $this->numNonLeafStates;
  260. } else {
  261. $rule = -$action;
  262. }
  263. } else {
  264. $rule = $this->actionDefault[$state];
  265. }
  266. }
  267. for (;;) {
  268. if ($rule === 0) {
  269. /* accept */
  270. //$this->traceAccept();
  271. return $this->semValue;
  272. }
  273. if ($rule !== $this->unexpectedTokenRule) {
  274. /* reduce */
  275. //$this->traceReduce($rule);
  276. $ruleLength = $this->ruleToLength[$rule];
  277. try {
  278. $callback = $this->reduceCallbacks[$rule];
  279. if ($callback !== null) {
  280. $callback($this, $stackPos);
  281. } elseif ($ruleLength > 0) {
  282. $this->semValue = $this->semStack[$stackPos - $ruleLength + 1];
  283. }
  284. } catch (Error $e) {
  285. if (-1 === $e->getStartLine()) {
  286. $e->setStartLine($this->tokens[$this->tokenPos]->line);
  287. }
  288. $this->emitError($e);
  289. // Can't recover from this type of error
  290. return null;
  291. }
  292. /* Goto - shift nonterminal */
  293. $lastTokenEnd = $this->tokenEndStack[$stackPos];
  294. $stackPos -= $ruleLength;
  295. $nonTerminal = $this->ruleToNonTerminal[$rule];
  296. $idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos];
  297. if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) {
  298. $state = $this->goto[$idx];
  299. } else {
  300. $state = $this->gotoDefault[$nonTerminal];
  301. }
  302. ++$stackPos;
  303. $stateStack[$stackPos] = $state;
  304. $this->semStack[$stackPos] = $this->semValue;
  305. $this->tokenEndStack[$stackPos] = $lastTokenEnd;
  306. if ($ruleLength === 0) {
  307. // Empty productions use the start attributes of the lookahead token.
  308. $this->tokenStartStack[$stackPos] = $this->tokenPos;
  309. }
  310. } else {
  311. /* error */
  312. switch ($this->errorState) {
  313. case 0:
  314. $msg = $this->getErrorMessage($symbol, $state);
  315. $this->emitError(new Error($msg, $this->getAttributesForToken($this->tokenPos)));
  316. // Break missing intentionally
  317. // no break
  318. case 1:
  319. case 2:
  320. $this->errorState = 3;
  321. // Pop until error-expecting state uncovered
  322. while (!(
  323. (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0
  324. && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol)
  325. || ($state < $this->YY2TBLSTATE
  326. && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $this->errorSymbol) >= 0
  327. && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol)
  328. ) || ($action = $this->action[$idx]) === $this->defaultAction) { // Not totally sure about this
  329. if ($stackPos <= 0) {
  330. // Could not recover from error
  331. return null;
  332. }
  333. $state = $stateStack[--$stackPos];
  334. //$this->tracePop($state);
  335. }
  336. //$this->traceShift($this->errorSymbol);
  337. ++$stackPos;
  338. $stateStack[$stackPos] = $state = $action;
  339. // We treat the error symbol as being empty, so we reset the end attributes
  340. // to the end attributes of the last non-error symbol
  341. $this->tokenStartStack[$stackPos] = $this->tokenPos;
  342. $this->tokenEndStack[$stackPos] = $this->tokenEndStack[$stackPos - 1];
  343. break;
  344. case 3:
  345. if ($symbol === 0) {
  346. // Reached EOF without recovering from error
  347. return null;
  348. }
  349. //$this->traceDiscard($symbol);
  350. $symbol = self::SYMBOL_NONE;
  351. break 2;
  352. }
  353. }
  354. if ($state < $this->numNonLeafStates) {
  355. break;
  356. }
  357. /* >= numNonLeafStates means shift-and-reduce */
  358. $rule = $state - $this->numNonLeafStates;
  359. }
  360. }
  361. }
  362. protected function emitError(Error $error): void {
  363. $this->errorHandler->handleError($error);
  364. }
  365. /**
  366. * Format error message including expected tokens.
  367. *
  368. * @param int $symbol Unexpected symbol
  369. * @param int $state State at time of error
  370. *
  371. * @return string Formatted error message
  372. */
  373. protected function getErrorMessage(int $symbol, int $state): string {
  374. $expectedString = '';
  375. if ($expected = $this->getExpectedTokens($state)) {
  376. $expectedString = ', expecting ' . implode(' or ', $expected);
  377. }
  378. return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString;
  379. }
  380. /**
  381. * Get limited number of expected tokens in given state.
  382. *
  383. * @param int $state State
  384. *
  385. * @return string[] Expected tokens. If too many, an empty array is returned.
  386. */
  387. protected function getExpectedTokens(int $state): array {
  388. $expected = [];
  389. $base = $this->actionBase[$state];
  390. foreach ($this->symbolToName as $symbol => $name) {
  391. $idx = $base + $symbol;
  392. if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
  393. || $state < $this->YY2TBLSTATE
  394. && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0
  395. && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
  396. ) {
  397. if ($this->action[$idx] !== $this->unexpectedTokenRule
  398. && $this->action[$idx] !== $this->defaultAction
  399. && $symbol !== $this->errorSymbol
  400. ) {
  401. if (count($expected) === 4) {
  402. /* Too many expected tokens */
  403. return [];
  404. }
  405. $expected[] = $name;
  406. }
  407. }
  408. }
  409. return $expected;
  410. }
  411. /**
  412. * Get attributes for a node with the given start and end token positions.
  413. *
  414. * @param int $tokenStartPos Token position the node starts at
  415. * @param int $tokenEndPos Token position the node ends at
  416. * @return array<string, mixed> Attributes
  417. */
  418. protected function getAttributes(int $tokenStartPos, int $tokenEndPos): array {
  419. $startToken = $this->tokens[$tokenStartPos];
  420. $afterEndToken = $this->tokens[$tokenEndPos + 1];
  421. return [
  422. 'startLine' => $startToken->line,
  423. 'startTokenPos' => $tokenStartPos,
  424. 'startFilePos' => $startToken->pos,
  425. 'endLine' => $afterEndToken->line,
  426. 'endTokenPos' => $tokenEndPos,
  427. 'endFilePos' => $afterEndToken->pos - 1,
  428. ];
  429. }
  430. /**
  431. * Get attributes for a single token at the given token position.
  432. *
  433. * @return array<string, mixed> Attributes
  434. */
  435. protected function getAttributesForToken(int $tokenPos): array {
  436. if ($tokenPos < \count($this->tokens) - 1) {
  437. return $this->getAttributes($tokenPos, $tokenPos);
  438. }
  439. // Get attributes for the sentinel token.
  440. $token = $this->tokens[$tokenPos];
  441. return [
  442. 'startLine' => $token->line,
  443. 'startTokenPos' => $tokenPos,
  444. 'startFilePos' => $token->pos,
  445. 'endLine' => $token->line,
  446. 'endTokenPos' => $tokenPos,
  447. 'endFilePos' => $token->pos,
  448. ];
  449. }
  450. /*
  451. * Tracing functions used for debugging the parser.
  452. */
  453. /*
  454. protected function traceNewState($state, $symbol): void {
  455. echo '% State ' . $state
  456. . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
  457. }
  458. protected function traceRead($symbol): void {
  459. echo '% Reading ' . $this->symbolToName[$symbol] . "\n";
  460. }
  461. protected function traceShift($symbol): void {
  462. echo '% Shift ' . $this->symbolToName[$symbol] . "\n";
  463. }
  464. protected function traceAccept(): void {
  465. echo "% Accepted.\n";
  466. }
  467. protected function traceReduce($n): void {
  468. echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
  469. }
  470. protected function tracePop($state): void {
  471. echo '% Recovering, uncovered state ' . $state . "\n";
  472. }
  473. protected function traceDiscard($symbol): void {
  474. echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
  475. }
  476. */
  477. /*
  478. * Helper functions invoked by semantic actions
  479. */
  480. /**
  481. * Moves statements of semicolon-style namespaces into $ns->stmts and checks various error conditions.
  482. *
  483. * @param Node\Stmt[] $stmts
  484. * @return Node\Stmt[]
  485. */
  486. protected function handleNamespaces(array $stmts): array {
  487. $hasErrored = false;
  488. $style = $this->getNamespacingStyle($stmts);
  489. if (null === $style) {
  490. // not namespaced, nothing to do
  491. return $stmts;
  492. }
  493. if ('brace' === $style) {
  494. // For braced namespaces we only have to check that there are no invalid statements between the namespaces
  495. $afterFirstNamespace = false;
  496. foreach ($stmts as $stmt) {
  497. if ($stmt instanceof Node\Stmt\Namespace_) {
  498. $afterFirstNamespace = true;
  499. } elseif (!$stmt instanceof Node\Stmt\HaltCompiler
  500. && !$stmt instanceof Node\Stmt\Nop
  501. && $afterFirstNamespace && !$hasErrored) {
  502. $this->emitError(new Error(
  503. 'No code may exist outside of namespace {}', $stmt->getAttributes()));
  504. $hasErrored = true; // Avoid one error for every statement
  505. }
  506. }
  507. return $stmts;
  508. } else {
  509. // For semicolon namespaces we have to move the statements after a namespace declaration into ->stmts
  510. $resultStmts = [];
  511. $targetStmts = &$resultStmts;
  512. $lastNs = null;
  513. foreach ($stmts as $stmt) {
  514. if ($stmt instanceof Node\Stmt\Namespace_) {
  515. if ($lastNs !== null) {
  516. $this->fixupNamespaceAttributes($lastNs);
  517. }
  518. if ($stmt->stmts === null) {
  519. $stmt->stmts = [];
  520. $targetStmts = &$stmt->stmts;
  521. $resultStmts[] = $stmt;
  522. } else {
  523. // This handles the invalid case of mixed style namespaces
  524. $resultStmts[] = $stmt;
  525. $targetStmts = &$resultStmts;
  526. }
  527. $lastNs = $stmt;
  528. } elseif ($stmt instanceof Node\Stmt\HaltCompiler) {
  529. // __halt_compiler() is not moved into the namespace
  530. $resultStmts[] = $stmt;
  531. } else {
  532. $targetStmts[] = $stmt;
  533. }
  534. }
  535. if ($lastNs !== null) {
  536. $this->fixupNamespaceAttributes($lastNs);
  537. }
  538. return $resultStmts;
  539. }
  540. }
  541. private function fixupNamespaceAttributes(Node\Stmt\Namespace_ $stmt): void {
  542. // We moved the statements into the namespace node, as such the end of the namespace node
  543. // needs to be extended to the end of the statements.
  544. if (empty($stmt->stmts)) {
  545. return;
  546. }
  547. // We only move the builtin end attributes here. This is the best we can do with the
  548. // knowledge we have.
  549. $endAttributes = ['endLine', 'endFilePos', 'endTokenPos'];
  550. $lastStmt = $stmt->stmts[count($stmt->stmts) - 1];
  551. foreach ($endAttributes as $endAttribute) {
  552. if ($lastStmt->hasAttribute($endAttribute)) {
  553. $stmt->setAttribute($endAttribute, $lastStmt->getAttribute($endAttribute));
  554. }
  555. }
  556. }
  557. /** @return array<string, mixed> */
  558. private function getNamespaceErrorAttributes(Namespace_ $node): array {
  559. $attrs = $node->getAttributes();
  560. // Adjust end attributes to only cover the "namespace" keyword, not the whole namespace.
  561. if (isset($attrs['startLine'])) {
  562. $attrs['endLine'] = $attrs['startLine'];
  563. }
  564. if (isset($attrs['startTokenPos'])) {
  565. $attrs['endTokenPos'] = $attrs['startTokenPos'];
  566. }
  567. if (isset($attrs['startFilePos'])) {
  568. $attrs['endFilePos'] = $attrs['startFilePos'] + \strlen('namespace') - 1;
  569. }
  570. return $attrs;
  571. }
  572. /**
  573. * Determine namespacing style (semicolon or brace)
  574. *
  575. * @param Node[] $stmts Top-level statements.
  576. *
  577. * @return null|string One of "semicolon", "brace" or null (no namespaces)
  578. */
  579. private function getNamespacingStyle(array $stmts): ?string {
  580. $style = null;
  581. $hasNotAllowedStmts = false;
  582. foreach ($stmts as $i => $stmt) {
  583. if ($stmt instanceof Node\Stmt\Namespace_) {
  584. $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace';
  585. if (null === $style) {
  586. $style = $currentStyle;
  587. if ($hasNotAllowedStmts) {
  588. $this->emitError(new Error(
  589. 'Namespace declaration statement has to be the very first statement in the script',
  590. $this->getNamespaceErrorAttributes($stmt)
  591. ));
  592. }
  593. } elseif ($style !== $currentStyle) {
  594. $this->emitError(new Error(
  595. 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations',
  596. $this->getNamespaceErrorAttributes($stmt)
  597. ));
  598. // Treat like semicolon style for namespace normalization
  599. return 'semicolon';
  600. }
  601. continue;
  602. }
  603. /* declare(), __halt_compiler() and nops can be used before a namespace declaration */
  604. if ($stmt instanceof Node\Stmt\Declare_
  605. || $stmt instanceof Node\Stmt\HaltCompiler
  606. || $stmt instanceof Node\Stmt\Nop) {
  607. continue;
  608. }
  609. /* There may be a hashbang line at the very start of the file */
  610. if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) {
  611. continue;
  612. }
  613. /* Everything else if forbidden before namespace declarations */
  614. $hasNotAllowedStmts = true;
  615. }
  616. return $style;
  617. }
  618. /** @return Name|Identifier */
  619. protected function handleBuiltinTypes(Name $name) {
  620. if (!$name->isUnqualified()) {
  621. return $name;
  622. }
  623. $lowerName = $name->toLowerString();
  624. if (!$this->phpVersion->supportsBuiltinType($lowerName)) {
  625. return $name;
  626. }
  627. return new Node\Identifier($lowerName, $name->getAttributes());
  628. }
  629. /**
  630. * Get combined start and end attributes at a stack location
  631. *
  632. * @param int $stackPos Stack location
  633. *
  634. * @return array<string, mixed> Combined start and end attributes
  635. */
  636. protected function getAttributesAt(int $stackPos): array {
  637. return $this->getAttributes($this->tokenStartStack[$stackPos], $this->tokenEndStack[$stackPos]);
  638. }
  639. protected function getFloatCastKind(string $cast): int {
  640. $cast = strtolower($cast);
  641. if (strpos($cast, 'float') !== false) {
  642. return Double::KIND_FLOAT;
  643. }
  644. if (strpos($cast, 'real') !== false) {
  645. return Double::KIND_REAL;
  646. }
  647. return Double::KIND_DOUBLE;
  648. }
  649. /** @param array<string, mixed> $attributes */
  650. protected function parseLNumber(string $str, array $attributes, bool $allowInvalidOctal = false): Int_ {
  651. try {
  652. return Int_::fromString($str, $attributes, $allowInvalidOctal);
  653. } catch (Error $error) {
  654. $this->emitError($error);
  655. // Use dummy value
  656. return new Int_(0, $attributes);
  657. }
  658. }
  659. /**
  660. * Parse a T_NUM_STRING token into either an integer or string node.
  661. *
  662. * @param string $str Number string
  663. * @param array<string, mixed> $attributes Attributes
  664. *
  665. * @return Int_|String_ Integer or string node.
  666. */
  667. protected function parseNumString(string $str, array $attributes) {
  668. if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) {
  669. return new String_($str, $attributes);
  670. }
  671. $num = +$str;
  672. if (!is_int($num)) {
  673. return new String_($str, $attributes);
  674. }
  675. return new Int_($num, $attributes);
  676. }
  677. /** @param array<string, mixed> $attributes */
  678. protected function stripIndentation(
  679. string $string, int $indentLen, string $indentChar,
  680. bool $newlineAtStart, bool $newlineAtEnd, array $attributes
  681. ): string {
  682. if ($indentLen === 0) {
  683. return $string;
  684. }
  685. $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)';
  686. $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])';
  687. $regex = '/' . $start . '([ \t]*)(' . $end . ')?/';
  688. return preg_replace_callback(
  689. $regex,
  690. function ($matches) use ($indentLen, $indentChar, $attributes) {
  691. $prefix = substr($matches[1], 0, $indentLen);
  692. if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) {
  693. $this->emitError(new Error(
  694. 'Invalid indentation - tabs and spaces cannot be mixed', $attributes
  695. ));
  696. } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) {
  697. $this->emitError(new Error(
  698. 'Invalid body indentation level ' .
  699. '(expecting an indentation level of at least ' . $indentLen . ')',
  700. $attributes
  701. ));
  702. }
  703. return substr($matches[0], strlen($prefix));
  704. },
  705. $string
  706. );
  707. }
  708. /**
  709. * @param string|(Expr|InterpolatedStringPart)[] $contents
  710. * @param array<string, mixed> $attributes
  711. * @param array<string, mixed> $endTokenAttributes
  712. */
  713. protected function parseDocString(
  714. string $startToken, $contents, string $endToken,
  715. array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape
  716. ): Expr {
  717. $kind = strpos($startToken, "'") === false
  718. ? String_::KIND_HEREDOC : String_::KIND_NOWDOC;
  719. $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/';
  720. $result = preg_match($regex, $startToken, $matches);
  721. assert($result === 1);
  722. $label = $matches[1];
  723. $result = preg_match('/\A[ \t]*/', $endToken, $matches);
  724. assert($result === 1);
  725. $indentation = $matches[0];
  726. $attributes['kind'] = $kind;
  727. $attributes['docLabel'] = $label;
  728. $attributes['docIndentation'] = $indentation;
  729. $indentHasSpaces = false !== strpos($indentation, " ");
  730. $indentHasTabs = false !== strpos($indentation, "\t");
  731. if ($indentHasSpaces && $indentHasTabs) {
  732. $this->emitError(new Error(
  733. 'Invalid indentation - tabs and spaces cannot be mixed',
  734. $endTokenAttributes
  735. ));
  736. // Proceed processing as if this doc string is not indented
  737. $indentation = '';
  738. }
  739. $indentLen = \strlen($indentation);
  740. $indentChar = $indentHasSpaces ? " " : "\t";
  741. if (\is_string($contents)) {
  742. if ($contents === '') {
  743. $attributes['rawValue'] = $contents;
  744. return new String_('', $attributes);
  745. }
  746. $contents = $this->stripIndentation(
  747. $contents, $indentLen, $indentChar, true, true, $attributes
  748. );
  749. $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents);
  750. $attributes['rawValue'] = $contents;
  751. if ($kind === String_::KIND_HEREDOC) {
  752. $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape);
  753. }
  754. return new String_($contents, $attributes);
  755. } else {
  756. assert(count($contents) > 0);
  757. if (!$contents[0] instanceof Node\InterpolatedStringPart) {
  758. // If there is no leading encapsed string part, pretend there is an empty one
  759. $this->stripIndentation(
  760. '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes()
  761. );
  762. }
  763. $newContents = [];
  764. foreach ($contents as $i => $part) {
  765. if ($part instanceof Node\InterpolatedStringPart) {
  766. $isLast = $i === \count($contents) - 1;
  767. $part->value = $this->stripIndentation(
  768. $part->value, $indentLen, $indentChar,
  769. $i === 0, $isLast, $part->getAttributes()
  770. );
  771. if ($isLast) {
  772. $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value);
  773. }
  774. $part->setAttribute('rawValue', $part->value);
  775. $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape);
  776. if ('' === $part->value) {
  777. continue;
  778. }
  779. }
  780. $newContents[] = $part;
  781. }
  782. return new InterpolatedString($newContents, $attributes);
  783. }
  784. }
  785. protected function createCommentFromToken(Token $token, int $tokenPos): Comment {
  786. assert($token->id === \T_COMMENT || $token->id == \T_DOC_COMMENT);
  787. return \T_DOC_COMMENT === $token->id
  788. ? new Comment\Doc($token->text, $token->line, $token->pos, $tokenPos,
  789. $token->getEndLine(), $token->getEndPos() - 1, $tokenPos)
  790. : new Comment($token->text, $token->line, $token->pos, $tokenPos,
  791. $token->getEndLine(), $token->getEndPos() - 1, $tokenPos);
  792. }
  793. /**
  794. * Get last comment before the given token position, if any
  795. */
  796. protected function getCommentBeforeToken(int $tokenPos): ?Comment {
  797. while (--$tokenPos >= 0) {
  798. $token = $this->tokens[$tokenPos];
  799. if (!isset($this->dropTokens[$token->id])) {
  800. break;
  801. }
  802. if ($token->id === \T_COMMENT || $token->id === \T_DOC_COMMENT) {
  803. return $this->createCommentFromToken($token, $tokenPos);
  804. }
  805. }
  806. return null;
  807. }
  808. /**
  809. * Create a zero-length nop to capture preceding comments, if any.
  810. */
  811. protected function maybeCreateZeroLengthNop(int $tokenPos): ?Nop {
  812. $comment = $this->getCommentBeforeToken($tokenPos);
  813. if ($comment === null) {
  814. return null;
  815. }
  816. $commentEndLine = $comment->getEndLine();
  817. $commentEndFilePos = $comment->getEndFilePos();
  818. $commentEndTokenPos = $comment->getEndTokenPos();
  819. $attributes = [
  820. 'startLine' => $commentEndLine,
  821. 'endLine' => $commentEndLine,
  822. 'startFilePos' => $commentEndFilePos + 1,
  823. 'endFilePos' => $commentEndFilePos,
  824. 'startTokenPos' => $commentEndTokenPos + 1,
  825. 'endTokenPos' => $commentEndTokenPos,
  826. ];
  827. return new Nop($attributes);
  828. }
  829. protected function maybeCreateNop(int $tokenStartPos, int $tokenEndPos): ?Nop {
  830. if ($this->getCommentBeforeToken($tokenStartPos) === null) {
  831. return null;
  832. }
  833. return new Nop($this->getAttributes($tokenStartPos, $tokenEndPos));
  834. }
  835. protected function handleHaltCompiler(): string {
  836. // Prevent the lexer from returning any further tokens.
  837. $nextToken = $this->tokens[$this->tokenPos + 1];
  838. $this->tokenPos = \count($this->tokens) - 2;
  839. // Return text after __halt_compiler.
  840. return $nextToken->id === \T_INLINE_HTML ? $nextToken->text : '';
  841. }
  842. protected function inlineHtmlHasLeadingNewline(int $stackPos): bool {
  843. $tokenPos = $this->tokenStartStack[$stackPos];
  844. $token = $this->tokens[$tokenPos];
  845. assert($token->id == \T_INLINE_HTML);
  846. if ($tokenPos > 0) {
  847. $prevToken = $this->tokens[$tokenPos - 1];
  848. assert($prevToken->id == \T_CLOSE_TAG);
  849. return false !== strpos($prevToken->text, "\n")
  850. || false !== strpos($prevToken->text, "\r");
  851. }
  852. return true;
  853. }
  854. /**
  855. * @return array<string, mixed>
  856. */
  857. protected function createEmptyElemAttributes(int $tokenPos): array {
  858. return $this->getAttributesForToken($tokenPos);
  859. }
  860. protected function fixupArrayDestructuring(Array_ $node): Expr\List_ {
  861. $this->createdArrays->detach($node);
  862. return new Expr\List_(array_map(function (Node\ArrayItem $item) {
  863. if ($item->value instanceof Expr\Error) {
  864. // We used Error as a placeholder for empty elements, which are legal for destructuring.
  865. return null;
  866. }
  867. if ($item->value instanceof Array_) {
  868. return new Node\ArrayItem(
  869. $this->fixupArrayDestructuring($item->value),
  870. $item->key, $item->byRef, $item->getAttributes());
  871. }
  872. return $item;
  873. }, $node->items), ['kind' => Expr\List_::KIND_ARRAY] + $node->getAttributes());
  874. }
  875. protected function postprocessList(Expr\List_ $node): void {
  876. foreach ($node->items as $i => $item) {
  877. if ($item->value instanceof Expr\Error) {
  878. // We used Error as a placeholder for empty elements, which are legal for destructuring.
  879. $node->items[$i] = null;
  880. }
  881. }
  882. }
  883. /** @param ElseIf_|Else_ $node */
  884. protected function fixupAlternativeElse($node): void {
  885. // Make sure a trailing nop statement carrying comments is part of the node.
  886. $numStmts = \count($node->stmts);
  887. if ($numStmts !== 0 && $node->stmts[$numStmts - 1] instanceof Nop) {
  888. $nopAttrs = $node->stmts[$numStmts - 1]->getAttributes();
  889. if (isset($nopAttrs['endLine'])) {
  890. $node->setAttribute('endLine', $nopAttrs['endLine']);
  891. }
  892. if (isset($nopAttrs['endFilePos'])) {
  893. $node->setAttribute('endFilePos', $nopAttrs['endFilePos']);
  894. }
  895. if (isset($nopAttrs['endTokenPos'])) {
  896. $node->setAttribute('endTokenPos', $nopAttrs['endTokenPos']);
  897. }
  898. }
  899. }
  900. protected function checkClassModifier(int $a, int $b, int $modifierPos): void {
  901. try {
  902. Modifiers::verifyClassModifier($a, $b);
  903. } catch (Error $error) {
  904. $error->setAttributes($this->getAttributesAt($modifierPos));
  905. $this->emitError($error);
  906. }
  907. }
  908. protected function checkModifier(int $a, int $b, int $modifierPos): void {
  909. // Jumping through some hoops here because verifyModifier() is also used elsewhere
  910. try {
  911. Modifiers::verifyModifier($a, $b);
  912. } catch (Error $error) {
  913. $error->setAttributes($this->getAttributesAt($modifierPos));
  914. $this->emitError($error);
  915. }
  916. }
  917. protected function checkParam(Param $node): void {
  918. if ($node->variadic && null !== $node->default) {
  919. $this->emitError(new Error(
  920. 'Variadic parameter cannot have a default value',
  921. $node->default->getAttributes()
  922. ));
  923. }
  924. }
  925. protected function checkTryCatch(TryCatch $node): void {
  926. if (empty($node->catches) && null === $node->finally) {
  927. $this->emitError(new Error(
  928. 'Cannot use try without catch or finally', $node->getAttributes()
  929. ));
  930. }
  931. }
  932. protected function checkNamespace(Namespace_ $node): void {
  933. if (null !== $node->stmts) {
  934. foreach ($node->stmts as $stmt) {
  935. if ($stmt instanceof Namespace_) {
  936. $this->emitError(new Error(
  937. 'Namespace declarations cannot be nested', $stmt->getAttributes()
  938. ));
  939. }
  940. }
  941. }
  942. }
  943. private function checkClassName(?Identifier $name, int $namePos): void {
  944. if (null !== $name && $name->isSpecialClassName()) {
  945. $this->emitError(new Error(
  946. sprintf('Cannot use \'%s\' as class name as it is reserved', $name),
  947. $this->getAttributesAt($namePos)
  948. ));
  949. }
  950. }
  951. /** @param Name[] $interfaces */
  952. private function checkImplementedInterfaces(array $interfaces): void {
  953. foreach ($interfaces as $interface) {
  954. if ($interface->isSpecialClassName()) {
  955. $this->emitError(new Error(
  956. sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
  957. $interface->getAttributes()
  958. ));
  959. }
  960. }
  961. }
  962. protected function checkClass(Class_ $node, int $namePos): void {
  963. $this->checkClassName($node->name, $namePos);
  964. if ($node->extends && $node->extends->isSpecialClassName()) {
  965. $this->emitError(new Error(
  966. sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
  967. $node->extends->getAttributes()
  968. ));
  969. }
  970. $this->checkImplementedInterfaces($node->implements);
  971. }
  972. protected function checkInterface(Interface_ $node, int $namePos): void {
  973. $this->checkClassName($node->name, $namePos);
  974. $this->checkImplementedInterfaces($node->extends);
  975. }
  976. protected function checkEnum(Enum_ $node, int $namePos): void {
  977. $this->checkClassName($node->name, $namePos);
  978. $this->checkImplementedInterfaces($node->implements);
  979. }
  980. protected function checkClassMethod(ClassMethod $node, int $modifierPos): void {
  981. if ($node->flags & Modifiers::STATIC) {
  982. switch ($node->name->toLowerString()) {
  983. case '__construct':
  984. $this->emitError(new Error(
  985. sprintf('Constructor %s() cannot be static', $node->name),
  986. $this->getAttributesAt($modifierPos)));
  987. break;
  988. case '__destruct':
  989. $this->emitError(new Error(
  990. sprintf('Destructor %s() cannot be static', $node->name),
  991. $this->getAttributesAt($modifierPos)));
  992. break;
  993. case '__clone':
  994. $this->emitError(new Error(
  995. sprintf('Clone method %s() cannot be static', $node->name),
  996. $this->getAttributesAt($modifierPos)));
  997. break;
  998. }
  999. }
  1000. if ($node->flags & Modifiers::READONLY) {
  1001. $this->emitError(new Error(
  1002. sprintf('Method %s() cannot be readonly', $node->name),
  1003. $this->getAttributesAt($modifierPos)));
  1004. }
  1005. }
  1006. protected function checkClassConst(ClassConst $node, int $modifierPos): void {
  1007. foreach ([Modifiers::STATIC, Modifiers::ABSTRACT, Modifiers::READONLY] as $modifier) {
  1008. if ($node->flags & $modifier) {
  1009. $this->emitError(new Error(
  1010. "Cannot use '" . Modifiers::toString($modifier) . "' as constant modifier",
  1011. $this->getAttributesAt($modifierPos)));
  1012. }
  1013. }
  1014. }
  1015. protected function checkUseUse(UseItem $node, int $namePos): void {
  1016. if ($node->alias && $node->alias->isSpecialClassName()) {
  1017. $this->emitError(new Error(
  1018. sprintf(
  1019. 'Cannot use %s as %s because \'%2$s\' is a special class name',
  1020. $node->name, $node->alias
  1021. ),
  1022. $this->getAttributesAt($namePos)
  1023. ));
  1024. }
  1025. }
  1026. protected function checkPropertyHooksForMultiProperty(Property $property, int $hookPos): void {
  1027. if (count($property->props) > 1) {
  1028. $this->emitError(new Error(
  1029. 'Cannot use hooks when declaring multiple properties', $this->getAttributesAt($hookPos)));
  1030. }
  1031. }
  1032. /** @param PropertyHook[] $hooks */
  1033. protected function checkEmptyPropertyHookList(array $hooks, int $hookPos): void {
  1034. if (empty($hooks)) {
  1035. $this->emitError(new Error(
  1036. 'Property hook list cannot be empty', $this->getAttributesAt($hookPos)));
  1037. }
  1038. }
  1039. protected function checkPropertyHook(PropertyHook $hook, ?int $paramListPos): void {
  1040. $name = $hook->name->toLowerString();
  1041. if ($name !== 'get' && $name !== 'set') {
  1042. $this->emitError(new Error(
  1043. 'Unknown hook "' . $hook->name . '", expected "get" or "set"',
  1044. $hook->name->getAttributes()));
  1045. }
  1046. if ($name === 'get' && $paramListPos !== null) {
  1047. $this->emitError(new Error(
  1048. 'get hook must not have a parameter list', $this->getAttributesAt($paramListPos)));
  1049. }
  1050. }
  1051. protected function checkPropertyHookModifiers(int $a, int $b, int $modifierPos): void {
  1052. try {
  1053. Modifiers::verifyModifier($a, $b);
  1054. } catch (Error $error) {
  1055. $error->setAttributes($this->getAttributesAt($modifierPos));
  1056. $this->emitError($error);
  1057. }
  1058. if ($b != Modifiers::FINAL) {
  1059. $this->emitError(new Error(
  1060. 'Cannot use the ' . Modifiers::toString($b) . ' modifier on a property hook',
  1061. $this->getAttributesAt($modifierPos)));
  1062. }
  1063. }
  1064. protected function checkConstantAttributes(Const_ $node): void {
  1065. if ($node->attrGroups !== [] && count($node->consts) > 1) {
  1066. $this->emitError(new Error(
  1067. 'Cannot use attributes on multiple constants at once', $node->getAttributes()));
  1068. }
  1069. }
  1070. /**
  1071. * @param Property|Param $node
  1072. */
  1073. protected function addPropertyNameToHooks(Node $node): void {
  1074. if ($node instanceof Property) {
  1075. $name = $node->props[0]->name->toString();
  1076. } else {
  1077. $name = $node->var->name;
  1078. }
  1079. foreach ($node->hooks as $hook) {
  1080. $hook->setAttribute('propertyName', $name);
  1081. }
  1082. }
  1083. /** @param array<Node\Arg|Node\VariadicPlaceholder> $args */
  1084. private function isSimpleExit(array $args): bool {
  1085. if (\count($args) === 0) {
  1086. return true;
  1087. }
  1088. if (\count($args) === 1) {
  1089. $arg = $args[0];
  1090. return $arg instanceof Arg && $arg->name === null &&
  1091. $arg->byRef === false && $arg->unpack === false;
  1092. }
  1093. return false;
  1094. }
  1095. /**
  1096. * @param array<Node\Arg|Node\VariadicPlaceholder> $args
  1097. * @param array<string, mixed> $attrs
  1098. */
  1099. protected function createExitExpr(string $name, int $namePos, array $args, array $attrs): Expr {
  1100. if ($this->isSimpleExit($args)) {
  1101. // Create Exit node for backwards compatibility.
  1102. $attrs['kind'] = strtolower($name) === 'exit' ? Expr\Exit_::KIND_EXIT : Expr\Exit_::KIND_DIE;
  1103. return new Expr\Exit_(\count($args) === 1 ? $args[0]->value : null, $attrs);
  1104. }
  1105. return new Expr\FuncCall(new Name($name, $this->getAttributesAt($namePos)), $args, $attrs);
  1106. }
  1107. /**
  1108. * Creates the token map.
  1109. *
  1110. * The token map maps the PHP internal token identifiers
  1111. * to the identifiers used by the Parser. Additionally it
  1112. * maps T_OPEN_TAG_WITH_ECHO to T_ECHO and T_CLOSE_TAG to ';'.
  1113. *
  1114. * @return array<int, int> The token map
  1115. */
  1116. protected function createTokenMap(): array {
  1117. $tokenMap = [];
  1118. // Single-char tokens use an identity mapping.
  1119. for ($i = 0; $i < 256; ++$i) {
  1120. $tokenMap[$i] = $i;
  1121. }
  1122. foreach ($this->symbolToName as $name) {
  1123. if ($name[0] === 'T') {
  1124. $tokenMap[\constant($name)] = constant(static::class . '::' . $name);
  1125. }
  1126. }
  1127. // T_OPEN_TAG_WITH_ECHO with dropped T_OPEN_TAG results in T_ECHO
  1128. $tokenMap[\T_OPEN_TAG_WITH_ECHO] = static::T_ECHO;
  1129. // T_CLOSE_TAG is equivalent to ';'
  1130. $tokenMap[\T_CLOSE_TAG] = ord(';');
  1131. // We have created a map from PHP token IDs to external symbol IDs.
  1132. // Now map them to the internal symbol ID.
  1133. $fullTokenMap = [];
  1134. foreach ($tokenMap as $phpToken => $extSymbol) {
  1135. $intSymbol = $this->tokenToSymbol[$extSymbol];
  1136. if ($intSymbol === $this->invalidSymbol) {
  1137. continue;
  1138. }
  1139. $fullTokenMap[$phpToken] = $intSymbol;
  1140. }
  1141. return $fullTokenMap;
  1142. }
  1143. }