diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 450b0c8cd3..8cfc08ca5b 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -16,3 +16,4 @@ parameters: checkUnresolvableParameterTypes: true readOnlyByPhpDoc: true phpDocParserRequireWhitespaceBeforeDescription: true + runtimeReflectionRules: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 482aae9d64..272494e32b 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -12,6 +12,8 @@ conditionalTags: phpstan.rules.rule: %checkUninitializedProperties% PHPStan\Rules\Methods\ConsistentConstructorRule: phpstan.rules.rule: %featureToggles.consistentConstructor% + PHPStan\Rules\Api\RuntimeReflectionFunctionRule: + phpstan.rules.rule: %featureToggles.runtimeReflectionRules% rules: - PHPStan\Rules\Api\ApiInstantiationRule @@ -78,6 +80,8 @@ rules: services: - class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule + - + class: PHPStan\Rules\Api\RuntimeReflectionFunctionRule - class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule tags: diff --git a/conf/config.neon b/conf/config.neon index b0296b0e7b..4859022f35 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -41,6 +41,7 @@ parameters: checkUnresolvableParameterTypes: false readOnlyByPhpDoc: false phpDocParserRequireWhitespaceBeforeDescription: false + runtimeReflectionRules: false fileExtensions: - php checkAdvancedIsset: false @@ -243,6 +244,7 @@ parametersSchema: checkUnresolvableParameterTypes: bool() readOnlyByPhpDoc: bool() phpDocParserRequireWhitespaceBeforeDescription: bool() + runtimeReflectionRules: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7e9dabe832..ebd898d91c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,15 @@ parameters: ignoreErrors: + - + message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + count: 1 + path: src/Analyser/DirectScopeFactory.php + + - + message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" + count: 1 + path: src/Analyser/LazyScopeFactory.php + - message: """ #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php new file mode 100644 index 0000000000..d71a62d1eb --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionFunctionRule.php @@ -0,0 +1,76 @@ + + */ +class RuntimeReflectionFunctionRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if (!in_array($functionReflection->getName(), [ + 'is_a', + 'is_subclass_of', + 'class_parents', + 'class_implements', + 'class_uses', + ], true)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (strpos($interfaceName, 'PHPStan\\') !== 0) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Function %s() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $functionReflection->getName()), + )->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php new file mode 100644 index 0000000000..f043e062fc --- /dev/null +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php @@ -0,0 +1,45 @@ + + */ +class RuntimeReflectionFunctionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RuntimeReflectionFunctionRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/runtime-reflection-function.php'], [ + [ + 'Function is_a() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 43, + ], + [ + 'Function is_subclass_of() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 46, + ], + [ + 'Function class_parents() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 49, + ], + [ + 'Function class_implements() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 50, + ], + [ + 'Function class_uses() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', + 51, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/runtime-reflection-function.php b/tests/PHPStan/Rules/Api/data/runtime-reflection-function.php new file mode 100644 index 0000000000..6c43df0557 --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/runtime-reflection-function.php @@ -0,0 +1,54 @@ +