Mercurial > hg > isophonics-drupal-site
diff vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php @ 17:129ea1e6d783
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:21:36 +0000 |
parents | 5fb285c0d0e3 |
children |
line wrap: on
line diff
--- a/vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php Tue Jul 10 15:07:59 2018 +0100 +++ b/vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php Thu Feb 28 13:21:36 2019 +0000 @@ -2,7 +2,271 @@ namespace PhpParser\Lexer; +use PhpParser\Error; +use PhpParser\ErrorHandler; +use PhpParser\Parser; + class Emulative extends \PhpParser\Lexer { - /* No features requiring emulation have been added in PHP > 7.0 */ + const PHP_7_3 = '7.3.0dev'; + const PHP_7_4 = '7.4.0dev'; + + const FLEXIBLE_DOC_STRING_REGEX = <<<'REGEX' +/<<<[ \t]*(['"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\1\r?\n +(?:.*\r?\n)*? +(?<indentation>\h*)\2(?![a-zA-Z_\x80-\xff])(?<separator>(?:;?[\r\n])?)/x +REGEX; + + const T_COALESCE_EQUAL = 1007; + + /** + * @var mixed[] Patches used to reverse changes introduced in the code + */ + private $patches = []; + + /** + * @param mixed[] $options + */ + public function __construct(array $options = []) + { + parent::__construct($options); + + // add emulated tokens here + $this->tokenMap[self::T_COALESCE_EQUAL] = Parser\Tokens::T_COALESCE_EQUAL; + } + + public function startLexing(string $code, ErrorHandler $errorHandler = null) { + $this->patches = []; + + if ($this->isEmulationNeeded($code) === false) { + // Nothing to emulate, yay + parent::startLexing($code, $errorHandler); + return; + } + + $collector = new ErrorHandler\Collecting(); + + // 1. emulation of heredoc and nowdoc new syntax + $preparedCode = $this->processHeredocNowdoc($code); + parent::startLexing($preparedCode, $collector); + + // 2. emulation of ??= token + $this->processCoaleseEqual($code); + $this->fixupTokens(); + + $errors = $collector->getErrors(); + if (!empty($errors)) { + $this->fixupErrors($errors); + foreach ($errors as $error) { + $errorHandler->handleError($error); + } + } + } + + private function isCoalesceEqualEmulationNeeded(string $code): bool + { + // skip version where this works without emulation + if (version_compare(\PHP_VERSION, self::PHP_7_4, '>=')) { + return false; + } + + return strpos($code, '??=') !== false; + } + + private function processCoaleseEqual(string $code) + { + if ($this->isCoalesceEqualEmulationNeeded($code) === false) { + return; + } + + // We need to manually iterate and manage a count because we'll change + // the tokens array on the way + $line = 1; + for ($i = 0, $c = count($this->tokens); $i < $c; ++$i) { + if (isset($this->tokens[$i + 1])) { + if ($this->tokens[$i][0] === T_COALESCE && $this->tokens[$i + 1] === '=') { + array_splice($this->tokens, $i, 2, [ + [self::T_COALESCE_EQUAL, '??=', $line] + ]); + $c--; + continue; + } + } + if (\is_array($this->tokens[$i])) { + $line += substr_count($this->tokens[$i][1], "\n"); + } + } + } + + private function isHeredocNowdocEmulationNeeded(string $code): bool + { + // skip version where this works without emulation + if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) { + return false; + } + + return strpos($code, '<<<') !== false; + } + + private function processHeredocNowdoc(string $code): string + { + if ($this->isHeredocNowdocEmulationNeeded($code) === false) { + return $code; + } + + if (!preg_match_all(self::FLEXIBLE_DOC_STRING_REGEX, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { + // No heredoc/nowdoc found + return $code; + } + + // Keep track of how much we need to adjust string offsets due to the modifications we + // already made + $posDelta = 0; + foreach ($matches as $match) { + $indentation = $match['indentation'][0]; + $indentationStart = $match['indentation'][1]; + + $separator = $match['separator'][0]; + $separatorStart = $match['separator'][1]; + + if ($indentation === '' && $separator !== '') { + // Ordinary heredoc/nowdoc + continue; + } + + if ($indentation !== '') { + // Remove indentation + $indentationLen = strlen($indentation); + $code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen); + $this->patches[] = [$indentationStart + $posDelta, 'add', $indentation]; + $posDelta -= $indentationLen; + } + + if ($separator === '') { + // Insert newline as separator + $code = substr_replace($code, "\n", $separatorStart + $posDelta, 0); + $this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"]; + $posDelta += 1; + } + } + + return $code; + } + + private function isEmulationNeeded(string $code): bool + { + if ($this->isHeredocNowdocEmulationNeeded($code)) { + return true; + } + + if ($this->isCoalesceEqualEmulationNeeded($code)) { + return true; + } + + return false; + } + + private function fixupTokens() + { + if (\count($this->patches) === 0) { + return; + } + + // Load first patch + $patchIdx = 0; + + list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; + + // We use a manual loop over the tokens, because we modify the array on the fly + $pos = 0; + for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) { + $token = $this->tokens[$i]; + if (\is_string($token)) { + // We assume that patches don't apply to string tokens + $pos += \strlen($token); + continue; + } + + $len = \strlen($token[1]); + $posDelta = 0; + while ($patchPos >= $pos && $patchPos < $pos + $len) { + $patchTextLen = \strlen($patchText); + if ($patchType === 'remove') { + if ($patchPos === $pos && $patchTextLen === $len) { + // Remove token entirely + array_splice($this->tokens, $i, 1, []); + $i--; + $c--; + } else { + // Remove from token string + $this->tokens[$i][1] = substr_replace( + $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen + ); + $posDelta -= $patchTextLen; + } + } elseif ($patchType === 'add') { + // Insert into the token string + $this->tokens[$i][1] = substr_replace( + $token[1], $patchText, $patchPos - $pos + $posDelta, 0 + ); + $posDelta += $patchTextLen; + } else { + assert(false); + } + + // Fetch the next patch + $patchIdx++; + if ($patchIdx >= \count($this->patches)) { + // No more patches, we're done + return; + } + + list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; + + // Multiple patches may apply to the same token. Reload the current one to check + // If the new patch applies + $token = $this->tokens[$i]; + } + + $pos += $len; + } + + // A patch did not apply + assert(false); + } + + /** + * Fixup line and position information in errors. + * + * @param Error[] $errors + */ + private function fixupErrors(array $errors) { + foreach ($errors as $error) { + $attrs = $error->getAttributes(); + + $posDelta = 0; + $lineDelta = 0; + foreach ($this->patches as $patch) { + list($patchPos, $patchType, $patchText) = $patch; + if ($patchPos >= $attrs['startFilePos']) { + // No longer relevant + break; + } + + if ($patchType === 'add') { + $posDelta += strlen($patchText); + $lineDelta += substr_count($patchText, "\n"); + } else { + $posDelta -= strlen($patchText); + $lineDelta -= substr_count($patchText, "\n"); + } + } + + $attrs['startFilePos'] += $posDelta; + $attrs['endFilePos'] += $posDelta; + $attrs['startLine'] += $lineDelta; + $attrs['endLine'] += $lineDelta; + $error->setAttributes($attrs); + } + } }