Skip to content

Commit

Permalink
Intersection types - check for unresolvable type
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 5, 2021
1 parent 194ff6e commit 021e25e
Show file tree
Hide file tree
Showing 20 changed files with 536 additions and 26 deletions.
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ public function supportsEnums(): bool
return $this->versionId >= 80100;
}

public function supportsPureIntersectionTypes(): bool
{
return $this->versionId >= 80100;
}

public function supportsCaseInsensitiveConstantNames(): bool
{
return $this->versionId < 80000;
Expand Down
5 changes: 3 additions & 2 deletions src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ private function getRuleRegistry(Container $container): Registry
$missingTypehintCheck = $container->getByType(MissingTypehintCheck::class);
$unresolvableTypeHelper = $container->getByType(UnresolvableTypeHelper::class);
$crossCheckInterfacesHelper = $container->getByType(CrossCheckInterfacesHelper::class);
$phpVersion = $container->getByType(PhpVersion::class);

$rules = [
// level 0
Expand All @@ -146,8 +147,8 @@ private function getRuleRegistry(Container $container): Registry
new ExistingClassInTraitUseRule($classCaseSensitivityCheck, $reflectionProvider),
new ExistingClassesInTypehintsRule($functionDefinitionCheck),
new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck),
new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, true, false),
new OverridingMethodRule($container->getByType(PhpVersion::class), new MethodSignatureRule(true, true), true),
new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false),
new OverridingMethodRule($phpVersion, new MethodSignatureRule(true, true), true),

// level 2
new ClassAncestorsRule($fileTypeMapper, $genericAncestorsCheck, $crossCheckInterfacesHelper),
Expand Down
66 changes: 55 additions & 11 deletions src/Rules/FunctionDefinitionCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\NonexistentParentClassType;
use PHPStan\Type\ParserNodeTypeToPHPStanType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
Expand All @@ -33,6 +35,8 @@ class FunctionDefinitionCheck

private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck;

private UnresolvableTypeHelper $unresolvableTypeHelper;

private PhpVersion $phpVersion;

private bool $checkClassCaseSensitivity;
Expand All @@ -42,13 +46,15 @@ class FunctionDefinitionCheck
public function __construct(
ReflectionProvider $reflectionProvider,
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
UnresolvableTypeHelper $unresolvableTypeHelper,
PhpVersion $phpVersion,
bool $checkClassCaseSensitivity,
bool $checkThisOnly
)
{
$this->reflectionProvider = $reflectionProvider;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->unresolvableTypeHelper = $unresolvableTypeHelper;
$this->phpVersion = $phpVersion;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
$this->checkThisOnly = $checkThisOnly;
Expand All @@ -68,7 +74,9 @@ public function checkFunction(
string $parameterMessage,
string $returnMessage,
string $unionTypesMessage,
string $templateTypeMissingInParameterMessage
string $templateTypeMissingInParameterMessage,
string $unresolvableParameterTypeMessage,
string $unresolvableReturnTypeMessage
): array
{
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants());
Expand All @@ -79,7 +87,9 @@ public function checkFunction(
$parameterMessage,
$returnMessage,
$unionTypesMessage,
$templateTypeMissingInParameterMessage
$templateTypeMissingInParameterMessage,
$unresolvableParameterTypeMessage,
$unresolvableReturnTypeMessage
);
}

Expand All @@ -98,7 +108,9 @@ public function checkAnonymousFunction(
$returnTypeNode,
string $parameterMessage,
string $returnMessage,
string $unionTypesMessage
string $unionTypesMessage,
string $unresolvableParameterTypeMessage,
string $unresolvableReturnTypeMessage
): array
{
$errors = [];
Expand All @@ -123,6 +135,13 @@ public function checkAnonymousFunction(
if ($type instanceof VoidType) {
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void'))->line($param->type->getLine())->nonIgnorable()->build();
}
if (
$this->phpVersion->supportsPureIntersectionTypes()
&& $this->unresolvableTypeHelper->containsUnresolvableType($type)
) {
$errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name))->line($param->type->getLine())->nonIgnorable()->build();
}

