diff --git a/Makefile b/Makefile index e7483368f0..b87d62bd7c 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \ --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ + --exclude tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php \ --exclude tests/PHPStan/Rules/Properties/data/properties-in-interface.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php \ diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 1060295132..ce89c7763b 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -88,6 +88,7 @@ rules: - PHPStan\Rules\Operators\InvalidAssignVarRule - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule + - PHPStan\Rules\Properties\InvalidCallablePropertyTypeRule - PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule - PHPStan\Rules\Properties\PropertiesInInterfaceRule - PHPStan\Rules\Properties\PropertyAttributesRule diff --git a/src/Rules/Properties/InvalidCallablePropertyTypeRule.php b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php new file mode 100644 index 0000000000..2a8ed41bd8 --- /dev/null +++ b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php @@ -0,0 +1,65 @@ + + */ +class InvalidCallablePropertyTypeRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if (!$propertyReflection->hasNativeType()) { + return []; + } + + $nativeType = $propertyReflection->getNativeType(); + $callableTypes = []; + + TypeTraverser::map($nativeType, static function (Type $type, callable $traverse) use (&$callableTypes): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof CallableType) { + $callableTypes[] = $type; + } + + return $type; + }); + + if ($callableTypes === []) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Property %s::$%s cannot have callable in its type declaration.', + $classReflection->getDisplayName(), + $node->getName(), + ))->identifier('property.callableType')->nonIgnorable()->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php new file mode 100644 index 0000000000..581a70e179 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php @@ -0,0 +1,41 @@ + + */ +class InvalidCallablePropertyTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidCallablePropertyTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-callable-property-type.php'], [ + [ + 'Property InvalidCallablePropertyType\HelloWorld::$a cannot have callable in its type declaration.', + 9, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$b cannot have callable in its type declaration.', + 12, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$c cannot have callable in its type declaration.', + 15, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$callback cannot have callable in its type declaration.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php new file mode 100644 index 0000000000..053c0bfb1c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php @@ -0,0 +1,31 @@ +a = $closure; + $this->b = $closure; + $this->c = $closure; + } + +}