From a60d16c569235c16ee39bd0dceb50bee6ae15e04 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 26 Aug 2025 08:51:14 +0200 Subject: [PATCH 1/4] Extract TooWideReturnTypeCheck --- ...TooWideArrowFunctionReturnTypehintRule.php | 27 +++--------- .../TooWideClosureReturnTypehintRule.php | 28 +++--------- .../TooWideReturnTypeCheck.php | 44 +++++++++++++++++++ ...ideArrowFunctionReturnTypehintRuleTest.php | 4 +- .../TooWideClosureReturnTypehintRuleTest.php | 4 +- 5 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index de3111883d..6aa9811f4c 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -7,10 +7,7 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; -use function sprintf; /** * @implements Rule @@ -19,6 +16,12 @@ final class TooWideArrowFunctionReturnTypehintRule implements Rule { + public function __construct( + private TooWideReturnTypeCheck $check, + ) + { + } + public function getNodeType(): string { return InArrowFunctionNode::class; @@ -41,23 +44,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $returnType = $scope->getType($expr); - if ($returnType->isNull()->yes()) { - return []; - } - $messages = []; - foreach ($functionReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return type.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->identifier('return.unusedType')->build(); - } - - return $messages; + return $this->check->checkAnonymousFunction($scope->getType($expr), $functionReturnType); } } diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 76e8800284..3fb012b673 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -7,12 +7,9 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; use function count; -use function sprintf; /** * @implements Rule @@ -21,6 +18,12 @@ final class TooWideClosureReturnTypehintRule implements Rule { + public function __construct( + private TooWideReturnTypeCheck $check, + ) + { + } + public function getNodeType(): string { return ClosureReturnStatementsNode::class; @@ -62,24 +65,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $returnType = TypeCombinator::union(...$returnTypes); - if ($returnType->isNull()->yes()) { - return []; - } - - $messages = []; - foreach ($closureReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return type.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->identifier('return.unusedType')->build(); - } - - return $messages; + return $this->check->checkAnonymousFunction(TypeCombinator::union(...$returnTypes), $closureReturnType); } } diff --git a/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php b/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php new file mode 100644 index 0000000000..13c871319a --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php @@ -0,0 +1,44 @@ + + */ + public function checkAnonymousFunction( + Type $returnType, + UnionType $functionReturnType, + ): array + { + if ($returnType->isNull()->yes()) { + return []; + } + $messages = []; + foreach ($functionReturnType->getTypes() as $type) { + if (!$type->isSuperTypeOf($returnType)->no()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + '%s never returns %s so it can be removed from the return type.', + 'Anonymous function', + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->identifier('return.unusedType')->build(); + } + + return $messages; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index 3abc8d8643..9ca9350728 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -13,7 +13,9 @@ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideArrowFunctionReturnTypehintRule(); + return new TooWideArrowFunctionReturnTypehintRule( + new TooWideReturnTypeCheck(), + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index f47472a2dd..daf1421628 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -13,7 +13,9 @@ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideClosureReturnTypehintRule(); + return new TooWideClosureReturnTypehintRule( + new TooWideReturnTypeCheck(), + ); } public function testRule(): void From e7d218d169a5bbc1cd156b3d1d94a6553740ac62 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 26 Aug 2025 09:42:30 +0200 Subject: [PATCH 2/4] Extract TooWideReturnTypeCheck->checkFunction() --- .../TooWideFunctionReturnTypehintRule.php | 75 ++++------------- .../TooWideMethodReturnTypehintRule.php | 77 ++--------------- .../TooWideReturnTypeCheck.php | 84 ++++++++++++++++++- .../TooWideFunctionReturnTypehintRuleTest.php | 2 +- .../TooWideMethodReturnTypehintRuleTest.php | 2 +- 5 files changed, 107 insertions(+), 133 deletions(-) diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 9a83747ce5..d49f9b6c77 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -7,13 +7,6 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; -use function count; use function sprintf; /** @@ -23,6 +16,12 @@ final class TooWideFunctionReturnTypehintRule implements Rule { + public function __construct( + private TooWideReturnTypeCheck $check, + ) + { + } + public function getNodeType(): string { return FunctionReturnStatementsNode::class; @@ -33,61 +32,15 @@ public function processNode(Node $node, Scope $scope): array $function = $node->getFunctionReflection(); $functionReturnType = $function->getReturnType(); - $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); - if (!$functionReturnType instanceof UnionType) { - return []; - } - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return []; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return []; - } - - $returnTypes = []; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - $returnTypes[] = new VoidType(); - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (!$statementResult->isAlwaysTerminating()) { - $returnTypes[] = new VoidType(); - } - - $returnType = TypeCombinator::union(...$returnTypes); - - $messages = []; - foreach ($functionReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { - foreach ($node->getExecutionEnds() as $executionEnd) { - if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { - continue; - } - - continue 2; - } - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() never returns %s so it can be removed from the return type.', + return $this->check->checkFunction( + $node, + $functionReturnType, + sprintf( + 'Function %s()', $function->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->identifier('return.unusedType')->build(); - } - - return $messages; + ), + false, + ); } } diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 750dd7375a..35b3371554 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -8,13 +8,6 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; -use function count; use function sprintf; /** @@ -27,6 +20,7 @@ final class TooWideMethodReturnTypehintRule implements Rule public function __construct( #[AutowiredParameter(ref: '%checkTooWideReturnTypesInProtectedAndPublicMethods%')] private bool $checkProtectedAndPublicMethods, + private TooWideReturnTypeCheck $check, ) { } @@ -56,69 +50,16 @@ public function processNode(Node $node, Scope $scope): array } $methodReturnType = $method->getReturnType(); - $methodReturnType = TypeUtils::resolveLateResolvableTypes($methodReturnType); - if (!$methodReturnType instanceof UnionType) { - return []; - } - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return []; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return []; - } - - $returnTypes = []; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - $returnTypes[] = new VoidType(); - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (!$statementResult->isAlwaysTerminating()) { - $returnTypes[] = new VoidType(); - } - - $returnType = TypeCombinator::union(...$returnTypes); - if ( - !$isFirstDeclaration - && !$method->isPrivate() - && ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) - ) { - return []; - } - - $messages = []; - foreach ($methodReturnType->getTypes() as $type) { - if (!$type->isSuperTypeOf($returnType)->no()) { - continue; - } - - if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { - foreach ($node->getExecutionEnds() as $executionEnd) { - if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { - continue; - } - - continue 2; - } - } - - $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() never returns %s so it can be removed from the return type.', + return $this->check->checkFunction( + $node, + $methodReturnType, + sprintf( + 'Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->identifier('return.unusedType')->build(); - } - - return $messages; + ), + !$isFirstDeclaration && !$method->isPrivate(), + ); } } diff --git a/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php b/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php index 13c871319a..5e6d513ca0 100644 --- a/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php +++ b/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php @@ -3,17 +3,98 @@ namespace PHPStan\Rules\TooWideTypehints; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\FunctionReturnStatementsNode; +use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; +use function count; use function sprintf; #[AutowiredService] final class TooWideReturnTypeCheck { + /** + * @return list + */ + public function checkFunction( + MethodReturnStatementsNode|FunctionReturnStatementsNode $node, + Type $functionReturnType, + string $functionDescription, + bool $checkDescendantClass, + ): array + { + $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); + if (!$functionReturnType instanceof UnionType) { + return []; + } + $statementResult = $node->getStatementResult(); + if ($statementResult->hasYield()) { + return []; + } + + $returnStatements = $node->getReturnStatements(); + if (count($returnStatements) === 0) { + return []; + } + + $returnTypes = []; + foreach ($returnStatements as $returnStatement) { + $returnNode = $returnStatement->getReturnNode(); + if ($returnNode->expr === null) { + $returnTypes[] = new VoidType(); + continue; + } + + $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); + } + + if (!$statementResult->isAlwaysTerminating()) { + $returnTypes[] = new VoidType(); + } + + $returnType = TypeCombinator::union(...$returnTypes); + + // Do not require to have @return null/true/false in descendant classes + if ( + $checkDescendantClass + && ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) + ) { + return []; + } + + $messages = []; + foreach ($functionReturnType->getTypes() as $type) { + if (!$type->isSuperTypeOf($returnType)->no()) { + continue; + } + + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + continue 2; + } + } + + $messages[] = RuleErrorBuilder::message(sprintf( + '%s never returns %s so it can be removed from the return type.', + $functionDescription, + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + ))->identifier('return.unusedType')->build(); + } + + return $messages; + } + /** * @return list */ @@ -32,8 +113,7 @@ public function checkAnonymousFunction( } $messages[] = RuleErrorBuilder::message(sprintf( - '%s never returns %s so it can be removed from the return type.', - 'Anonymous function', + 'Anonymous function never returns %s so it can be removed from the return type.', $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), ))->identifier('return.unusedType')->build(); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 9c86812e92..87d9d75b45 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -13,7 +13,7 @@ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideFunctionReturnTypehintRule(); + return new TooWideFunctionReturnTypehintRule(new TooWideReturnTypeCheck()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 2d84f5dff5..c10faccd06 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -17,7 +17,7 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideReturnTypeCheck()); } public function testPrivate(): void From e503274ef418d66cbfacfa46273645b22879b6a0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 26 Aug 2025 16:24:45 +0200 Subject: [PATCH 3/4] extract TooWideReturnTypeCheck->checkProperty() --- ...TooWideArrowFunctionReturnTypehintRule.php | 2 +- .../TooWideClosureReturnTypehintRule.php | 2 +- .../TooWideFunctionReturnTypehintRule.php | 2 +- .../TooWideMethodReturnTypehintRule.php | 2 +- .../TooWidePropertyTypeRule.php | 26 ++----------- ...turnTypeCheck.php => TooWideTypeCheck.php} | 39 ++++++++++++++++++- ...ideArrowFunctionReturnTypehintRuleTest.php | 2 +- .../TooWideClosureReturnTypehintRuleTest.php | 2 +- .../TooWideFunctionReturnTypehintRuleTest.php | 2 +- .../TooWideMethodReturnTypehintRuleTest.php | 2 +- .../TooWidePropertyTypeRuleTest.php | 1 + 11 files changed, 50 insertions(+), 32 deletions(-) rename src/Rules/TooWideTypehints/{TooWideReturnTypeCheck.php => TooWideTypeCheck.php} (76%) diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index 6aa9811f4c..de9c582b33 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -17,7 +17,7 @@ final class TooWideArrowFunctionReturnTypehintRule implements Rule { public function __construct( - private TooWideReturnTypeCheck $check, + private TooWideTypeCheck $check, ) { } diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 3fb012b673..d95debd14a 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -19,7 +19,7 @@ final class TooWideClosureReturnTypehintRule implements Rule { public function __construct( - private TooWideReturnTypeCheck $check, + private TooWideTypeCheck $check, ) { } diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index d49f9b6c77..738203a6bc 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -17,7 +17,7 @@ final class TooWideFunctionReturnTypehintRule implements Rule { public function __construct( - private TooWideReturnTypeCheck $check, + private TooWideTypeCheck $check, ) { } diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 35b3371554..6b2c8f81b1 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -20,7 +20,7 @@ final class TooWideMethodReturnTypehintRule implements Rule public function __construct( #[AutowiredParameter(ref: '%checkTooWideReturnTypesInProtectedAndPublicMethods%')] private bool $checkProtectedAndPublicMethods, - private TooWideReturnTypeCheck $check, + private TooWideTypeCheck $check, ) { } diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index f3b03d0dfa..4be9a14c9f 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -10,11 +10,8 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; @@ -28,6 +25,7 @@ final class TooWidePropertyTypeRule implements Rule public function __construct( private ReadWritePropertiesExtensionProvider $extensionProvider, private PropertyReflectionFinder $propertyReflectionFinder, + private TooWideTypeCheck $check, ) { } @@ -100,27 +98,9 @@ public function processNode(Node $node, Scope $scope): array $assignedType = TypeCombinator::union(...$assignedTypes); $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyName); - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType); - foreach ($propertyType->getTypes() as $type) { - if (!$type->isSuperTypeOf($assignedType)->no()) { - continue; - } - - if ($property->getNativeType() === null && (new NullType())->isSuperTypeOf($type)->yes()) { - continue; - } - - $errors[] = RuleErrorBuilder::message(sprintf( - '%s (%s) is never assigned %s so it can be removed from the property type.', - $propertyDescription, - $propertyType->describe($verbosityLevel), - $type->describe($verbosityLevel), - )) - ->identifier('property.unusedType') - ->line($property->getStartLine()) - ->build(); + foreach ($this->check->checkProperty($property, $propertyType, $propertyDescription, $assignedType) as $error) { + $errors[] = $error; } - } return $errors; } diff --git a/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php b/src/Rules/TooWideTypehints/TooWideTypeCheck.php similarity index 76% rename from src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php rename to src/Rules/TooWideTypehints/TooWideTypeCheck.php index 5e6d513ca0..9a301bcf57 100644 --- a/src/Rules/TooWideTypehints/TooWideReturnTypeCheck.php +++ b/src/Rules/TooWideTypehints/TooWideTypeCheck.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\TooWideTypehints; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\IdentifierRuleError; @@ -17,12 +18,48 @@ use function sprintf; #[AutowiredService] -final class TooWideReturnTypeCheck +final class TooWideTypeCheck { /** * @return list */ + public function checkProperty( + ClassPropertyNode $property, + UnionType $propertyType, + string $propertyDescription, + Type $assignedType, + ): array + { + $errors = []; + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedType); + foreach ($propertyType->getTypes() as $type) { + if (!$type->isSuperTypeOf($assignedType)->no()) { + continue; + } + + if ($property->getNativeType() === null && $type->isNull()->yes()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s (%s) is never assigned %s so it can be removed from the property type.', + $propertyDescription, + $propertyType->describe($verbosityLevel), + $type->describe($verbosityLevel), + )) + ->identifier('property.unusedType') + ->line($property->getStartLine()) + ->build(); + } + + return $errors; + } + + /** + * @return list + */ public function checkFunction( MethodReturnStatementsNode|FunctionReturnStatementsNode $node, Type $functionReturnType, diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index 9ca9350728..daaac7cb2a 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -14,7 +14,7 @@ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { return new TooWideArrowFunctionReturnTypehintRule( - new TooWideReturnTypeCheck(), + new TooWideTypeCheck(), ); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index daf1421628..794fed67ae 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -14,7 +14,7 @@ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { return new TooWideClosureReturnTypehintRule( - new TooWideReturnTypeCheck(), + new TooWideTypeCheck(), ); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 87d9d75b45..36c224c65f 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -13,7 +13,7 @@ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideFunctionReturnTypehintRule(new TooWideReturnTypeCheck()); + return new TooWideFunctionReturnTypehintRule(new TooWideTypeCheck()); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index c10faccd06..36ad02ad92 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -17,7 +17,7 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideReturnTypeCheck()); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, new TooWideTypeCheck()); } public function testPrivate(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php index 213d98342d..e590aea5eb 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -19,6 +19,7 @@ protected function getRule(): Rule return new TooWidePropertyTypeRule( new DirectReadWritePropertiesExtensionProvider([]), new PropertyReflectionFinder(), + new TooWideTypeCheck(), ); } From 858ffb821eb73646e0dff06348df98ba2f209750 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 26 Aug 2025 16:28:56 +0200 Subject: [PATCH 4/4] cs --- src/Rules/TooWideTypehints/TooWideTypeCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rules/TooWideTypehints/TooWideTypeCheck.php b/src/Rules/TooWideTypehints/TooWideTypeCheck.php index 9a301bcf57..14eff168dd 100644 --- a/src/Rules/TooWideTypehints/TooWideTypeCheck.php +++ b/src/Rules/TooWideTypehints/TooWideTypeCheck.php @@ -57,9 +57,9 @@ public function checkProperty( return $errors; } - /** - * @return list - */ + /** + * @return list + */ public function checkFunction( MethodReturnStatementsNode|FunctionReturnStatementsNode $node, Type $functionReturnType,