Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Type projections #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
ondrejmirtes merged 6 commits into phpstan:1.9.x from jiripudil:type-projections
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
View file Open in desktop
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

@jiripudil jiripudil Jun 20, 2022

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
View file Open in desktop
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];
Copy link
Member

@ondrejmirtes ondrejmirtes Nov 27, 2022

Choose a reason for hiding this comment

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

This offset might not exist.

jiripudil reacted with thumbs up emoji
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
View file Open in desktop
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

@ondrejmirtes ondrejmirtes Nov 27, 2022

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
edited
Loading

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

AltStyle によって変換されたページ (->オリジナル) /