foreach ($type->getReferencedClasses() as $class) {
if (!$this->reflectionProvider->hasClass($class) || $this->reflectionProvider->getClass($class)->isTrait()) {
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class))->line($param->type->getLine())->build();
Expand Down Expand Up @@ -154,6 +173,13 @@ public function checkAnonymousFunction(
}

$returnType = $scope->getFunctionType($returnTypeNode, false, false);
if (
$this->phpVersion->supportsPureIntersectionTypes()
&& $this->unresolvableTypeHelper->containsUnresolvableType($returnType)
) {
$errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build();
}

foreach ($returnType->getReferencedClasses() as $returnTypeClass) {
if (!$this->reflectionProvider->hasClass($returnTypeClass) || $this->reflectionProvider->getClass($returnTypeClass)->isTrait()) {
$errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass))->line($returnTypeNode->getLine())->build();
Expand Down Expand Up @@ -185,7 +211,9 @@ public function checkClassMethod(
string $parameterMessage,
string $returnMessage,
string $unionTypesMessage,
string $templateTypeMissingInParameterMessage
string $templateTypeMissingInParameterMessage,
string $unresolvableParameterTypeMessage,
string $unresolvableReturnTypeMessage
): array
{
/** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs $parametersAcceptor */
Expand All @@ -197,7 +225,9 @@ public function checkClassMethod(
$parameterMessage,
$returnMessage,
$unionTypesMessage,
$templateTypeMissingInParameterMessage
$templateTypeMissingInParameterMessage,
$unresolvableParameterTypeMessage,
$unresolvableReturnTypeMessage
);
}

Expand All @@ -216,7 +246,9 @@ private function checkParametersAcceptor(
string $parameterMessage,
string $returnMessage,
string $unionTypesMessage,
string $templateTypeMissingInParameterMessage
string $templateTypeMissingInParameterMessage,
string $unresolvableParameterTypeMessage,
string $unresolvableReturnTypeMessage
): array
{
$errors = [];
Expand Down Expand Up @@ -253,15 +285,20 @@ private function checkParametersAcceptor(

return $parameterNode;
};
if (
$parameter instanceof ParameterReflectionWithPhpDocs
&& $parameter->getNativeType() instanceof VoidType
) {
if ($parameter instanceof ParameterReflectionWithPhpDocs) {
$parameterVar = $parameterNodeCallback()->var;
if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) {
throw new \PHPStan\ShouldNotHappenException();
}
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build();
if ($parameter->getNativeType() instanceof VoidType) {
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build();
}
if (
$this->phpVersion->supportsPureIntersectionTypes()
&& $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType())
) {
$errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build();
}
}
foreach ($referencedClasses as $class) {
if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) {
Expand Down Expand Up @@ -290,6 +327,13 @@ private function checkParametersAcceptor(
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))->line($parameterNodeCallback()->getLine())->build();
}

if ($this->phpVersion->supportsPureIntersectionTypes() && $functionNode->getReturnType() !== null) {
$nativeReturnType = ParserNodeTypeToPHPStanType::resolve($functionNode->getReturnType(), null);
if ($this->unresolvableTypeHelper->containsUnresolvableType($nativeReturnType)) {
$errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->nonIgnorable()->line($returnTypeNode->getLine())->build();
}
}

$returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor);

foreach ($returnTypeReferencedClasses as $class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public function processNode(Node $node, Scope $scope): array
$node->getReturnType(),
'Parameter $%s of anonymous function has invalid type %s.',
'Anonymous function has invalid return type %s.',
'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.'
'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.',
'Parameter $%s of anonymous function has unresolvable native type.',
'Anonymous function has unresolvable native return type.'
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public function processNode(Node $node, Scope $scope): array
$node->getReturnType(),
'Parameter $%s of anonymous function has invalid type %s.',
'Anonymous function has invalid return type %s.',
'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.'
'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.',
'Parameter $%s of anonymous function has unresolvable native type.',
'Anonymous function has unresolvable native return type.'
);
}

