PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.2
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Dependencies / PhpParser / Lexer.php

Lexer.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.2, at includes/Dependencies/PhpParser/Lexer.php

383 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:ignoreFile -- Bundled third-party (Mozart) dependency; exempt from plugin coding standards.
3
4 namespace WPDeveloper\BetterDocs\Dependencies\PhpParser;
5
6 use WPDeveloper\BetterDocs\Dependencies\PhpParser\Parser\Tokens;
7
8 class Lexer
9 {
10 protected $code;
11 protected $tokens;
12 protected $pos;
13 protected $line;
14 protected $filePos;
15 protected $prevCloseTagHasNewline;
16
17 protected $tokenMap;
18 protected $dropTokens;
19
20 protected $usedAttributes;
21
22 /**
23 * Creates a Lexer.
24 *
25 * @param array $options Options array. Currently only the 'usedAttributes' option is supported,
26 * which is an array of attributes to add to the AST nodes. Possible
27 * attributes are: 'comments', 'startLine', 'endLine', 'startTokenPos',
28 * 'endTokenPos', 'startFilePos', 'endFilePos'. The option defaults to the
29 * first three. For more info see getNextToken() docs.
30 */
31 public function __construct(array $options = array()) {
32 // map from internal tokens to WPDeveloper\BetterDocs\Dependencies\PhpParser tokens
33 $this->tokenMap = $this->createTokenMap();
34
35 // map of tokens to drop while lexing (the map is only used for isset lookup,
36 // that's why the value is simply set to 1; the value is never actually used.)
37 $this->dropTokens = array_fill_keys(
38 array(T_WHITESPACE, T_OPEN_TAG, T_COMMENT, T_DOC_COMMENT), 1
39 );
40
41 // the usedAttributes member is a map of the used attribute names to a dummy
42 // value (here "true")
43 $options += array(
44 'usedAttributes' => array('comments', 'startLine', 'endLine'),
45 );
46 $this->usedAttributes = array_fill_keys($options['usedAttributes'], true);
47 }
48
49 /**
50 * Initializes the lexer for lexing the provided source code.
51 *
52 * This function does not throw if lexing errors occur. Instead, errors may be retrieved using
53 * the getErrors() method.
54 *
55 * @param string $code The source code to lex
56 * @param ErrorHandler|null $errorHandler Error handler to use for lexing errors. Defaults to
57 * ErrorHandler\Throwing
58 */
59 public function startLexing($code, ?ErrorHandler $errorHandler = null) {
60 if (null === $errorHandler) {
61 $errorHandler = new ErrorHandler\Throwing();
62 }
63
64 $this->code = $code; // keep the code around for __halt_compiler() handling
65 $this->pos = -1;
66 $this->line = 1;
67 $this->filePos = 0;
68
69 // If inline HTML occurs without preceding code, treat it as if it had a leading newline.
70 // This ensures proper composability, because having a newline is the "safe" assumption.
71 $this->prevCloseTagHasNewline = true;
72
73 $scream = ini_set('xdebug.scream', '0');
74
75 $this->resetErrors();
76 $this->tokens = @token_get_all($code);
77 $this->handleErrors($errorHandler);
78
79 if (false !== $scream) {
80 ini_set('xdebug.scream', $scream);
81 }
82 }
83
84 protected function resetErrors() {
85 if (function_exists('error_clear_last')) {
86 error_clear_last();
87 } else {
88 // set error_get_last() to defined state by forcing an undefined variable error
89 set_error_handler(function() { return false; }, 0);
90 @$undefinedVariable;
91 restore_error_handler();
92 }
93 }
94
95 private function handleInvalidCharacterRange($start, $end, $line, ErrorHandler $errorHandler) {
96 for ($i = $start; $i < $end; $i++) {
97 $chr = $this->code[$i];
98 if ($chr === 'b' || $chr === 'B') {
99 // HHVM does not treat b" tokens correctly, so ignore these
100 continue;
101 }
102
103 if ($chr === "\0") {
104 // PHP cuts error message after null byte, so need special case
105 $errorMsg = 'Unexpected null byte';
106 } else {
107 $errorMsg = sprintf(
108 'Unexpected character "%s" (ASCII %d)', $chr, ord($chr)
109 );
110 }
111
112 $errorHandler->handleError(new Error($errorMsg, [
113 'startLine' => $line,
114 'endLine' => $line,
115 'startFilePos' => $i,
116 'endFilePos' => $i,
117 ]));
118 }
119 }
120
121 private function isUnterminatedComment($token) {
122 return ($token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT)
123 && substr($token[1], 0, 2) === '/*'
124 && substr($token[1], -2) !== '*/';
125 }
126
127 private function errorMayHaveOccurred() {
128 if (defined('HHVM_VERSION')) {
129 // In HHVM token_get_all() does not throw warnings, so we need to conservatively
130 // assume that an error occurred
131 return true;
132 }
133
134 $error = error_get_last();
135 return null !== $error
136 && false === strpos($error['message'], 'Undefined variable');
137 }
138
139 protected function handleErrors(ErrorHandler $errorHandler) {
140 if (!$this->errorMayHaveOccurred()) {
141 return;
142 }
143
144 // PHP's error handling for token_get_all() is rather bad, so if we want detailed
145 // error information we need to compute it ourselves. Invalid character errors are
146 // detected by finding "gaps" in the token array. Unterminated comments are detected
147 // by checking if a trailing comment has a "*/" at the end.
148
149 $filePos = 0;
150 $line = 1;
151 foreach ($this->tokens as $i => $token) {
152 $tokenValue = \is_string($token) ? $token : $token[1];
153 $tokenLen = \strlen($tokenValue);
154
155 if (substr($this->code, $filePos, $tokenLen) !== $tokenValue) {
156 // Something is missing, must be an invalid character
157 $nextFilePos = strpos($this->code, $tokenValue, $filePos);
158 $this->handleInvalidCharacterRange(
159 $filePos, $nextFilePos, $line, $errorHandler);
160 $filePos = $nextFilePos;
161 }
162
163 $filePos += $tokenLen;
164 $line += substr_count($tokenValue, "\n");
165 }
166
167 if ($filePos !== \strlen($this->code)) {
168 if (substr($this->code, $filePos, 2) === '/*') {
169 // Unlike PHP, HHVM will drop unterminated comments entirely
170 $comment = substr($this->code, $filePos);
171 $errorHandler->handleError(new Error('Unterminated comment', [
172 'startLine' => $line,
173 'endLine' => $line + substr_count($comment, "\n"),
174 'startFilePos' => $filePos,
175 'endFilePos' => $filePos + \strlen($comment),
176 ]));
177
178 // Emulate the PHP behavior
179 $isDocComment = isset($comment[3]) && $comment[3] === '*';
180 $this->tokens[] = [$isDocComment ? T_DOC_COMMENT : T_COMMENT, $comment, $line];
181 } else {
182 // Invalid characters at the end of the input
183 $this->handleInvalidCharacterRange(
184 $filePos, \strlen($this->code), $line, $errorHandler);
185 }
186 return;
187 }
188
189 if (count($this->tokens) > 0) {
190 // Check for unterminated comment
191 $lastToken = $this->tokens[count($this->tokens) - 1];
192 if ($this->isUnterminatedComment($lastToken)) {
193 $errorHandler->handleError(new Error('Unterminated comment', [
194 'startLine' => $line - substr_count($lastToken[1], "\n"),
195 'endLine' => $line,
196 'startFilePos' => $filePos - \strlen($lastToken[1]),
197 'endFilePos' => $filePos,
198 ]));
199 }
200 }
201 }
202
203 /**
204 * Fetches the next token.
205 *
206 * The available attributes are determined by the 'usedAttributes' option, which can
207 * be specified in the constructor. The following attributes are supported:
208 *
209 * * 'comments' => Array of WPDeveloper\BetterDocs\Dependencies\PhpParser\Comment or WPDeveloper\BetterDocs\Dependencies\PhpParser\Comment\Doc instances,
210 * representing all comments that occurred between the previous
211 * non-discarded token and the current one.
212 * * 'startLine' => Line in which the node starts.
213 * * 'endLine' => Line in which the node ends.
214 * * 'startTokenPos' => Offset into the token array of the first token in the node.
215 * * 'endTokenPos' => Offset into the token array of the last token in the node.
216 * * 'startFilePos' => Offset into the code string of the first character that is part of the node.
217 * * 'endFilePos' => Offset into the code string of the last character that is part of the node.
218 *
219 * @param mixed $value Variable to store token content in
220 * @param mixed $startAttributes Variable to store start attributes in
221 * @param mixed $endAttributes Variable to store end attributes in
222 *
223 * @return int Token id
224 */
225 public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) {
226 $startAttributes = array();
227 $endAttributes = array();
228
229 while (1) {
230 if (isset($this->tokens[++$this->pos])) {
231 $token = $this->tokens[$this->pos];
232 } else {
233 // EOF token with ID 0
234 $token = "\0";
235 }
236
237 if (isset($this->usedAttributes['startLine'])) {
238 $startAttributes['startLine'] = $this->line;
239 }
240 if (isset($this->usedAttributes['startTokenPos'])) {
241 $startAttributes['startTokenPos'] = $this->pos;
242 }
243 if (isset($this->usedAttributes['startFilePos'])) {
244 $startAttributes['startFilePos'] = $this->filePos;
245 }
246
247 if (\is_string($token)) {
248 $value = $token;
249 if (isset($token[1])) {
250 // bug in token_get_all
251 $this->filePos += 2;
252 $id = ord('"');
253 } else {
254 $this->filePos += 1;
255 $id = ord($token);
256 }
257 } elseif (!isset($this->dropTokens[$token[0]])) {
258 $value = $token[1];
259 $id = $this->tokenMap[$token[0]];
260 if (T_CLOSE_TAG === $token[0]) {
261 $this->prevCloseTagHasNewline = false !== strpos($token[1], "\n");
262 } else if (T_INLINE_HTML === $token[0]) {
263 $startAttributes['hasLeadingNewline'] = $this->prevCloseTagHasNewline;
264 }
265
266 $this->line += substr_count($value, "\n");
267 $this->filePos += \strlen($value);
268 } else {
269 if (T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0]) {
270 if (isset($this->usedAttributes['comments'])) {
271 $comment = T_DOC_COMMENT === $token[0]
272 ? new Comment\Doc($token[1], $this->line, $this->filePos)
273 : new Comment($token[1], $this->line, $this->filePos);
274 $startAttributes['comments'][] = $comment;
275 }
276 }
277
278 $this->line += substr_count($token[1], "\n");
279 $this->filePos += \strlen($token[1]);
280 continue;
281 }
282
283 if (isset($this->usedAttributes['endLine'])) {
284 $endAttributes['endLine'] = $this->line;
285 }
286 if (isset($this->usedAttributes['endTokenPos'])) {
287 $endAttributes['endTokenPos'] = $this->pos;
288 }
289 if (isset($this->usedAttributes['endFilePos'])) {
290 $endAttributes['endFilePos'] = $this->filePos - 1;
291 }
292
293 return $id;
294 }
295
296 throw new \RuntimeException('Reached end of lexer loop');
297 }
298
299 /**
300 * Returns the token array for current code.
301 *
302 * The token array is in the same format as provided by the
303 * token_get_all() function and does not discard tokens (i.e.
304 * whitespace and comments are included). The token position
305 * attributes are against this token array.
306 *
307 * @return array Array of tokens in token_get_all() format
308 */
309 public function getTokens() {
310 return $this->tokens;
311 }
312
313 /**
314 * Handles __halt_compiler() by returning the text after it.
315 *
316 * @return string Remaining text
317 */
318 public function handleHaltCompiler() {
319 // text after T_HALT_COMPILER, still including ();
320 $textAfter = substr($this->code, $this->filePos);
321
322 // ensure that it is followed by ();
323 // this simplifies the situation, by not allowing any comments
324 // in between of the tokens.
325 if (!preg_match('~^\s*\(\s*\)\s*(?:;|\?>\r?\n?)~', $textAfter, $matches)) {
326 throw new Error('__HALT_COMPILER must be followed by "();"');
327 }
328
329 // prevent the lexer from returning any further tokens
330 $this->pos = count($this->tokens);
331
332 // return with (); removed
333 return (string) substr($textAfter, strlen($matches[0])); // (string) converts false to ''
334 }
335
336 /**
337 * Creates the token map.
338 *
339 * The token map maps the PHP internal token identifiers
340 * to the identifiers used by the Parser. Additionally it
341 * maps T_OPEN_TAG_WITH_ECHO to T_ECHO and T_CLOSE_TAG to ';'.
342 *
343 * @return array The token map
344 */
345 protected function createTokenMap() {
346 $tokenMap = array();
347
348 // 256 is the minimum possible token number, as everything below
349 // it is an ASCII value
350 for ($i = 256; $i < 1000; ++$i) {
351 if (T_DOUBLE_COLON === $i) {
352 // T_DOUBLE_COLON is equivalent to T_PAAMAYIM_NEKUDOTAYIM
353 $tokenMap[$i] = Tokens::T_PAAMAYIM_NEKUDOTAYIM;
354 } elseif(T_OPEN_TAG_WITH_ECHO === $i) {
355 // T_OPEN_TAG_WITH_ECHO with dropped T_OPEN_TAG results in T_ECHO
356 $tokenMap[$i] = Tokens::T_ECHO;
357 } elseif(T_CLOSE_TAG === $i) {
358 // T_CLOSE_TAG is equivalent to ';'
359 $tokenMap[$i] = ord(';');
360 } elseif ('UNKNOWN' !== $name = token_name($i)) {
361 if ('T_HASHBANG' === $name) {
362 // HHVM uses a special token for #! hashbang lines
363 $tokenMap[$i] = Tokens::T_INLINE_HTML;
364 } else if (defined($name = Tokens::class . '::' . $name)) {
365 // Other tokens can be mapped directly
366 $tokenMap[$i] = constant($name);
367 }
368 }
369 }
370
371 // HHVM uses a special token for numbers that overflow to double
372 if (defined('T_ONUMBER')) {
373 $tokenMap[T_ONUMBER] = Tokens::T_DNUMBER;
374 }
375 // HHVM also has a separate token for the __COMPILER_HALT_OFFSET__ constant
376 if (defined('T_COMPILER_HALT_OFFSET')) {
377 $tokenMap[T_COMPILER_HALT_OFFSET] = Tokens::T_STRING;
378 }
379
380 return $tokenMap;
381 }
382 }
383