Emulative.php 8.3 KB

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