Chris@13: \h*)\2(?![a-zA-Z_\x80-\xff])(?(?:;?[\r\n])?)/x Chris@17: REGEX; Chris@17: Chris@17: const T_COALESCE_EQUAL = 1007; Chris@17: Chris@17: /** Chris@17: * @var mixed[] Patches used to reverse changes introduced in the code Chris@17: */ Chris@17: private $patches = []; Chris@17: Chris@17: /** Chris@17: * @param mixed[] $options Chris@17: */ Chris@17: public function __construct(array $options = []) Chris@17: { Chris@17: parent::__construct($options); Chris@17: Chris@17: // add emulated tokens here Chris@17: $this->tokenMap[self::T_COALESCE_EQUAL] = Parser\Tokens::T_COALESCE_EQUAL; Chris@17: } Chris@17: Chris@17: public function startLexing(string $code, ErrorHandler $errorHandler = null) { Chris@17: $this->patches = []; Chris@17: Chris@17: if ($this->isEmulationNeeded($code) === false) { Chris@17: // Nothing to emulate, yay Chris@17: parent::startLexing($code, $errorHandler); Chris@17: return; Chris@17: } Chris@17: Chris@17: $collector = new ErrorHandler\Collecting(); Chris@17: Chris@17: // 1. emulation of heredoc and nowdoc new syntax Chris@17: $preparedCode = $this->processHeredocNowdoc($code); Chris@17: parent::startLexing($preparedCode, $collector); Chris@17: Chris@17: // 2. emulation of ??= token Chris@17: $this->processCoaleseEqual($code); Chris@17: $this->fixupTokens(); Chris@17: Chris@17: $errors = $collector->getErrors(); Chris@17: if (!empty($errors)) { Chris@17: $this->fixupErrors($errors); Chris@17: foreach ($errors as $error) { Chris@17: $errorHandler->handleError($error); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: private function isCoalesceEqualEmulationNeeded(string $code): bool Chris@17: { Chris@17: // skip version where this works without emulation Chris@17: if (version_compare(\PHP_VERSION, self::PHP_7_4, '>=')) { Chris@17: return false; Chris@17: } Chris@17: Chris@17: return strpos($code, '??=') !== false; Chris@17: } Chris@17: Chris@17: private function processCoaleseEqual(string $code) Chris@17: { Chris@17: if ($this->isCoalesceEqualEmulationNeeded($code) === false) { Chris@17: return; Chris@17: } Chris@17: Chris@17: // We need to manually iterate and manage a count because we'll change Chris@17: // the tokens array on the way Chris@17: $line = 1; Chris@17: for ($i = 0, $c = count($this->tokens); $i < $c; ++$i) { Chris@17: if (isset($this->tokens[$i + 1])) { Chris@17: if ($this->tokens[$i][0] === T_COALESCE && $this->tokens[$i + 1] === '=') { Chris@17: array_splice($this->tokens, $i, 2, [ Chris@17: [self::T_COALESCE_EQUAL, '??=', $line] Chris@17: ]); Chris@17: $c--; Chris@17: continue; Chris@17: } Chris@17: } Chris@17: if (\is_array($this->tokens[$i])) { Chris@17: $line += substr_count($this->tokens[$i][1], "\n"); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: private function isHeredocNowdocEmulationNeeded(string $code): bool Chris@17: { Chris@17: // skip version where this works without emulation Chris@17: if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) { Chris@17: return false; Chris@17: } Chris@17: Chris@17: return strpos($code, '<<<') !== false; Chris@17: } Chris@17: Chris@17: private function processHeredocNowdoc(string $code): string Chris@17: { Chris@17: if ($this->isHeredocNowdocEmulationNeeded($code) === false) { Chris@17: return $code; Chris@17: } Chris@17: Chris@17: if (!preg_match_all(self::FLEXIBLE_DOC_STRING_REGEX, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { Chris@17: // No heredoc/nowdoc found Chris@17: return $code; Chris@17: } Chris@17: Chris@17: // Keep track of how much we need to adjust string offsets due to the modifications we Chris@17: // already made Chris@17: $posDelta = 0; Chris@17: foreach ($matches as $match) { Chris@17: $indentation = $match['indentation'][0]; Chris@17: $indentationStart = $match['indentation'][1]; Chris@17: Chris@17: $separator = $match['separator'][0]; Chris@17: $separatorStart = $match['separator'][1]; Chris@17: Chris@17: if ($indentation === '' && $separator !== '') { Chris@17: // Ordinary heredoc/nowdoc Chris@17: continue; Chris@17: } Chris@17: Chris@17: if ($indentation !== '') { Chris@17: // Remove indentation Chris@17: $indentationLen = strlen($indentation); Chris@17: $code = substr_replace($code, '', $indentationStart + $posDelta, $indentationLen); Chris@17: $this->patches[] = [$indentationStart + $posDelta, 'add', $indentation]; Chris@17: $posDelta -= $indentationLen; Chris@17: } Chris@17: Chris@17: if ($separator === '') { Chris@17: // Insert newline as separator Chris@17: $code = substr_replace($code, "\n", $separatorStart + $posDelta, 0); Chris@17: $this->patches[] = [$separatorStart + $posDelta, 'remove', "\n"]; Chris@17: $posDelta += 1; Chris@17: } Chris@17: } Chris@17: Chris@17: return $code; Chris@17: } Chris@17: Chris@17: private function isEmulationNeeded(string $code): bool Chris@17: { Chris@17: if ($this->isHeredocNowdocEmulationNeeded($code)) { Chris@17: return true; Chris@17: } Chris@17: Chris@17: if ($this->isCoalesceEqualEmulationNeeded($code)) { Chris@17: return true; Chris@17: } Chris@17: Chris@17: return false; Chris@17: } Chris@17: Chris@17: private function fixupTokens() Chris@17: { Chris@17: if (\count($this->patches) === 0) { Chris@17: return; Chris@17: } Chris@17: Chris@17: // Load first patch Chris@17: $patchIdx = 0; Chris@17: Chris@17: list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; Chris@17: Chris@17: // We use a manual loop over the tokens, because we modify the array on the fly Chris@17: $pos = 0; Chris@17: for ($i = 0, $c = \count($this->tokens); $i < $c; $i++) { Chris@17: $token = $this->tokens[$i]; Chris@17: if (\is_string($token)) { Chris@17: // We assume that patches don't apply to string tokens Chris@17: $pos += \strlen($token); Chris@17: continue; Chris@17: } Chris@17: Chris@17: $len = \strlen($token[1]); Chris@17: $posDelta = 0; Chris@17: while ($patchPos >= $pos && $patchPos < $pos + $len) { Chris@17: $patchTextLen = \strlen($patchText); Chris@17: if ($patchType === 'remove') { Chris@17: if ($patchPos === $pos && $patchTextLen === $len) { Chris@17: // Remove token entirely Chris@17: array_splice($this->tokens, $i, 1, []); Chris@17: $i--; Chris@17: $c--; Chris@17: } else { Chris@17: // Remove from token string Chris@17: $this->tokens[$i][1] = substr_replace( Chris@17: $token[1], '', $patchPos - $pos + $posDelta, $patchTextLen Chris@17: ); Chris@17: $posDelta -= $patchTextLen; Chris@17: } Chris@17: } elseif ($patchType === 'add') { Chris@17: // Insert into the token string Chris@17: $this->tokens[$i][1] = substr_replace( Chris@17: $token[1], $patchText, $patchPos - $pos + $posDelta, 0 Chris@17: ); Chris@17: $posDelta += $patchTextLen; Chris@17: } else { Chris@17: assert(false); Chris@17: } Chris@17: Chris@17: // Fetch the next patch Chris@17: $patchIdx++; Chris@17: if ($patchIdx >= \count($this->patches)) { Chris@17: // No more patches, we're done Chris@17: return; Chris@17: } Chris@17: Chris@17: list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx]; Chris@17: Chris@17: // Multiple patches may apply to the same token. Reload the current one to check Chris@17: // If the new patch applies Chris@17: $token = $this->tokens[$i]; Chris@17: } Chris@17: Chris@17: $pos += $len; Chris@17: } Chris@17: Chris@17: // A patch did not apply Chris@17: assert(false); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Fixup line and position information in errors. Chris@17: * Chris@17: * @param Error[] $errors Chris@17: */ Chris@17: private function fixupErrors(array $errors) { Chris@17: foreach ($errors as $error) { Chris@17: $attrs = $error->getAttributes(); Chris@17: Chris@17: $posDelta = 0; Chris@17: $lineDelta = 0; Chris@17: foreach ($this->patches as $patch) { Chris@17: list($patchPos, $patchType, $patchText) = $patch; Chris@17: if ($patchPos >= $attrs['startFilePos']) { Chris@17: // No longer relevant Chris@17: break; Chris@17: } Chris@17: Chris@17: if ($patchType === 'add') { Chris@17: $posDelta += strlen($patchText); Chris@17: $lineDelta += substr_count($patchText, "\n"); Chris@17: } else { Chris@17: $posDelta -= strlen($patchText); Chris@17: $lineDelta -= substr_count($patchText, "\n"); Chris@17: } Chris@17: } Chris@17: Chris@17: $attrs['startFilePos'] += $posDelta; Chris@17: $attrs['endFilePos'] += $posDelta; Chris@17: $attrs['startLine'] += $lineDelta; Chris@17: $attrs['endLine'] += $lineDelta; Chris@17: $error->setAttributes($attrs); Chris@17: } Chris@17: } Chris@0: }