diff --git a/conf/config.level0.neon b/conf/config.level0.neon index fc3bfc84f2..6493abd868 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -216,6 +216,7 @@ services: class: PHPStan\Rules\Properties\SetPropertyHookParameterRule arguments: checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + checkMissingTypehints: %checkMissingTypehints% tags: - phpstan.rules.rule diff --git a/src/Rules/Properties/SetPropertyHookParameterRule.php b/src/Rules/Properties/SetPropertyHookParameterRule.php index 941dd84973..e8de30667e 100644 --- a/src/Rules/Properties/SetPropertyHookParameterRule.php +++ b/src/Rules/Properties/SetPropertyHookParameterRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InPropertyHookNode; +use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -18,7 +19,11 @@ final class SetPropertyHookParameterRule implements Rule { - public function __construct(private bool $checkPhpDocMethodSignatures) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + private bool $checkPhpDocMethodSignatures, + private bool $checkMissingTypehints, + ) { } @@ -87,10 +92,12 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - if (!$parameter->getType()->isSuperTypeOf($propertyReflection->getReadableType())->yes()) { + $parameterType = $parameter->getType(); + + if (!$parameterType->isSuperTypeOf($propertyReflection->getReadableType())->yes()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Type %s of set hook parameter $%s is not contravariant with type %s of property %s::$%s.', - $parameter->getType()->describe(VerbosityLevel::value()), + $parameterType->describe(VerbosityLevel::value()), $parameter->getName(), $propertyReflection->getReadableType()->describe(VerbosityLevel::value()), $classReflection->getDisplayName(), @@ -99,6 +106,51 @@ public function processNode(Node $node, Scope $scope): array ->build(); } + if (!$this->checkMissingTypehints) { + return $errors; + } + + if ($parameter->getNativeType()->equals($propertyReflection->getReadableType())) { + return $errors; + } + + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with no value type specified in iterable type %s.', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $iterableTypeDescription, + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with generic %s but does not specify its types: %s', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $name, + $genericTypeNames, + )) + ->identifier('missingType.generics') + ->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Set hook for property %s::$%s has parameter $%s with no signature specified for %s.', + $classReflection->getDisplayName(), + $hookReflection->getHookedPropertyName(), + $parameter->getName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + return $errors; } diff --git a/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php b/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php index 76e7b06b8c..0b879f0ad5 100644 --- a/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php +++ b/tests/PHPStan/Rules/Properties/SetPropertyHookParameterRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -14,7 +15,7 @@ class SetPropertyHookParameterRuleTest extends RuleTestCase protected function getRule(): TRule { - return new SetPropertyHookParameterRule(true); + return new SetPropertyHookParameterRule(new MissingTypehintCheck(true, []), true, true); } public function testRule(): void @@ -48,6 +49,19 @@ public function testRule(): void 'Type array|int<1, max> of set hook parameter $v is not contravariant with type int of property SetPropertyHookParameter\Bar::$f.', 73, ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$f has parameter $v with no value type specified in iterable type array.', + 123, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$g has parameter $value with generic class SetPropertyHookParameter\GenericFoo but does not specify its types: T', + 129, + ], + [ + 'Set hook for property SetPropertyHookParameter\MissingTypes::$h has parameter $value with no signature specified for callable.', + 135, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php b/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php index a8279832b2..12c82ddc0a 100644 --- a/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php +++ b/tests/PHPStan/Rules/Properties/data/set-property-hook-parameter.php @@ -20,7 +20,7 @@ class Foo /** @var positive-int */ public int $ok3 { - /** @param positive-int|array */ + /** @param positive-int|array $v */ set (int|array $v) { } @@ -76,3 +76,65 @@ class Bar } } + +/** + * @template T + */ +class GenericFoo +{ + +} + +class MissingTypes +{ + + public array $a { + set { // do not report, taken care of above the property + } + } + + /** @var array */ + public array $b { + set { // do not report, inherited from property + } + } + + public array $c { + set (array $v) { // do not report, taken care of above the property + + } + } + + /** @var array */ + public array $d { + set (array $v) { // do not report, inherited from property + + } + } + + public int $e { + /** @param array $v */ + set (int|array $v) { // do not report, type specified + + } + } + + public int $f { + set (int|array $v) { // report + + } + } + + public int $g { + set (int|GenericFoo $value) { // report + + } + } + + public int $h { + set (int|callable $value) { // report + + } + } + +}