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 c2a00c0
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 8 deletions.
44 changes: 44 additions & 0 deletions src/Ast/Type/ConditionalTypeNode.php
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;

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,
);
}

}
46 changes: 38 additions & 8 deletions src/Parser/TypeParser.php
Expand Up @@ -2,11 +2,8 @@

namespace PHPStan\PhpDocParser\Parser;

use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function strpos;
use function trim;

class TypeParser
{
Expand All @@ -33,6 +30,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 +43,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 @@ -107,7 +108,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
$tokens->dropSavePoint(); // because of ConstFetchNode
}

$exception = new ParserException(
$exception = new \PHPStan\PhpDocParser\Parser\ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Expand All @@ -125,7 +126,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
}

return new Ast\Type\ConstTypeNode($constExpr);
} catch (LogicException $e) {
} catch (\LogicException $e) {
throw $exception;
}
}
Expand Down Expand Up @@ -157,6 +158,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 Expand Up @@ -315,7 +345,7 @@ private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierType
$type = $this->parseCallable($tokens, $identifier);
$tokens->dropSavePoint();

} catch (ParserException $e) {
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$tokens->rollback();
$type = $identifier;
}
Expand All @@ -336,7 +366,7 @@ private function tryParseArray(TokenIterator $tokens, Ast\Type\TypeNode $type):
$type = new Ast\Type\ArrayTypeNode($type);
}

} catch (ParserException $e) {
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$tokens->rollback();
}

Expand Down Expand Up @@ -386,7 +416,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape
$tokens->dropSavePoint();

return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
} catch (ParserException $e) {
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$tokens->rollback();
$value = $this->parse($tokens);

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 c2a00c0

Please sign in to comment.