From 5f13698464773fa6f5392a9e311f81e23e9c3ba7 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Thu, 20 Oct 2022 09:28:03 +0200 Subject: [PATCH] Allow unparenthesized conditional type in conditional else branch --- doc/grammars/type.abnf | 2 +- src/Parser/TypeParser.php | 4 +- tests/PHPStan/Parser/PhpDocParserTest.php | 174 ++++++++++++++++++++++ tests/PHPStan/Parser/TypeParserTest.php | 50 +++++++ 4 files changed, 227 insertions(+), 3 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index e804a45..827bc8d 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -17,7 +17,7 @@ Intersection = 1*(TokenIntersection Atomic) Conditional - = 1*ByteHorizontalWs TokenIs [TokenNot] Atomic TokenNullable Atomic TokenColon Atomic + = 1*ByteHorizontalWs TokenIs [TokenNot] Atomic TokenNullable Type TokenColon ParenthesizedType Nullable = TokenNullable Atomic diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 170b258..1b07c74 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -242,7 +242,7 @@ class TypeParser $tokens->consumeTokenType(Lexer::TOKEN_COLON); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $elseType = $this->parse($tokens); + $elseType = $this->subParse($tokens); return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated); } @@ -271,7 +271,7 @@ class TypeParser $tokens->consumeTokenType(Lexer::TOKEN_COLON); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $elseType = $this->parse($tokens); + $elseType = $this->subParse($tokens); return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 69186a4..da56e90 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -34,6 +34,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -4381,6 +4383,178 @@ Finder::findFiles('*.php') ), ]), ]; + + yield [ + 'complex stub from Psalm', + '/**' . PHP_EOL . + ' * @psalm-pure' . PHP_EOL . + ' * @template TFlags as int-mask<0, 256, 512>' . PHP_EOL . + ' *' . PHP_EOL . + ' * @param string $pattern' . PHP_EOL . + ' * @param string $subject' . PHP_EOL . + ' * @param mixed $matches' . PHP_EOL . + ' * @param TFlags $flags' . PHP_EOL . + " * @param-out (TFlags is 256 ? array :" . PHP_EOL . + ' * TFlags is 512 ? array :' . PHP_EOL . + ' * TFlags is 768 ? array :' . PHP_EOL . + ' * array' . PHP_EOL . + ' * ) $matches' . PHP_EOL . + ' * @return 1|0|false' . PHP_EOL . + ' * @psalm-ignore-falsable-return' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@psalm-pure', new GenericTagValueNode('')), + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'TFlags', + new GenericTypeNode( + new IdentifierTypeNode('int-mask'), + [ + new ConstTypeNode(new ConstExprIntegerNode('0')), + new ConstTypeNode(new ConstExprIntegerNode('256')), + new ConstTypeNode(new ConstExprIntegerNode('512')), + ] + ), + '' + ) + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('string'), + false, + '$pattern', + '' + ) + ), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('string'), + false, + '$subject', + '' + ) + ), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('mixed'), + false, + '$matches', + '' + ) + ), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('TFlags'), + false, + '$flags', + '' + ) + ), + new PhpDocTagNode( + '@param-out', + new ParamOutTagValueNode( + new ConditionalTypeNode( + new IdentifierTypeNode('TFlags'), + new ConstTypeNode(new ConstExprIntegerNode('256')), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new UnionTypeNode([ + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('string')), + new ArrayShapeItemNode( + null, + false, + new UnionTypeNode([ + new ConstTypeNode(new ConstExprIntegerNode('0')), + new IdentifierTypeNode('positive-int'), + ]) + ), + ]), + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new ConstTypeNode(new ConstExprStringNode(''))), + new ArrayShapeItemNode(null, false, new ConstTypeNode(new ConstExprIntegerNode('-1'))), + ]), + ]), + ] + ), + new ConditionalTypeNode( + new IdentifierTypeNode('TFlags'), + new ConstTypeNode(new ConstExprIntegerNode('512')), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new UnionTypeNode([ + new IdentifierTypeNode('string'), + new IdentifierTypeNode('null'), + ]), + ] + ), + new ConditionalTypeNode( + new IdentifierTypeNode('TFlags'), + new ConstTypeNode(new ConstExprIntegerNode('768')), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new UnionTypeNode([ + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('string')), + new ArrayShapeItemNode( + null, + false, + new UnionTypeNode([ + new ConstTypeNode(new ConstExprIntegerNode('0')), + new IdentifierTypeNode('positive-int'), + ]) + ), + ]), + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('null')), + new ArrayShapeItemNode(null, false, new ConstTypeNode(new ConstExprIntegerNode('-1'))), + ]), + ]), + ] + ), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new IdentifierTypeNode('string'), + ] + ), + false + ), + false + ), + false + ), + '$matches', + '' + ) + ), + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new UnionTypeNode([ + new ConstTypeNode(new ConstExprIntegerNode('1')), + new ConstTypeNode(new ConstExprIntegerNode('0')), + new IdentifierTypeNode('false'), + ]), + '' + ) + ), + new PhpDocTagNode('@psalm-ignore-falsable-return', new GenericTagValueNode('')), + ]), + ]; } public function provideDescriptionWithOrWithoutHtml(): Iterator diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 8289488..da21df9 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1266,6 +1266,56 @@ class TypeParserTest extends TestCase ) ), ], + [ + '(T is Foo ? true : T is Bar ? false : null)', + new ConditionalTypeNode( + new IdentifierTypeNode('T'), + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('true'), + new ConditionalTypeNode( + new IdentifierTypeNode('T'), + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('false'), + new IdentifierTypeNode('null'), + false + ), + false + ), + ], + [ + '(T is Foo ? T is Bar ? true : false : null)', + new ParserException( + 'is', + Lexer::TOKEN_IDENTIFIER, + 14, + Lexer::TOKEN_COLON + ), + ], + [ + '($foo is Foo ? true : $foo is Bar ? false : null)', + new ConditionalTypeForParameterNode( + '$foo', + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('true'), + new ConditionalTypeForParameterNode( + '$foo', + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('false'), + new IdentifierTypeNode('null'), + false + ), + false + ), + ], + [ + '($foo is Foo ? $foo is Bar ? true : false : null)', + new ParserException( + '$foo', + Lexer::TOKEN_VARIABLE, + 15, + Lexer::TOKEN_IDENTIFIER + ), + ], ]; }