From f9a264832bb10783c6a0f2f4cd2df9985786bd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Wed, 9 Oct 2024 09:35:32 +0200 Subject: [PATCH 1/3] Implement template default types Co-authored-by: Richard van Velzen Co-authored-by: Richard van Velzen --- src/Analyser/MutatingScope.php | 6 + src/Dependency/DependencyResolver.php | 11 ++ src/PhpDoc/PhpDocNodeResolver.php | 8 +- src/PhpDoc/Tag/TemplateTag.php | 7 +- src/PhpDoc/TypeNodeResolver.php | 13 ++ src/Reflection/ClassReflection.php | 4 +- src/Rules/Classes/LocalTypeAliasesCheck.php | 3 +- src/Rules/Classes/MethodTagCheck.php | 3 +- src/Rules/Classes/MixinCheck.php | 3 +- src/Rules/Classes/PropertyTagCheck.php | 3 +- .../MissingClassConstantTypehintRule.php | 3 +- src/Rules/FunctionCallParametersCheck.php | 4 +- .../MissingFunctionParameterTypehintRule.php | 3 +- .../MissingFunctionReturnTypehintRule.php | 3 +- src/Rules/Generics/ClassTemplateTypeRule.php | 3 + .../Generics/FunctionTemplateTypeRule.php | 3 + src/Rules/Generics/GenericAncestorsCheck.php | 16 ++- src/Rules/Generics/GenericObjectTypeCheck.php | 19 ++- .../Generics/InterfaceTemplateTypeRule.php | 3 + .../Generics/MethodTagTemplateTypeCheck.php | 3 + src/Rules/Generics/MethodTemplateTypeRule.php | 3 + src/Rules/Generics/TemplateTypeCheck.php | 63 ++++++++ src/Rules/Generics/TraitTemplateTypeRule.php | 3 + .../MissingMethodParameterTypehintRule.php | 3 +- .../MissingMethodReturnTypehintRule.php | 3 +- .../Methods/MissingMethodSelfOutTypeRule.php | 3 +- src/Rules/MissingTypehintCheck.php | 20 ++- src/Rules/PhpDoc/AssertRuleHelper.php | 3 +- .../PhpDoc/GenericCallableRuleHelper.php | 3 + .../PhpDoc/InvalidPhpDocVarTagTypeRule.php | 3 +- .../MissingPropertyTypehintRule.php | 3 +- src/Type/Generic/TemplateArrayType.php | 3 + .../Generic/TemplateBenevolentUnionType.php | 3 + src/Type/Generic/TemplateBooleanType.php | 3 + .../Generic/TemplateConstantArrayType.php | 3 + .../Generic/TemplateConstantIntegerType.php | 3 + .../Generic/TemplateConstantStringType.php | 3 + src/Type/Generic/TemplateFloatType.php | 3 + .../Generic/TemplateGenericObjectType.php | 3 + src/Type/Generic/TemplateIntegerType.php | 3 + src/Type/Generic/TemplateIntersectionType.php | 3 + src/Type/Generic/TemplateKeyOfType.php | 3 + src/Type/Generic/TemplateMixedType.php | 3 + src/Type/Generic/TemplateObjectShapeType.php | 3 + src/Type/Generic/TemplateObjectType.php | 3 + .../TemplateObjectWithoutClassType.php | 3 + src/Type/Generic/TemplateStrictMixedType.php | 2 + src/Type/Generic/TemplateStringType.php | 3 + src/Type/Generic/TemplateType.php | 2 + src/Type/Generic/TemplateTypeFactory.php | 42 +++--- src/Type/Generic/TemplateTypeHelper.php | 2 +- src/Type/Generic/TemplateTypeMap.php | 6 +- src/Type/Generic/TemplateTypeTrait.php | 33 ++++- src/Type/Generic/TemplateUnionType.php | 3 + src/Type/TypeCombinator.php | 2 + src/Type/TypeUtils.php | 1 + .../Analyser/NodeScopeResolverTest.php | 3 + .../Analyser/data/template-default.php | 136 ++++++++++++++++++ .../Generics/ClassTemplateTypeRuleTest.php | 12 ++ .../Generics/FunctionTemplateTypeRuleTest.php | 12 ++ .../InterfaceTemplateTypeRuleTest.php | 12 ++ .../Generics/MethodTemplateTypeRuleTest.php | 12 ++ .../Generics/TraitTemplateTypeRuleTest.php | 12 ++ .../Generics/data/class-ancestors-extends.php | 13 ++ .../data/class-ancestors-implements.php | 15 ++ .../Rules/Generics/data/class-template.php | 26 ++++ .../Rules/Generics/data/enum-ancestors.php | 13 ++ .../Rules/Generics/data/function-template.php | 26 ++++ .../Generics/data/interface-template.php | 26 ++++ .../Rules/Generics/data/method-template.php | 31 ++++ .../Rules/Generics/data/trait-template.php | 26 ++++ .../Rules/Generics/data/used-traits.php | 14 ++ .../Rules/Methods/CallMethodsRuleTest.php | 9 ++ ...MissingMethodParameterTypehintRuleTest.php | 4 + .../MissingMethodReturnTypehintRuleTest.php | 4 + tests/PHPStan/Rules/Methods/data/bug-4801.php | 25 ++++ .../missing-method-parameter-typehint.php | 32 +++++ .../data/missing-method-return-typehint.php | 32 +++++ .../InvalidPhpDocVarTagTypeRuleTest.php | 6 +- .../data/invalid-phpdoc-definitions.php | 17 +++ .../PhpDoc/data/invalid-var-tag-type.php | 6 + .../MissingPropertyTypehintRuleTest.php | 4 + .../data/missing-property-typehint.php | 28 ++++ 83 files changed, 861 insertions(+), 67 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/template-default.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-4801.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index d8cc9faf069..5d6636683fb 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2539,6 +2539,7 @@ private function createFirstClassCallable( $templateTags[$templateType->getName()] = new TemplateTag( $templateType->getName(), $templateType->getBound(), + $templateType->getDefault(), $templateType->getVariance(), ); } @@ -5606,6 +5607,11 @@ private function exactInstantiation(New_ $node, string $className): ?Type $list[] = $templateType; continue; } + $default = $tag->getDefault(); + if ($default !== null) { + $list[] = $default; + continue; + } $bound = $tag->getBound(); if ($bound instanceof MixedType && $bound->isExplicitMixed()) { $bound = new MixedType(false); diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index f9bfcf314b1..231db1ba7d7 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -531,6 +531,17 @@ private function addClassToDependencies(string $className, array &$dependenciesR } $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); } + + $default = $templateTag->getDefault(); + if ($default === null) { + continue; + } + foreach ($default->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } } foreach ($classReflection->getPropertyTags() as $propertyTag) { diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 402f8c7dd1f..02fed04bcc6 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -176,6 +176,9 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): $templateType->bound !== null ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) : new MixedType(), + $templateType->default !== null + ? $this->typeNodeResolver->resolve($templateType->default, $nameScope) + : null, TemplateTypeVariance::createInvariant(), ); } @@ -327,9 +330,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } } + $nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name); + $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true), + $valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null, $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index a14fa2c6abb..4ae755597fa 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -15,7 +15,7 @@ class TemplateTag /** * @param non-empty-string $name */ - public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) + public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance) { } @@ -32,6 +32,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function getVariance(): TemplateTypeVariance { return $this->variance; diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 06098d79388..6e483222d7b 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -106,6 +106,7 @@ use Traversable; use function array_key_exists; use function array_map; +use function array_values; use function count; use function explode; use function get_class; @@ -792,6 +793,15 @@ static function (string $variance): TemplateTypeVariance { $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); if ($classReflection->isGeneric()) { + $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); + for ($i = count($genericTypes), $templateTypesCount = count($templateTypes); $i < $templateTypesCount; $i++) { + $templateType = $templateTypes[$i]; + if (!$templateType instanceof TemplateType || $templateType->getDefault() === null) { + continue; + } + $genericTypes[] = $templateType->getDefault(); + } + if (in_array($mainTypeClassName, [ Traversable::class, IteratorAggregate::class, @@ -910,6 +920,9 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $ $templateType->bound !== null ? $this->resolve($templateType->bound, $nameScope) : new MixedType(), + $templateType->default !== null + ? $this->resolve($templateType->default, $nameScope) + : null, TemplateTypeVariance::createInvariant(), ); } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index d980be78649..0254204a639 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1442,7 +1442,7 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? $tag->getBound(); + $map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); $i++; } @@ -1479,7 +1479,7 @@ public function typeMapToList(TemplateTypeMap $typeMap): array $list = []; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound(); + $list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound(); } return $list; diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php index 5347681a906..e2d463ab9cd 100644 --- a/src/Rules/Classes/LocalTypeAliasesCheck.php +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -24,7 +24,6 @@ use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function array_merge; -use function implode; use function in_array; use function sprintf; @@ -211,7 +210,7 @@ public function checkInTraitDefinitionContext(ClassReflection $reflection): arra $reflection->getDisplayName(), $aliasName, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/MethodTagCheck.php b/src/Rules/Classes/MethodTagCheck.php index b37d373772a..5730ea3a9bb 100644 --- a/src/Rules/Classes/MethodTagCheck.php +++ b/src/Rules/Classes/MethodTagCheck.php @@ -16,7 +16,6 @@ use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class MethodTagCheck @@ -174,7 +173,7 @@ private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classR $methodName, $description, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/MixinCheck.php b/src/Rules/Classes/MixinCheck.php index a17ef3d2001..3ce9535164b 100644 --- a/src/Rules/Classes/MixinCheck.php +++ b/src/Rules/Classes/MixinCheck.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class MixinCheck @@ -90,7 +89,7 @@ public function checkInTraitDefinitionContext(ClassReflection $classReflection): $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/PropertyTagCheck.php b/src/Rules/Classes/PropertyTagCheck.php index e05c4c676bb..c3e9fda73fb 100644 --- a/src/Rules/Classes/PropertyTagCheck.php +++ b/src/Rules/Classes/PropertyTagCheck.php @@ -18,7 +18,6 @@ use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class PropertyTagCheck @@ -155,7 +154,7 @@ private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $clas $classReflection->getDisplayName(), $propertyName, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 8a9aee13556..e0cbaa844c6 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -12,7 +12,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; /** @@ -76,7 +75,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 6a1d2f16b11..40fb657bcce 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -432,7 +432,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $type = $type->resolve(); } - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $returnTemplateTypes[$type->getName()] = true; return $type; } @@ -444,7 +444,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $parameterTemplateTypes = []; foreach ($originalParametersAcceptor->getParameters() as $parameter) { TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type { - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $parameterTemplateTypes[$type->getName()] = true; return $type; } diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index a7519de7a57..c988c70c08d 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -100,7 +99,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, $functionReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index d49e7f9aa93..648636973e8 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -58,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type with generic %s does not specify its types: %s', $functionReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 6c21c3a33d3..f574d76460e 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -49,6 +49,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName), sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), + sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName), + sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName), ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 2fe0ab6bfbf..d4b56da5f2b 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -60,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName), + sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index ef9ce469b57..c2eaef580e7 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -9,11 +9,13 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_fill_keys; +use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -173,10 +175,22 @@ public function check( continue; } + $templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + continue; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $genericClassInNonGenericObjectType, $unusedName, - implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index 46901218ef2..3f437a0b91e 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -14,6 +14,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_filter; use function array_keys; use function array_values; use function count; @@ -59,15 +60,26 @@ public function check( $genericTypeVariances = $genericType->getVariances(); $templateTypesCount = count($templateTypes); $genericTypeTypesCount = count($genericTypeTypes); - if ($templateTypesCount > $genericTypeTypesCount) { + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount > $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required).', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $notEnoughTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName(false), - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, ))->identifier('generics.lessTypes')->build(); } elseif ($templateTypesCount < $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $extraTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), @@ -75,11 +87,10 @@ public function check( $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, ))->identifier('generics.moreTypes')->build(); } - $templateTypesCount = count($templateTypes); for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 30be451eaed..53adafb43a8 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -46,6 +46,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName), + sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeCheck.php b/src/Rules/Generics/MethodTagTemplateTypeCheck.php index b0b6441c922..afa672f9837 100644 --- a/src/Rules/Generics/MethodTagTemplateTypeCheck.php +++ b/src/Rules/Generics/MethodTagTemplateTypeCheck.php @@ -61,6 +61,9 @@ public function check( sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), )); foreach (array_keys($methodTemplateTags) as $name) { diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index fa9a6ecb060..65653f833f6 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -66,6 +66,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid default type %%s.', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @template %%s for method %s::%s() is not subtype of bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 5eb8d8bdf4d..dda84c8629e 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -62,9 +62,13 @@ public function check( string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, string $notSupportedBoundMessage, + string $invalidDefaultTypeMessage, + string $defaultNotSubtypeOfBoundMessage, + string $requiredTypeAfterOptionalMessage, ): array { $messages = []; + $templateTagWithDefaultType = null; foreach ($templateTags as $templateTag) { $templateTagName = $scope->resolveName(new Node\Name($templateTag->getName())); if ($this->reflectionProvider->hasClass($templateTagName)) { @@ -141,6 +145,65 @@ public function check( foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; } + + $defaultType = $templateTag->getDefault(); + if ($defaultType === null) { + if ($templateTagWithDefaultType !== null) { + $messages[] = RuleErrorBuilder::message(sprintf( + $requiredTypeAfterOptionalMessage, + $templateTagName, + $templateTagWithDefaultType, + ))->identifier('generics.requiredTypeAfterOptional')->build(); + } + + continue; + } + + $templateTagWithDefaultType = $templateTagName; + + foreach ($defaultType->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('class.notFound')->build(); + continue; + } + if (!$this->reflectionProvider->getClass($referencedClass)->isTrait()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('generics.traitBound')->build(); + } + + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $defaultType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity)); + + $genericDefaultErrors = $this->genericObjectTypeCheck->check( + $defaultType, + sprintf('PHPDoc tag @template %s default contains generic type %%s but class %%s is not generic.', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which does not specify all template types of class %%s: %%s', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $escapedTemplateTagName), + sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s default is not subtype of template type %%s of class %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is in conflict with %%s template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is redundant, template type %%s of %%s %%s has the same variance.', $escapedTemplateTagName), + ); + foreach ($genericDefaultErrors as $genericDefaultError) { + $messages[] = $genericDefaultError; + } + + if (!$boundType->accepts($defaultType, $scope->isDeclareStrictTypes())->no()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf($defaultNotSubtypeOfBoundMessage, $defaultType->describe(VerbosityLevel::typeOnly()), $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.templateDefaultOutOfBounds') + ->build(); } return $messages; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index b08f12f32d4..27ce74e2980 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -60,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), + sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s does not have a default type but follows an optional @template %%s.', $escapedTraitName), ); } diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 32d64a68ade..8d5edd78900 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -103,7 +102,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, string $methodReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index e48ed2d7850..2b3c563f2d0 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -70,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Methods/MissingMethodSelfOutTypeRule.php b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php index 4b602b5fa14..e4e023ce211 100644 --- a/src/Rules/Methods/MissingMethodSelfOutTypeRule.php +++ b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -63,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $phpDocTagMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 75dd681fb1d..34677039213 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -21,8 +21,11 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use Traversable; +use function array_filter; use function array_keys; use function array_merge; +use function count; +use function implode; use function in_array; use function sprintf; use function strtolower; @@ -100,7 +103,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @return array + * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array { @@ -140,9 +143,22 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array if (!$resolvedType instanceof ObjectType) { throw new ShouldNotHappenException(); } + + $templateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + return $type; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $objectTypes[] = [ sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)), - array_keys($classReflection->getTemplateTypeMap()->getTypes()), + $templateTypesList, ]; return $type; } diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php index 073d131922c..0dec8f4d240 100644 --- a/src/Rules/PhpDoc/AssertRuleHelper.php +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -23,7 +23,6 @@ use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function array_merge; -use function implode; use function sprintf; use function substr; @@ -198,7 +197,7 @@ public function check( $tagName, $assertedExprString, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php index 3e849cd1dc8..c32491fe427 100644 --- a/src/Rules/PhpDoc/GenericCallableRuleHelper.php +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -60,6 +60,9 @@ public function check( sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid default type %%s.', $location, $typeDescription), + sprintf('Default type %%s in PHPDoc tag %s template %%s of %s is not subtype of bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s does not have a default type but follows an optional template %%s.', $location, $typeDescription), ); $templateTags = $type->getTemplateTags(); diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index 4a227ffd07f..53d4c4e6a68 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -16,7 +16,6 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; -use function implode; use function is_string; use function sprintf; @@ -116,7 +115,7 @@ public function processNode(Node $node, Scope $scope): array '%s contains generic %s but does not specify its types: %s', $identifier, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index c429a30fddd..84c8a20325c 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -68,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php index bb0192d02b3..e6658632cd1 100644 --- a/src/Type/Generic/TemplateArrayType.php +++ b/src/Type/Generic/TemplateArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateArrayType extends ArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyType(), $bound->getItemType()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 3107542f29d..cc630fd0dde 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -21,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BenevolentUnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -30,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } /** @param Type[] $types */ @@ -41,6 +43,7 @@ public function withTypes(array $types): self $this->variance, $this->name, new BenevolentUnionType($types), + $this->default, ); } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 66ce5db4ece..27fc50f21be 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -4,6 +4,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateBooleanType extends BooleanType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BooleanType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index b291dab5771..53ea9949357 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantArrayType extends ConstantArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php index e411af4edc2..a4bc35b8489 100644 --- a/src/Type/Generic/TemplateConstantIntegerType.php +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantIntegerType extends ConstantIntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantIntegerType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php index bcb3b0cf949..f4d3b8dbbb0 100644 --- a/src/Type/Generic/TemplateConstantStringType.php +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantStringType extends ConstantStringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantStringType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index 32332bb3ef8..b3df6ccd2e9 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -4,6 +4,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateFloatType extends FloatType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, FloatType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 3810841ec9c..0c58b3b41e7 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -22,6 +22,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, GenericObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType @@ -41,6 +43,7 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy $this->variance, $this->name, $this->getBound(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 64c631980d8..b4057fa3271 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateIntegerType extends IntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntegerType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateIntersectionType.php b/src/Type/Generic/TemplateIntersectionType.php index 87f1ca18a74..7576541dbcb 100644 --- a/src/Type/Generic/TemplateIntersectionType.php +++ b/src/Type/Generic/TemplateIntersectionType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\Type\IntersectionType; +use PHPStan\Type\Type; /** @api */ final class TemplateIntersectionType extends IntersectionType implements TemplateType @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntersectionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateKeyOfType.php b/src/Type/Generic/TemplateKeyOfType.php index 7312ea2ef4e..d8522eb5034 100644 --- a/src/Type/Generic/TemplateKeyOfType.php +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -23,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, KeyOfType $bound, + ?Type $default, ) { parent::__construct($bound->getType()); @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function getResult(): Type @@ -43,6 +45,7 @@ protected function getResult(): Type $result, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 132cb20d6bf..c06b082d52e 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -33,6 +34,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic @@ -62,6 +64,7 @@ public function toStrictMixedType(): TemplateStrictMixedType $this->variance, $this->name, new StrictMixedType(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php index 5b1f187c6db..270af37931c 100644 --- a/src/Type/Generic/TemplateObjectShapeType.php +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectShapeType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectShapeType extends ObjectShapeType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectShapeType $bound, + ?Type $default, ) { parent::__construct($bound->getProperties(), $bound->getOptionalProperties()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index a67aa723ddd..220414ca148 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectType extends ObjectType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName()); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 3d3cb9e8ca3..7d6aebc6f98 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 071475e2154..fa204aade2a 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StrictMixedType $bound, + ?Type $default, ) { $this->scope = $scope; @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 084612c6414..1ae72a33840 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateStringType extends StringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StringType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index 7661078ca1b..8374c6c0acd 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -18,6 +18,8 @@ public function getScope(): TemplateTypeScope; public function getBound(): Type; + public function getDefault(): ?Type; + public function toArgument(): TemplateType; public function isArgument(): bool; diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index c29a175d2c8..8f56cc6cbbf 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -28,91 +28,91 @@ final class TemplateTypeFactory /** * @param non-empty-string $name */ - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null): TemplateType + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null, ?Type $default = null): TemplateType { $strategy ??= new TemplateTypeParameterStrategy(); if ($bound === null) { - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } $boundClass = get_class($bound); if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) { - return new TemplateObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) { - return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) { - return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { - return new TemplateArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantArrayType && ($boundClass === ConstantArrayType::class || $bound instanceof TemplateType)) { - return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectShapeType && ($boundClass === ObjectShapeType::class || $bound instanceof TemplateType)) { - return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { - return new TemplateStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantStringType && ($boundClass === ConstantStringType::class || $bound instanceof TemplateType)) { - return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) { - return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantIntegerType && ($boundClass === ConstantIntegerType::class || $bound instanceof TemplateType)) { - return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { - return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); + return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { - return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound); + return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { - return new TemplateMixedType($scope, $strategy, $variance, $name, $bound); + return new TemplateMixedType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof UnionType) { if ($boundClass === UnionType::class || $bound instanceof TemplateUnionType) { - return new TemplateUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateUnionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BenevolentUnionType) { - return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound, $default); } } if ($bound instanceof IntersectionType) { - return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof KeyOfType && ($boundClass === KeyOfType::class || $bound instanceof TemplateType)) { - return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound); + return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound, $default); } - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): TemplateType { - return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance()); + return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance(), null, $tag->getDefault()); } } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index a38d6565564..58460efaee1 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -45,7 +45,7 @@ public static function resolveTemplateTypes( } if ($newType instanceof ErrorType && !$keepErrorTypes) { - return $traverse($type->getBound()); + return $traverse($type->getDefault() ?? $type->getBound()); } $callSiteVariance = $callSiteVariances->getVariance($type->getName()); diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index 00fbc34a1dc..30cd0e52c0a 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -5,6 +5,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use function array_key_exists; use function count; @@ -211,7 +212,10 @@ public function resolveToBounds(): self if ($this->resolvedToBounds !== null) { return $this->resolvedToBounds; } - return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map( + $type, + static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type), + )); } /** diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index b6cca8b2905..9a0fa73a59d 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -38,6 +38,8 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + private ?Type $default; + /** @return non-empty-string */ public function getName(): string { @@ -55,6 +57,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { @@ -64,10 +71,12 @@ public function describe(VerbosityLevel $level): string } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } + $defaultDescription = $this->default !== null ? sprintf(' = %s', $this->default->describe($level)) : ''; return sprintf( - '%s%s', + '%s%s%s', $this->name, $boundDescription, + $defaultDescription, ); }; @@ -91,6 +100,7 @@ public function toArgument(): TemplateType $this->variance, $this->name, TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } @@ -113,6 +123,7 @@ public function subtract(Type $typeToRemove): Type $removedBound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -129,6 +140,7 @@ public function getTypeWithoutSubtractedType(): Type $bound->getTypeWithoutSubtractedType(), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -145,6 +157,7 @@ public function changeSubtractedType(?Type $subtractedType): Type $bound->changeSubtractedType($subtractedType), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -163,7 +176,11 @@ public function equals(Type $type): bool return $type instanceof self && $type->scope->equals($this->scope) && $type->name === $this->name - && $this->bound->equals($type->bound); + && $this->bound->equals($type->bound) + && ( + ($this->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic @@ -317,7 +334,9 @@ protected function shouldGeneralizeInferredType(): bool public function traverse(callable $cb): Type { $bound = $cb($this->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null ? $cb($this->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -327,6 +346,7 @@ public function traverse(callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -337,7 +357,9 @@ public function traverseSimultaneously(Type $right, callable $cb): Type } $bound = $cb($this->getBound(), $right->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null && $right->getDefault() !== null ? $cb($this->getDefault(), $right->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -347,6 +369,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -363,6 +386,7 @@ public function tryRemove(Type $typeToRemove): ?Type $bound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -382,6 +406,7 @@ public static function __set_state(array $properties): Type $properties['variance'], $properties['name'], $properties['bound'], + $properties['default'] ?? null, ); } diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 997fb21238b..cc196a07f4c 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Type\Type; use PHPStan\Type\UnionType; /** @api */ @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, UnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 1db0ce23a26..d27fd6fbdb6 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -750,6 +750,7 @@ private static function processArrayTypes(array $arrayTypes): array $templateArray->getVariance(), $templateArray->getName(), $arrayType, + $templateArray->getDefault(), ); } @@ -1015,6 +1016,7 @@ public static function intersect(Type ...$types): Type $union, $type->getVariance(), $type->getStrategy(), + $type->getDefault(), ); } diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 8ae601b8323..9998d644226 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -292,6 +292,7 @@ public static function toStrictUnion(Type $type): Type $type->getVariance(), $type->getName(), static::toStrictUnion($type->getBound()), + $type->getDefault(), ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e0998e52525..1e3afb4e700 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -206,6 +206,9 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Classes/data/bug-11591-method-tag.php'; yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; + + yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; + yield __DIR__ . '/data/template-default.php'; } /** diff --git a/tests/PHPStan/Analyser/data/template-default.php b/tests/PHPStan/Analyser/data/template-default.php new file mode 100644 index 00000000000..979fbc3636b --- /dev/null +++ b/tests/PHPStan/Analyser/data/template-default.php @@ -0,0 +1,136 @@ + $one + * @param Test $two + * @param Test $three + */ +function foo(Test $one, Test $two, Test $three) +{ + assertType('TemplateDefault\\Test', $one); + assertType('TemplateDefault\\Test', $two); + assertType('TemplateDefault\\Test', $three); +} + + +/** + * @template S = false + * @template T = false + */ +class Builder +{ + /** + * @phpstan-self-out self + */ + public function one(): void + { + } + + /** + * @phpstan-self-out self + */ + public function two(): void + { + } + + /** + * @return ($this is self ? void : never) + */ + public function execute(): void + { + } +} + +class FormData {} +class Form +{ + /** + * @template Data of object = \stdClass + * @param Data|null $values + * @return Data + */ + public function mapValues(object|null $values = null): object + { + $values ??= new \stdClass; + // ... map into $values ... + return $values; + } +} + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + assertType('null', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('null', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('never', $qb->execute()); +}; + +function () { + $form = new Form(); + + assertType('TemplateDefault\\FormData', $form->mapValues(new FormData)); + assertType('stdClass', $form->mapValues()); +}; + +/** + * @template T + * @template U = string + */ +interface Foo +{ + /** + * @return U + */ + public function get(): mixed; +} + +/** + * @extends Foo + */ +interface Bar extends Foo +{ +} + +/** + * @extends Foo + */ +interface Baz extends Foo +{ +} + +function (Bar $bar, Baz $baz) { + assertType('string', $bar->get()); + assertType('bool', $baz->get()); +}; diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 788be4cbd56..0538311ee53 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -86,6 +86,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template W is in conflict with covariant template type T of class ClassTemplateType\Consecteur.', 113, ], + [ + 'PHPDoc tag @template T for class ClassTemplateType\Elit has invalid default type ClassTemplateType\Zazzzu.', + 121, + ], + [ + 'Default type bool in PHPDoc tag @template T for class ClassTemplateType\Venenatis is not subtype of bound type object.', + 129, + ], + [ + 'PHPDoc tag @template V for class ClassTemplateType\Mauris does not have a default type but follows an optional @template U.', + 139, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 35df2068658..f9d15da674b 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -67,6 +67,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template W is in conflict with covariant template type T of class FunctionTemplateType\GenericCovariant.', 94, ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\invalidDefault() has invalid default type FunctionTemplateType\Zazzzu.', + 102, + ], + [ + 'Default type bool in PHPDoc tag @template T for function FunctionTemplateType\outOfBoundsDefault() is not subtype of bound type object.', + 110, + ], + [ + 'PHPDoc tag @template V for function FunctionTemplateType\requiredAfterOptional() does not have a default type but follows an optional @template U.', + 120, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 0623a7d1c26..b997498703f 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -65,6 +65,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', 74, ], + [ + 'PHPDoc tag @template T for interface InterfaceTemplateType\InvalidDefault has invalid default type InterfaceTemplateType\Zazzzu.', + 82, + ], + [ + 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', + 90, + ], + [ + 'PHPDoc tag @template V for interface InterfaceTemplateType\RequiredAfterOptional does not have a default type but follows an optional @template U.', + 100, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index a8459933446..9276fec7c16 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -73,6 +73,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class MethodTemplateType\Dolor.', 109, ], + [ + 'PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::invalid() has invalid default type MethodTemplateType\Zazzzu.', + 122, + ], + [ + 'Default type bool in PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::outOfBounds() is not subtype of bound type object.', + 130, + ], + [ + 'PHPDoc tag @template V for method MethodTemplateType\InvalidDefault::requiredAfterOptional() does not have a default type but follows an optional @template U.', + 140, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 99ad8391231..84351d9ea9a 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -69,6 +69,18 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class TraitTemplateType\Dolor.', 64, ], + [ + 'PHPDoc tag @template T for trait TraitTemplateType\Adipiscing has invalid default type TraitTemplateType\Zazzzu.', + 72, + ], + [ + 'Default type bool in PHPDoc tag @template T for trait TraitTemplateType\Elit is not subtype of bound type object.', + 80, + ], + [ + 'PHPDoc tag @template V for trait TraitTemplateType\Consecteur does not have a default type but follows an optional @template U.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index e66591168ed..c04a5665a4d 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -260,3 +260,16 @@ class TraitInExtends extends FooGeneric { } + +/** + * @template T = string + */ +class FooGenericDefault +{ + +} + +class FooGenericExtendsDefault extends FooGenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php index 7a016c36ce7..abbc514279a 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php @@ -242,3 +242,18 @@ class FooCollection implements AbstractFooCollection class FooTypeProjection implements FooGeneric { } + +/** + * @template T = string + */ +interface FooGenericDefault +{ +} + +interface FooGenericExtendsDefault extends FooGenericDefault +{ +} + +class FooGenericImplementsDefault implements FooGenericDefault +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index a76c2eeab03..06400bd5364 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -114,3 +114,29 @@ class Adipiscing { } + +/** + * @template T = Zazzzu + */ +class Elit +{ + +} + +/** + * @template T of object = bool + */ +class Venenatis +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +class Mauris +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php index e5d78484989..1cda1bcbcd4 100644 --- a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -94,3 +94,16 @@ enum TypeProjection implements Generic { } + +/** + * @template T = string + */ +interface GenericDefault +{ + +} + +enum Foo9 implements GenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 8b9de2cdaff..8a1ff456f9a 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -95,3 +95,29 @@ function typeProjections() { } + +/** + * @template T = Zazzzu + */ +function invalidDefault() +{ + +} + +/** + * @template T of object = bool + */ +function outOfBoundsDefault() +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +function requiredAfterOptional() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index 8ae819c99b9..7f0da436e7e 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -75,3 +75,29 @@ interface TypeProjections { } + +/** + * @template T = Zazzzu + */ +interface InvalidDefault +{ + +} + +/** + * @template T of object = bool + */ +interface OutOfBoundsDefault +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +interface RequiredAfterOptional +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index 0fc67c1b245..edf5d622019 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -112,3 +112,34 @@ public function doSit() } } + +class InvalidDefault +{ + + /** + * @template T = Zazzzu + */ + public function invalid() + { + + } + + /** + * @template T of object = bool + */ + public function outOfBounds() + { + + } + + /** + * @template T + * @template U = string + * @template V + */ + public function requiredAfterOptional() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 7c9e1792959..39126a88f2f 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -65,3 +65,29 @@ trait Sit { } + +/** + * @template T = Zazzzu + */ +trait Adipiscing +{ + +} + +/** + * @template T of object = bool + */ +trait Elit +{ + +} + +/** + * @template T + * @template U = string + * @template V + */ +trait Consecteur +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php index f01c5e9dfb2..f34fb5ffb99 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -69,3 +69,17 @@ class Dolor use GenericTrait; } + +/** + * @template T = string + */ +trait GenericDefault +{ +} + +class Sit +{ + + use GenericDefault; + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 93b5b9303e2..7a46d849d32 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3386,4 +3386,13 @@ public function testBug10159(): void $this->analyse([__DIR__ . '/data/bug-10159.php'], []); } + public function testBug4801(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-4801.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index fecb8a1045a..89e29bb6063 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -82,6 +82,10 @@ public function testRule(): void 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', 238, ], + [ + 'Method MissingMethodParameterTypehint\Baz::acceptsGenericWithSomeDefaults() has parameter $c with generic class MissingMethodParameterTypehint\GenericClassWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 270, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 07629009018..49fc0b97d17 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -53,6 +53,10 @@ public function testRule(): void 'Method MissingMethodReturnTypehint\CallableSignature::doFoo() return type has no signature specified for callable.', 99, ], + [ + 'Method MissingMethodReturnTypehint\Baz::returnsGenericWithSomeDefaults() return type with generic class MissingMethodReturnTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 142, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php new file mode 100644 index 00000000000..a09d1133677 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -0,0 +1,25 @@ + + */ + public function work(?callable $a): I; +} + +/** + * @param I $i + */ +function x(I $i) { + assertType('Bug4801\\I', $i->work(null)); + assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index d5a333491b1..27fa039ef4d 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -241,3 +241,35 @@ function doFoo(\Closure $cb): void } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function acceptsGenericWithDefault(GenericClassWithDefault $i) + { + + } + + public function acceptsGenericWithSomeDefaults(GenericClassWithSomeDefaults $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php index 5b708cad893..480373825ae 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php @@ -113,3 +113,35 @@ public function doFoo(): \Traversable } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function returnsGenericWithDefault(): GenericClassWithDefault + { + + } + + public function returnsGenericWithSomeDefaults(): GenericClassWithSomeDefaults + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 9d4dc0bb3e3..c2916f98648 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -107,8 +107,12 @@ public function testRule(): void 73, ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGenericWithSomeDefaults but does not specify its types: T, U (1-2 required)', 79, + ], + [ + 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 85, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php index f52cae0d042..74ad37a7799 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php @@ -23,3 +23,20 @@ class FooCovariantGeneric { } + +/** + * @template T = string + */ +class FooGenericWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class FooGenericWithSomeDefaults +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php index fd9688445d5..cd996b4a21f 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php @@ -71,6 +71,12 @@ public function doFoo() /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithDefault $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithSomeDefaults $test */ + $test = doFoo(); } public function doBar($foo) diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index bd92fe752eb..324946835af 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -54,6 +54,10 @@ public function testRule(): void 106, MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], + [ + 'Property MissingPropertyTypehint\Baz::$bar with generic class MissingPropertyTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 134, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php index 952016e8603..374397cd0aa 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -106,3 +106,31 @@ class NestedArrayInProperty public $args; } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + /** @var \MissingPropertyTypehint\GenericClassWithDefault */ + private $foo; + + /** @var \MissingPropertyTypehint\GenericClassWithSomeDefaults */ + private $bar; + +} From 8dd41e9e3a1fc5cfaaec0659ec2c9161fe31d179 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 9 Oct 2024 16:49:30 +0900 Subject: [PATCH 2/3] Improve return type of token_name() and PhpToken::getTokenName() --- resources/functionMap.php | 2 +- resources/functionMap_php80delta.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 2dcf0feb8a7..c9eba8373a8 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -12606,7 +12606,7 @@ 'timezone_version_get' => ['string'], 'tmpfile' => ['__benevolent'], 'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], -'token_name' => ['non-empty-string', 'type'=>'int'], +'token_name' => ['non-falsy-string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], 'TokyoTyrant::add' => ['int|float', 'key'=>'string', 'increment'=>'float', 'type='=>'int'], 'TokyoTyrant::connect' => ['TokyoTyrant', 'host'=>'string', 'port='=>'int', 'options='=>'array'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index 11c65e5258d..d242a5410a4 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -91,7 +91,7 @@ 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], 'PhpToken::isIgnorable' => ['bool'], - 'PhpToken::getTokenName' => ['string'], + 'PhpToken::getTokenName' => ['non-falsy-string'], 'preg_match_all' => ['0|positive-int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'], 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], From ce3ffbd0725e2a5b0527b6fe8b0208c917649eac Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Tue, 8 Oct 2024 19:30:15 +0200 Subject: [PATCH 3/3] Use argument types as parameter types for inline closures when assigned The functionality was introduced in #1628. It works. But as soon as you use an inline assign expression it breaks. Let's support this case too. Sometimes, you want to call something inline and also use the callback later. --- src/Parser/ArrowFunctionArgVisitor.php | 25 +++++++++++++++---- src/Parser/ClosureArgVisitor.php | 25 +++++++++++++++---- .../nsrt/arrow-function-argument-type.php | 2 ++ .../Analyser/nsrt/closure-argument-type.php | 4 +++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/Parser/ArrowFunctionArgVisitor.php b/src/Parser/ArrowFunctionArgVisitor.php index f8149dad21c..93cce45dd93 100644 --- a/src/Parser/ArrowFunctionArgVisitor.php +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -13,13 +13,28 @@ final class ArrowFunctionArgVisitor extends NodeVisitorAbstract public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\ArrowFunction && !$node->isFirstClassCallable()) { - $args = $node->getArgs(); + if (!$node instanceof Node\Expr\FuncCall) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } - if (count($args) > 0) { - $node->name->setAttribute(self::ATTRIBUTE_NAME, $args); - } + if ($node->name instanceof Node\Expr\Assign && $node->name->expr instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name->expr; + } elseif ($node->name instanceof Node\Expr\ArrowFunction) { + $arrow = $node->name; + } else { + return null; } + + $args = $node->getArgs(); + + if (count($args) > 0) { + $arrow->setAttribute(self::ATTRIBUTE_NAME, $args); + } + return null; } diff --git a/src/Parser/ClosureArgVisitor.php b/src/Parser/ClosureArgVisitor.php index 58d53a808e3..c9435f826eb 100644 --- a/src/Parser/ClosureArgVisitor.php +++ b/src/Parser/ClosureArgVisitor.php @@ -13,13 +13,28 @@ final class ClosureArgVisitor extends NodeVisitorAbstract public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\Closure && !$node->isFirstClassCallable()) { - $args = $node->getArgs(); + if (!$node instanceof Node\Expr\FuncCall) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } - if (count($args) > 0) { - $node->name->setAttribute(self::ATTRIBUTE_NAME, $args); - } + if ($node->name instanceof Node\Expr\Assign && $node->name->expr instanceof Node\Expr\Closure) { + $closure = $node->name->expr; + } elseif ($node->name instanceof Node\Expr\Closure) { + $closure = $node->name; + } else { + return null; } + + $args = $node->getArgs(); + + if (count($args) > 0) { + $closure->setAttribute(self::ATTRIBUTE_NAME, $args); + } + return null; } diff --git a/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php index 3e1448e6ff1..a508035d19e 100644 --- a/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php +++ b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php @@ -21,6 +21,8 @@ public function doFoo(int $integer, array $array, ?string $nullableString) (fn($a, $b, $c) => assertType('array{int, array{a: int}, string|null}', [$a, $b, $c]))($integer, $array, $nullableString); (fn($a, $b, $c = null) => assertType('array{int, array{a: int}, mixed}', [$a, $b, $c]))($integer, $array); + + ($callback = fn($context) => assertType('int', $context))($integer); } } diff --git a/tests/PHPStan/Analyser/nsrt/closure-argument-type.php b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php index 6fd537211d5..b24570b298a 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-argument-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php @@ -35,6 +35,10 @@ public function doFoo(int $integer, array $array, ?string $nullableString) assertType('array{a: int}', $context2); assertType('mixed', $context3); })($integer, $array); + + ($callback = function($context) { + assertType('int', $context); + })($integer); } }