diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 827bc8d4..f00a191c 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -28,7 +28,11 @@ Atomic / TokenParenthesesOpen ParenthesizedType TokenParenthesesClose [Array] Generic - = TokenAngleBracketOpen Type *(TokenComma Type) TokenAngleBracketClose + = TokenAngleBracketOpen GenericTypeArgument *(TokenComma GenericTypeArgument) TokenAngleBracketClose + +GenericTypeArgument + = [TokenContravariant / TokenCovariant] Type + / TokenWildcard Callable = TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType @@ -188,6 +192,15 @@ TokenIs TokenNot = %s"not" 1*ByteHorizontalWs +TokenContravariant + = %s"contravariant" 1*ByteHorizontalWs + +TokenCovariant + = %s"covariant" 1*ByteHorizontalWs + +TokenWildcard + = "*" *ByteHorizontalWs + TokenIdentifier = [ByteBackslash] ByteIdentifierFirst *ByteIdentifierSecond *(ByteBackslash ByteIdentifierFirst *ByteIdentifierSecond) *ByteHorizontalWs diff --git a/src/Ast/Type/GenericTypeNode.php b/src/Ast/Type/GenericTypeNode.php index 6bb704d1..179de55a 100644 --- a/src/Ast/Type/GenericTypeNode.php +++ b/src/Ast/Type/GenericTypeNode.php @@ -4,10 +4,16 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use function implode; +use function sprintf; class GenericTypeNode implements TypeNode { + public const VARIANCE_INVARIANT = 'invariant'; + public const VARIANCE_COVARIANT = 'covariant'; + public const VARIANCE_CONTRAVARIANT = 'contravariant'; + public const VARIANCE_BIVARIANT = 'bivariant'; + use NodeAttributes; /** @var IdentifierTypeNode */ @@ -16,16 +22,33 @@ class GenericTypeNode implements TypeNode /** @var TypeNode[] */ public $genericTypes; - public function __construct(IdentifierTypeNode $type, array $genericTypes) + /** @var (self::VARIANCE_*)[] */ + public $variances; + + public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = []) { $this->type = $type; $this->genericTypes = $genericTypes; + $this->variances = $variances; } public function __toString(): string { - return $this->type . '<' . implode(', ', $this->genericTypes) . '>'; + $genericTypes = []; + + foreach ($this->genericTypes as $index => $type) { + $variance = $this->variances[$index] ?? self::VARIANCE_INVARIANT; + if ($variance === self::VARIANCE_INVARIANT) { + $genericTypes[] = (string) $type; + } elseif ($variance === self::VARIANCE_BIVARIANT) { + $genericTypes[] = '*'; + } else { + $genericTypes[] = sprintf('%s %s', $variance, $type); + } + } + + return $this->type . '<' . implode(', ', $genericTypes) . '>'; } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 1b07c74e..ef0fbc90 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -323,7 +323,11 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $genericTypes = [$this->parse($tokens)]; + + $genericTypes = []; + $variances = []; + + [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -331,16 +335,42 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { // trailing comma case - return new Ast\Type\GenericTypeNode($baseType, $genericTypes); + return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); } - $genericTypes[] = $this->parse($tokens); + [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); - return new Ast\Type\GenericTypeNode($baseType, $genericTypes); + return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); + } + + + /** + * @phpstan-impure + * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*} + */ + public function parseGenericTypeArgument(TokenIterator $tokens): array + { + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { + return [ + new Ast\Type\IdentifierTypeNode('mixed'), + Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT, + ]; + } + + if ($tokens->tryConsumeTokenValue('contravariant')) { + $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT; + } elseif ($tokens->tryConsumeTokenValue('covariant')) { + $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT; + } else { + $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT; + } + + $type = $this->parse($tokens); + return [$type, $variance]; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 5b9d5329..d7a45c4a 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -1279,6 +1279,9 @@ public function provideReturnTagsData(): Iterator new IdentifierTypeNode('B'), [ new ConstTypeNode(new ConstExprIntegerNode('123')), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), ]), @@ -1708,6 +1711,8 @@ public function provideMixinTagsData(): Iterator new MixinTagValueNode( new GenericTypeNode(new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('Bar'), + ], [ + GenericTypeNode::VARIANCE_INVARIANT, ]), '' ) @@ -3107,6 +3112,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new GenericTypeNode( @@ -3114,6 +3123,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new GenericTypeNode( @@ -3121,6 +3134,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ]), @@ -3149,6 +3166,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new ConditionalTypeNode( @@ -3159,6 +3180,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new UnionTypeNode([ @@ -3167,6 +3192,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new GenericTypeNode( @@ -3174,6 +3203,10 @@ public function provideMultiLinePhpDocData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ]), @@ -3411,6 +3444,9 @@ public function provideExtendsTagsData(): Iterator new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('A'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -3431,6 +3467,10 @@ public function provideExtendsTagsData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -3451,6 +3491,10 @@ public function provideExtendsTagsData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -3471,6 +3515,10 @@ public function provideExtendsTagsData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -3488,7 +3536,8 @@ public function provideExtendsTagsData(): Iterator new ExtendsTagValueNode( new GenericTypeNode( new IdentifierTypeNode('Foo'), - [new IdentifierTypeNode('A')] + [new IdentifierTypeNode('A')], + [GenericTypeNode::VARIANCE_INVARIANT] ), 'extends foo' ) @@ -4278,6 +4327,10 @@ public function provideRealWorldExampleData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -4303,6 +4356,10 @@ public function provideRealWorldExampleData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -4328,6 +4385,10 @@ public function provideRealWorldExampleData(): Iterator [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -4449,6 +4510,11 @@ public function provideRealWorldExampleData(): Iterator new ConstTypeNode(new ConstExprIntegerNode('0')), new ConstTypeNode(new ConstExprIntegerNode('256')), new ConstTypeNode(new ConstExprIntegerNode('512')), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), '' @@ -4518,6 +4584,10 @@ public function provideRealWorldExampleData(): Iterator new ArrayShapeItemNode(null, false, new ConstTypeNode(new ConstExprIntegerNode('-1'))), ]), ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new ConditionalTypeNode( @@ -4531,6 +4601,10 @@ public function provideRealWorldExampleData(): Iterator new IdentifierTypeNode('string'), new IdentifierTypeNode('null'), ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new ConditionalTypeNode( @@ -4557,6 +4631,10 @@ public function provideRealWorldExampleData(): Iterator new ArrayShapeItemNode(null, false, new ConstTypeNode(new ConstExprIntegerNode('-1'))), ]), ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new GenericTypeNode( @@ -4564,6 +4642,10 @@ public function provideRealWorldExampleData(): Iterator [ new IdentifierTypeNode('array-key'), new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), false @@ -4655,6 +4737,9 @@ public function provideDescriptionWithOrWithoutHtml(): Iterator new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('strong'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), 'Important description' @@ -4755,7 +4840,7 @@ public function provideSelfOutTagsData(): Iterator new PhpDocTagNode( '@phpstan-self-out', new SelfOutTagValueNode( - new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')]), + new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')], [GenericTypeNode::VARIANCE_INVARIANT]), '' ) ), @@ -4769,7 +4854,7 @@ public function provideSelfOutTagsData(): Iterator new PhpDocTagNode( '@phpstan-this-out', new SelfOutTagValueNode( - new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')]), + new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')], [GenericTypeNode::VARIANCE_INVARIANT]), '' ) ), @@ -4783,7 +4868,7 @@ public function provideSelfOutTagsData(): Iterator new PhpDocTagNode( '@psalm-self-out', new SelfOutTagValueNode( - new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')]), + new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')], [GenericTypeNode::VARIANCE_INVARIANT]), '' ) ), @@ -4797,7 +4882,7 @@ public function provideSelfOutTagsData(): Iterator new PhpDocTagNode( '@psalm-this-out', new SelfOutTagValueNode( - new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')]), + new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')], [GenericTypeNode::VARIANCE_INVARIANT]), '' ) ), @@ -4811,7 +4896,7 @@ public function provideSelfOutTagsData(): Iterator new PhpDocTagNode( '@phpstan-self-out', new SelfOutTagValueNode( - new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')]), + new GenericTypeNode(new IdentifierTypeNode('self'), [new IdentifierTypeNode('T')], [GenericTypeNode::VARIANCE_INVARIANT]), 'description' ) ), diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index da21df97..75b26fe7 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -279,6 +279,9 @@ public function provideParseData(): array new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ) ), @@ -290,6 +293,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('int'), new IdentifierTypeNode('Foo\\Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -619,6 +626,9 @@ public function provideParseData(): array new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ) ), @@ -722,6 +732,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('mixed'), new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new UnionTypeNode([ @@ -732,12 +746,19 @@ public function provideParseData(): array new IdentifierTypeNode('string'), [ new IdentifierTypeNode('foo'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), new IdentifierTypeNode('bar'), ]) ), ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new IdentifierTypeNode('Lorem'), @@ -769,6 +790,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('array-key'), new IdentifierTypeNode('int'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ) ), @@ -783,6 +808,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('array-key'), new IdentifierTypeNode('int'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ) ), @@ -935,6 +964,8 @@ public function provideParseData(): array 'list', new GenericTypeNode(new IdentifierTypeNode('list'), [ new ConstTypeNode(new ConstFetchNode('QueueAttributeName', '*')), + ], [ + GenericTypeNode::VARIANCE_INVARIANT, ]), ], [ @@ -945,6 +976,9 @@ public function provideParseData(): array new IdentifierTypeNode('array'), [ new IdentifierTypeNode('Foo'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -958,6 +992,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('Foo'), new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -970,6 +1008,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('Foo'), new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -988,8 +1030,15 @@ public function provideParseData(): array new IdentifierTypeNode('array'), [ new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -1008,8 +1057,15 @@ public function provideParseData(): array new IdentifierTypeNode('array'), [ new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, ] ), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ], @@ -1186,6 +1242,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new ConditionalTypeNode( @@ -1196,6 +1256,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new UnionTypeNode([ @@ -1204,6 +1268,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), new GenericTypeNode( @@ -1211,6 +1279,10 @@ public function provideParseData(): array [ new IdentifierTypeNode('TRandKey'), new IdentifierTypeNode('TRandVal'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), ]), @@ -1316,6 +1388,66 @@ public function provideParseData(): array Lexer::TOKEN_IDENTIFIER ), ], + [ + 'Foo', + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('Baz'), + ], + [ + GenericTypeNode::VARIANCE_COVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ] + ), + ], + [ + 'Foo', + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('Baz'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_CONTRAVARIANT, + ] + ), + ], + [ + 'Foo', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 13, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'Foo', + new ParserException( + 'Bar', + Lexer::TOKEN_IDENTIFIER, + 16, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'Foo', + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('mixed'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_BIVARIANT, + ] + ), + ], ]; }