diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index de3111883d..de9c582b33 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 TooWideTypeCheck $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..d95debd14a 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 TooWideTypeCheck $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/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 9a83747ce5..738203a6bc 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 TooWideTypeCheck $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..6b2c8f81b1 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 TooWideTypeCheck $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/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/TooWideTypeCheck.php b/src/Rules/TooWideTypehints/TooWideTypeCheck.php new file mode 100644 index 0000000000..14eff168dd --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideTypeCheck.php @@ -0,0 +1,161 @@ + + */ + 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, + 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 + */ + 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( + '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; + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index 3abc8d8643..daaac7cb2a 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 TooWideTypeCheck(), + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index f47472a2dd..794fed67ae 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 TooWideTypeCheck(), + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 9c86812e92..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(); + 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 2d84f5dff5..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); + 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(), ); }