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