Emulative.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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\ReadonlyTokenEmulator;
  16. use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
  17. use PhpParser\Lexer\TokenEmulator\TokenEmulator;
  18. class Emulative extends Lexer
  19. {
  20. const PHP_7_3 = '7.3dev';
  21. const PHP_7_4 = '7.4dev';
  22. const PHP_8_0 = '8.0dev';
  23. const PHP_8_1 = '8.1dev';
  24. /** @var mixed[] Patches used to reverse changes introduced in the code */
  25. private $patches = [];
  26. /** @var TokenEmulator[] */
  27. private $emulators = [];
  28. /** @var string */
  29. private $targetPhpVersion;
  30. /**
  31. * @param mixed[] $options Lexer options. In addition to the usual options,
  32. * accepts a 'phpVersion' string that specifies the
  33. * version to emulate. Defaults to newest supported.
  34. */
  35. public function __construct(array $options = [])
  36. {
  37. $this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_1;
  38. unset($options['phpVersion']);
  39. parent::__construct($options);
  40. $emulators = [
  41. new FlexibleDocStringEmulator(),
  42. new FnTokenEmulator(),
  43. new MatchTokenEmulator(),
  44. new CoaleseEqualTokenEmulator(),
  45. new NumericLiteralSeparatorEmulator(),
  46. new NullsafeTokenEmulator(),
  47. new AttributeEmulator(),
  48. new EnumTokenEmulator(),
  49. new ReadonlyTokenEmulator(),
  50. new ExplicitOctalEmulator(),
  51. ];
  52. // Collect emulators that are relevant for the PHP version we're running
  53. // and the PHP version we're targeting for emulation.
  54. foreach ($emulators as $emulator) {
  55. $emulatorPhpVersion = $emulator->getPhpVersion();
  56. if ($this->isForwardEmulationNeeded($emulatorPhpVersion)) {
  57. $this->emulators[] = $emulator;
  58. } else if ($this->isReverseEmulationNeeded($emulatorPhpVersion)) {
  59. $this->emulators[] = new ReverseEmulator($emulator);
  60. }
  61. }
  62. }
  63. public function startLexing(string $code, ErrorHandler $errorHandler = null) {
  64. $emulators = array_filter($this->emulators, function($emulator) use($code) {
  65. return $emulator->isEmulationNeeded($code);
  66. });
  67. if (empty($emulators)) {
  68. // Nothing to emulate, yay
  69. parent::startLexing($code, $errorHandler);
  70. return;
  71. }
  72. $this->patches = [];
  73. foreach ($emulators as $emulator) {
  74. $code = $emulator->preprocessCode($code, $this->patches);
  75. }
  76. $collector = new ErrorHandler\Collecting();
  77. parent::startLexing($code, $collector);
  78. $this->sortPatches();
  79. $this->fixupTokens();
  80. $errors = $collector->getErrors();
  81. if (!empty($errors)) {
  82. $this->fixupErrors($errors);
  83. foreach ($errors as $error) {
  84. $errorHandler->handleError($error);
  85. }
  86. }
  87. foreach ($emulators as $emulator) {
  88. $this->tokens = $emulator->emulate($code, $this->tokens);
  89. }
  90. }
  91. private function isForwardEmulationNeeded(string $emulatorPhpVersion): bool {
  92. return version_compare(\PHP_VERSION, $emulatorPhpVersion, '<')
  93. && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '>=');
  94. }
  95. private function isReverseEmulationNeeded(string $emulatorPhpVersion): bool {
  96. return version_compare(\PHP_VERSION, $emulatorPhpVersion, '>=')
  97. && version_compare($this->targetPhpVersion, $emulatorPhpVersion, '<');
  98. }
  99. private function sortPatches()
  100. {
  101. // Patches may be contributed by different emulators.
  102. // Make sure they are sorted by increasing patch position.
  103. usort($this->patches, function($p1, $p2) {
  104. return $p1[0] <=> $p2[0];
  105. });
  106. }
  107. private function fixupTokens()
  108. {
  109. if (\count($this->patches) === 0) {
  110. return;
  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. $pos = 0;
  117. for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
  118. $token = $this->tokens[$i];
  119. if (\is_string($token)) {
  120. if ($patchPos === $pos) {
  121. // Only support replacement for string tokens.
  122. assert($patchType === 'replace');
  123. $this->tokens[$i] = $patchText;
  124. // Fetch the next patch
  125. $patchIdx++;
  126. if ($patchIdx >= \count($this->patches)) {
  127. // No more patches, we're done
  128. return;
  129. }
  130. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  131. }
  132. $pos += \strlen($token);
  133. continue;
  134. }
  135. $len = \strlen($token[1]);
  136. $posDelta = 0;
  137. while ($patchPos >= $pos && $patchPos < $pos + $len) {
  138. $patchTextLen = \strlen($patchText);
  139. if ($patchType === 'remove') {
  140. if ($patchPos === $pos && $patchTextLen === $len) {
  141. // Remove token entirely
  142. array_splice($this->tokens, $i, 1, []);
  143. $i--;
  144. $c--;
  145. } else {
  146. // Remove from token string
  147. $this->tokens[$i][1] = substr_replace(
  148. $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
  149. );
  150. $posDelta -= $patchTextLen;
  151. }
  152. } elseif ($patchType === 'add') {
  153. // Insert into the token string
  154. $this->tokens[$i][1] = substr_replace(
  155. $token[1], $patchText, $patchPos - $pos + $posDelta, 0
  156. );
  157. $posDelta += $patchTextLen;
  158. } else if ($patchType === 'replace') {
  159. // Replace inside the token string
  160. $this->tokens[$i][1] = substr_replace(
  161. $token[1], $patchText, $patchPos - $pos + $posDelta, $patchTextLen
  162. );
  163. } else {
  164. assert(false);
  165. }
  166. // Fetch the next patch
  167. $patchIdx++;
  168. if ($patchIdx >= \count($this->patches)) {
  169. // No more patches, we're done
  170. return;
  171. }
  172. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  173. // Multiple patches may apply to the same token. Reload the current one to check
  174. // If the new patch applies
  175. $token = $this->tokens[$i];
  176. }
  177. $pos += $len;
  178. }
  179. // A patch did not apply
  180. assert(false);
  181. }
  182. /**
  183. * Fixup line and position information in errors.
  184. *
  185. * @param Error[] $errors
  186. */
  187. private function fixupErrors(array $errors) {
  188. foreach ($errors as $error) {
  189. $attrs = $error->getAttributes();
  190. $posDelta = 0;
  191. $lineDelta = 0;
  192. foreach ($this->patches as $patch) {
  193. list($patchPos, $patchType, $patchText) = $patch;
  194. if ($patchPos >= $attrs['startFilePos']) {
  195. // No longer relevant
  196. break;
  197. }
  198. if ($patchType === 'add') {
  199. $posDelta += strlen($patchText);
  200. $lineDelta += substr_count($patchText, "\n");
  201. } else if ($patchType === 'remove') {
  202. $posDelta -= strlen($patchText);
  203. $lineDelta -= substr_count($patchText, "\n");
  204. }
  205. }
  206. $attrs['startFilePos'] += $posDelta;
  207. $attrs['endFilePos'] += $posDelta;
  208. $attrs['startLine'] += $lineDelta;
  209. $attrs['endLine'] += $lineDelta;
  210. $error->setAttributes($attrs);
  211. }
  212. }
  213. }