|
8 | 8 | use PHPStan\PhpDocParser\Lexer\Lexer; |
9 | 9 | use PHPStan\PhpDocParser\ParserConfig; |
10 | 10 | use function in_array; |
| 11 | +use function str_contains; |
11 | 12 | use function str_replace; |
12 | 13 | use function strlen; |
13 | 14 | use function strpos; |
@@ -113,29 +114,68 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode |
113 | 114 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
114 | 115 | $type = $this->parseNullable($tokens); |
115 | 116 |
|
116 | | - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
117 | | - $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue()); |
| 117 | + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
| 118 | + $propertyExpression = $this->parsePropertyExpression($tokens); |
118 | 119 |
|
119 | | - } else { |
120 | | - $type = $this->parseAtomic($tokens); |
| 120 | + if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) { |
| 121 | + $type = $this->parseConditionalForProperty($tokens, $propertyExpression); |
| 122 | + } else { |
| 123 | + $type = $this->parseAtomic($tokens); |
| 124 | + $type = $this->parseUnionOrIntersectionIfPresent($tokens, $type); |
| 125 | + } |
121 | 126 |
|
122 | | - if ($tokens->isCurrentTokenValue('is')) { |
123 | | - $type = $this->parseConditional($tokens, $type); |
| 127 | + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
| 128 | + $parameterName = $tokens->currentTokenValue(); |
| 129 | + $propertyExpression = $this->parsePropertyExpression($tokens); |
| 130 | + |
| 131 | + if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) { |
| 132 | + $type = $this->parseConditionalForProperty($tokens, $propertyExpression); |
124 | 133 | } else { |
125 | | - $tokens->skipNewLineTokensAndConsumeComments(); |
| 134 | + $type = $this->parseConditionalForParameter($tokens, $parameterName); |
| 135 | + } |
126 | 136 |
|
127 | | - if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
128 | | - $type = $this->subParseUnion($tokens, $type); |
| 137 | + } else { |
| 138 | + if ($tokens->isCurrentTokenValue('self') || |
| 139 | + $tokens->isCurrentTokenValue('parent') || |
| 140 | + $tokens->isCurrentTokenValue('static')) { |
| 141 | + $propertyExpression = $this->parsePropertyExpression($tokens); |
129 | 142 |
|
130 | | - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
131 | | - $type = $this->subParseIntersection($tokens, $type); |
| 143 | + if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) { |
| 144 | + $type = $this->parseConditionalForProperty($tokens, $propertyExpression); |
| 145 | + } else { |
| 146 | + $type = $this->parseAtomic($tokens); |
| 147 | + $type = $this->parseUnionOrIntersectionIfPresent($tokens, $type); |
132 | 148 | } |
| 149 | + } else { |
| 150 | + $type = $this->parseAtomic($tokens); |
| 151 | + $type = $this->parseUnionOrIntersectionIfPresent($tokens, $type); |
133 | 152 | } |
134 | 153 | } |
135 | 154 |
|
136 | 155 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
137 | 156 | } |
138 | 157 |
|
| 158 | + /** @phpstan-impure */ |
| 159 | + private function parseUnionOrIntersectionIfPresent( |
| 160 | + TokenIterator $tokens, |
| 161 | + Ast\Type\TypeNode $type |
| 162 | + ): Ast\Type\TypeNode |
| 163 | + { |
| 164 | + if ($tokens->isCurrentTokenValue('is')) { |
| 165 | + return $this->parseConditional($tokens, $type); |
| 166 | + } |
| 167 | + |
| 168 | + $tokens->skipNewLineTokensAndConsumeComments(); |
| 169 | + |
| 170 | + if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
| 171 | + return $this->subParseUnion($tokens, $type); |
| 172 | + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
| 173 | + return $this->subParseIntersection($tokens, $type); |
| 174 | + } |
| 175 | + |
| 176 | + return $type; |
| 177 | + } |
| 178 | + |
139 | 179 | /** @phpstan-impure */ |
140 | 180 | private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode |
141 | 181 | { |
@@ -392,6 +432,101 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par |
392 | 432 | return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated); |
393 | 433 | } |
394 | 434 |
|
| 435 | + /** @phpstan-impure */ |
| 436 | + private function parsePropertyExpression(TokenIterator $tokens): ?string |
| 437 | + { |
| 438 | + $tokens->pushSavePoint(); |
| 439 | + |
| 440 | + // Handle $this->prop or $variable->prop |
| 441 | + if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE) || $tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
| 442 | + $expression = $tokens->currentTokenValue(); |
| 443 | + if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
| 444 | + $tokens->consumeTokenType(Lexer::TOKEN_THIS_VARIABLE); |
| 445 | + } else { |
| 446 | + $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
| 447 | + } |
| 448 | + |
| 449 | + // Parse chained property access: $this->a->b->c |
| 450 | + while ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) { |
| 451 | + $expression .= '->'; |
| 452 | + $tokens->consumeTokenType(Lexer::TOKEN_ARROW); |
| 453 | + |
| 454 | + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { |
| 455 | + $tokens->rollback(); |
| 456 | + return null; |
| 457 | + } |
| 458 | + |
| 459 | + $propertyName = $tokens->currentTokenValue(); |
| 460 | + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
| 461 | + $expression .= $propertyName; |
| 462 | + } |
| 463 | + |
| 464 | + if (!str_contains($expression, '->')) { |
| 465 | + $tokens->rollback(); |
| 466 | + return null; |
| 467 | + } |
| 468 | + |
| 469 | + $tokens->dropSavePoint(); |
| 470 | + return $expression; |
| 471 | + |
| 472 | + } elseif ($tokens->isCurrentTokenValue('self') || |
| 473 | + $tokens->isCurrentTokenValue('parent') || |
| 474 | + $tokens->isCurrentTokenValue('static')) { |
| 475 | + $expression = $tokens->currentTokenValue(); |
| 476 | + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
| 477 | + |
| 478 | + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { |
| 479 | + $tokens->rollback(); |
| 480 | + return null; |
| 481 | + } |
| 482 | + |
| 483 | + $expression .= '::'; |
| 484 | + $tokens->consumeTokenType(Lexer::TOKEN_DOUBLE_COLON); |
| 485 | + |
| 486 | + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
| 487 | + $tokens->rollback(); |
| 488 | + return null; |
| 489 | + } |
| 490 | + |
| 491 | + $expression .= $tokens->currentTokenValue(); |
| 492 | + $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
| 493 | + |
| 494 | + $tokens->dropSavePoint(); |
| 495 | + return $expression; |
| 496 | + } |
| 497 | + |
| 498 | + $tokens->dropSavePoint(); |
| 499 | + return null; |
| 500 | + } |
| 501 | + |
| 502 | + /** @phpstan-impure */ |
| 503 | + private function parseConditionalForProperty(TokenIterator $tokens, string $propertyExpression): Ast\Type\TypeNode |
| 504 | + { |
| 505 | + $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is'); |
| 506 | + |
| 507 | + $negated = false; |
| 508 | + if ($tokens->isCurrentTokenValue('not')) { |
| 509 | + $negated = true; |
| 510 | + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
| 511 | + } |
| 512 | + |
| 513 | + $targetType = $this->parse($tokens); |
| 514 | + |
| 515 | + $tokens->skipNewLineTokensAndConsumeComments(); |
| 516 | + $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
| 517 | + $tokens->skipNewLineTokensAndConsumeComments(); |
| 518 | + |
| 519 | + $ifType = $this->parse($tokens); |
| 520 | + |
| 521 | + $tokens->skipNewLineTokensAndConsumeComments(); |
| 522 | + $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
| 523 | + $tokens->skipNewLineTokensAndConsumeComments(); |
| 524 | + |
| 525 | + $elseType = $this->subParse($tokens); |
| 526 | + |
| 527 | + return new Ast\Type\ConditionalTypeForPropertyNode($propertyExpression, $targetType, $ifType, $elseType, $negated); |
| 528 | + } |
| 529 | + |
395 | 530 | /** @phpstan-impure */ |
396 | 531 | private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode |
397 | 532 | { |
|
0 commit comments