Emulative.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php declare(strict_types=1);
  2. namespace PhpParser\Lexer;
  3. use PhpParser\Error;
  4. use PhpParser\ErrorHandler;
  5. use PhpParser\Parser;
  6. class Emulative extends \PhpParser\Lexer
  7. {
  8. const PHP_7_3 = '7.3.0dev';
  9. const PHP_7_4 = '7.4.0dev';
  10. const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX'
  11. /<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n
  12. (?:.*\r?\n)*?
  13. (?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x
  14. REGEX;
  15. const T_COALESCE_EQUAL = 1007;
  16. /**
  17. * @var mixed[] Patches used to reverse changes introduced in the code
  18. */
  19. private $patches = [];
  20. /**
  21. * @param mixed[] $options
  22. */
  23. public function __construct(array $options = [])
  24. {
  25. parent::__construct($options);
  26. // add emulated tokens here
  27. $this->tokenMap[self::T_COALESCE_EQUAL] = Parser\Tokens::T_COALESCE_EQUAL;
  28. }
  29. public function startLexing(string $code, ErrorHandler $errorHandler = null) {
  30. $this->patches = [];
  31. if ($this->isEmulationNeeded($code) === false) {
  32. // Nothing to emulate, yay
  33. parent::startLexing($code, $errorHandler);
  34. return;
  35. }
  36. $collector = new ErrorHandler\Collecting();
  37. // 1. emulation of heredoc and nowdoc new syntax
  38. $preparedCode = $this->processHeredocNowdoc($code);
  39. parent::startLexing($preparedCode, $collector);
  40. // 2. emulation of ??= token
  41. $this->processCoaleseEqual($code);
  42. $this->fixupTokens();
  43. $errors = $collector->getErrors();
  44. if (!empty($errors)) {
  45. $this->fixupErrors($errors);
  46. foreach ($errors as $error) {
  47. $errorHandler->handleError($error);
  48. }
  49. }
  50. }
  51. private function isCoalesceEqualEmulationNeeded(string $code): bool
  52. {
  53. // skip version where this works without emulation
  54. if (version_compare(\PHP_VERSION, self::PHP_7_4, '>=')) {
  55. return false;
  56. }
  57. return strpos($code, '??=') !== false;
  58. }
  59. private function processCoaleseEqual(string $code)
  60. {
  61. if ($this->isCoalesceEqualEmulationNeeded($code) === false) {
  62. return;
  63. }
  64. // We need to manually iterate and manage a count because we'll change
  65. // the tokens array on the way
  66. $line = 1;
  67. for ($i = 0, $c = count($this->tokens); $i < $c; ++$i) {
  68. if (isset($this->tokens[$i + 1])) {
  69. if ($this->tokens[$i][0] === T_COALESCE && $this->tokens[$i + 1] === '=') {
  70. array_splice($this->tokens, $i, 2, [
  71. [self::T_COALESCE_EQUAL, '??=', $line]
  72. ]);
  73. $c--;
  74. continue;
  75. }
  76. }
  77. if (\is_array($this->tokens[$i])) {
  78. $line += substr_count($this->tokens[$i][1], "\n");
  79. }
  80. }
  81. }
  82. private function isHeredocNowdocEmulationNeeded(string $code): bool
  83. {
  84. // skip version where this works without emulation
  85. if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) {
  86. return false;
  87. }
  88. return strpos($code, '<<<') !== false;
  89. }
  90. private function processHeredocNowdoc(string $code): string
  91. {
  92. if ($this->isHeredocNowdocEmulationNeeded($code) === false) {
  93. return $code;
  94. }
  95. if (!preg_match_all(self::FLEXIBLE_DOC_STRING_REGEX, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
  96. // No heredoc/nowdoc found
  97. return $code;
  98. }
  99. // Keep track of how much we need to adjust string offsets due to the modifications we
  100. // already made
  101. $posDelta = 0;
  102. foreach ($matches as $match) {
  103. $indentation = $match['indentation'][0];
  104. $indentationStart = $match['indentation'][1];
  105. $separator = $match['separator'][0];
  106. $separatorStart = $match['separator'][1];
  107. if ($indentation === '' && $separator !== '') {
  108. // Ordinary heredoc/nowdoc
  109. continue;
  110. }
  111. if ($indentation !== '') {
  112. // Remove indentation
  113. $indentationLen = strlen($indentation);
  114. $code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen);
  115. $this->patches[] = [$indentationStart + $posDelta, 'add', $indentation];
  116. $posDelta -= $indentationLen;
  117. }
  118. if ($separator === '') {
  119. // Insert newline as separator
  120. $code = substr_replace($code, "\n", $separatorStart + $posDelta, 0);
  121. $this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"];
  122. $posDelta += 1;
  123. }
  124. }
  125. return $code;
  126. }
  127. private function isEmulationNeeded(string $code): bool
  128. {
  129. if ($this->isHeredocNowdocEmulationNeeded($code)) {
  130. return true;
  131. }
  132. if ($this->isCoalesceEqualEmulationNeeded($code)) {
  133. return true;
  134. }
  135. return false;
  136. }
  137. private function fixupTokens()
  138. {
  139. if (\count($this->patches) === 0) {
  140. return;
  141. }
  142. // Load first patch
  143. $patchIdx = 0;
  144. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  145. // We use a manual loop over the tokens, because we modify the array on the fly
  146. $pos = 0;
  147. for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) {
  148. $token = $this->tokens[$i];
  149. if (\is_string($token)) {
  150. // We assume that patches don't apply to string tokens
  151. $pos += \strlen($token);
  152. continue;
  153. }
  154. $len = \strlen($token[1]);
  155. $posDelta = 0;
  156. while ($patchPos >= $pos && $patchPos < $pos + $len) {
  157. $patchTextLen = \strlen($patchText);
  158. if ($patchType === 'remove') {
  159. if ($patchPos === $pos && $patchTextLen === $len) {
  160. // Remove token entirely
  161. array_splice($this->tokens, $i, 1, []);
  162. $i--;
  163. $c--;
  164. } else {
  165. // Remove from token string
  166. $this->tokens[$i][1] = substr_replace(
  167. $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen
  168. );
  169. $posDelta -= $patchTextLen;
  170. }
  171. } elseif ($patchType === 'add') {
  172. // Insert into the token string
  173. $this->tokens[$i][1] = substr_replace(
  174. $token[1], $patchText, $patchPos - $pos + $posDelta, 0
  175. );
  176. $posDelta += $patchTextLen;
  177. } else {
  178. assert(false);
  179. }
  180. // Fetch the next patch
  181. $patchIdx++;
  182. if ($patchIdx >= \count($this->patches)) {
  183. // No more patches, we're done
  184. return;
  185. }
  186. list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
  187. // Multiple patches may apply to the same token. Reload the current one to check
  188. // If the new patch applies
  189. $token = $this->tokens[$i];
  190. }
  191. $pos += $len;
  192. }
  193. // A patch did not apply
  194. assert(false);
  195. }
  196. /**
  197. * Fixup line and position information in errors.
  198. *
  199. * @param Error[] $errors
  200. */
  201. private function fixupErrors(array $errors) {
  202. foreach ($errors as $error) {
  203. $attrs = $error->getAttributes();
  204. $posDelta = 0;
  205. $lineDelta = 0;
  206. foreach ($this->patches as $patch) {
  207. list($patchPos, $patchType, $patchText) = $patch;
  208. if ($patchPos >= $attrs['startFilePos']) {
  209. // No longer relevant
  210. break;
  211. }
  212. if ($patchType === 'add') {
  213. $posDelta += strlen($patchText);
  214. $lineDelta += substr_count($patchText, "\n");
  215. } else {
  216. $posDelta -= strlen($patchText);
  217. $lineDelta -= substr_count($patchText, "\n");
  218. }
  219. }
  220. $attrs['startFilePos'] += $posDelta;
  221. $attrs['endFilePos'] += $posDelta;
  222. $attrs['startLine'] += $lineDelta;
  223. $attrs['endLine'] += $lineDelta;
  224. $error->setAttributes($attrs);
  225. }
  226. }
  227. }