Expand Down
10 changes: 9 additions & 1 deletion src/Rules/Functions/ExistingClassesInTypehintsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ public function processNode(Node $node, Scope $scope): array
$functionName
),
sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName),
sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName)
sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName),
sprintf(
'Parameter $%%s of function %s() has unresolvable native type.',
$functionName
),
sprintf(
'Function %s() has unresolvable native return type.',
$functionName
),
);
}

Expand Down
12 changes: 11 additions & 1 deletion src/Rules/Methods/ExistingClassesInTypehintsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ public function processNode(Node $node, Scope $scope): array
$methodName
),
sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $className, $methodName),
sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $className, $methodName)
sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $className, $methodName),
sprintf(
'Parameter $%%s of method %s::%s() has unresolvable native type.',
$className,
$methodName
),
sprintf(
'Method %s::%s() has unresolvable native return type.',
$className,
$methodName
),
);
}

Expand Down
21 changes: 21 additions & 0 deletions src/Rules/Properties/ExistingClassesInPropertiesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\RuleErrorBuilder;

/**
Expand All @@ -20,19 +22,27 @@ class ExistingClassesInPropertiesRule implements \PHPStan\Rules\Rule

private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck;

private UnresolvableTypeHelper $unresolvableTypeHelper;

private PhpVersion $phpVersion;

private bool $checkClassCaseSensitivity;

private bool $checkThisOnly;

public function __construct(
ReflectionProvider $reflectionProvider,
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
UnresolvableTypeHelper $unresolvableTypeHelper,
PhpVersion $phpVersion,
bool $checkClassCaseSensitivity,
bool $checkThisOnly
)
{
$this->reflectionProvider = $reflectionProvider;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->unresolvableTypeHelper = $unresolvableTypeHelper;
$this->phpVersion = $phpVersion;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
$this->checkThisOnly = $checkThisOnly;
}
Expand Down Expand Up @@ -89,6 +99,17 @@ public function processNode(Node $node, Scope $scope): array
);
}

if (
$this->phpVersion->supportsPureIntersectionTypes()
&& $this->unresolvableTypeHelper->containsUnresolvableType($propertyReflection->getNativeType())
) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Property %s::$%s has unresolvable native type.',
$propertyReflection->getDeclaringClass()->getDisplayName(),
$node->getName()
))->build();
}

return $errors;
}

Expand Down
7 changes: 6 additions & 1 deletion src/Type/ParserNodeTypeToPHPStanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public static function resolve($type, ?ClassReflection $classReflection): Type
} elseif ($type instanceof \PhpParser\Node\IntersectionType) {
$types = [];
foreach ($type->types as $intersectionTypeType) {
$types[] = self::resolve($intersectionTypeType, $classReflection);
$innerType = self::resolve($intersectionTypeType, $classReflection);
if (!$innerType instanceof ObjectType) {
return new NeverType();
}

$types[] = $innerType;
}

return TypeCombinator::intersect(...$types);
Expand Down
14 changes: 10 additions & 4 deletions src/Type/TypehintHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,17 @@ public static function decideTypeFromReflection(
}

if ($reflectionType instanceof ReflectionIntersectionType) {
$type = TypeCombinator::intersect(...array_map(static function (ReflectionType $type) use ($selfClass): Type {
return self::decideTypeFromReflection($type, null, $selfClass, false);
}, $reflectionType->getTypes()));
$types = [];
foreach ($reflectionType->getTypes() as $innerReflectionType) {
$innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false);
if (!$innerType instanceof ObjectType) {
return new NeverType();
}

return self::decideType($type, $phpDocType);
$types[] = $innerType;
}

return self::decideType(TypeCombinator::intersect(...$types), $phpDocType);
}

if (!$reflectionType instanceof ReflectionNamedType) {
Expand Down
Loading

0 comments on commit 021e25e

Please sign in to comment.