annotate vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.php @ 19:fa3358dc1485 tip

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