From bd5573780751bece2b36b75656826342b2bd54e0 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 2 Nov 2026 18:05:11 +0000 Subject: [PATCH 1/3] Report PHPStan attributes not covered by backward compatibility promise Using an attribute from the PHPStan namespace outside of PHPStan is reported with the phpstanApi.attribute identifier unless the attribute class has @api. Conditional, AutowiredService, NonAutowiredService, RegisteredRule, RegisteredCollector and GenerateFactory are @api, including their constructors, so that new RegisteredRule(...) in Conditional's arguments is not reported by ApiInstantiationRule. Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983 --- src/DependencyInjection/AutowiredService.php | 3 + src/DependencyInjection/Conditional.php | 3 + src/DependencyInjection/GenerateFactory.php | 3 + .../NonAutowiredService.php | 5 +- .../RegisteredCollector.php | 3 + src/DependencyInjection/RegisteredRule.php | 3 + src/Rules/AttributesCheck.php | 27 +++++++++ .../Rules/Classes/ClassAttributesRuleTest.php | 15 +++++ .../ClassConstantAttributesRuleTest.php | 2 + .../Rules/Classes/data/api-attributes.php | 60 +++++++++++++++++++ .../ConstantAttributesRuleConfigPhpTest.php | 2 + .../Constants/ConstantAttributesRuleTest.php | 2 + .../EnumCases/EnumCaseAttributesRuleTest.php | 2 + .../ArrowFunctionAttributesRuleTest.php | 2 + .../Functions/ClosureAttributesRuleTest.php | 2 + .../Functions/FunctionAttributesRuleTest.php | 2 + .../Functions/ParamAttributesRuleTest.php | 2 + .../Methods/MethodAttributesRuleTest.php | 2 + .../Properties/PropertyAttributesRuleTest.php | 2 + .../PropertyHookAttributesRuleTest.php | 2 + .../Rules/Traits/TraitAttributesRuleTest.php | 2 + 21 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Classes/data/api-attributes.php diff --git a/src/DependencyInjection/AutowiredService.php b/src/DependencyInjection/AutowiredService.php index 85778b23fa4..d117d87f967 100644 --- a/src/DependencyInjection/AutowiredService.php +++ b/src/DependencyInjection/AutowiredService.php @@ -13,12 +13,15 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class AutowiredService { /** + * @api * @param true|list|class-string $as */ public function __construct( diff --git a/src/DependencyInjection/Conditional.php b/src/DependencyInjection/Conditional.php index 9844ae7cdc1..fa195d0d47c 100644 --- a/src/DependencyInjection/Conditional.php +++ b/src/DependencyInjection/Conditional.php @@ -18,6 +18,8 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class Conditional @@ -29,6 +31,7 @@ final class Conditional * On PHP < 8.1, simple-downgrader writes `new` in attribute arguments as the array, * and composer-attribute-collector turns it back into the object in vendor/attributes.php. * + * @api * @param AutowiredService|RegisteredCollector|RegisteredRule|array{ * 'olvlvl\ComposerAttributeCollector\NewExpression': array{ * class-string, diff --git a/src/DependencyInjection/GenerateFactory.php b/src/DependencyInjection/GenerateFactory.php index c4b6a59ce03..8ea0ddf862e 100644 --- a/src/DependencyInjection/GenerateFactory.php +++ b/src/DependencyInjection/GenerateFactory.php @@ -15,12 +15,15 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class GenerateFactory { /** + * @api * @param class-string $interface * @param class-string $resultType */ diff --git a/src/DependencyInjection/NonAutowiredService.php b/src/DependencyInjection/NonAutowiredService.php index d9891975027..be41184b330 100644 --- a/src/DependencyInjection/NonAutowiredService.php +++ b/src/DependencyInjection/NonAutowiredService.php @@ -6,14 +6,17 @@ /** * Registers a non-autowired named service in the DI container. - + * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class NonAutowiredService { + /** @api */ public function __construct( public string $name, public ?string $factory = null, diff --git a/src/DependencyInjection/RegisteredCollector.php b/src/DependencyInjection/RegisteredCollector.php index 42af140bfcf..70525ff6ae5 100644 --- a/src/DependencyInjection/RegisteredCollector.php +++ b/src/DependencyInjection/RegisteredCollector.php @@ -9,11 +9,14 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class RegisteredCollector { + /** @api */ public function __construct(public int $level) { } diff --git a/src/DependencyInjection/RegisteredRule.php b/src/DependencyInjection/RegisteredRule.php index 3bb80348d50..cf7f6d6a765 100644 --- a/src/DependencyInjection/RegisteredRule.php +++ b/src/DependencyInjection/RegisteredRule.php @@ -9,11 +9,14 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_CLASS)] final class RegisteredRule { + /** @api */ public function __construct(public int $level) { } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index ecf86085892..4e34d853dd0 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -11,8 +11,10 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Internal\SprintfHelper; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Api\ApiRuleHelper; use function array_key_exists; use function count; use function sprintf; @@ -26,6 +28,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, private ClassNameCheck $classCheck, + private ApiRuleHelper $apiRuleHelper, #[AutowiredParameter] private bool $deprecationRulesInstalled, ) @@ -66,6 +69,19 @@ public function check( continue; } + if ( + $this->apiRuleHelper->isPhpStanCode($scope, $attributeClass->getName(), $attributeClass->getFileName()) + && !$this->isApi($attributeClass) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Using attribute %s is not covered by backward compatibility promise. The attribute might change in a minor PHPStan version.', + $attributeClass->getDisplayName(), + ))->identifier('phpstanApi.attribute')->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', + ))->line($attribute->getStartLine())->build(); + } + if ($attributeClass->isAbstract()) { $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name)) ->identifier('attribute.abstract') @@ -175,4 +191,15 @@ public function check( return $errors; } + private function isApi(ClassReflection $attributeClass): bool + { + foreach ($attributeClass->getResolvedPhpDoc()?->getPhpDocNodes() ?? [] as $phpDocNode) { + if (count($phpDocNode->getTagsByName('@api')) > 0) { + return true; + } + } + + return false; + } + } diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index d14148af373..8ba3815016d 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Classes; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -62,6 +63,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); @@ -219,4 +221,17 @@ public function testDeprecatedAttribute(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testPhpStanApiAttributes(): void + { + $tip = "If you think it should be covered by backward compatibility promise, open a discussion:\n https://github.com/phpstan/phpstan/discussions\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise"; + $this->analyse([__DIR__ . '/data/api-attributes.php'], [ + [ + 'Using attribute PHPStan\\DependencyInjection\\ContainerExtension is not covered by backward compatibility promise. The attribute might change in a minor PHPStan version.', + 56, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index 16509603b92..2364e67704d 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Classes; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Classes/data/api-attributes.php b/tests/PHPStan/Rules/Classes/data/api-attributes.php new file mode 100644 index 00000000000..e0de21dd947 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/api-attributes.php @@ -0,0 +1,60 @@ += 8.1 + +namespace ApiAttributes; + +use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Conditional; +use PHPStan\DependencyInjection\ContainerExtension; +use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\DependencyInjection\NonAutowiredService; +use PHPStan\DependencyInjection\RegisteredCollector; +use PHPStan\DependencyInjection\RegisteredRule; + +#[AutowiredService] +class Service +{ + +} + +#[RegisteredRule(level: 0)] +class Rule +{ + +} + +#[RegisteredCollector(level: 0)] +class Collector +{ + +} + +#[GenerateFactory(interface: Factory::class)] +class Product +{ + +} + +interface Factory +{ + + public function create(): Product; + +} + +#[Conditional(attribute: new RegisteredRule(level: 4), if: '%featureToggles.foo%')] +class ConditionalRule +{ + +} + +#[NonAutowiredService(name: 'foo')] +class NamedService +{ + +} + +#[ContainerExtension(name: 'foo')] +class Extension +{ + +} diff --git a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleConfigPhpTest.php b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleConfigPhpTest.php index d33aee440e6..265fb8629fd 100644 --- a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleConfigPhpTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleConfigPhpTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Constants; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -62,6 +63,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php index 623b5c6bacd..9b8e2967ba6 100644 --- a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Constants; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -62,6 +63,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index c6d8845f9e3..164068291e5 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\EnumCases; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -58,6 +59,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index c6537a84a02..b584fc83b27 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 57b53647009..7d5264277e7 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index cf972070ed8..6f022ef33b7 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 0584cb37b3a..5535334859d 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 06220b9f878..4a1325e712d 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -64,6 +65,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 7a41ee22d3e..8b1a8361b7a 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Properties; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -58,6 +59,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index dd12602a865..534ce9e128a 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Properties; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -57,6 +58,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index e23d8f53685..c1a6164a291 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\Api\ApiRuleHelper; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -64,6 +65,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); From b3b14ff7fdad3305b647203b1e969e23ca0347ce Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 2 Nov 2026 18:07:06 +0000 Subject: [PATCH 2/3] Make AutowiredParameter attribute @api Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983 --- src/DependencyInjection/AutowiredParameter.php | 3 +++ tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Classes/data/api-attributes.php | 8 ++++++++ tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php | 7 +++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/DependencyInjection/AutowiredParameter.php b/src/DependencyInjection/AutowiredParameter.php index d8547af2f86..5cd773fc20d 100644 --- a/src/DependencyInjection/AutowiredParameter.php +++ b/src/DependencyInjection/AutowiredParameter.php @@ -12,11 +12,14 @@ * * Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector * and AutowiredAttributeServicesExtension. + * + * @api */ #[Attribute(flags: Attribute::TARGET_PARAMETER)] final class AutowiredParameter { + /** @api */ public function __construct(public ?string $ref = null) { } diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 8ba3815016d..ef6bae22957 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -228,7 +228,7 @@ public function testPhpStanApiAttributes(): void $this->analyse([__DIR__ . '/data/api-attributes.php'], [ [ 'Using attribute PHPStan\\DependencyInjection\\ContainerExtension is not covered by backward compatibility promise. The attribute might change in a minor PHPStan version.', - 56, + 64, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Classes/data/api-attributes.php b/tests/PHPStan/Rules/Classes/data/api-attributes.php index e0de21dd947..65c7708107c 100644 --- a/tests/PHPStan/Rules/Classes/data/api-attributes.php +++ b/tests/PHPStan/Rules/Classes/data/api-attributes.php @@ -2,6 +2,7 @@ namespace ApiAttributes; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Conditional; use PHPStan\DependencyInjection\ContainerExtension; @@ -14,6 +15,13 @@ class Service { + public function __construct( + #[AutowiredParameter(ref: '%tmpDir%')] + private string $tmpDir, + ) + { + } + } #[RegisteredRule(level: 0)] diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 5535334859d..5978cf86715 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -17,6 +17,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -92,4 +93,10 @@ public function testBug10298(): void $this->analyse([__DIR__ . '/data/bug-10298.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testPhpStanApiAttributes(): void + { + $this->analyse([__DIR__ . '/../Classes/data/api-attributes.php'], []); + } + } From 8c222159781a117938b4a603a2ebf368b2a1ffc8 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 2 Nov 2026 18:09:16 +0000 Subject: [PATCH 3/3] Avoid nullsafe operator that is not downgraded to PHP 7.4 Amp-Thread-ID: https://ampcode.com/threads/T-01a19cd4-be31-75b4-bd15-67786eff5983 --- src/Rules/AttributesCheck.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 4e34d853dd0..52b52ffb546 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -193,7 +193,12 @@ public function check( private function isApi(ClassReflection $attributeClass): bool { - foreach ($attributeClass->getResolvedPhpDoc()?->getPhpDocNodes() ?? [] as $phpDocNode) { + $phpDoc = $attributeClass->getResolvedPhpDoc(); + if ($phpDoc === null) { + return false; + } + + foreach ($phpDoc->getPhpDocNodes() as $phpDocNode) { if (count($phpDocNode->getTagsByName('@api')) > 0) { return true; }