Emulative.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php declare(strict_types=1);
  2. namespace PhpParser\Lexer;
  3. use PhpParser\Error;
  4. use PhpParser\ErrorHandler;
  5. class Emulative extends \PhpParser\Lexer
  6. {
  7. const PHP_7_3 = '7.3.0dev';
  8. /**
  9. * @var array Patches used to reverse changes introduced in the code
  10. */
  11. private $patches;
  12. public function startLexing(string $code, ErrorHandler $errorHandler = null) {
  13. $this->patches = [];
  14. $preparedCode = $this->prepareCode($code);
  15. if (null === $preparedCode) {
  16. // Nothing to emulate, yay
  17. parent::startLexing($code, $errorHandler);
  18. return;
  19. }
  20. $collector = new ErrorHandler\Collecting();
  21. parent::startLexing($preparedCode, $collector);
  22. $this->fixupTokens();
  23. $errors = $collector->getErrors();
  24. if (!empty($errors)) {
  25. $this->fixupErrors($errors);
  26. foreach ($errors as $error) {
  27. $errorHandler->handleError($error);
  28. }
  29. }
  30. }
  31. /**
  32. * Prepares code for emulation. If nothing has to be emulated null is returned.
  33. *
  34. * @param string $code
  35. * @return null|string
  36. */
  37. private function prepareCode(string $code) {
  38. if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) {
  39. return null;
  40. }
  41. if (strpos($code, '<<<') === false) {
  42. // Definitely doesn't contain heredoc/nowdoc
  43. return null;
  44. }
  45. $flexibleDocStringRegex = <<<'REGEX'
  46. /<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
  47. (?:.*\r?\n)*?
  48. (?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x
  49. REGEX;
  50. if (!preg_match_all($flexibleDocStringRegex, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
  51. // No heredoc/nowdoc found
  52. return null;
  53. }
  54. // Keep track of how much we need to adjust string offsets due to the modifications we
  55. // already made
  56. $posDelta = 0;
  57. foreach ($matches as $match) {
  58. $indentation = $match['indentation'][0];
  59. $indentationStart = $match['indentation'][1];
  60. $separator = $match['separator'][0];
  61. $separatorStart = $match['separator'][1];
  62. if ($indentation === '' && $separator !== '') {
  63. // Ordinary heredoc/nowdoc
  64. continue;
  65. }
  66. if ($indentation !== '') {
  67. // Remove indentation
  68. $indentationLen = strlen($indentation);
  69. $code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen);
  70. $this->patches[] = [$indentationStart + $posDelta, 'add', $indentation];
  71. $posDelta -= $indentationLen;
  72. }
  73. if ($separator === '') {
  74. // Insert newline as separator
  75. $code = substr_replace($code, "\n", $separatorStart + $posDelta, 0);
  76. $this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"];
  77. $posDelta += 1;
  78. }
  79. }
  80. if (empty($this->patches)) {
  81. // We did not end up emulating anything
  82. return null;
  83. }
  84. return $code;
  85. }
  86. private function fixupTokens() {
  87. assert(count($this->patches) > 0);
  88. // Load first patch
  89. $patchIdx = 0;
  90. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  91. // We use a manual loop over the tokens, because we modify the array on the fly
  92. $pos = 0;
  93. for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
  94. $token = $this->tokens[$i];
  95. if (\is_string($token)) {
  96. // We assume that patches don't apply to string tokens
  97. $pos += \strlen($token);
  98. continue;
  99. }
  100. $len = \strlen($token[1]);
  101. $posDelta = 0;
  102. while ($patchPos >= $pos && $patchPos < $pos + $len) {
  103. $patchTextLen = \strlen($patchText);
  104. if ($patchType === 'remove') {
  105. if ($patchPos === $pos && $patchTextLen === $len) {
  106. // Remove token entirely
  107. array_splice($this->tokens, $i, 1, []);
  108. $i--;
  109. $c--;
  110. } else {
  111. // Remove from token string
  112. $this->tokens[$i][1] = substr_replace(
  113. $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
  114. );
  115. $posDelta -= $patchTextLen;
  116. }
  117. } elseif ($patchType === 'add') {
  118. // Insert into the token string
  119. $this->tokens[$i][1] = substr_replace(
  120. $token[1], $patchText, $patchPos - $pos + $posDelta, 0
  121. );
  122. $posDelta += $patchTextLen;
  123. } else {
  124. assert(false);
  125. }
  126. // Fetch the next patch
  127. $patchIdx++;
  128. if ($patchIdx >= \count($this->patches)) {
  129. // No more patches, we're done
  130. return;
  131. }
  132. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  133. // Multiple patches may apply to the same token. Reload the current one to check
  134. // If the new patch applies
  135. $token = $this->tokens[$i];
  136. }
  137. $pos += $len;
  138. }
  139. // A patch did not apply
  140. assert(false);
  141. }
  142. /**
  143. * Fixup line and position information in errors.
  144. *
  145. * @param Error[] $errors
  146. */
  147. private function fixupErrors(array $errors) {
  148. foreach ($errors as $error) {
  149. $attrs = $error->getAttributes();
  150. $posDelta = 0;
  151. $lineDelta = 0;
  152. foreach ($this->patches as $patch) {
  153. list($patchPos, $patchType, $patchText) = $patch;
  154. if ($patchPos >= $attrs['startFilePos']) {
  155. // No longer relevant
  156. break;
  157. }
  158. if ($patchType === 'add') {
  159. $posDelta += strlen($patchText);
  160. $lineDelta += substr_count($patchText, "\n");
  161. } else {
  162. $posDelta -= strlen($patchText);
  163. $lineDelta -= substr_count($patchText, "\n");
  164. }
  165. }
  166. $attrs['startFilePos'] += $posDelta;
  167. $attrs['endFilePos'] += $posDelta;
  168. $attrs['startLine'] += $lineDelta;
  169. $attrs['endLine'] += $lineDelta;
  170. $error->setAttributes($attrs);
  171. }
  172. }
  173. }