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