Skip to content

Commit

Permalink
Implement conditional types slightly better
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen committed Mar 31, 2022
1 parent 9240f16 commit e0e7ba4
Show file tree
Hide file tree
Showing 12 changed files with 757 additions and 46 deletions.
29 changes: 25 additions & 4 deletions src/PhpDoc/TypeNodeResolver.php
Expand Up @@ -44,6 +44,8 @@
use PHPStan\Type\CallableType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\ConditionalType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
Expand Down Expand Up @@ -123,9 +125,12 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
} elseif ($typeNode instanceof IntersectionTypeNode) {
return $this->resolveIntersectionTypeNode($typeNode, $nameScope);

} elseif ($typeNode instanceof ConditionalTypeNode || $typeNode instanceof ConditionalTypeForParameterNode) {
} elseif ($typeNode instanceof ConditionalTypeNode) {
return $this->resolveConditionalTypeNode($typeNode, $nameScope);

} elseif ($typeNode instanceof ConditionalTypeForParameterNode) {
return $this->resolveConditionalTypeForParameterNode($typeNode, $nameScope);

} elseif ($typeNode instanceof ArrayTypeNode) {
return $this->resolveArrayTypeNode($typeNode, $nameScope);

Expand Down Expand Up @@ -443,10 +448,26 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam
return TypeCombinator::intersect(...$types);
}

private function resolveConditionalTypeNode(ConditionalTypeNode|ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type
private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type
{
return ConditionalType::create(
$this->resolve($typeNode->subjectType, $nameScope),
$this->resolve($typeNode->targetType, $nameScope),
$this->resolve($typeNode->if, $nameScope),
$this->resolve($typeNode->else, $nameScope),
$typeNode->negated,
);
}

private function resolveConditionalTypeForParameterNode(ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type
{
$types = $this->resolveMultiple([$typeNode->if, $typeNode->else], $nameScope);
return TypeCombinator::union(...$types);
return new ConditionalTypeForParameter(
$typeNode->parameterName,
$this->resolve($typeNode->targetType, $nameScope),
$this->resolve($typeNode->if, $nameScope),
$this->resolve($typeNode->else, $nameScope),
$typeNode->negated,
);
}

private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type
Expand Down
3 changes: 3 additions & 0 deletions src/Reflection/GenericParametersAcceptorResolver.php
Expand Up @@ -17,6 +17,7 @@ class GenericParametersAcceptorResolver
public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor
{
$typeMap = TemplateTypeMap::createEmpty();
$passedArgs = [];

foreach ($parametersAcceptor->getParameters() as $i => $param) {
if (isset($argTypes[$i])) {
Expand All @@ -31,6 +32,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc

$paramType = $param->getType();
$typeMap = $typeMap->union($paramType->inferTemplateTypes($argType));
$passedArgs['$' . $param->getName()] = $argType;
}

return new ResolvedFunctionVariant(
Expand All @@ -39,6 +41,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
$parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(),
$typeMap->getTypes(),
)),
$passedArgs,
);
}

Expand Down
20 changes: 20 additions & 0 deletions src/Reflection/ResolvedFunctionVariant.php
Expand Up @@ -3,9 +3,12 @@
namespace PHPStan\Reflection;

use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use function array_key_exists;
use function array_map;

class ResolvedFunctionVariant implements ParametersAcceptor
Expand All @@ -16,9 +19,13 @@ class ResolvedFunctionVariant implements ParametersAcceptor

private ?Type $returnType = null;

/**
* @param array<string, Type> $passedArgs
*/
public function __construct(
private ParametersAcceptor $parametersAcceptor,
private TemplateTypeMap $resolvedTemplateTypeMap,
private array $passedArgs,
)
{
}
Expand Down Expand Up @@ -73,10 +80,23 @@ public function getReturnType(): Type
$this->resolvedTemplateTypeMap,
);

$type = $this->resolveConditionalTypes($type);

$this->returnType = $type;
}

return $type;
}

private function resolveConditionalTypes(Type $type): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
while ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) {
$type = $type->toConditional($this->passedArgs[$type->getParameterName()]);
}

return $traverse($type);
});
}

}
1 change: 1 addition & 0 deletions src/Reflection/ResolvedMethodReflection.php
Expand Up @@ -42,6 +42,7 @@ public function getVariants(): array
$variants[] = new ResolvedFunctionVariant(
$variant,
$this->resolvedTemplateTypeMap,
[],
);
}

Expand Down
147 changes: 147 additions & 0 deletions src/Type/ConditionalType.php
@@ -0,0 +1,147 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Traits\ConditionalTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use function array_merge;
use function sprintf;

