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: * @return list<array{string, int, int}>
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: * @throws ParserException
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: * @throws ParserException
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: /** @phpstan-impure */
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: * @return list<Comment>
175: */
176: public function flushComments(): array
177: {
178: $res = $this->comments;
179: $this->comments = [];
180: return $res;
181: }
182:
183: /** @phpstan-impure */
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: * @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type)
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: * @throws ParserException
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: * Check whether the position is directly preceded by a certain token type.
337: *
338: * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
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: * Check whether the position is directly followed by a certain token type.
362: *
363: * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
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: * Whether the given position is immediately surrounded by parenthesis.
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: