Skip to content
Open
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
58 changes: 58 additions & 0 deletions src/Ast/Type/ConditionalTypeForPropertyNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

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

class ConditionalTypeForPropertyNode implements TypeNode
{

use NodeAttributes;

public string $propertyExpression;

public TypeNode $targetType;

public TypeNode $if;

public TypeNode $else;

public bool $negated;

public function __construct(string $propertyExpression, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
{
$this->propertyExpression = $propertyExpression;
$this->targetType = $targetType;
$this->if = $if;
$this->else = $else;
$this->negated = $negated;
}

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

/**
* @param array<string, mixed> $properties
*/
public static function __set_state(array $properties): self
{
$instance = new self($properties['propertyExpression'], $properties['targetType'], $properties['if'], $properties['else'], $properties['negated']);
if (isset($properties['attributes'])) {
foreach ($properties['attributes'] as $key => $value) {
$instance->setAttribute($key, $value);
}
}
return $instance;
}

}
2 changes: 1 addition & 1 deletion src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private function generateRegexp(): string
$patterns = [
self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',

self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++',
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF](?:[0-9a-z_\\x80-\\xFF]|(?!->)-)*+)++',
self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',

Expand Down
157 changes: 146 additions & 11 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function in_array;
use function str_contains;
use function str_replace;
use function strlen;
use function strpos;
Expand Down Expand Up @@ -113,29 +114,68 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$propertyExpression = $this->parsePropertyExpression($tokens);

} else {
$type = $this->parseAtomic($tokens);
if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditionalForProperty($tokens, $propertyExpression);
} else {
$type = $this->parseAtomic($tokens);
$type = $this->parseUnionOrIntersectionIfPresent($tokens, $type);
}

if ($tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditional($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$parameterName = $tokens->currentTokenValue();
$propertyExpression = $this->parsePropertyExpression($tokens);

if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditionalForProperty($tokens, $propertyExpression);
} else {
$tokens->skipNewLineTokensAndConsumeComments();
$type = $this->parseConditionalForParameter($tokens, $parameterName);
}

if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
$type = $this->subParseUnion($tokens, $type);
} else {
if ($tokens->isCurrentTokenValue('self') ||
$tokens->isCurrentTokenValue('parent') ||
$tokens->isCurrentTokenValue('static')) {
$propertyExpression = $this->parsePropertyExpression($tokens);

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
$type = $this->subParseIntersection($tokens, $type);
if ($propertyExpression !== null && $tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditionalForProperty($tokens, $propertyExpression);
} else {
$type = $this->parseAtomic($tokens);
$type = $this->parseUnionOrIntersectionIfPresent($tokens, $type);
}
} else {
$type = $this->parseAtomic($tokens);
$type = $this->parseUnionOrIntersectionIfPresent($tokens, $type);
}
}

return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}

/** @phpstan-impure */
private function parseUnionOrIntersectionIfPresent(
TokenIterator $tokens,
Ast\Type\TypeNode $type
): Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenValue('is')) {
return $this->parseConditional($tokens, $type);
}

$tokens->skipNewLineTokensAndConsumeComments();

if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
return $this->subParseUnion($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
return $this->subParseIntersection($tokens, $type);
}

return $type;
}

/** @phpstan-impure */
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
{
Expand Down Expand Up @@ -392,6 +432,101 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par
return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
}

/** @phpstan-impure */
private function parsePropertyExpression(TokenIterator $tokens): ?string
{
$tokens->pushSavePoint();

// Handle $this->prop or $variable->prop
if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE) || $tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$expression = $tokens->currentTokenValue();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$tokens->consumeTokenType(Lexer::TOKEN_THIS_VARIABLE);
} else {
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
}

// Parse chained property access: $this->a->b->c
while ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
$expression .= '->';
$tokens->consumeTokenType(Lexer::TOKEN_ARROW);

if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$tokens->rollback();
return null;
}

$propertyName = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$expression .= $propertyName;
}

if (!str_contains($expression, '->')) {
$tokens->rollback();
return null;
}

$tokens->dropSavePoint();
return $expression;

} elseif ($tokens->isCurrentTokenValue('self') ||
$tokens->isCurrentTokenValue('parent') ||
$tokens->isCurrentTokenValue('static')) {
$expression = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->rollback();
return null;
}

$expression .= '::';
$tokens->consumeTokenType(Lexer::TOKEN_DOUBLE_COLON);

if (!$tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$tokens->rollback();
return null;
}

$expression .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);

$tokens->dropSavePoint();
return $expression;
}

$tokens->dropSavePoint();
return null;
}

/** @phpstan-impure */
private function parseConditionalForProperty(TokenIterator $tokens, string $propertyExpression): Ast\Type\TypeNode
{
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');

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

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

$tokens->skipNewLineTokensAndConsumeComments();
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->skipNewLineTokensAndConsumeComments();

$ifType = $this->parse($tokens);

$tokens->skipNewLineTokensAndConsumeComments();
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$tokens->skipNewLineTokensAndConsumeComments();

$elseType = $this->subParse($tokens);

return new Ast\Type\ConditionalTypeForPropertyNode($propertyExpression, $targetType, $ifType, $elseType, $negated);
}

/** @phpstan-impure */
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
{
Expand Down
11 changes: 11 additions & 0 deletions src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForPropertyNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
Expand Down Expand Up @@ -434,6 +435,16 @@ private function printType(TypeNode $node): string
$this->printType($node->else),
);
}
if ($node instanceof ConditionalTypeForPropertyNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
$node->propertyExpression,
$node->negated ? 'is not' : 'is',
$this->printType($node->targetType),
$this->printType($node->if),
$this->printType($node->else),
);
}
if ($node instanceof ConditionalTypeNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
Expand Down
Loading