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