Emulative.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php declare(strict_types=1);
  2. namespace PhpParser\Lexer;
  3. use PhpParser\Error;
  4. use PhpParser\ErrorHandler;
  5. use PhpParser\Lexer;
  6. use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
  7. use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
  8. use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
  9. use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
  10. use PhpParser\Lexer\TokenEmulator\FlexibleDocStringEmulator;
  11. use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
  12. use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
  13. use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
  14. use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
  15. use PhpParser\Lexer\TokenEmulator\ReadonlyFunctionTokenEmulator;
  16. use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
  17. use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
  18. use PhpParser\Lexer\TokenEmulator\TokenEmulator;
  19. use PhpParser\PhpVersion;
  20. use PhpParser\Token;
  21. class Emulative extends Lexer {
  22. /** @var array{int, string, string}[] Patches used to reverse changes introduced in the code */
  23. private array $patches = [];
  24. /** @var list<TokenEmulator> */
  25. private array $emulators = [];
  26. private PhpVersion $targetPhpVersion;
  27. private PhpVersion $hostPhpVersion;
  28. /**
  29. * @param PhpVersion|null $phpVersion PHP version to emulate. Defaults to newest supported.
  30. */
  31. public function __construct(?PhpVersion $phpVersion = null) {
  32. $this->targetPhpVersion = $phpVersion ?? PhpVersion::getNewestSupported();
  33. $this->hostPhpVersion = PhpVersion::getHostVersion();
  34. $emulators = [
  35. new MatchTokenEmulator(),
  36. new NullsafeTokenEmulator(),
  37. new AttributeEmulator(),
  38. new EnumTokenEmulator(),
  39. new ReadonlyTokenEmulator(),
  40. new ExplicitOctalEmulator(),
  41. new ReadonlyFunctionTokenEmulator(),
  42. ];
  43. // Collect emulators that are relevant for the PHP version we're running
  44. // and the PHP version we're targeting for emulation.
  45. foreach ($emulators as $emulator) {
  46. $emulatorPhpVersion = $emulator->getPhpVersion();
  47. if ($this->isForwardEmulationNeeded($emulatorPhpVersion)) {
  48. $this->emulators[] = $emulator;
  49. } elseif ($this->isReverseEmulationNeeded($emulatorPhpVersion)) {
  50. $this->emulators[] = new ReverseEmulator($emulator);
  51. }
  52. }
  53. }
  54. public function tokenize(string $code, ?ErrorHandler $errorHandler = null): array {
  55. $emulators = array_filter($this->emulators, function ($emulator) use ($code) {
  56. return $emulator->isEmulationNeeded($code);
  57. });
  58. if (empty($emulators)) {
  59. // Nothing to emulate, yay
  60. return parent::tokenize($code, $errorHandler);
  61. }
  62. if ($errorHandler === null) {
  63. $errorHandler = new ErrorHandler\Throwing();
  64. }
  65. $this->patches = [];
  66. foreach ($emulators as $emulator) {
  67. $code = $emulator->preprocessCode($code, $this->patches);
  68. }
  69. $collector = new ErrorHandler\Collecting();
  70. $tokens = parent::tokenize($code, $collector);
  71. $this->sortPatches();
  72. $tokens = $this->fixupTokens($tokens);
  73. $errors = $collector->getErrors();
  74. if (!empty($errors)) {
  75. $this->fixupErrors($errors);
  76. foreach ($errors as $error) {
  77. $errorHandler->handleError($error);
  78. }
  79. }
  80. foreach ($emulators as $emulator) {
  81. $tokens = $emulator->emulate($code, $tokens);
  82. }
  83. return $tokens;
  84. }
  85. private function isForwardEmulationNeeded(PhpVersion $emulatorPhpVersion): bool {
  86. return $this->hostPhpVersion->older($emulatorPhpVersion)
  87. && $this->targetPhpVersion->newerOrEqual($emulatorPhpVersion);
  88. }
  89. private function isReverseEmulationNeeded(PhpVersion $emulatorPhpVersion): bool {
  90. return $this->hostPhpVersion->newerOrEqual($emulatorPhpVersion)
  91. && $this->targetPhpVersion->older($emulatorPhpVersion);
  92. }
  93. private function sortPatches(): void {
  94. // Patches may be contributed by different emulators.
  95. // Make sure they are sorted by increasing patch position.
  96. usort($this->patches, function ($p1, $p2) {
  97. return $p1[0] <=> $p2[0];
  98. });
  99. }
  100. /**
  101. * @param list<Token> $tokens
  102. * @return list<Token>
  103. */
  104. private function fixupTokens(array $tokens): array {
  105. if (\count($this->patches) === 0) {
  106. return $tokens;
  107. }
  108. // Load first patch
  109. $patchIdx = 0;
  110. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  111. // We use a manual loop over the tokens, because we modify the array on the fly
  112. $posDelta = 0;
  113. $lineDelta = 0;
  114. for ($i = 0, $c = \count($tokens); $i < $c; $i++) {
  115. $token = $tokens[$i];
  116. $pos = $token->pos;
  117. $token->pos += $posDelta;
  118. $token->line += $lineDelta;
  119. $localPosDelta = 0;
  120. $len = \strlen($token->text);
  121. while ($patchPos >= $pos && $patchPos < $pos + $len) {
  122. $patchTextLen = \strlen($patchText);
  123. if ($patchType === 'remove') {
  124. if ($patchPos === $pos && $patchTextLen === $len) {
  125. // Remove token entirely
  126. array_splice($tokens, $i, 1, []);
  127. $i--;
  128. $c--;
  129. } else {
  130. // Remove from token string
  131. $token->text = substr_replace(
  132. $token->text, '', $patchPos - $pos + $localPosDelta, $patchTextLen
  133. );
  134. $localPosDelta -= $patchTextLen;
  135. }
  136. $lineDelta -= \substr_count($patchText, "\n");
  137. } elseif ($patchType === 'add') {
  138. // Insert into the token string
  139. $token->text = substr_replace(
  140. $token->text, $patchText, $patchPos - $pos + $localPosDelta, 0
  141. );
  142. $localPosDelta += $patchTextLen;
  143. $lineDelta += \substr_count($patchText, "\n");
  144. } elseif ($patchType === 'replace') {
  145. // Replace inside the token string
  146. $token->text = substr_replace(
  147. $token->text, $patchText, $patchPos - $pos + $localPosDelta, $patchTextLen
  148. );
  149. } else {
  150. assert(false);
  151. }
  152. // Fetch the next patch
  153. $patchIdx++;
  154. if ($patchIdx >= \count($this->patches)) {
  155. // No more patches. However, we still need to adjust position.
  156. $patchPos = \PHP_INT_MAX;
  157. break;
  158. }
  159. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  160. }
  161. $posDelta += $localPosDelta;
  162. }
  163. return $tokens;
  164. }
  165. /**
  166. * Fixup line and position information in errors.
  167. *
  168. * @param Error[] $errors
  169. */
  170. private function fixupErrors(array $errors): void {
  171. foreach ($errors as $error) {
  172. $attrs = $error->getAttributes();
  173. $posDelta = 0;
  174. $lineDelta = 0;
  175. foreach ($this->patches as $patch) {
  176. list($patchPos, $patchType, $patchText) = $patch;
  177. if ($patchPos >= $attrs['startFilePos']) {
  178. // No longer relevant
  179. break;
  180. }
  181. if ($patchType === 'add') {
  182. $posDelta += strlen($patchText);
  183. $lineDelta += substr_count($patchText, "\n");
  184. } elseif ($patchType === 'remove') {
  185. $posDelta -= strlen($patchText);
  186. $lineDelta -= substr_count($patchText, "\n");
  187. }
  188. }
  189. $attrs['startFilePos'] += $posDelta;
  190. $attrs['endFilePos'] += $posDelta;
  191. $attrs['startLine'] += $lineDelta;
  192. $attrs['endLine'] += $lineDelta;
  193. $error->setAttributes($attrs);
  194. }
  195. }
  196. }