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/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..52b52ffb546 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,20 @@ public function check( return $errors; } + private function isApi(ClassReflection $attributeClass): bool + { + $phpDoc = $attributeClass->getResolvedPhpDoc(); + if ($phpDoc === null) { + return false; + } + + foreach ($phpDoc->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..ef6bae22957 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.', + 64, + $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..65c7708107c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/api-attributes.php @@ -0,0 +1,68 @@ += 8.1 + +namespace ApiAttributes; + +use PHPStan\DependencyInjection\AutowiredParameter; +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 +{ + + public function __construct( + #[AutowiredParameter(ref: '%tmpDir%')] + private string $tmpDir, + ) + { + } + +} + +#[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..5978cf86715 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; @@ -16,6 +17,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -57,6 +59,7 @@ protected function getRule(): Rule $reflectionProvider, $container->getExtensionsCollection(RestrictedClassNameUsageExtension::class), ), + new ApiRuleHelper(), deprecationRulesInstalled: true, ), ); @@ -90,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'], []); + } + } 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, ), );