Skip to content

Commit

Permalink
Support conditional types
Browse files Browse the repository at this point in the history
Part of implementation for phpstan/phpstan#3853
  • Loading branch information
rvanvelzen committed Mar 25, 2022
1 parent 4bda9e3 commit 3113c32
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/Ast/Type/ConditionalTypeNode.php
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ConditionalTypeNode implements TypeNode
{

use NodeAttributes;

/** @var TypeNode */
public $subject;

/** @var TypeNode */
public $targetType;

/** @var TypeNode */
public $trueType;

/** @var TypeNode */
public $falseType;

/** @var bool */
public $negated;

public function __construct(TypeNode $subject, TypeNode $targetType, TypeNode $trueType, TypeNode $falseType, bool $negated)
{
$this->subject = $subject;
$this->targetType = $targetType;
$this->trueType = $trueType;
$this->falseType = $falseType;
$this->negated = $negated;
}

public function __toString(): string
{
return sprintf(
'%s %s %s ? %s : %s',
$this->subject,
$this->negated ? 'is not' : 'is',
$this->targetType,
$this->trueType,
$this->falseType,
);
}

}
33 changes: 33 additions & 0 deletions src/Parser/TypeParser.php
Expand Up @@ -33,6 +33,8 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
$type = $this->parseIntersection($tokens, $type);
} elseif ($tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditional($tokens, $type);
}
}

Expand All @@ -44,7 +46,9 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$type = $this->parse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
Expand Down Expand Up @@ -157,6 +161,35 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ
}


/** @phpstan-impure */
private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subject): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

$negated = false;
if ($tokens->isCurrentTokenValue('not')) {
$negated = true;
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}

$targetType = $this->parseAtomic($tokens);

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

$trueType = $this->parseAtomic($tokens);

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

$falseType = $this->parseAtomic($tokens);

return new Ast\Type\ConditionalTypeNode($subject, $targetType, $trueType, $falseType, $negated);
}


/** @phpstan-impure */
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
{
Expand Down
73 changes: 73 additions & 0 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Expand Up @@ -31,6 +31,7 @@
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
Expand Down Expand Up @@ -1119,6 +1120,78 @@ public function provideReturnTagsData(): Iterator
),
]),
];

yield [
'OK with conditional type',
'/** @return Foo is Bar ? never : int */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new ConditionalTypeNode(
new IdentifierTypeNode('Foo'),
new IdentifierTypeNode('Bar'),
new IdentifierTypeNode('never'),
new IdentifierTypeNode('int'),
false
),
''
)
),
]),
];

yield [
'OK with negated conditional type',
'/** @return Foo is not Bar ? never : int */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new ConditionalTypeNode(
new IdentifierTypeNode('Foo'),
new IdentifierTypeNode('Bar'),
new IdentifierTypeNode('never'),
new IdentifierTypeNode('int'),
true
),
''
)
),
]),
];

yield [
'OK with multiline conditional type',
'/**
* @return (
* T is self::TYPE_STRING
* ? string
* : (T is self::TYPE_INT ? int : bool)
* )
*/',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new ConditionalTypeNode(
new IdentifierTypeNode('T'),
new ConstTypeNode(new ConstFetchNode('self', 'TYPE_STRING')),
new IdentifierTypeNode('string'),
new ConditionalTypeNode(
new IdentifierTypeNode('T'),
new ConstTypeNode(new ConstFetchNode('self', 'TYPE_INT')),
new IdentifierTypeNode('int'),
new IdentifierTypeNode('bool'),
false
),
false
),
''
)
),
]),
];
}


Expand Down

0 comments on commit 3113c32

Please sign in to comment.