From 6736748e290a5813927a631920e108ab86146e9f Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 9 Nov 2026 09:00:13 +0000 Subject: [PATCH 1/4] Allow restricted usage extensions to return a tip RestrictedUsage::create() accepts an optional tip that is attached to the reported error. Amp-Thread-ID: https://ampcode.com/threads/T-01a1bf5f-ca34-7733-8886-c850b9908584 --- src/Rules/ClassNameCheck.php | 10 +++++++--- src/Rules/Classes/InstantiationRule.php | 10 +++++++--- .../RestrictedClassConstantUsageExtension.php | 2 +- .../RestrictedClassConstantUsageRule.php | 10 +++++++--- .../RestrictedClassNameUsageExtension.php | 2 +- .../RestrictedFunctionCallableUsageRule.php | 10 +++++++--- .../RestrictedFunctionUsageExtension.php | 2 +- .../RestrictedUsage/RestrictedFunctionUsageRule.php | 10 +++++++--- .../RestrictedMethodCallableUsageRule.php | 10 +++++++--- .../RestrictedUsage/RestrictedMethodUsageExtension.php | 2 +- .../RestrictedUsage/RestrictedMethodUsageRule.php | 10 +++++++--- .../RestrictedPropertyUsageExtension.php | 2 +- .../RestrictedUsage/RestrictedPropertyUsageRule.php | 10 +++++++--- .../RestrictedStaticMethodCallableUsageRule.php | 10 +++++++--- .../RestrictedStaticMethodUsageRule.php | 10 +++++++--- .../RestrictedStaticPropertyUsageRule.php | 10 +++++++--- src/Rules/RestrictedUsage/RestrictedUsage.php | 4 +++- .../RestrictedUsageOfDeprecatedStringCastRule.php | 10 +++++++--- .../RestrictedFunctionCallableUsageRuleTest.php | 1 + .../RestrictedFunctionUsageRuleTest.php | 1 + .../Rules/RestrictedUsage/data/FunctionExtension.php | 2 +- 21 files changed, 95 insertions(+), 43 deletions(-) diff --git a/src/Rules/ClassNameCheck.php b/src/Rules/ClassNameCheck.php index 6764eee7761..33fdc82dc67 100644 --- a/src/Rules/ClassNameCheck.php +++ b/src/Rules/ClassNameCheck.php @@ -69,10 +69,14 @@ public function checkClassNames( continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) ->identifier($restrictedUsage->identifier) - ->line($pair->getNode()->getStartLine()) - ->build(); + ->line($pair->getNode()->getStartLine()); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index d16a83e19c3..3c9a6285c1a 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -288,9 +288,13 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope&N } } - $messages[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $messages[] = $errorBuilder->build(); } $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php index 93db6d72919..7a7e13e6281 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageExtension.php @@ -10,7 +10,7 @@ * Extensions implementing this interface are called for each analysed class constant access. * * Extension can decide to create RestrictedUsage object - * with error message & error identifier to be reported for this class constant access. + * with error message, error identifier & optional tip to be reported for this class constant access. * * Typical usage is to report errors for constants marked as @-deprecated or @-internal. * diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index 7aa7257df68..7dc3b4b6b9e 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php @@ -101,9 +101,13 @@ public function processNode(Node $node, Scope $scope): array } } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php index 46a1750c4ee..12b4e97b5a6 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php +++ b/src/Rules/RestrictedUsage/RestrictedClassNameUsageExtension.php @@ -11,7 +11,7 @@ * Extensions implementing this interface are called for each analysed class name usage. * * Extension can decide to create RestrictedUsage object - * with error message & error identifier to be reported for this method call. + * with error message, error identifier & optional tip to be reported for this method call. * * Typical usage is to report errors for class names marked as @-deprecated or @-internal. * diff --git a/src/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRule.php index 8f570c8e640..41367dd8323 100644 --- a/src/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRule.php @@ -60,9 +60,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } return $errors; diff --git a/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php index 0d6acef1721..e1ad5201d93 100644 --- a/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php +++ b/src/Rules/RestrictedUsage/RestrictedFunctionUsageExtension.php @@ -10,7 +10,7 @@ * Extensions implementing this interface are called for each analysed function call. * * Extension can decide to create RestrictedUsage object - * with error message & error identifier to be reported for this function call. + * with error message, error identifier & optional tip to be reported for this function call. * * Typical usage is to report errors for functions marked as @-deprecated or @-internal. * diff --git a/src/Rules/RestrictedUsage/RestrictedFunctionUsageRule.php b/src/Rules/RestrictedUsage/RestrictedFunctionUsageRule.php index 5c27299f6fd..6888baede1d 100644 --- a/src/Rules/RestrictedUsage/RestrictedFunctionUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedFunctionUsageRule.php @@ -59,9 +59,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } return $errors; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php index 2cfbf92c4d7..0356056da59 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -75,9 +75,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php index 730b64a1371..ba458aeadc1 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageExtension.php @@ -10,7 +10,7 @@ * Extensions implementing this interface are called for each analysed method call. * * Extension can decide to create RestrictedUsage object - * with error message & error identifier to be reported for this method call. + * with error message, error identifier & optional tip to be reported for this method call. * * Typical usage is to report errors for methods marked as @-deprecated or @-internal. * diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php index 1f04eb0bbeb..a0a200285b1 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php @@ -75,9 +75,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php index 114bc66800a..3edf1f6abd4 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageExtension.php @@ -10,7 +10,7 @@ * Extensions implementing this interface are called for each analysed property access. * * Extension can decide to create RestrictedUsage object - * with error message & error identifier to be reported for this property access. + * with error message, error identifier & optional tip to be reported for this property access. * * Typical usage is to report errors for properties marked as @-deprecated or @-internal. * diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index b86084ed174..dfc28e19836 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php @@ -74,9 +74,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index 9ed23eed04b..442b4ae78fc 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -102,9 +102,13 @@ public function processNode(Node $node, Scope $scope): array } } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index 711fc8dcaac..853b3ca2ca3 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -101,9 +101,13 @@ public function processNode(Node $node, Scope $scope): array } } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index 6046e368fac..b09d15aad31 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -101,9 +101,13 @@ public function processNode(Node $node, Scope $scope): array } } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/src/Rules/RestrictedUsage/RestrictedUsage.php b/src/Rules/RestrictedUsage/RestrictedUsage.php index d6325829815..3a2c8d2b524 100644 --- a/src/Rules/RestrictedUsage/RestrictedUsage.php +++ b/src/Rules/RestrictedUsage/RestrictedUsage.php @@ -11,6 +11,7 @@ final class RestrictedUsage private function __construct( public readonly string $errorMessage, public readonly string $identifier, + public readonly ?string $tip, ) { } @@ -18,9 +19,10 @@ private function __construct( public static function create( string $errorMessage, string $identifier, + ?string $tip = null, ): self { - return new self($errorMessage, $identifier); + return new self($errorMessage, $identifier, $tip); } } diff --git a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php index 1cb985bd801..289e4e622ea 100644 --- a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php +++ b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php @@ -66,9 +66,13 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage) - ->identifier($restrictedUsage->identifier) - ->build(); + $errorBuilder = RuleErrorBuilder::message($restrictedUsage->errorMessage) + ->identifier($restrictedUsage->identifier); + if ($restrictedUsage->tip !== null) { + $errorBuilder->tip($restrictedUsage->tip); + } + + $errors[] = $errorBuilder->build(); } } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php index 6d1eea1db8a..4d671f3f246 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRuleTest.php @@ -27,6 +27,7 @@ public function testRule(): void [ 'Cannot call doFoo', 7, + 'Use doBar() instead.', ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php index 10b5f9fce00..8408f2e0691 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedFunctionUsageRuleTest.php @@ -25,6 +25,7 @@ public function testRule(): void [ 'Cannot call doFoo', 17, + 'Use doBar() instead.', ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php b/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php index 555b5b0a520..02c2288c90f 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php +++ b/tests/PHPStan/Rules/RestrictedUsage/data/FunctionExtension.php @@ -19,7 +19,7 @@ public function isRestrictedFunctionUsage( return null; } - return RestrictedUsage::create('Cannot call doFoo', 'restrictedUsage.doFoo'); + return RestrictedUsage::create('Cannot call doFoo', 'restrictedUsage.doFoo', 'Use doBar() instead.'); } } From 926c98f357f201a45632dd6fa6892cc7ba496100 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 9 Nov 2026 09:00:13 +0000 Subject: [PATCH 2/4] Rewrite runtime reflection Api rules as restricted usage extensions Amp-Thread-ID: https://ampcode.com/threads/T-01a1bf5f-ca34-7733-8886-c850b9908584 --- .../Api/RuntimeReflectionFunctionRule.php | 78 ------------------- ...untimeReflectionFunctionUsageExtension.php | 58 ++++++++++++++ ...ReflectionInstantiationUsageExtension.php} | 57 +++++--------- ...eReflectionFunctionUsageExtensionTest.php} | 11 ++- ...ectionInstantiationUsageExtensionTest.php} | 11 ++- 5 files changed, 95 insertions(+), 120 deletions(-) delete mode 100644 src/Rules/Api/RuntimeReflectionFunctionRule.php create mode 100644 src/Rules/Api/RuntimeReflectionFunctionUsageExtension.php rename src/Rules/Api/{RuntimeReflectionInstantiationRule.php => RuntimeReflectionInstantiationUsageExtension.php} (52%) rename tests/PHPStan/Rules/Api/{RuntimeReflectionFunctionRuleTest.php => RuntimeReflectionFunctionUsageExtensionTest.php} (76%) rename tests/PHPStan/Rules/Api/{RuntimeReflectionInstantiationRuleTest.php => RuntimeReflectionInstantiationUsageExtensionTest.php} (89%) diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php deleted file mode 100644 index 43d812ae91d..00000000000 --- a/src/Rules/Api/RuntimeReflectionFunctionRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - */ -#[RegisteredRule(level: 0)] -final 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 (!$scope->isInClass()) { - 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 []; - } - - $classReflection = $scope->getClassReflection(); - $hasPhpStanInterface = false; - foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { - if (!str_starts_with($interfaceName, 'PHPStan\\')) { - 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()), - )->identifier('phpstanApi.runtimeReflection')->build(), - ]; - } - -} diff --git a/src/Rules/Api/RuntimeReflectionFunctionUsageExtension.php b/src/Rules/Api/RuntimeReflectionFunctionUsageExtension.php new file mode 100644 index 00000000000..6c976515b87 --- /dev/null +++ b/src/Rules/Api/RuntimeReflectionFunctionUsageExtension.php @@ -0,0 +1,58 @@ +isInClass()) { + return null; + } + + if (!in_array($functionReflection->getName(), [ + 'is_a', + 'is_subclass_of', + 'class_parents', + 'class_implements', + 'class_uses', + ], true)) { + return null; + } + + $classReflection = $scope->getClassReflection(); + $hasPhpStanInterface = false; + foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { + continue; + } + + $hasPhpStanInterface = true; + } + + if (!$hasPhpStanInterface) { + return null; + } + + return RestrictedUsage::create( + 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()), + 'phpstanApi.runtimeReflection', + ); + } + +} diff --git a/src/Rules/Api/RuntimeReflectionInstantiationRule.php b/src/Rules/Api/RuntimeReflectionInstantiationUsageExtension.php similarity index 52% rename from src/Rules/Api/RuntimeReflectionInstantiationRule.php rename to src/Rules/Api/RuntimeReflectionInstantiationUsageExtension.php index 065ed554ca3..9c74fb3c3cc 100644 --- a/src/Rules/Api/RuntimeReflectionInstantiationRule.php +++ b/src/Rules/Api/RuntimeReflectionInstantiationUsageExtension.php @@ -2,12 +2,12 @@ namespace PHPStan\Rules\Api; -use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\ClassNameUsageLocation; +use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedUsage; use ReflectionClass; use ReflectionClassConstant; use ReflectionExtension; @@ -23,38 +23,24 @@ use function sprintf; use function str_starts_with; -/** - * @implements Rule - */ -#[RegisteredRule(level: 0)] -final class RuntimeReflectionInstantiationRule implements Rule +#[AutowiredService] +final class RuntimeReflectionInstantiationUsageExtension implements RestrictedClassNameUsageExtension { - public function __construct(private ReflectionProvider $reflectionProvider) + public function isRestrictedClassNameUsage( + ClassReflection $classReflection, + Scope $scope, + ClassNameUsageLocation $location, + ): ?RestrictedUsage { - } - - public function getNodeType(): string - { - return Node\Expr\New_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!$node->class instanceof Node\Name) { - return []; + if ($location->value !== ClassNameUsageLocation::INSTANTIATION) { + return null; } if (!$scope->isInClass()) { - return []; - } - - $className = $scope->resolveName($node->class); - if (!$this->reflectionProvider->hasClass($className)) { - return []; + return null; } - $classReflection = $this->reflectionProvider->getClass($className); if (!in_array($classReflection->getName(), [ ReflectionMethod::class, ReflectionClass::class, @@ -70,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array ReflectionGenerator::class, 'ReflectionFiber', ], true)) { - return []; + return null; } $scopeClassReflection = $scope->getClassReflection(); @@ -84,14 +70,13 @@ public function processNode(Node $node, Scope $scope): array } if (!$hasPhpStanInterface) { - return []; + return null; } - return [ - RuleErrorBuilder::message( - sprintf('Creating new %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.', $classReflection->getName()), - )->identifier('phpstanApi.runtimeReflection')->build(), - ]; + return RestrictedUsage::create( + sprintf('Creating new %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.', $classReflection->getName()), + 'phpstanApi.runtimeReflection', + ); } } diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionUsageExtensionTest.php similarity index 76% rename from tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php rename to tests/PHPStan/Rules/Api/RuntimeReflectionFunctionUsageExtensionTest.php index a1b02038972..2ed4b10650b 100644 --- a/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionFunctionUsageExtensionTest.php @@ -2,18 +2,23 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class RuntimeReflectionFunctionRuleTest extends RuleTestCase +class RuntimeReflectionFunctionUsageExtensionTest extends RuleTestCase { protected function getRule(): Rule { - return new RuntimeReflectionFunctionRule(self::createReflectionProvider()); + return new RestrictedFunctionUsageRule( + self::getContainer()->getExtensionsCollection(RestrictedFunctionUsageExtension::class), + self::createReflectionProvider(), + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php similarity index 89% rename from tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php rename to tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php index 0ec03856c5a..575dc7f327b 100644 --- a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php @@ -2,19 +2,20 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Classes\InstantiationRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class RuntimeReflectionInstantiationRuleTest extends RuleTestCase +class RuntimeReflectionInstantiationUsageExtensionTest extends RuleTestCase { protected function getRule(): Rule { - return new RuntimeReflectionInstantiationRule(self::createReflectionProvider()); + return self::getContainer()->getByType(InstantiationRule::class); } public function testRule(): void @@ -72,6 +73,10 @@ public function testRule(): void 'Creating new ReflectionEnum is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', 56, ]; + $errors[] = [ + 'Parameter #1 $objectOrClass of class ReflectionEnum constructor expects class-string|T of UnitEnum, string given.', + 56, + ]; $errors[] = [ 'Creating new ReflectionEnumBackedCase is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', 57, From 04fdb9ca72e54aa9f569a82522794a1fd0211bcc Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 9 Nov 2026 09:00:13 +0000 Subject: [PATCH 3/4] Rewrite Api extends, implements and trait use rules as a restricted class name usage extension Amp-Thread-ID: https://ampcode.com/threads/T-01a1bf5f-ca34-7733-8886-c850b9908584 --- src/Rules/Api/ApiClassExtendsRule.php | 78 ---------------- src/Rules/Api/ApiClassImplementsRule.php | 89 ------------------- src/Rules/Api/ApiClassNameUsageExtension.php | 87 ++++++++++++++++++ src/Rules/Api/ApiInterfaceExtendsRule.php | 89 ------------------- src/Rules/Api/ApiTraitUseRule.php | 69 -------------- ...assNameUsageExtensionClassExtendsTest.php} | 18 +++- ...NameUsageExtensionClassImplementsTest.php} | 7 +- ...ameUsageExtensionInterfaceExtendsTest.php} | 7 +- ...piClassNameUsageExtensionTraitUseTest.php} | 7 +- 9 files changed, 113 insertions(+), 338 deletions(-) delete mode 100644 src/Rules/Api/ApiClassExtendsRule.php delete mode 100644 src/Rules/Api/ApiClassImplementsRule.php create mode 100644 src/Rules/Api/ApiClassNameUsageExtension.php delete mode 100644 src/Rules/Api/ApiInterfaceExtendsRule.php delete mode 100644 src/Rules/Api/ApiTraitUseRule.php rename tests/PHPStan/Rules/Api/{ApiClassExtendsRuleTest.php => ApiClassNameUsageExtensionClassExtendsTest.php} (64%) rename tests/PHPStan/Rules/Api/{ApiClassImplementsRuleTest.php => ApiClassNameUsageExtensionClassImplementsTest.php} (86%) rename tests/PHPStan/Rules/Api/{ApiInterfaceExtendsRuleTest.php => ApiClassNameUsageExtensionInterfaceExtendsTest.php} (83%) rename tests/PHPStan/Rules/Api/{ApiTraitUseRuleTest.php => ApiClassNameUsageExtensionTraitUseTest.php} (77%) diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php deleted file mode 100644 index 4a2e7c09425..00000000000 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ /dev/null @@ -1,78 +0,0 @@ - - */ -#[RegisteredRule(level: 0)] -final class ApiClassExtendsRule implements Rule -{ - - public function __construct( - private ApiRuleHelper $apiRuleHelper, - private ReflectionProvider $reflectionProvider, - ) - { - } - - public function getNodeType(): string - { - return Class_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node->extends === null) { - return []; - } - - $extendedClassName = (string) $node->extends; - if (!$this->reflectionProvider->hasClass($extendedClassName)) { - return []; - } - - $extendedClassReflection = $this->reflectionProvider->getClass($extendedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedClassReflection->getName(), $extendedClassReflection->getFileName())) { - return []; - } - - if ($extendedClassReflection->getName() === MutatingScope::class) { - return []; - } - - $ruleError = RuleErrorBuilder::message(sprintf( - 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', - $extendedClassReflection->getDisplayName(), - ))->identifier('phpstanApi.class')->tip(sprintf( - "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions', - ))->build(); - - $docBlock = $extendedClassReflection->getResolvedPhpDoc(); - if ($docBlock === null) { - return [$ruleError]; - } - - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - $apiTags = $phpDocNode->getTagsByName('@api'); - if (count($apiTags) > 0) { - return []; - } - } - - return [$ruleError]; - } - -} diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php deleted file mode 100644 index 00a4bf8d884..00000000000 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -#[RegisteredRule(level: 0)] -final class ApiClassImplementsRule implements Rule -{ - - public function __construct( - private ApiRuleHelper $apiRuleHelper, - private ReflectionProvider $reflectionProvider, - ) - { - } - - public function getNodeType(): string - { - return Class_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $errors = []; - foreach ($node->implements as $implements) { - $errors = array_merge($errors, $this->checkName($scope, $implements)); - } - - return $errors; - } - - /** - * @return list - */ - private function checkName(Scope $scope, Node\Name $name): array - { - $implementedClassName = (string) $name; - if (!$this->reflectionProvider->hasClass($implementedClassName)) { - return []; - } - - $implementedClassReflection = $this->reflectionProvider->getClass($implementedClassName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $implementedClassReflection->getName(), $implementedClassReflection->getFileName())) { - return []; - } - - $ruleError = RuleErrorBuilder::message(sprintf( - 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $implementedClassReflection->getDisplayName(), - ))->identifier('phpstanApi.interface')->tip(sprintf( - "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions', - ))->build(); - - $docBlock = $implementedClassReflection->getResolvedPhpDoc(); - if ($docBlock === null) { - return [$ruleError]; - } - - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - if (count($phpDocNode->getTagsByName('@api-do-not-implement')) > 0) { - return [$ruleError]; - } - } - - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - if (count($phpDocNode->getTagsByName('@api')) > 0) { - return []; - } - } - - return [$ruleError]; - } - -} diff --git a/src/Rules/Api/ApiClassNameUsageExtension.php b/src/Rules/Api/ApiClassNameUsageExtension.php new file mode 100644 index 00000000000..82948698fff --- /dev/null +++ b/src/Rules/Api/ApiClassNameUsageExtension.php @@ -0,0 +1,87 @@ +value === ClassNameUsageLocation::CLASS_EXTENDS) { + if ($classReflection->getName() === MutatingScope::class) { + return null; + } + $message = 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.'; + $identifier = 'phpstanApi.class'; + $isImplementing = false; + } elseif ($location->value === ClassNameUsageLocation::CLASS_IMPLEMENTS) { + $message = 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.'; + $identifier = 'phpstanApi.interface'; + $isImplementing = true; + } elseif ($location->value === ClassNameUsageLocation::INTERFACE_EXTENDS) { + $message = 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.'; + $identifier = 'phpstanApi.interface'; + $isImplementing = true; + } elseif ($location->value === ClassNameUsageLocation::TRAIT_USE) { + $message = 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.'; + $identifier = 'phpstanApi.trait'; + $isImplementing = false; + } else { + return null; + } + + if (!$this->apiRuleHelper->isPhpStanCode($scope, $classReflection->getName(), $classReflection->getFileName())) { + return null; + } + + $restrictedUsage = RestrictedUsage::create( + sprintf($message, $classReflection->getDisplayName()), + $identifier, + sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ), + ); + + $docBlock = $classReflection->getResolvedPhpDoc(); + if ($docBlock === null) { + return $restrictedUsage; + } + + if ($isImplementing) { + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + if (count($phpDocNode->getTagsByName('@api-do-not-implement')) > 0) { + return $restrictedUsage; + } + } + } + + foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { + if (count($phpDocNode->getTagsByName('@api')) > 0) { + return null; + } + } + + return $restrictedUsage; + } + +} diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php deleted file mode 100644 index 85d04ed7a08..00000000000 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -#[RegisteredRule(level: 0)] -final class ApiInterfaceExtendsRule implements Rule -{ - - public function __construct( - private ApiRuleHelper $apiRuleHelper, - private ReflectionProvider $reflectionProvider, - ) - { - } - - public function getNodeType(): string - { - return Interface_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $errors = []; - foreach ($node->extends as $extends) { - $errors = array_merge($errors, $this->checkName($scope, $extends)); - } - - return $errors; - } - - /** - * @return list - */ - private function checkName(Scope $scope, Node\Name $name): array - { - $extendedInterface = (string) $name; - if (!$this->reflectionProvider->hasClass($extendedInterface)) { - return []; - } - - $extendedInterfaceReflection = $this->reflectionProvider->getClass($extendedInterface); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $extendedInterfaceReflection->getName(), $extendedInterfaceReflection->getFileName())) { - return []; - } - - $ruleError = RuleErrorBuilder::message(sprintf( - 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $extendedInterfaceReflection->getDisplayName(), - ))->identifier('phpstanApi.interface')->tip(sprintf( - "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions', - ))->build(); - - $docBlock = $extendedInterfaceReflection->getResolvedPhpDoc(); - if ($docBlock === null) { - return [$ruleError]; - } - - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - if (count($phpDocNode->getTagsByName('@api-do-not-implement')) > 0) { - return [$ruleError]; - } - } - - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - if (count($phpDocNode->getTagsByName('@api')) > 0) { - return []; - } - } - - return [$ruleError]; - } - -} diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php deleted file mode 100644 index e688e6eebbd..00000000000 --- a/src/Rules/Api/ApiTraitUseRule.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ -#[RegisteredRule(level: 0)] -final class ApiTraitUseRule implements Rule -{ - - public function __construct( - private ApiRuleHelper $apiRuleHelper, - private ReflectionProvider $reflectionProvider, - ) - { - } - - public function getNodeType(): string - { - return Node\Stmt\TraitUse::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $errors = []; - $tip = sprintf( - "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions', - ); - foreach ($node->traits as $traitName) { - $traitName = $traitName->toString(); - if (!$this->reflectionProvider->hasClass($traitName)) { - continue; - } - - $traitReflection = $this->reflectionProvider->getClass($traitName); - if (!$this->apiRuleHelper->isPhpStanCode($scope, $traitReflection->getName(), $traitReflection->getFileName())) { - continue; - } - - $docBlock = $traitReflection->getResolvedPhpDoc(); - if ($docBlock !== null) { - foreach ($docBlock->getPhpDocNodes() as $phpDocNode) { - if (count($phpDocNode->getTagsByName('@api')) > 0) { - continue 2; - } - } - } - - $errors[] = RuleErrorBuilder::message(sprintf( - 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', - $traitReflection->getDisplayName(), - ))->identifier('phpstanApi.trait')->tip($tip)->build(); - } - - return $errors; - } - -} diff --git a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassExtendsTest.php similarity index 64% rename from tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php rename to tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassExtendsTest.php index f62d2c2225b..0c21095b1ee 100644 --- a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassExtendsTest.php @@ -2,24 +2,30 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Classes\ExistingClassInClassExtendsRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class ApiClassExtendsRuleTest extends RuleTestCase +class ApiClassNameUsageExtensionClassExtendsTest extends RuleTestCase { protected function getRule(): Rule { - return new ApiClassExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); + return self::getContainer()->getByType(ExistingClassInClassExtendsRule::class); } public function testRuleInPhpStan(): void { - $this->analyse([__DIR__ . '/data/class-extends-in-phpstan.php'], []); + $this->analyse([__DIR__ . '/data/class-extends-in-phpstan.php'], [ + [ + 'Class PHPStan\ClassExtends\Foo extends final class PHPStan\Type\FileTypeMapper.', + 8, + ], + ]); } public function testRuleOutOfPhpStan(): void @@ -30,6 +36,10 @@ public function testRuleOutOfPhpStan(): void ); $this->analyse([__DIR__ . '/data/class-extends-out-of-phpstan.php'], [ + [ + 'Class AppClassExtends\Foo extends final class PHPStan\Type\FileTypeMapper.', + 9, + ], [ 'Extending PHPStan\Type\FileTypeMapper is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', 9, diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassImplementsTest.php similarity index 86% rename from tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php rename to tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassImplementsTest.php index 088b9b87de9..b0cb43f165b 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionClassImplementsTest.php @@ -2,19 +2,20 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class ApiClassImplementsRuleTest extends RuleTestCase +class ApiClassNameUsageExtensionClassImplementsTest extends RuleTestCase { protected function getRule(): Rule { - return new ApiClassImplementsRule(new ApiRuleHelper(), self::createReflectionProvider()); + return self::getContainer()->getByType(ExistingClassesInClassImplementsRule::class); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionInterfaceExtendsTest.php similarity index 83% rename from tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php rename to tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionInterfaceExtendsTest.php index 9f2d7bed52d..6c28c26b36f 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionInterfaceExtendsTest.php @@ -2,19 +2,20 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class ApiInterfaceExtendsRuleTest extends RuleTestCase +class ApiClassNameUsageExtensionInterfaceExtendsTest extends RuleTestCase { protected function getRule(): Rule { - return new ApiInterfaceExtendsRule(new ApiRuleHelper(), self::createReflectionProvider()); + return self::getContainer()->getByType(ExistingClassesInInterfaceExtendsRule::class); } public function testRuleInPhpStan(): void diff --git a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionTraitUseTest.php similarity index 77% rename from tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php rename to tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionTraitUseTest.php index bebf04977ee..1af009fdfa8 100644 --- a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassNameUsageExtensionTraitUseTest.php @@ -2,19 +2,20 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Classes\ExistingClassInTraitUseRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function sprintf; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class ApiTraitUseRuleTest extends RuleTestCase +class ApiClassNameUsageExtensionTraitUseTest extends RuleTestCase { protected function getRule(): Rule { - return new ApiTraitUseRule(new ApiRuleHelper(), self::createReflectionProvider()); + return self::getContainer()->getByType(ExistingClassInTraitUseRule::class); } public function testRuleInPhpStan(): void From d6762da60d59542b682df41855bb53f67a8dd825 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 9 Nov 2026 09:26:10 +0000 Subject: [PATCH 4/4] Make runtime reflection instantiation test independent of InstantiationRule's PHP-version-specific errors Amp-Thread-ID: https://ampcode.com/threads/T-01a1bf5f-ca34-7733-8886-c850b9908584 --- ...eflectionInstantiationUsageExtensionTest.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php index 575dc7f327b..d3d68d78220 100644 --- a/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php +++ b/tests/PHPStan/Rules/Api/RuntimeReflectionInstantiationUsageExtensionTest.php @@ -73,16 +73,23 @@ public function testRule(): void 'Creating new ReflectionEnum is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', 56, ]; - $errors[] = [ - 'Parameter #1 $objectOrClass of class ReflectionEnum constructor expects class-string|T of UnitEnum, string given.', - 56, - ]; $errors[] = [ 'Creating new ReflectionEnumBackedCase is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', 57, ]; } - $this->analyse([__DIR__ . '/data/runtime-reflection-instantiation.php'], $errors); + + // InstantiationRule's own errors on this file differ between PHP versions + $actualErrors = []; + foreach ($this->gatherAnalyserErrors([__DIR__ . '/data/runtime-reflection-instantiation.php']) as $error) { + if ($error->getIdentifier() !== 'phpstanApi.runtimeReflection') { + continue; + } + + $actualErrors[] = [$error->getMessage(), $error->getLine()]; + } + + $this->assertSame($errors, $actualErrors); } }