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/phpstan-baseline.neon b/phpstan-baseline.neon index 7b7d712ce8..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 @@ -166,6 +171,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..8fab406e27 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; @@ -468,7 +469,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 +492,7 @@ private function processStmtNode( $isPure, $acceptsNamedArguments, $asserts, + $selfOutType, ); if ($stmt->name->toLowerString() === '__construct') { @@ -526,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); @@ -2063,6 +2065,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 +3989,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 +4005,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 +4115,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->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->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/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; } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 8f369bd82e..dda42a4a96 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; @@ -35,6 +36,7 @@ use function array_reverse; use function count; use function in_array; +use function method_exists; use function strpos; use function substr; @@ -451,6 +453,22 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco return $resolved; } + 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); + 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..8f943eca66 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 = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); $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->selfOutTypeTag; $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 getSelfOutTag(): ?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)) { @@ -793,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/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..572dec4a2f 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->getSelfOutTag(); + 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->getSelfOutTag(); + 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->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->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/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php new file mode 100644 index 0000000000..3206b2300b --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -0,0 +1,52 @@ + + */ +class IncompatibleSelfOutTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $selfOutType = $method->getSelfOutType(); + + if ($selfOutType === null) { + return []; + } + + $classReflection = $method->getDeclaringClass(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + if ($classType->isSuperTypeOf($selfOutType)->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + '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/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..39fb2ed6e8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/self-out.php @@ -0,0 +1,91 @@ + + */ + 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 { + } +} + +/** + * @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> + 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()); +}; + +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 new file mode 100644 index 0000000000..96ea886257 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class IncompatibleSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleSelfOutTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-self-out-type.php'], [ + [ + '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 new file mode 100644 index 0000000000..a0c4e977a0 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -0,0 +1,29 @@ + + */ + public function two($param); + + /** + * @phpstan-self-out int + */ + public function three(); + + /** + * @phpstan-self-out self|null + */ + public function four(); +}