Skip to content

Commit

Permalink
Generics type projections (call-site variance)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil authored Nov 27, 2022
1 parent aac4411 commit df1a794
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 13 deletions.
15 changes: 14 additions & 1 deletion doc/grammars/type.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
27 changes: 25 additions & 2 deletions src/Ast/Type/GenericTypeNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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) . '>';
}

}
38 changes: 34 additions & 4 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,24 +323,54 @@ 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);

while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$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];
}


Expand Down
Loading

0 comments on commit df1a794

Please sign in to comment.