1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\PhpDocParser\Parser; |
4: | |
5: | use LogicException; |
6: | use PHPStan\PhpDocParser\Ast\Comment; |
7: | use PHPStan\PhpDocParser\Lexer\Lexer; |
8: | use function array_pop; |
9: | use function assert; |
10: | use function count; |
11: | use function in_array; |
12: | use function strlen; |
13: | use function substr; |
14: | |
15: | class TokenIterator |
16: | { |
17: | |
18: | |
19: | private array $tokens; |
20: | |
21: | private int $index; |
22: | |
23: | |
24: | private array $comments = []; |
25: | |
26: | |
27: | private array $savePoints = []; |
28: | |
29: | |
30: | private array $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS]; |
31: | |
32: | private ?string $newline = null; |
33: | |
34: | |
35: | |
36: | |
37: | public function __construct(array $tokens, int $index = 0) |
38: | { |
39: | $this->tokens = $tokens; |
40: | $this->index = $index; |
41: | |
42: | $this->skipIrrelevantTokens(); |
43: | } |
44: | |
45: | |
46: | |
47: | |
48: | public function getTokens(): array |
49: | { |
50: | return $this->tokens; |
51: | } |
52: | |
53: | public function getContentBetween(int $startPos, int $endPos): string |
54: | { |
55: | if ($startPos < 0 || $endPos > count($this->tokens)) { |
56: | throw new LogicException(); |
57: | } |
58: | |
59: | $content = ''; |
60: | for ($i = $startPos; $i < $endPos; $i++) { |
61: | $content .= $this->tokens[$i][Lexer::VALUE_OFFSET]; |
62: | } |
63: | |
64: | return $content; |
65: | } |
66: | |
67: | public function getTokenCount(): int |
68: | { |
69: | return count($this->tokens); |
70: | } |
71: | |
72: | public function currentTokenValue(): string |
73: | { |
74: | return $this->tokens[$this->index][Lexer::VALUE_OFFSET]; |
75: | } |
76: | |
77: | public function currentTokenType(): int |
78: | { |
79: | return $this->tokens[$this->index][Lexer::TYPE_OFFSET]; |
80: | } |
81: | |
82: | public function currentTokenOffset(): int |
83: | { |
84: | $offset = 0; |
85: | for ($i = 0; $i < $this->index; $i++) { |
86: | $offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]); |
87: | } |
88: | |
89: | return $offset; |
90: | } |
91: | |
92: | public function currentTokenLine(): int |
93: | { |
94: | return $this->tokens[$this->index][Lexer::LINE_OFFSET]; |
95: | } |
96: | |
97: | public function currentTokenIndex(): int |
98: | { |
99: | return $this->index; |
100: | } |
101: | |
102: | public function endIndexOfLastRelevantToken(): int |
103: | { |
104: | $endIndex = $this->currentTokenIndex(); |
105: | $endIndex--; |
106: | while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) { |
107: | if (!isset($this->tokens[$endIndex - 1])) { |
108: | break; |
109: | } |
110: | $endIndex--; |
111: | } |
112: | |
113: | return $endIndex; |
114: | } |
115: | |
116: | public function isCurrentTokenValue(string $tokenValue): bool |
117: | { |
118: | return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue; |
119: | } |
120: | |
121: | public function isCurrentTokenType(int ...$tokenType): bool |
122: | { |
123: | return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true); |
124: | } |
125: | |
126: | public function isPrecededByHorizontalWhitespace(): bool |
127: | { |
128: | return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS; |
129: | } |
130: | |
131: | |
132: | |
133: | |
134: | public function consumeTokenType(int $tokenType): void |
135: | { |
136: | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) { |
137: | $this->throwError($tokenType); |
138: | } |
139: | |
140: | if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) { |
141: | if ($this->newline === null) { |
142: | $this->detectNewline(); |
143: | } |
144: | } |
145: | |
146: | $this->next(); |
147: | } |
148: | |
149: | |
150: | |
151: | |
152: | public function consumeTokenValue(int $tokenType, string $tokenValue): void |
153: | { |
154: | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) { |
155: | $this->throwError($tokenType, $tokenValue); |
156: | } |
157: | |
158: | $this->next(); |
159: | } |
160: | |
161: | |
162: | public function tryConsumeTokenValue(string $tokenValue): bool |
163: | { |
164: | if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) { |
165: | return false; |
166: | } |
167: | |
168: | $this->next(); |
169: | |
170: | return true; |
171: | } |
172: | |
173: | |
174: | |
175: | |
176: | public function flushComments(): array |
177: | { |
178: | $res = $this->comments; |
179: | $this->comments = []; |
180: | return $res; |
181: | } |
182: | |
183: | |
184: | public function tryConsumeTokenType(int $tokenType): bool |
185: | { |
186: | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) { |
187: | return false; |
188: | } |
189: | |
190: | if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) { |
191: | if ($this->newline === null) { |
192: | $this->detectNewline(); |
193: | } |
194: | } |
195: | |
196: | $this->next(); |
197: | |
198: | return true; |
199: | } |
200: | |
201: | |
202: | |
203: | |
204: | public function skipNewLineTokens(): void |
205: | { |
206: | if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { |
207: | return; |
208: | } |
209: | |
210: | do { |
211: | $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
212: | } while ($foundNewLine === true); |
213: | } |
214: | |
215: | public function skipNewLineTokensAndConsumeComments(): void |
216: | { |
217: | if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) { |
218: | $this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex()); |
219: | $this->next(); |
220: | } |
221: | |
222: | if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { |
223: | return; |
224: | } |
225: | |
226: | do { |
227: | $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
228: | if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) { |
229: | continue; |
230: | } |
231: | |
232: | $this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex()); |
233: | $this->next(); |
234: | } while ($foundNewLine === true); |
235: | } |
236: | |
237: | private function detectNewline(): void |
238: | { |
239: | $value = $this->currentTokenValue(); |
240: | if (substr($value, 0, 2) === "\r\n") { |
241: | $this->newline = "\r\n"; |
242: | } elseif (substr($value, 0, 1) === "\n") { |
243: | $this->newline = "\n"; |
244: | } |
245: | } |
246: | |
247: | public function getSkippedHorizontalWhiteSpaceIfAny(): string |
248: | { |
249: | if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { |
250: | return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET]; |
251: | } |
252: | |
253: | return ''; |
254: | } |
255: | |
256: | |
257: | public function joinUntil(int ...$tokenType): string |
258: | { |
259: | $s = ''; |
260: | while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) { |
261: | $s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET]; |
262: | } |
263: | return $s; |
264: | } |
265: | |
266: | public function next(): void |
267: | { |
268: | $this->index++; |
269: | $this->skipIrrelevantTokens(); |
270: | } |
271: | |
272: | private function skipIrrelevantTokens(): void |
273: | { |
274: | if (!isset($this->tokens[$this->index])) { |
275: | return; |
276: | } |
277: | |
278: | while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) { |
279: | if (!isset($this->tokens[$this->index + 1])) { |
280: | break; |
281: | } |
282: | $this->index++; |
283: | } |
284: | } |
285: | |
286: | public function addEndOfLineToSkippedTokens(): void |
287: | { |
288: | $this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL]; |
289: | } |
290: | |
291: | public function removeEndOfLineFromSkippedTokens(): void |
292: | { |
293: | $this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS]; |
294: | } |
295: | |
296: | |
297: | public function forwardToTheEnd(): void |
298: | { |
299: | $lastToken = count($this->tokens) - 1; |
300: | $this->index = $lastToken; |
301: | } |
302: | |
303: | public function pushSavePoint(): void |
304: | { |
305: | $this->savePoints[] = [$this->index, $this->comments]; |
306: | } |
307: | |
308: | public function dropSavePoint(): void |
309: | { |
310: | array_pop($this->savePoints); |
311: | } |
312: | |
313: | public function rollback(): void |
314: | { |
315: | $savepoint = array_pop($this->savePoints); |
316: | assert($savepoint !== null); |
317: | [$this->index, $this->comments] = $savepoint; |
318: | } |
319: | |
320: | |
321: | |
322: | |
323: | private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void |
324: | { |
325: | throw new ParserException( |
326: | $this->currentTokenValue(), |
327: | $this->currentTokenType(), |
328: | $this->currentTokenOffset(), |
329: | $expectedTokenType, |
330: | $expectedTokenValue, |
331: | $this->currentTokenLine(), |
332: | ); |
333: | } |
334: | |
335: | |
336: | |
337: | |
338: | |
339: | |
340: | public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool |
341: | { |
342: | $tokens = $this->tokens; |
343: | $pos--; |
344: | for (; $pos >= 0; $pos--) { |
345: | $token = $tokens[$pos]; |
346: | $type = $token[Lexer::TYPE_OFFSET]; |
347: | if ($type === $expectedTokenType) { |
348: | return true; |
349: | } |
350: | if (!in_array($type, [ |
351: | Lexer::TOKEN_HORIZONTAL_WS, |
352: | Lexer::TOKEN_PHPDOC_EOL, |
353: | ], true)) { |
354: | break; |
355: | } |
356: | } |
357: | return false; |
358: | } |
359: | |
360: | |
361: | |
362: | |
363: | |
364: | |
365: | public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool |
366: | { |
367: | $tokens = $this->tokens; |
368: | $pos++; |
369: | for ($c = count($tokens); $pos < $c; $pos++) { |
370: | $token = $tokens[$pos]; |
371: | $type = $token[Lexer::TYPE_OFFSET]; |
372: | if ($type === $expectedTokenType) { |
373: | return true; |
374: | } |
375: | if (!in_array($type, [ |
376: | Lexer::TOKEN_HORIZONTAL_WS, |
377: | Lexer::TOKEN_PHPDOC_EOL, |
378: | ], true)) { |
379: | break; |
380: | } |
381: | } |
382: | |
383: | return false; |
384: | } |
385: | |
386: | public function getDetectedNewline(): ?string |
387: | { |
388: | return $this->newline; |
389: | } |
390: | |
391: | |
392: | |
393: | |
394: | public function hasParentheses(int $startPos, int $endPos): bool |
395: | { |
396: | return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES) |
397: | && $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES); |
398: | } |
399: | |
400: | } |
401: | |