mirror of
https://github.com/danog/PHP-Parser.git
synced 2024-12-04 02:07:59 +01:00
203 lines
6.6 KiB
PHP
203 lines
6.6 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace PhpParser\Lexer;
|
|
|
|
use PhpParser\Error;
|
|
use PhpParser\ErrorHandler;
|
|
|
|
class Emulative extends \PhpParser\Lexer
|
|
{
|
|
const PHP_7_3 = '7.3.0dev';
|
|
|
|
/**
|
|
* @var array Patches used to reverse changes introduced in the code
|
|
*/
|
|
private $patches;
|
|
|
|
public function startLexing(string $code, ErrorHandler $errorHandler = null) {
|
|
$this->patches = [];
|
|
$preparedCode = $this->prepareCode($code);
|
|
if (null === $preparedCode) {
|
|
// Nothing to emulate, yay
|
|
parent::startLexing($code, $errorHandler);
|
|
return;
|
|
}
|
|
|
|
$collector = new ErrorHandler\Collecting();
|
|
parent::startLexing($preparedCode, $collector);
|
|
$this->fixupTokens();
|
|
|
|
$errors = $collector->getErrors();
|
|
if (!empty($errors)) {
|
|
$this->fixupErrors($errors);
|
|
foreach ($errors as $error) {
|
|
$errorHandler->handleError($error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares code for emulation. If nothing has to be emulated null is returned.
|
|
*
|
|
* @param string $code
|
|
* @return null|string
|
|
*/
|
|
private function prepareCode(string $code) {
|
|
if (version_compare(\PHP_VERSION, self::PHP_7_3, '>=')) {
|
|
return null;
|
|
}
|
|
|
|
if (strpos($code, '<<<') === false) {
|
|
// Definitely doesn't contain heredoc/nowdoc
|
|
return null;
|
|
}
|
|
|
|
$flexibleDocStringRegex = <<<'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;
|
|
if (!preg_match_all($flexibleDocStringRegex, $code, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) {
|
|
// No heredoc/nowdoc found
|
|
return null;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
if (empty($this->patches)) {
|
|
// We did not end up emulating anything
|
|
return null;
|
|
}
|
|
|
|
return $code;
|
|
}
|
|
|
|
private function fixupTokens() {
|
|
assert(count($this->patches) > 0);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
} |