/** @api */
final class ConditionalType implements CompoundType
{

use ConditionalTypeTrait;
use NonGeneralizableTypeTrait;

private function __construct(
private Type $subject,
private Type $target,
Type $if,
Type $else,
private bool $negated,
)
{
$this->if = $if;
$this->else = $else;
}

public function getReferencedClasses(): array
{
return array_merge(
$this->subject->getReferencedClasses(),
$this->target->getReferencedClasses(),
$this->if->getReferencedClasses(),
$this->else->getReferencedClasses(),
);
}

public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
{
return array_merge(
$this->subject->getReferencedTemplateTypes($positionVariance),
$this->target->getReferencedTemplateTypes($positionVariance),
$this->if->getReferencedTemplateTypes($positionVariance),
$this->else->getReferencedTemplateTypes($positionVariance),
);
}

public function equals(Type $type): bool
{
return $type instanceof self
&& $this->subject->equals($type->subject)
&& $this->target->equals($type->target)
&& $this->if->equals($type->if)
&& $this->else->equals($type->else);
}

public function describe(VerbosityLevel $level): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->subject->describe($level),
$this->negated ? 'is not' : 'is',
$this->target->describe($level),
$this->if->describe($level),
$this->else->describe($level),
);
}

public static function create(
Type $subject,
Type $target,
Type $if,
Type $else,
bool $negated,
): Type
{
return (new self($subject, $target, $if, $else, $negated))->resolve();
}

private function resolve(): Type
{
$isSuperType = $this->target->isSuperTypeOf($this->subject);

if ($isSuperType->yes()) {
return !$this->negated ? $this->if : $this->else;
} elseif ($isSuperType->no()) {
return !$this->negated ? $this->else : $this->if;
}

if ($this->isResolved()) {
return TypeCombinator::union($this->if, $this->else);
}

return $this;
}

public function traverse(callable $cb): Type
{
$subject = $cb($this->subject);
$target = $cb($this->target);
$if = $cb($this->if);
$else = $cb($this->else);

if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) {
return $this;
}

return self::create($subject, $target, $if, $else, $this->negated);
}

private function isResolved(): bool
{
return !$this->containsTemplate($this->subject) && !$this->containsTemplate($this->target);
}

private function containsTemplate(Type $type): bool
{
$containsTemplate = false;
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsTemplate): Type {
if ($type instanceof TemplateType) {
$containsTemplate = true;
}

return $containsTemplate ? $type : $traverse($type);
});

return $containsTemplate;
}

/**
* @param mixed[] $properties
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['subject'],
$properties['target'],
$properties['if'],
$properties['else'],
$properties['negated'],
);
}

}
115 changes: 115 additions & 0 deletions src/Type/ConditionalTypeForParameter.php
@@ -0,0 +1,115 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Traits\ConditionalTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use function array_merge;
use function sprintf;

/** @api */
final class ConditionalTypeForParameter implements CompoundType
{

use ConditionalTypeTrait;
use NonGeneralizableTypeTrait;

public function __construct(
private string $parameterName,
private Type $target,
Type $if,
Type $else,
private bool $negated,
)
{
$this->if = $if;
$this->else = $else;
}

public function getParameterName(): string
{
return $this->parameterName;
}

public function toConditional(Type $subject): Type
{
return ConditionalType::create(
$subject,
$this->target,
$this->if,
$this->else,
$this->negated,
);
}

public function getReferencedClasses(): array
{
return array_merge(
$this->target->getReferencedClasses(),
$this->if->getReferencedClasses(),
$this->else->getReferencedClasses(),
);
}

public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
{
return array_merge(
$this->target->getReferencedTemplateTypes($positionVariance),
$this->if->getReferencedTemplateTypes($positionVariance),
$this->else->getReferencedTemplateTypes($positionVariance),
);
}

public function equals(Type $type): bool
{
return $type instanceof self
&& $this->parameterName === $type->parameterName
&& $this->target->equals($type->target)
&& $this->if->equals($type->if)
&& $this->else->equals($type->else);
}

public function describe(VerbosityLevel $level): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->parameterName,
$this->negated ? 'is not' : 'is',
$this->target->describe($level),
$this->if->describe($level),
$this->else->describe($level),
);
}

/**
* @param callable(Type): Type $cb
*/
public function traverse(callable $cb): Type
{
$target = $cb($this->target);
$if = $cb($this->if);
$else = $cb($this->else);

if ($this->target === $target && $this->if === $if && $this->else === $else) {
return $this;
}

return new ConditionalTypeForParameter($this->parameterName, $target, $if, $else, $this->negated);
}

/**
* @param mixed[] $properties
*/
public static function __set_state(array $properties): Type
{
return new self(
$properties['parameterName'],
$properties['target'],
$properties['if'],
$properties['else'],
$properties['negated'],
);
}

}

0 comments on commit e0e7ba4

Please sign in to comment.