From 678a7105d16ccb654c87fa1a2849c733c318022c Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 4 Oct 2022 14:47:21 +0200 Subject: [PATCH 1/7] Add @phpstan-self-out support --- phpstan-baseline.neon | 5 ++ src/Analyser/MutatingScope.php | 2 + src/Analyser/NodeScopeResolver.php | 16 ++++- src/PhpDoc/PhpDocNodeResolver.php | 13 ++++ src/PhpDoc/ResolvedPhpDocBlock.php | 18 +++++ src/PhpDoc/Tag/SelfOutTypeTag.php | 27 +++++++ .../AnnotationMethodReflection.php | 5 ++ .../Dummy/ChangedTypeMethodReflection.php | 5 ++ .../Dummy/DummyMethodReflection.php | 5 ++ src/Reflection/ExtendedMethodReflection.php | 4 ++ .../Native/NativeMethodReflection.php | 6 ++ .../Php/ClosureCallMethodReflection.php | 5 ++ .../Php/EnumCasesMethodReflection.php | 5 ++ .../Php/PhpClassReflectionExtension.php | 17 ++++- .../Php/PhpMethodFromParserNodeReflection.php | 6 ++ src/Reflection/Php/PhpMethodReflection.php | 6 ++ .../Php/PhpMethodReflectionFactory.php | 1 + src/Reflection/ResolvedMethodReflection.php | 16 +++++ .../Type/IntersectionTypeMethodReflection.php | 5 ++ .../Type/UnionTypeMethodReflection.php | 5 ++ .../WrappedExtendedMethodReflection.php | 5 ++ .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/self-out.php | 70 +++++++++++++++++++ 23 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/PhpDoc/Tag/SelfOutTypeTag.php create mode 100644 tests/PHPStan/Analyser/data/self-out.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7b7d712ce8..56bdf0cd77 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -166,6 +166,11 @@ parameters: count: 1 path: src/PhpDoc/Tag/ReturnTag.php + - + message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + count: 1 + path: src/PhpDoc/Tag/SelfOutTypeTag.php + - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" count: 1 diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 841539b37c..2a015f22a8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2532,6 +2532,7 @@ public function enterClassMethod( ?bool $isPure = null, bool $acceptsNamedArguments = true, ?Assertions $asserts = null, + ?Type $selfOutType = null, ): self { if (!$this->isInClass()) { @@ -2557,6 +2558,7 @@ public function enterClassMethod( $isPure, $acceptsNamedArguments, $asserts ?? Assertions::createEmpty(), + $selfOutType, ), !$classMethod->isStatic(), ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 367c6c3e74..4a12ec0e43 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -468,7 +468,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts, $selfOutType] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($param, $scope, $nodeCallback); @@ -491,6 +491,7 @@ private function processStmtNode( $isPure, $acceptsNamedArguments, $asserts, + $selfOutType, ); if ($stmt->name->toLowerString() === '__construct') { @@ -2063,6 +2064,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope = $scope->invalidateExpression($arg->value, true); } } + + if ($parametersAcceptor !== null) { + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $scope = $scope->assignExpression($expr->var, TemplateTypeHelper::resolveTemplateTypes($selfOutType, $parametersAcceptor->getResolvedTemplateTypeMap())); + } + } } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } @@ -3980,7 +3988,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } /** - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions} + * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -3996,6 +4004,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $acceptsNamedArguments = true; $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); $asserts = Assertions::createEmpty(); + $selfOutType = null; $docComment = $node->getDocComment() !== null ? $node->getDocComment()->getText() : null; @@ -4105,9 +4114,10 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getThisOutTag() !== null ? $resolvedPhpDoc->getThisOutTag()->getType() : null; } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 8f369bd82e..ad8d55ce89 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -14,6 +14,7 @@ use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -451,6 +452,18 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco return $resolved; } + public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag + { + foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) { + foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) { + $type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope); + return new SelfOutTypeTag($type); + } + } + + return null; + } + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 726f7c4d17..f070252ceb 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -12,6 +12,7 @@ use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -90,6 +91,8 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $assertTags = false; + private SelfOutTypeTag|false|null $selfOutTypeTag = false; + private DeprecatedTag|false|null $deprecatedTag = false; private ?bool $isDeprecated = null; @@ -164,6 +167,7 @@ public static function createEmpty(): self $self->typeAliasTags = []; $self->typeAliasImportTags = []; $self->assertTags = []; + $self->selfOutTypeTag = null; $self->deprecatedTag = null; $self->isDeprecated = false; $self->isInternal = false; @@ -216,6 +220,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); + $result->selfOutTypeTag = $this->getThisOutTag(); $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; $result->isInternal = $this->isInternal(); @@ -297,6 +302,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->typeAliasTags = $this->typeAliasTags; $self->typeAliasImportTags = $this->typeAliasImportTags; $self->assertTags = $assertTags; + $self->selfOutTypeTag = $this->getThisOutTag(); $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; $self->isInternal = $this->isInternal; @@ -522,6 +528,18 @@ public function getAssertTags(): array return $this->assertTags; } + public function getThisOutTag(): ?SelfOutTypeTag + { + if ($this->selfOutTypeTag === false) { + $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->selfOutTypeTag; + } + public function getDeprecatedTag(): ?DeprecatedTag { if (is_bool($this->deprecatedTag)) { diff --git a/src/PhpDoc/Tag/SelfOutTypeTag.php b/src/PhpDoc/Tag/SelfOutTypeTag.php new file mode 100644 index 0000000000..bbd5a2ca09 --- /dev/null +++ b/src/PhpDoc/Tag/SelfOutTypeTag.php @@ -0,0 +1,27 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 0688016888..dc8c88f3ee 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -121,4 +121,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 64c33628ff..9c2434d60d 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -95,4 +95,9 @@ public function getAsserts(): Assertions return $this->reflection->getAsserts(); } + public function getSelfOutType(): ?Type + { + return $this->reflection->getSelfOutType(); + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index ad6e5b5798..19a6d690ac 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -102,4 +102,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 2f69db735b..e78785fabc 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection; +use PHPStan\Type\Type; + /** * The purpose of this interface is to be able to * answer more questions about methods @@ -18,4 +20,6 @@ interface ExtendedMethodReflection extends MethodReflection public function getAsserts(): Assertions; + public function getSelfOutType(): ?Type; + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 2aaa78ab97..3369852923 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -31,6 +31,7 @@ public function __construct( private TrinaryLogic $hasSideEffects, private ?Type $throwType, private Assertions $assertions, + private ?Type $selfOutType, ) { } @@ -160,4 +161,9 @@ public function getAsserts(): Assertions return $this->assertions; } + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index 62c9f97402..e5dda920c6 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -124,4 +124,9 @@ public function getAsserts(): Assertions return $this->nativeMethodReflection->getAsserts(); } + public function getSelfOutType(): ?Type + { + return $this->nativeMethodReflection->getSelfOutType(); + } + } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index f44dbf3f3a..733bb69f79 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -111,4 +111,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 31519b6c77..594f590134 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -502,6 +502,7 @@ private function createMethod( $reflectionMethod = null; $throwType = null; $asserts = Assertions::createEmpty(); + $selfOutType = null; if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); } @@ -544,6 +545,11 @@ private function createMethod( } $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + + $selfOutTypeTag = $stubPhpDoc->getThisOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } } } if ($stubPhpDocPair === null && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { @@ -569,6 +575,11 @@ private function createMethod( } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + $selfOutTypeTag = $phpDocBlock->getThisOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } + $signatureParameters = $methodSignature->getParameters(); foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { if (!array_key_exists($paramI, $signatureParameters)) { @@ -595,6 +606,7 @@ private function createMethod( $hasSideEffects, $throwType, $asserts, + $selfOutType, ); } @@ -715,6 +727,7 @@ private function createMethod( $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getThisOutTag() !== null ? $resolvedPhpDoc->getThisOutTag()->getType() : null; return $this->methodReflectionFactory->create( $declaringClass, @@ -730,6 +743,7 @@ private function createMethod( $isFinal, $isPure, $asserts, + $selfOutType, ); } @@ -885,7 +899,7 @@ private function inferAndCachePropertyTypes( $constructor, $namespace, )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , , $asserts, $selfOutType] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -899,6 +913,7 @@ private function inferAndCachePropertyTypes( $isPure, $acceptsNamedArguments, $asserts, + $selfOutType, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index ff87c37140..384a3930b3 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -46,6 +46,7 @@ public function __construct( ?bool $isPure, bool $acceptsNamedArguments, Assertions $assertions, + private ?Type $selfOutType, ) { $name = strtolower($classMethod->name->name); @@ -137,4 +138,9 @@ public function isBuiltin(): bool return false; } + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index e30af288e0..2a8174e662 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -80,6 +80,7 @@ public function __construct( private bool $isFinal, private ?bool $isPure, private Assertions $asserts, + private ?Type $selfOutType, ) { } @@ -426,4 +427,9 @@ public function getAsserts(): Assertions return $this->asserts; } + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index d979960cbc..c078217bed 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -27,6 +27,7 @@ public function create( bool $isFinal, ?bool $isPure, Assertions $asserts, + ?Type $selfOutType, ): PhpMethodReflection; } diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 0757eb2921..b4225cd2ad 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -16,6 +16,8 @@ class ResolvedMethodReflection implements ExtendedMethodReflection private ?Assertions $asserts = null; + private Type|false|null $selfOutType = false; + public function __construct(private ExtendedMethodReflection $reflection, private TemplateTypeMap $resolvedTemplateTypeMap) { } @@ -123,4 +125,18 @@ public function getAsserts(): Assertions return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $this->resolvedTemplateTypeMap)); } + public function getSelfOutType(): ?Type + { + if ($this->selfOutType === false) { + $selfOutType = $this->reflection->getSelfOutType(); + if ($selfOutType !== null) { + $selfOutType = TemplateTypeHelper::resolveTemplateTypes($selfOutType, $this->resolvedTemplateTypeMap); + } + + $this->selfOutType = $selfOutType; + } + + return $this->selfOutType; + } + } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index ec155d00fd..3046b0285a 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -159,4 +159,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index f60bf5c817..f72d36090f 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -153,4 +153,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 4438d02549..38774b738c 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -87,4 +87,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function getSelfOutType(): ?Type + { + return null; + } + } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 86acde5156..a6a20f8bd0 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1065,6 +1065,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-extra-int-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/list-count.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-7839.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/self-out.php'); } /** diff --git a/tests/PHPStan/Analyser/data/self-out.php b/tests/PHPStan/Analyser/data/self-out.php new file mode 100644 index 0000000000..6fb5350621 --- /dev/null +++ b/tests/PHPStan/Analyser/data/self-out.php @@ -0,0 +1,70 @@ + + */ + private array $data; + /** + * @param T $data + */ + public function __construct($data) { + $this->data = [$data]; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function addData($data) { + /** @var self $this */ + $this->data []= $data; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function setData($data) { + /** @var self $this */ + $this->data = [$data]; + } + /** + * @return ($this is a ? void : never) + */ + public function test(): void { + } +} + +function () { + $i = new a(123); + // OK - $i is a<123> + assertType('SelfOut\\a', $i); + assertType('void', $i->test()); + + $i->addData(321); + // OK - $i is a<123|321> + assertType('SelfOut\\a', $i); + assertType('void', $i->test()); + + $i->setData("test"); + // IfThisIsMismatch - Class is not a as required + assertType('SelfOut\\a', $i); + assertType('*NEVER*', $i->test()); +}; From eee2c03e2b05d59c413b8dae67b5ecbf0bafc3a9 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Thu, 6 Oct 2022 10:29:25 +0200 Subject: [PATCH 2/7] Add compatibility rule --- conf/config.level2.neon | 1 + .../PhpDoc/IncompatibleSelfOutTypeRule.php | 53 +++++++++++++++++++ .../IncompatibleSelfOutTypeRuleTest.php | 29 ++++++++++ .../data/incompatible-self-out-type.php | 24 +++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php create mode 100644 tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php create mode 100644 tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 5c3ca451e0..c30d85a244 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -35,6 +35,7 @@ rules: - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule - PHPStan\Rules\PhpDoc\FunctionAssertRule - PHPStan\Rules\PhpDoc\MethodAssertRule + - PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php new file mode 100644 index 0000000000..b8e49bfd32 --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -0,0 +1,53 @@ + + */ +class IncompatibleSelfOutTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $scope->getFunction(); + if ($method === null) { + throw new ShouldNotHappenException(); + } + + $selfOutType = $method->getSelfOutType(); + if ($selfOutType === null) { + return []; + } + + $classReflection = $method->getDeclaringClass(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + if (!$classType->isSuperTypeOf($selfOutType)->no()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Out type %s is not compatible with %s.', + $selfOutType->describe(VerbosityLevel::precise()), + $classType->describe(VerbosityLevel::precise()), + ))->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php new file mode 100644 index 0000000000..6106f7779f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -0,0 +1,29 @@ + + */ +class IncompatibleSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleSelfOutTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-self-out-type.php'], [ + [ + 'Out type int is not compatible with IncompatibleSelfOutType\A.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php new file mode 100644 index 0000000000..857796daf1 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -0,0 +1,24 @@ + + */ + public function two($param); + + /** + * @phpstan-self-out int + */ + public function three(); +} From 8954d8dff12a17d04d626e166ee219217c54e55b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Oct 2022 11:45:19 +0200 Subject: [PATCH 3/7] Fix CS --- src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php index b8e49bfd32..e320c368a0 100644 --- a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -10,6 +10,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule From 05d42df9a32cde7c94d3a670eee71755dca549a3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Oct 2022 11:45:24 +0200 Subject: [PATCH 4/7] Temporary Rector fix --- phpstan-baseline.neon | 5 +++++ src/PhpDoc/PhpDocNodeResolver.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 56bdf0cd77..d58a3077e5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -143,6 +143,11 @@ parameters: count: 1 path: src/PhpDoc/PhpDocBlock.php + - + message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getSelfOutTypeTagVa…' will always evaluate to true\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + - message: "#^Method PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock\\:\\:getNameScope\\(\\) should return PHPStan\\\\Analyser\\\\NameScope but returns PHPStan\\\\Analyser\\\\NameScope\\|null\\.$#" count: 1 diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ad8d55ce89..dda42a4a96 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -36,6 +36,7 @@ use function array_reverse; use function count; use function in_array; +use function method_exists; use function strpos; use function substr; @@ -454,6 +455,10 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag { + if (!method_exists($phpDocNode, 'getSelfOutTypeTagValues')) { + return null; + } + foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) { foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) { $type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope); From 5f16444525585ffca08976b1376c9a5c0643e01f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Oct 2022 11:46:19 +0200 Subject: [PATCH 5/7] Fix custom rule --- src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php index e320c368a0..4cca859f8d 100644 --- a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -7,7 +7,6 @@ use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -25,11 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if ($method === null) { - throw new ShouldNotHappenException(); - } - + $method = $node->getMethodReflection(); $selfOutType = $method->getSelfOutType(); if ($selfOutType === null) { return []; From 2cd967eb92dc6b89c47afc975a04e31245c149a7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Oct 2022 11:47:33 +0200 Subject: [PATCH 6/7] InClassMethodNode - contains ExtendedMethodReflection --- src/Analyser/NodeScopeResolver.php | 3 ++- src/Node/InClassMethodNode.php | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 4a12ec0e43..06e1bbc25d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -107,6 +107,7 @@ use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; @@ -527,7 +528,7 @@ private function processStmtNode( if ($stmt->getAttribute('virtual', false) === false) { $methodReflection = $methodScope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { + if (!$methodReflection instanceof ExtendedMethodReflection) { throw new ShouldNotHappenException(); } $nodeCallback(new InClassMethodNode($methodReflection, $stmt), $methodScope); diff --git a/src/Node/InClassMethodNode.php b/src/Node/InClassMethodNode.php index 78ab0c7c06..d7ba07e4c0 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -3,21 +3,21 @@ namespace PHPStan\Node; use PhpParser\Node; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; /** @api */ class InClassMethodNode extends Node\Stmt implements VirtualNode { public function __construct( - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, private Node\Stmt\ClassMethod $originalNode, ) { parent::__construct($originalNode->getAttributes()); } - public function getMethodReflection(): MethodReflection + public function getMethodReflection(): ExtendedMethodReflection { return $this->methodReflection; } From ad444fcab9174de55d442c7fba54cb11ca34a7a4 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Sat, 8 Oct 2022 16:05:46 +0200 Subject: [PATCH 7/7] Improve consistency --- src/Analyser/NodeScopeResolver.php | 2 +- src/PhpDoc/ResolvedPhpDocBlock.php | 25 ++++++++++++++++--- .../Php/PhpClassReflectionExtension.php | 6 ++--- .../PhpDoc/IncompatibleSelfOutTypeRule.php | 7 ++++-- src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 2 ++ tests/PHPStan/Analyser/data/self-out.php | 21 ++++++++++++++++ .../IncompatibleSelfOutTypeRuleTest.php | 6 ++++- .../data/incompatible-self-out-type.php | 5 ++++ 8 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 06e1bbc25d..8fab406e27 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4115,7 +4115,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); - $selfOutType = $resolvedPhpDoc->getThisOutTag() !== null ? $resolvedPhpDoc->getThisOutTag()->getType() : null; + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; } return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType]; diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index f070252ceb..8f943eca66 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -220,7 +220,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); - $result->selfOutTypeTag = $this->getThisOutTag(); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; $result->isInternal = $this->isInternal(); @@ -302,7 +302,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->typeAliasTags = $this->typeAliasTags; $self->typeAliasImportTags = $this->typeAliasImportTags; $self->assertTags = $assertTags; - $self->selfOutTypeTag = $this->getThisOutTag(); + $self->selfOutTypeTag = $this->selfOutTypeTag; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; $self->isInternal = $this->isInternal; @@ -528,7 +528,7 @@ public function getAssertTags(): array return $this->assertTags; } - public function getThisOutTag(): ?SelfOutTypeTag + public function getSelfOutTag(): ?SelfOutTypeTag { if ($this->selfOutTypeTag === false) { $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag( @@ -811,6 +811,25 @@ private static function mergeAssertTags(array $assertTags, array $parents, array return $assertTags; } + /** + * @param array $parents + */ + private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag + { + if ($selfOutTypeTag !== null) { + return $selfOutTypeTag; + } + foreach ($parents as $parent) { + $result = $parent->getSelfOutTag(); + if ($result === null) { + continue; + } + return $result; + } + + return null; + } + /** * @param array $parents */ diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 594f590134..572dec4a2f 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -546,7 +546,7 @@ private function createMethod( $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); - $selfOutTypeTag = $stubPhpDoc->getThisOutTag(); + $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); if ($selfOutTypeTag !== null) { $selfOutType = $selfOutTypeTag->getType(); } @@ -575,7 +575,7 @@ private function createMethod( } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); - $selfOutTypeTag = $phpDocBlock->getThisOutTag(); + $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); if ($selfOutTypeTag !== null) { $selfOutType = $selfOutTypeTag->getType(); } @@ -727,7 +727,7 @@ private function createMethod( $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); - $selfOutType = $resolvedPhpDoc->getThisOutTag() !== null ? $resolvedPhpDoc->getThisOutTag()->getType() : null; + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; return $this->methodReflectionFactory->create( $declaringClass, diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php index 4cca859f8d..3206b2300b 100644 --- a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -26,6 +26,7 @@ public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); $selfOutType = $method->getSelfOutType(); + if ($selfOutType === null) { return []; } @@ -33,14 +34,16 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $method->getDeclaringClass(); $classType = new ObjectType($classReflection->getName(), null, $classReflection); - if (!$classType->isSuperTypeOf($selfOutType)->no()) { + if ($classType->isSuperTypeOf($selfOutType)->yes()) { return []; } return [ RuleErrorBuilder::message(sprintf( - 'Out type %s is not compatible with %s.', + 'Self-out type %s of method %s::%s is not subtype of %s.', $selfOutType->describe(VerbosityLevel::precise()), + $classReflection->getName(), + $method->getName(), $classType->describe(VerbosityLevel::precise()), ))->build(), ]; diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 9e0e8218ae..cc564741d8 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -44,6 +44,8 @@ class InvalidPHPStanDocTagRule implements Rule '@phpstan-assert', '@phpstan-assert-if-true', '@phpstan-assert-if-false', + '@phpstan-self-out', + '@phpstan-this-out', ]; public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) diff --git a/tests/PHPStan/Analyser/data/self-out.php b/tests/PHPStan/Analyser/data/self-out.php index 6fb5350621..39fb2ed6e8 100644 --- a/tests/PHPStan/Analyser/data/self-out.php +++ b/tests/PHPStan/Analyser/data/self-out.php @@ -52,6 +52,19 @@ public function test(): void { } } +/** + * @template T + * @extends a + */ +class b extends a { + /** + * @param T $data + */ + public function __construct($data) { + parent::__construct($data); + } +} + function () { $i = new a(123); // OK - $i is a<123> @@ -68,3 +81,11 @@ function () { assertType('SelfOut\\a', $i); assertType('*NEVER*', $i->test()); }; + +function () { + $i = new b(123); + assertType('SelfOut\\b', $i); + + $i->addData(321); + assertType('SelfOut\\a', $i); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php index 6106f7779f..96ea886257 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -20,9 +20,13 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/incompatible-self-out-type.php'], [ [ - 'Out type int is not compatible with IncompatibleSelfOutType\A.', + 'Self-out type int of method IncompatibleSelfOutType\A::three is not subtype of IncompatibleSelfOutType\A.', 23, ], + [ + 'Self-out type IncompatibleSelfOutType\A|null of method IncompatibleSelfOutType\A::four is not subtype of IncompatibleSelfOutType\A.', + 28, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php index 857796daf1..a0c4e977a0 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -21,4 +21,9 @@ public function two($param); * @phpstan-self-out int */ public function three(); + + /** + * @phpstan-self-out self|null + */ + public function four(); }