Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: tabs vs. spaces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But which one is correct? It's pretty inconsistent across this whole file 😃

/ 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some unsupported covariance name like bullshitriant isn't gonna cause a parse error and will silently fall back to invariant, right? Maybe we should address that or at least test it.

Copy link
Contributor Author

@jiripudil jiripudil Nov 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It falls back to invariant and proceeds to parse the value as a type. Thus,

  • Foo<bullshitriant> creates a generic type with IdentifierTypeNode('bullshitriant') in it (supposedly failing later due to the unknown class). That seems correct to me, I don't think there's a reliable way to distinguish between a typo and a type on the parser level.
  • Foo<bullshitriant Type> throws because 'bullshitriant' is parsed as an identifier and the parser expects a comma or a closing angle bracket to follow. I'll add this case to the test.

}

$type = $this->parse($tokens);
return [$type, $variance];
}


Expand Down
Loading