diff --git a/composer.json b/composer.json index 149508f..11e1ea5 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.3" + "phpstan/phpstan": "^1.10.24" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/rules.neon b/rules.neon index 704579b..2a88088 100644 --- a/rules.neon +++ b/rules.neon @@ -15,6 +15,12 @@ services: class: PHPStan\Rules\Deprecations\DefaultDeprecatedScopeResolver tags: - phpstan.deprecations.deprecatedScopeResolver + - + class: PHPStan\Rules\Deprecations\OverrideDeprecatedPropertyRule + - + class: PHPStan\Rules\Deprecations\OverrideDeprecatedMethodRule + - + class: PHPStan\Rules\Deprecations\OverrideDeprecatedConstantRule rules: - PHPStan\Rules\Deprecations\AccessDeprecatedPropertyRule @@ -33,3 +39,11 @@ rules: - PHPStan\Rules\Deprecations\TypeHintDeprecatedInFunctionSignatureRule - PHPStan\Rules\Deprecations\UsageOfDeprecatedCastRule - PHPStan\Rules\Deprecations\UsageOfDeprecatedTraitRule + +conditionalTags: + PHPStan\Rules\Deprecations\OverrideDeprecatedPropertyRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\Deprecations\OverrideDeprecatedMethodRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\Deprecations\OverrideDeprecatedConstantRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php b/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php index 8acab06..c47fb89 100644 --- a/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php +++ b/src/Rules/Deprecations/InheritanceOfDeprecatedInterfaceRule.php @@ -31,10 +31,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->extends === null) { - return []; - } - $interfaceName = isset($node->namespacedName) ? (string) $node->namespacedName : (string) $node->name; diff --git a/src/Rules/Deprecations/OverrideDeprecatedConstantRule.php b/src/Rules/Deprecations/OverrideDeprecatedConstantRule.php new file mode 100644 index 0000000..3627e6a --- /dev/null +++ b/src/Rules/Deprecations/OverrideDeprecatedConstantRule.php @@ -0,0 +1,73 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PhpParser\Node; +use PhpParser\Node\Stmt\ClassConst; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; + +/** + * @implements Rule<ClassConst> + */ +class OverrideDeprecatedConstantRule implements Rule +{ + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + if ($node->isPrivate()) { + return []; + } + + $class = $scope->getClassReflection(); + + $parents = $class->getParents(); + + $name = (string) $node->consts[0]->name; + + foreach ($parents as $parent) { + if (!$parent->hasConstant($name)) { + continue; + } + + $parentConst = $parent->getConstant($name); + + if (!$parentConst->isDeprecated()->yes()) { + return []; + } + + return [RuleErrorBuilder::message(sprintf( + 'Class %s overrides deprecated const %s of class %s.', + $class->getName(), + $name, + $parent->getName() + ))->identifier('constant.deprecated')->build()]; + } + + return []; + } + +} diff --git a/src/Rules/Deprecations/OverrideDeprecatedMethodRule.php b/src/Rules/Deprecations/OverrideDeprecatedMethodRule.php new file mode 100644 index 0000000..07ed031 --- /dev/null +++ b/src/Rules/Deprecations/OverrideDeprecatedMethodRule.php @@ -0,0 +1,88 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PhpParser\Node; +use PhpParser\Node\Stmt\ClassMethod; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; + +/** + * @implements Rule<ClassMethod> + */ +class OverrideDeprecatedMethodRule implements Rule +{ + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + if ($node->isPrivate()) { + return []; + } + + $class = $scope->getClassReflection(); + + $ancestors = $class->getAncestors(); + + $methodName = (string) $node->name; + + $method = $class->getMethod($methodName, $scope); + + if ($method->isDeprecated()->no()) { + return []; + } + + foreach ($ancestors as $ancestor) { + if ($ancestor === $class) { + continue; + } + + if ($ancestor->isTrait()) { + continue; + } + + if (!$ancestor->hasMethod($methodName)) { + continue; + } + + $ancestorMethod = $ancestor->getMethod($methodName, $scope); + + if (!$ancestorMethod->isDeprecated()->yes()) { + return []; + } + + return [RuleErrorBuilder::message(sprintf( + 'Class %s overrides deprecated method %s of %s %s.', + $class->getName(), + $methodName, + $ancestor->isInterface() ? 'interface' : 'class', + $ancestor->getName() + ))->identifier('method.deprecated')->build()]; + } + + return []; + } + +} diff --git a/src/Rules/Deprecations/OverrideDeprecatedPropertyRule.php b/src/Rules/Deprecations/OverrideDeprecatedPropertyRule.php new file mode 100644 index 0000000..e88cbad --- /dev/null +++ b/src/Rules/Deprecations/OverrideDeprecatedPropertyRule.php @@ -0,0 +1,79 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PhpParser\Node; +use PhpParser\Node\Stmt\Property; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; + +/** + * @implements Rule<Property> + */ +class OverrideDeprecatedPropertyRule implements Rule +{ + + /** @var DeprecatedScopeHelper */ + private $deprecatedScopeHelper; + + public function __construct(DeprecatedScopeHelper $deprecatedScopeHelper) + { + $this->deprecatedScopeHelper = $deprecatedScopeHelper; + } + + public function getNodeType(): string + { + return Property::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->deprecatedScopeHelper->isScopeDeprecated($scope)) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + if ($node->isPrivate()) { + return []; + } + + $class = $scope->getClassReflection(); + + $parents = $class->getParents(); + + $propertyName = (string) $node->props[0]->name; + + $property = $class->getProperty($propertyName, $scope); + + if ($property->isDeprecated()->no()) { + return []; + } + + foreach ($parents as $parent) { + if (!$parent->hasProperty($propertyName)) { + continue; + } + + $parentProperty = $parent->getProperty($propertyName, $scope); + + if (!$parentProperty->isDeprecated()->yes()) { + return []; + } + + return [RuleErrorBuilder::message(sprintf( + 'Class %s overrides deprecated property %s of class %s.', + $class->getName(), + $propertyName, + $parent->getName() + ))->identifier('property.deprecated')->build()]; + } + + return []; + } + +} diff --git a/tests/Rules/Deprecations/OverrideDeprecatedConstantRuleTest.php b/tests/Rules/Deprecations/OverrideDeprecatedConstantRuleTest.php new file mode 100644 index 0000000..114d965 --- /dev/null +++ b/tests/Rules/Deprecations/OverrideDeprecatedConstantRuleTest.php @@ -0,0 +1,32 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + +/** + * @extends RuleTestCase<OverrideDeprecatedConstantRule> + */ +class OverrideDeprecatedConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverrideDeprecatedConstantRule(new DeprecatedScopeHelper([new DefaultDeprecatedScopeResolver()])); + } + + public function testDeprecatedConstantOverride(): void + { + $this->analyse( + [__DIR__ . '/data/override-deprecated-constant.php'], + [ + [ + 'Class OverrideDeprecatedConstant\Child overrides deprecated const DEPRECATED of class OverrideDeprecatedConstant\Ancestor.', + 20, + ], + ] + ); + } + +} diff --git a/tests/Rules/Deprecations/OverrideDeprecatedMethodRuleTest.php b/tests/Rules/Deprecations/OverrideDeprecatedMethodRuleTest.php new file mode 100644 index 0000000..bc784f4 --- /dev/null +++ b/tests/Rules/Deprecations/OverrideDeprecatedMethodRuleTest.php @@ -0,0 +1,44 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + +/** + * @extends RuleTestCase<OverrideDeprecatedMethodRule> + */ +class OverrideDeprecatedMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverrideDeprecatedMethodRule(new DeprecatedScopeHelper([new DefaultDeprecatedScopeResolver()])); + } + + public function testDeprecatedMethodOverride(): void + { + $this->analyse( + [__DIR__ . '/data/override-deprecated-method.php'], + [ + [ + 'Class OverrideDeprecatedMethod\Child overrides deprecated method deprecatedMethod of class OverrideDeprecatedMethod\Ancestor.', + 49, + ], + [ + 'Class OverrideDeprecatedMethod\Child overrides deprecated method deprecatedInInterface of interface OverrideDeprecatedMethod\Deprecated.', + 61, + ], + [ + 'Class OverrideDeprecatedMethod\Child overrides deprecated method deprecatedInTrait of class OverrideDeprecatedMethod\Ancestor.', + 64, + ], + [ + 'Class OverrideDeprecatedMethod\GrandChild overrides deprecated method deprecatedInChild of class OverrideDeprecatedMethod\Child.', + 73, + ], + ] + ); + } + +} diff --git a/tests/Rules/Deprecations/OverrideDeprecatedPropertyRuleTest.php b/tests/Rules/Deprecations/OverrideDeprecatedPropertyRuleTest.php new file mode 100644 index 0000000..9664768 --- /dev/null +++ b/tests/Rules/Deprecations/OverrideDeprecatedPropertyRuleTest.php @@ -0,0 +1,32 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Rules\Deprecations; + +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + +/** + * @extends RuleTestCase<OverrideDeprecatedPropertyRule> + */ +class OverrideDeprecatedPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverrideDeprecatedPropertyRule(new DeprecatedScopeHelper([new DefaultDeprecatedScopeResolver()])); + } + + public function testDeprecatedPropertyOverride(): void + { + $this->analyse( + [__DIR__ . '/data/override-deprecated-property.php'], + [ + [ + 'Class OverrideDeprecatedProperty\Child overrides deprecated property deprecatedProperty of class OverrideDeprecatedProperty\Ancestor.', + 25, + ], + ] + ); + } + +} diff --git a/tests/Rules/Deprecations/data/override-deprecated-constant.php b/tests/Rules/Deprecations/data/override-deprecated-constant.php new file mode 100644 index 0000000..e69ffb5 --- /dev/null +++ b/tests/Rules/Deprecations/data/override-deprecated-constant.php @@ -0,0 +1,23 @@ +<?php + +namespace OverrideDeprecatedConstant; + +class Ancestor +{ + /** + * @deprecated + */ + public const DEPRECATED = ''; + + /** + * @deprecated + */ + private const PRIVATE_DEPRECATED = ''; +} + +class Child extends Ancestor +{ + public const DEPRECATED = ''; + + private const PRIVATE_DEPRECATED = ''; +} diff --git a/tests/Rules/Deprecations/data/override-deprecated-method.php b/tests/Rules/Deprecations/data/override-deprecated-method.php new file mode 100644 index 0000000..7ac3216 --- /dev/null +++ b/tests/Rules/Deprecations/data/override-deprecated-method.php @@ -0,0 +1,75 @@ +<?php + +namespace OverrideDeprecatedMethod; + +trait DeprecationTrait +{ + /** + * @deprecated + */ + public function deprecatedInTrait(): void + {} +} + +class Ancestor +{ + use DeprecationTrait; + /** + * @deprecated + */ + public function deprecatedMethod(): void + {} + + /** + * @deprecated + */ + private function privateDeprecatedMethod(): void + {} + + /** + * @deprecated + */ + public function explicitlyNotDeprecatedMethod(): void + {} +} + +interface Deprecated +{ + /** + * @deprecated + */ + public function deprecatedInInterface(): void; +} + +class Child extends Ancestor implements Deprecated +{ + use DeprecationTrait { + deprecatedInTrait as deprecatedInChild; + } + public function deprecatedMethod(): void + {} + + private function privateDeprecatedMethod(): void + {} + + /** + * @not-deprecated + */ + public function explicitlyNotDeprecatedMethod(): void + {} + + public function deprecatedInInterface(): void + {} + + public function deprecatedInTrait(): void + {} +} + +class GrandChild extends Child +{ + public function explicitlyNotDeprecatedMethod(): void + {} + + public function deprecatedInChild(): void + {} +} diff --git a/tests/Rules/Deprecations/data/override-deprecated-property.php b/tests/Rules/Deprecations/data/override-deprecated-property.php new file mode 100644 index 0000000..fbcc885 --- /dev/null +++ b/tests/Rules/Deprecations/data/override-deprecated-property.php @@ -0,0 +1,38 @@ +<?php + +namespace OverrideDeprecatedProperty; + +class Ancestor +{ + /** + * @deprecated + */ + public $deprecatedProperty; + + /** + * @deprecated + */ + private $privateDeprecatedProperty; + + /** + * @deprecated + */ + public $explicitlyNotDeprecated; +} + +class Child extends Ancestor +{ + public $deprecatedProperty; + + private $privateDeprecatedProperty; + + /** + * @not-deprecated + */ + public $explicitlyNotDeprecated; +} + +class GrandChild extends Child +{ + public $explicitlyNotDeprecated; +}