diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index bb16d18b31..ccaa4e633d 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -517,6 +517,13 @@ public function resolveIsDeprecated(PhpDocNode $phpDocNode): bool return count($deprecatedTags) > 0; } + public function resolveIsNotDeprecated(PhpDocNode $phpDocNode): bool + { + $notDeprecatedTags = $phpDocNode->getTagsByName('@not-deprecated'); + + return count($notDeprecatedTags) > 0; + } + public function resolveIsInternal(PhpDocNode $phpDocNode): bool { $internalTags = $phpDocNode->getTagsByName('@internal'); diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index c3215d3058..b1a736e2ef 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -103,6 +103,8 @@ class ResolvedPhpDocBlock private ?bool $isDeprecated = null; + private ?bool $isNotDeprecated = null; + private ?bool $isInternal = null; private ?bool $isFinal = null; @@ -177,6 +179,7 @@ public static function createEmpty(): self $self->selfOutTypeTag = null; $self->deprecatedTag = null; $self->isDeprecated = false; + $self->isNotDeprecated = false; $self->isInternal = false; $self->isFinal = false; $self->isPure = null; @@ -229,8 +232,9 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->typeAliasImportTags = $this->getTypeAliasImportTags(); $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); - $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; + $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); $result->isPure = $this->isPure(); @@ -324,6 +328,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->selfOutTypeTag = $this->selfOutTypeTag; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; + $self->isNotDeprecated = $this->isNotDeprecated; $self->isInternal = $this->isInternal; $self->isFinal = $this->isFinal; $self->isPure = $this->isPure; @@ -599,6 +604,19 @@ public function isDeprecated(): bool return $this->isDeprecated; } + /** + * @internal + */ + public function isNotDeprecated(): bool + { + if ($this->isNotDeprecated === null) { + $this->isNotDeprecated = $this->phpDocNodeResolver->resolveIsNotDeprecated( + $this->phpDocNode, + ); + } + return $this->isNotDeprecated; + } + public function isInternal(): bool { if ($this->isInternal === null) { @@ -871,14 +889,19 @@ private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, ar /** * @param array $parents */ - private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, array $parents): ?DeprecatedTag + private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, array $parents): ?DeprecatedTag { if ($deprecatedTag !== null) { return $deprecatedTag; } + + if ($hasNotDeprecatedTag) { + return null; + } + foreach ($parents as $parent) { $result = $parent->getDeprecatedTag(); - if ($result === null) { + if ($result === null && !$parent->isNotDeprecated()) { continue; } return $result; diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 2d108fb0c9..75fbfa4527 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,11 +2,14 @@ namespace PHPStan\Reflection\Annotations; +use DeprecatedAnnotations\Baz; +use DeprecatedAnnotations\BazInterface; use DeprecatedAnnotations\DeprecatedBar; use DeprecatedAnnotations\DeprecatedFoo; use DeprecatedAnnotations\DeprecatedWithMultipleTags; use DeprecatedAnnotations\Foo; use DeprecatedAnnotations\FooInterface; +use DeprecatedAnnotations\SubBazInterface; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; @@ -141,4 +144,13 @@ public function testDeprecatedMethodsFromInterface(): void $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); } + public function testNotDeprecatedChildMethods(): void + { + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->getClass(BazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->yes()); + $this->assertTrue($reflectionProvider->getClass(SubBazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + $this->assertTrue($reflectionProvider->getClass(Baz::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + } + } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 1095002d92..553c2444ef 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php @@ -215,3 +215,26 @@ public function superDeprecated() } } + +interface BazInterface +{ + /** + * @deprecated Use the SubBazInterface instead. + */ + public function superDeprecated(); +} + +interface SubBazInterface extends BazInterface +{ + /** + * @not-deprecated + */ + public function superDeprecated(); +} + +class Baz implements SubBazInterface +{ + public function superDeprecated() + { + } +}