diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9a90cb0cd19..1b18a785303 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1656,6 +1656,7 @@ public function enterTrait(ClassReflection $traitReflection): self * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param array $phpDocPureUnlessCallableIsImpureParameters + * @param array $phpDocClosureScopeTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -1678,6 +1679,7 @@ public function enterClassMethod( bool $isConstructor = false, ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null, array $phpDocPureUnlessCallableIsImpureParameters = [], + array $phpDocClosureScopeTypeParameters = [], ): self { if (!$this->isInClass()) { @@ -1714,6 +1716,7 @@ public function enterClassMethod( $isConstructor, $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)), $phpDocPureUnlessCallableIsImpureParameters, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureScopeTypeParameters), ), !$classMethod->isStatic(), ); @@ -1881,6 +1884,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param array $pureUnlessCallableIsImpureParameters + * @param array $phpDocClosureScopeTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -1899,6 +1903,7 @@ public function enterFunction( array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], array $pureUnlessCallableIsImpureParameters = [], + array $phpDocClosureScopeTypeParameters = [], ): self { return $this->enterFunctionLike( @@ -1925,6 +1930,7 @@ public function enterFunction( $phpDocClosureThisTypeParameters, $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)), $pureUnlessCallableIsImpureParameters, + $phpDocClosureScopeTypeParameters, ), false, ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 90e7657c837..67d71dd79c4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1797,6 +1797,43 @@ private function resolveClosureThisType( return null; } + /** + * @param FunctionReflection|MethodReflection|null $calleeReflection + * @return array{MutatingScope, MutatingScope|null} + */ + private function applyClosureThisAndScope( + MutatingScope $scopeToPass, + ?CallLike $call, + $calleeReflection, + ParameterReflection $parameter, + bool $isStatic, + ): array + { + if (!$parameter instanceof ExtendedParameterReflection) { + return [$scopeToPass, null]; + } + + $closureThisType = null; + if (!$isStatic) { + $closureThisType = $this->resolveClosureThisType($call, $calleeReflection, $parameter, $scopeToPass); + } + $closureScopeType = $parameter->getClosureScopeType(); + + if ($closureThisType === null && $closureScopeType === null) { + return [$scopeToPass, null]; + } + + $restoreThisScope = $scopeToPass; + if ($closureThisType !== null) { + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } + if ($closureScopeType !== null) { + $scopeToPass = $scopeToPass->withClosureBindScopeClasses($closureScopeType->getObjectClassNames()); + } + + return [$scopeToPass, $restoreThisScope]; + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection * @param ParametersAcceptor[] $parametersAcceptors @@ -2039,14 +2076,8 @@ public function processArgs( if ( $closureBindScopeFactory === null && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } + [$scopeToPass, $restoreThisScope] = $this->applyClosureThisAndScope($scopeToPass, $callLike, $calleeReflection, $parameter, $arg->value->static); } if ($parameter !== null) { @@ -2131,13 +2162,8 @@ public function processArgs( if ( $closureBindScopeFactory === null && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } + [$scopeToPass] = $this->applyClosureThisAndScope($scopeToPass, $callLike, $calleeReflection, $parameter, $arg->value->static); } if ($parameter !== null) { diff --git a/src/Analyser/PhpDocsResolver.php b/src/Analyser/PhpDocsResolver.php index abdbeeb2e55..bda21218344 100644 --- a/src/Analyser/PhpDocsResolver.php +++ b/src/Analyser/PhpDocsResolver.php @@ -40,7 +40,7 @@ public function __construct( } /** - * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array, array} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -48,6 +48,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $phpDocParameterTypes = []; $phpDocImmediatelyInvokedCallableParameters = []; $phpDocClosureThisTypeParameters = []; + $phpDocClosureScopeTypeParameters = []; $phpDocReturnType = null; $phpDocThrowType = null; $deprecatedDescription = null; @@ -181,6 +182,16 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; } + foreach ($resolvedPhpDoc->getParamClosureScopeTags() as $paramName => $paramClosureScopeTag) { + if (array_key_exists($paramName, $phpDocClosureScopeTypeParameters)) { + continue; + } + $paramClosureScopeType = $paramClosureScopeTag->getType(); + if ($scope->isInClass()) { + $paramClosureScopeType = $this->transformStaticType($scope->getClassReflection(), $paramClosureScopeType); + } + $phpDocClosureScopeTypeParameters[$paramName] = $paramClosureScopeType; + } foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); @@ -228,7 +239,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters, $phpDocClosureScopeTypeParameters]; } private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index 7a3aaaa654a..5f45dd46e78 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -66,7 +66,7 @@ public function processStmt( ): InternalStatementResult { $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $phpDocClosureScopeTypeParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); @@ -104,6 +104,7 @@ public function processStmt( $isConstructor, null, $pureUnlessCallableIsImpureParameters, + $phpDocClosureScopeTypeParameters, ); if (!$scope->isInClass()) { diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 3dd52154152..784c4e5799c 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -57,7 +57,7 @@ public function processStmt( ): InternalStatementResult { $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $phpDocClosureScopeTypeParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); @@ -88,6 +88,7 @@ public function processStmt( $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $pureUnlessCallableIsImpureParameters, + $phpDocClosureScopeTypeParameters, ); $functionReflection = $functionScope->getFunction(); if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index caaa2117b07..457720cb50f 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -190,11 +190,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getClosureThisType() !== null) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } } @@ -229,11 +233,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getClosureThisType() !== null) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } } @@ -267,11 +275,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getClosureThisType() !== null) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } } @@ -334,18 +346,22 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); foreach ($methodReflection->getVariants() as $methodVariant) { foreach ($methodVariant->getParameters() as $parameter) { - if ($parameter->getOutType() !== null) { - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); - } - } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + } + if ($parameter->getClosureThisType() !== null) { foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } } } } @@ -360,11 +376,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getClosureThisType() !== null) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } } @@ -838,11 +858,15 @@ private function extractFromParametersAcceptor( $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - if ($parameter->getClosureThisType() === null) { - continue; + if ($parameter->getClosureThisType() !== null) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } - foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + if ($parameter->getClosureScopeType() !== null) { + foreach ($parameter->getClosureScopeType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 2304edc1490..54465763dd7 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -12,6 +12,7 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MethodTagParameter; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureScopeTag; use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; @@ -47,7 +48,6 @@ use function array_reverse; use function count; use function in_array; -use function method_exists; use function str_starts_with; use function substr; @@ -424,6 +424,27 @@ public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $n return $closureThisTypes; } + /** + * @return array + */ + public function resolveParamClosureScopeTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $closureScopeTypes = []; + foreach (['@param-closure-scope', '@phpstan-param-closure-scope'] as $tagName) { + foreach ($phpDocNode->getParamClosureScopeTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $closureScopeTypes[$parameterName] = new ParamClosureScopeTag( + TypeCombinator::intersect( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + new ObjectWithoutClassType(), + ), + ); + } + } + + return $closureScopeTypes; + } + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 12d3dcec1ff..3a9ef58888c 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -9,6 +9,7 @@ use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureScopeTag; use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; @@ -103,6 +104,9 @@ final class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramClosureThisTags = false; + /** @var array|false */ + private array|false $paramClosureScopeTags = false; + private ReturnTag|false|null $returnTag = false; private ThrowsTag|false|null $throwsTag = false; @@ -226,6 +230,7 @@ public static function createEmpty(): self $self->paramsImmediatelyInvokedCallable = []; $self->paramsPureUnlessCallableIsImpure = []; $self->paramClosureThisTags = []; + $self->paramClosureScopeTags = []; $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; @@ -283,6 +288,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping); $result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping); $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass); + $result->paramClosureScopeTags = self::mergeParamClosureScopeTags($this->getParamClosureScopeTags(), $parent, $parameterMapping, $parentClass); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent); $result->mixinTags = $this->getMixinTags(); @@ -369,6 +375,17 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $newParamClosureThisTags[$parameterNameMapping[$key]] = $paramClosureThisTag->withType($transformedType); } + $paramClosureScopeTags = $this->getParamClosureScopeTags(); + $newParamClosureScopeTags = []; + foreach ($paramClosureScopeTags as $key => $paramClosureScopeTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramClosureScopeTag->getType(), $mapParameterCb); + $newParamClosureScopeTags[$parameterNameMapping[$key]] = $paramClosureScopeTag->withType($transformedType); + } + $returnTag = $this->getReturnTag(); if ($returnTag !== null) { $transformedType = TypeTraverser::map($returnTag->getType(), $mapParameterCb); @@ -406,6 +423,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->paramOutTags = $newParamOutTags; $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable; $self->paramClosureThisTags = $newParamClosureThisTags; + $self->paramClosureScopeTags = $newParamClosureScopeTags; $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; @@ -621,6 +639,21 @@ public function getParamClosureThisTags(): array return $this->paramClosureThisTags; } + /** + * @return array + */ + public function getParamClosureScopeTags(): array + { + if ($this->paramClosureScopeTags === false) { + $this->paramClosureScopeTags = $this->phpDocNodeResolver->resolveParamClosureScopeTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->paramClosureScopeTags; + } + public function getReturnTag(): ?ReturnTag { if (is_bool($this->returnTag)) { @@ -1169,6 +1202,40 @@ private static function mergeOneParentParamClosureThisTag(array $paramsClosureTh return $paramsClosureThisTags; } + /** + * @param array $paramsClosureScopeTags + * @return array + */ + private static function mergeParamClosureScopeTags(array $paramsClosureScopeTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array + { + return self::mergeOneParentParamClosureScopeTag($paramsClosureScopeTags, $parent, $parameterMapping, $parentClass); + } + + /** + * @param array $paramsClosureScopeTags + * @return array + */ + private static function mergeOneParentParamClosureScopeTag(array $paramsClosureScopeTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array + { + $parentClosureScopeTags = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamClosureScopeTags()); + + foreach ($parentClosureScopeTags as $name => $parentParamClosureScopeTag) { + if (array_key_exists($name, $paramsClosureScopeTags)) { + continue; + } + + $paramsClosureScopeTags[$name] = self::resolveTemplateTypeInTag( + $parentParamClosureScopeTag->withType( + $parameterMapping->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureScopeTag->getType()), + ), + $parentClass, + TemplateTypeVariance::createContravariant(), + ); + } + + return $paramsClosureScopeTags; + } + private static function mergePureTags(?bool $isPure, self $parent): ?bool { if ($isPure !== null) { diff --git a/src/PhpDoc/Tag/ParamClosureScopeTag.php b/src/PhpDoc/Tag/ParamClosureScopeTag.php new file mode 100644 index 00000000000..95cc612cf94 --- /dev/null +++ b/src/PhpDoc/Tag/ParamClosureScopeTag.php @@ -0,0 +1,29 @@ +type; + } + + public function withType(Type $type): self + { + return new self($type); + } + +} diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 60849ddc98e..964ed3242f8 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -62,6 +62,11 @@ public function getClosureThisType(): ?Type return null; } + public function getClosureScopeType(): ?Type + { + return null; + } + public function passedByReference(): PassedByReference { return $this->passedByReference; diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index ac322c0081f..d71cb834c48 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -31,6 +31,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\Tag\ParamClosureScopeTag; use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\Reflection\Assertions; @@ -295,6 +296,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterOutTags = []; $phpDocParameterImmediatelyInvokedCallable = []; $phpDocParameterClosureThisTypeTags = []; + $phpDocParameterClosureScopeTypeTags = []; $phpDocParameterPureUnlessCallableIsImpure = []; $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); @@ -322,6 +324,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); + $phpDocParameterClosureScopeTypeTags = $resolvedPhpDoc->getParamClosureScopeTags(); $phpDocParameterPureUnlessCallableIsImpure = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure(); } @@ -356,6 +359,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), $this->attributeReflectionFactory->fromNativeReflection($reflectionFunction->getAttributes(), InitializerExprContext::fromFunction($reflectionFunction->getName(), $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null)), $phpDocParameterPureUnlessCallableIsImpure, + array_map(static fn (ParamClosureScopeTag $tag): Type => $tag->getType(), $phpDocParameterClosureScopeTypeTags), ); } diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index 3f2df446330..f85c07b1dbe 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -24,6 +24,8 @@ public function isImmediatelyInvokedCallable(): TrinaryLogic; public function getClosureThisType(): ?Type; + public function getClosureScopeType(): ?Type; + /** * @return list */ diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 58224858714..3b9436f75c6 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -17,6 +17,7 @@ interface FunctionReflectionFactory * @param array $phpDocParameterClosureThisTypes * @param list $attributes * @param array $phpDocParameterPureUnlessCallableIsImpure + * @param array $phpDocParameterClosureScopeTypes */ public function create( ReflectionFunction $reflection, @@ -37,6 +38,7 @@ public function create( array $phpDocParameterClosureThisTypes, array $attributes, array $phpDocParameterPureUnlessCallableIsImpure, + array $phpDocParameterClosureScopeTypes = [], ): PhpFunctionReflection; } diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index d4957240fd1..44a6659a2d4 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -32,6 +32,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private ?Type $closureScopeType = null, ) { } @@ -96,6 +97,11 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function getClosureScopeType(): ?Type + { + return $this->closureScopeType; + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index d3d1773f802..842298ff04e 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -611,6 +611,14 @@ public static function hasAcceptorTemplateOrLateResolvableParameterType(Paramete return true; } + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getClosureScopeType() !== null + && $parameter->getClosureScopeType()->hasTemplateOrLateResolvableType() + ) { + return true; + } + if (!$parameter->getType()->hasTemplateOrLateResolvableType()) { continue; } @@ -842,6 +850,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], $parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null, $parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessCallableIsImpureParameter() : TrinaryLogic::createNo(), + $parameter instanceof ExtendedParameterReflection ? $parameter->getClosureScopeType() : null, ); continue; } @@ -861,6 +870,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $outType = $parameters[$i]->getOutType(); $immediatelyInvokedCallable = $parameters[$i]->isImmediatelyInvokedCallable(); $closureThisType = $parameters[$i]->getClosureThisType(); + $closureScopeType = $parameters[$i]->getClosureScopeType(); $attributes = $parameters[$i]->getAttributes(); if ($parameter instanceof ExtendedParameterReflection) { $nativeType = TypeCombinator::union($nativeType, $parameter->getNativeType()); @@ -878,6 +888,12 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $closureThisType = null; } + if ($parameter->getClosureScopeType() !== null && $closureScopeType !== null) { + $closureScopeType = TypeCombinator::union($closureScopeType, $parameter->getClosureScopeType()); + } else { + $closureScopeType = null; + } + $immediatelyInvokedCallable = $parameter->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable); $attributes = array_merge($attributes, $parameter->getAttributes()); } else { @@ -886,6 +902,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $outType = null; $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); $closureThisType = null; + $closureScopeType = null; } $allowedConstants = $parameters[$i]->getAllowedConstants(); @@ -915,6 +932,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $attributes, $allowedConstants, $pureUnlessCallableIsImpureParameter, + $closureScopeType, ); if ($isVariadic) { @@ -1375,6 +1393,7 @@ private static function overrideParameterType(ParameterReflection $original, Typ $wrapped->getAttributes(), $wrapped->getAllowedConstants(), $wrapped->isPureUnlessCallableIsImpureParameter(), + $wrapped->getClosureScopeType(), ); } diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index adf1315738f..52b83d09636 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -32,6 +32,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private ?Type $closureScopeType = null, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -67,6 +68,11 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function getClosureScopeType(): ?Type + { + return $this->closureScopeType; + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 82863c68cc4..f4b3ea42ac6 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -657,6 +657,7 @@ private function createMethod( $phpDocParameterOutTypes = []; $immediatelyInvokedCallableParameters = []; $closureThisParameters = []; + $closureScopeParameters = []; $currentResolvedPhpDoc = null; $phpDocDeclaringClass = $declaringClass; $phpDocFromStubs = false; @@ -700,6 +701,7 @@ private function createMethod( } $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $currentResolvedPhpDoc->getParamClosureThisTags()); + $closureScopeParameters = array_map(static fn ($tag) => $tag->getType(), $currentResolvedPhpDoc->getParamClosureScopeTags()); foreach ($currentResolvedPhpDoc->getParamTags() as $name => $paramTag) { $phpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( $paramTag->getType(), @@ -747,7 +749,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($declaringClassName, $methodReflection->getName(), $methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($declaringClassName, $methodReflection->getName(), $methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named', $closureScopeParameters); } } @@ -903,6 +905,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $templateTypeMap = TemplateTypeMap::createEmpty(); $immediatelyInvokedCallableParameters = []; $closureThisParameters = []; + $closureScopeParameters = []; $phpDocThrowType = null; $isInternal = false; $isFinal = false; @@ -914,6 +917,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); + $closureScopeParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureScopeTags()); foreach ($resolvedPhpDoc->getParamsPureUnlessCallableIsImpure() as $paramName => $isPureUnlessCallableIsImpure) { $pureUnlessCallableIsImpureParameters[$paramName] = $isPureUnlessCallableIsImpure; } @@ -997,6 +1001,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $acceptsNamedArguments, $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), $pureUnlessCallableIsImpureParameters, + $closureScopeParameters, ); } @@ -1006,6 +1011,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla * @param array $phpDocParameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $closureThisParameters + * @param array $closureScopeParameters */ private function createNativeMethodVariant( string $declaringClassName, @@ -1019,6 +1025,7 @@ private function createNativeMethodVariant( array $closureThisParameters, bool $phpDocFromStubs, bool $usePhpDocParameterNames, + array $closureScopeParameters = [], ): ExtendedFunctionVariant { $parameters = []; @@ -1049,6 +1056,11 @@ private function createNativeMethodVariant( $closureThisType = $closureThisParameters[$phpDocParameterName]; } + $closureScopeType = null; + if (isset($closureScopeParameters[$phpDocParameterName])) { + $closureScopeType = $closureScopeParameters[$phpDocParameterName]; + } + $parameters[] = new ExtendedNativeParameterReflection( $usePhpDocParameterNames ? $phpDocParameterName @@ -1068,6 +1080,7 @@ private function createNativeMethodVariant( // pure-unless-callable-is-impure is not threaded here because no built-in method // carries it (there are no Class::method entries in functionMetadata.php). TrinaryLogic::createNo(), + $closureScopeType, ); } @@ -1175,7 +1188,7 @@ private function inferAndCachePropertyTypes( $classScope = $classScope->enterNamespace($namespace); } $classScope = $classScope->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters, $phpDocClosureScopeTypeParameters] = $this->phpDocsResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -1197,6 +1210,7 @@ private function inferAndCachePropertyTypes( false, null, $phpDocPureUnlessCallableIsImpureParameters, + $phpDocClosureScopeTypeParameters, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index ef46fe63ac1..018815abfb3 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -50,6 +50,7 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection, Extende * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $phpDocClosureScopeTypeParameters */ public function __construct( FunctionLike $functionLike, @@ -74,6 +75,7 @@ public function __construct( private array $phpDocClosureThisTypeParameters, private array $attributes, private array $pureUnlessCallableIsImpureParameters, + private array $phpDocClosureScopeTypeParameters = [], ) { $this->functionLike = $functionLike; @@ -180,6 +182,12 @@ public function getParameters(): array $closureThisType = null; } + if (isset($this->phpDocClosureScopeTypeParameters[$parameter->var->name])) { + $closureScopeType = $this->phpDocClosureScopeTypeParameters[$parameter->var->name]; + } else { + $closureScopeType = null; + } + $pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$parameter->var->name] ?? false); $parameters[] = new PhpParameterFromParserNodeReflection( @@ -197,6 +205,7 @@ public function getParameters(): array $closureThisType, $this->parameterAttributes[$parameter->var->name] ?? [], $pureUnlessCallableIsImpureParameter, + $closureScopeType, ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 15afca5bd98..fcb4f501b5e 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -41,6 +41,7 @@ final class PhpFunctionReflection implements FunctionReflection * @param array $phpDocParameterClosureThisTypes * @param list $attributes * @param array $phpDocParameterPureUnlessCallableIsImpure + * @param array $phpDocParameterClosureScopeTypes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -64,6 +65,7 @@ public function __construct( private array $phpDocParameterClosureThisTypes, private array $attributes, private array $phpDocParameterPureUnlessCallableIsImpure, + private array $phpDocParameterClosureScopeTypes = [], ) { } @@ -133,6 +135,7 @@ private function getParameters(): array $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), $this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()), TrinaryLogic::createFromBoolean($this->phpDocParameterPureUnlessCallableIsImpure[$reflection->getName()] ?? false), + $this->phpDocParameterClosureScopeTypes[$reflection->getName()] ?? null, ); }, $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 290e0cb4b76..8048f61e178 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -43,6 +43,7 @@ final class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeR * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param list $attributes + * @param array $phpDocClosureScopeTypeParameters */ public function __construct( private ClassReflection $declaringClass, @@ -73,6 +74,7 @@ public function __construct( private bool $isConstructor, array $attributes, array $pureUnlessCallableIsImpureParameters, + array $phpDocClosureScopeTypeParameters = [], ) { if ($this->classMethod instanceof Node\PropertyHook) { @@ -140,6 +142,7 @@ public function __construct( $phpDocClosureThisTypeParameters, $attributes, $pureUnlessCallableIsImpureParameters, + $phpDocClosureScopeTypeParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index e75049d69ef..ad09396090c 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -64,6 +64,7 @@ final class PhpMethodReflection implements ExtendedMethodReflection * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $phpDocClosureScopeTypeParameters */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -92,6 +93,7 @@ public function __construct( private array $phpDocClosureThisTypeParameters, private array $attributes, private array $pureUnlessCallableIsImpureParameters, + private array $phpDocClosureScopeTypeParameters = [], ) { } @@ -232,6 +234,7 @@ private function getParameters(): array $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), $this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()), TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$reflection->getName()] ?? false), + $this->phpDocClosureScopeTypeParameters[$reflection->getName()] ?? null, ), $this->reflection->getParameters()); } @@ -445,6 +448,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->phpDocClosureThisTypeParameters, $this->attributes, $this->pureUnlessCallableIsImpureParameters, + $this->phpDocClosureScopeTypeParameters, ); } @@ -480,6 +484,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->phpDocClosureThisTypeParameters, $this->attributes, $this->pureUnlessCallableIsImpureParameters, + $this->phpDocClosureScopeTypeParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index c2978529d86..cf76c316623 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -21,6 +21,7 @@ interface PhpMethodReflectionFactory * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $phpDocClosureScopeTypeParameters */ public function create( ClassReflection $declaringClass, @@ -45,6 +46,7 @@ public function create( bool $acceptsNamedArguments, array $attributes, array $pureUnlessCallableIsImpureParameters, + array $phpDocClosureScopeTypeParameters = [], ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 754f6622c2a..0ba0a7849a1 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -34,6 +34,7 @@ public function __construct( private ?Type $closureThisType, private array $attributes, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private ?Type $closureScopeType = null, ) { } @@ -111,6 +112,11 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function getClosureScopeType(): ?Type + { + return $this->closureScopeType; + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 75365f543ef..b632a6ca029 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -38,6 +38,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private ?Type $closureScopeType = null, ) { } @@ -142,6 +143,11 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function getClosureScopeType(): ?Type + { + return $this->closureScopeType; + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 0151110214b..2c6f6599e75 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -108,6 +108,19 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { ); } + $closureScopeType = $param->getClosureScopeType(); + if ($closureScopeType !== null) { + $closureScopeType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($closureScopeType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + return new ExtendedDummyParameter( $param->getName(), $paramType, @@ -123,6 +136,7 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { $param->getAttributes(), $param->getAllowedConstants(), $param->isPureUnlessCallableIsImpureParameter(), + $closureScopeType, ); }, $this->parametersAcceptor->getParameters(), diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 6b40422c1d4..2e5f129ac8c 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -133,6 +133,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $phpDocType = null; $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); $closureThisType = null; + $closureScopeType = null; $pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($pureUnlessCallableIsImpureParameters[$name] ?? false); if ($phpDoc !== null) { if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) { @@ -144,6 +145,9 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamClosureThisTags())) { $closureThisType = $phpDoc->getParamClosureThisTags()[$parameterSignature->getName()]->getType(); } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamClosureScopeTags())) { + $closureScopeType = $phpDoc->getParamClosureScopeTags()[$parameterSignature->getName()]->getType(); + } if (($phpDoc->getParamsPureUnlessCallableIsImpure()[$parameterSignature->getName()] ?? false) === true) { $pureUnlessCallableIsImpureParameter = TrinaryLogic::createYes(); } @@ -164,6 +168,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef [], $allowedConstantsMapProvider->getForFunctionParameter($lowerCasedFunctionName, $parameterSignature->getName()), $pureUnlessCallableIsImpureParameter, + $closureScopeType, ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 72b5def04fe..77125a04695 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -120,6 +120,7 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->getClosureScopeType() !== null ? $this->transformStaticType($parameter->getClosureScopeType()) : null, ); }, $acceptor->getParameters(), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 8e6099d8886..09252b670fb 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -107,6 +107,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->getClosureScopeType() !== null ? $this->transformStaticType($parameter->getClosureScopeType()) : null, ), $acceptor->getParameters(), ), diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index c1666ebdacd..45e3cfa7b40 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -754,6 +754,9 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): if ($parameter->getClosureThisType() !== null) { $moreClasses = array_merge($moreClasses, $parameter->getClosureThisType()->getReferencedClasses()); } + if ($parameter->getClosureScopeType() !== null) { + $moreClasses = array_merge($moreClasses, $parameter->getClosureScopeType()->getReferencedClasses()); + } return array_merge( $parameter->getNativeType()->getReferencedClasses(), diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 4476427835c..e147202f5d6 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -52,6 +52,12 @@ public function processNode(Node $node, Scope $scope): array } } + if ($parameterReflection->getClosureScopeType() !== null) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-closure-scope PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureScopeType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + if ($parameterReflection->getOutType() === null) { continue; } diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index a0a5cd3b936..3cfcfe42a02 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -52,6 +52,12 @@ public function processNode(Node $node, Scope $scope): array } } + if ($parameterReflection->getClosureScopeType() !== null) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-closure-scope PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureScopeType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + if ($parameterReflection->getOutType() === null) { continue; } diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php index af7cfda1556..3abd20dd737 100644 --- a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -58,6 +58,16 @@ public function check(ExtendedParametersAcceptor $acceptor): array }); } + if ($parameter->getClosureScopeType() !== null) { + TypeTraverser::map($parameter->getClosureScopeType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + $parametersByName[$parameter->getName()] = $parameter; } diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php index 7e2963cf1a0..214cc4775d7 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeCheck.php @@ -49,7 +49,7 @@ public function check( { $errors = []; - foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags()] as $tagName => $parameters) { + foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags(), '@param-closure-scope' => $resolvedPhpDoc->getParamClosureScopeTags()] as $tagName => $parameters) { foreach ($parameters as $parameterName => $phpDocParamTag) { $phpDocParamType = $phpDocParamTag->getType(); $unresolvableType = $this->unresolvableTypeHelper->getUnresolvableType($phpDocParamType); @@ -169,7 +169,7 @@ public function check( } } - if ($tagName === '@param-closure-this') { + if ($tagName === '@param-closure-this' || $tagName === '@param-closure-scope') { $isNonClosure = (new ClosureType())->isSuperTypeOf($nativeParamType)->no(); if ($isNonClosure) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -177,7 +177,7 @@ public function check( $tagName, $parameterName, $nativeParamType->describe(VerbosityLevel::typeOnly()), - ))->identifier('paramClosureThis.nonClosure')->build(); + ))->identifier($tagName === '@param-closure-this' ? 'paramClosureThis.nonClosure' : 'paramClosureScope.nonClosure')->build(); } } } diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index fcd5702fafe..2769ec63178 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -64,6 +64,7 @@ final class InvalidPHPStanDocTagRule implements Rule '@phpstan-param-immediately-invoked-callable', '@phpstan-param-later-invoked-callable', '@phpstan-param-closure-this', + '@phpstan-param-closure-scope', '@phpstan-all-methods-pure', '@phpstan-all-methods-impure', ]; diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 3b445ac27c4..442cba8e81a 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -864,6 +864,9 @@ public function hasTemplateOrLateResolvableType(): bool if ($parameter->getClosureThisType() !== null && $parameter->getClosureThisType()->hasTemplateOrLateResolvableType()) { return true; } + if ($parameter->getClosureScopeType() !== null && $parameter->getClosureScopeType()->hasTemplateOrLateResolvableType()) { + return true; + } } foreach ($this->assertions->getAll() as $assertTag) { diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 14516ffee7a..209b90aade7 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -997,6 +997,9 @@ public function hasTemplateOrLateResolvableType(): bool if ($parameter->getClosureThisType() !== null && $parameter->getClosureThisType()->hasTemplateOrLateResolvableType()) { return true; } + if ($parameter->getClosureScopeType() !== null && $parameter->getClosureScopeType()->hasTemplateOrLateResolvableType()) { + return true; + } } foreach ($this->assertions->getAll() as $assertTag) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-15197.php b/tests/PHPStan/Analyser/nsrt/bug-15197.php new file mode 100644 index 00000000000..b20276eeba7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15197.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug15197; + +use function PHPStan\Testing\assertType; + +interface DiInterface +{ + /** + * @param-closure-this DiInterface $definition + */ + public function set(string $name, mixed $definition): void; + + public function get(string $service): object; +} + +class MyServiceProvider +{ + const SERVICE_FOO = 'foo'; + + public function provide(DiInterface $di): void { + $di->set('foobar', function () { + assertType(DiInterface::class, $this); + assertType("'foo'", self::SERVICE_FOO); + assertType('object', $this->get(self::SERVICE_FOO)); + }); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/param-closure-this.php b/tests/PHPStan/Analyser/nsrt/param-closure-this.php index 6486cf0952a..9ff24163935 100644 --- a/tests/PHPStan/Analyser/nsrt/param-closure-this.php +++ b/tests/PHPStan/Analyser/nsrt/param-closure-this.php @@ -334,6 +334,7 @@ class StaticReturnClosureThis /** * @param-closure-this FooChild $cb + * @param-closure-scope FooChild $cb */ public function withFooChild(callable $cb): void { @@ -342,6 +343,7 @@ public function withFooChild(callable $cb): void /** * @param-closure-this Foo $cb + * @param-closure-scope Foo $cb */ public function withFoo(callable $cb): void { @@ -388,3 +390,84 @@ public function testNullableStaticReturnType(StaticReturnClosureThis $o): void } } + +class ClosureThisWithoutScope +{ + + public const SERVICE_FOO = 'foo'; + + /** + * @param-closure-this Some $cb + */ + public function paramClosureThisOnly(callable $cb): void + { + + } + + /** + * @param-closure-scope Some $cb + */ + public function paramClosureScopeOnly(callable $cb): void + { + + } + + /** + * @param-closure-this Some $cb + * @param-closure-scope Some $cb + */ + public function paramClosureThisAndScope(callable $cb): void + { + + } + + public function testThisWithoutScope(): void + { + $this->paramClosureThisOnly(function () { + assertType(Some::class, $this); + assertType("'foo'", self::SERVICE_FOO); + }); + + $this->paramClosureThisOnly(fn () => assertType("'foo'", self::SERVICE_FOO)); + } + + public function testScopeWithoutThis(): void + { + $this->paramClosureScopeOnly(function () { + assertType(sprintf('$this(%s)', self::class), $this); + }); + } + + public function testThisAndScope(): void + { + $this->paramClosureThisAndScope(function () { + assertType(Some::class, $this); + }); + } + +} + +class BoundScopeClass +{ + public const X = 'bound'; +} + +class ClosureScopeOnly +{ + public const X = 'enclosing'; + + /** + * @param-closure-scope BoundScopeClass $cb + */ + public function withScope(callable $cb): void + { + } + + public function test(): void + { + $this->withScope(function () { + assertType(sprintf('$this(%s)', ClosureScopeOnly::class), $this); + }); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 2189ca2ceef..989888772a4 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -618,4 +618,15 @@ public function testBug12827Enum(bool $checkImportedClassNameCase): void $this->analyse([__DIR__ . '/data/bug-12827-enum.php'], $expectedErrors); } + public function testBug15197(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-15197.php'], [ + [ + 'Access to undefined constant Bug15197\BoundScope::MISSING.', + 61, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-15197.php b/tests/PHPStan/Rules/Classes/data/bug-15197.php new file mode 100644 index 00000000000..11dff0e1092 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15197.php @@ -0,0 +1,64 @@ += 8.0 + +namespace Bug15197; + +interface DiInterface +{ + /** + * @param-closure-this DiInterface $definition + */ + public function set(string $name, mixed $definition): void; + + public function get(string $service): object; +} + +class MyServiceProvider +{ + const SERVICE_FOO = 'foo'; + + public function provide(DiInterface $di): void { + $di->set('foobar', function () { + return $this->get(self::SERVICE_FOO); + }); + } +} + +class BoundScope +{ + public const X = 'bound'; +} + +class Enclosing +{ + public const X = 'enclosing'; + + /** + * @param-closure-scope BoundScope $cb + */ + public function withScope(callable $cb): void + { + } + + /** + * @param-closure-this BoundScope $cb + * @param-closure-scope BoundScope $cb + */ + public function withThisAndScope(callable $cb): void + { + } + + public function test(): void + { + $this->withScope(function () { + echo self::X; + }); + + $this->withThisAndScope(function () { + echo self::X; + }); + + $this->withScope(function () { + echo self::MISSING; + }); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11010.php b/tests/PHPStan/Rules/Methods/data/bug-11010.php index 07c621cc96c..1e4ee993f65 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-11010.php +++ b/tests/PHPStan/Rules/Methods/data/bug-11010.php @@ -11,6 +11,7 @@ protected function sayHello(): void /** * @param-closure-this self $cb + * @param-closure-scope self $cb */ public static function cb(\Closure $cb): void { diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 808c165f1b2..41c9693aa69 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -454,6 +454,44 @@ public function testParamClosureThis(): void ]); } + public function testParamClosureScope(): void + { + $this->analyse([__DIR__ . '/data/param-closure-scope.php'], [ + [ + 'PHPDoc tag @param-closure-scope references unknown parameter: $b', + 20, + ], + [ + 'PHPDoc tag @param-closure-scope for parameter $i contains unresolvable type.', + 27, + ], + [ + 'PHPDoc tag @param-closure-scope for parameter $i contains unresolvable type.', + 34, + ], + [ + 'PHPDoc tag @param-closure-scope is for parameter $i with non-Closure type string.', + 41, + ], + [ + 'PHPDoc tag @param-closure-scope for parameter $i contains generic type Exception but class Exception is not generic.', + 48, + ], + [ + 'Generic type ParamClosureScopePhpDocRule\FooBar in PHPDoc tag @param-closure-scope for parameter $i does not specify all template types of class ParamClosureScopePhpDocRule\FooBar: T, TT', + 55, + ], + [ + 'Type mixed in generic type ParamClosureScopePhpDocRule\FooBar in PHPDoc tag @param-closure-scope for parameter $i is not subtype of template type T of int of class ParamClosureScopePhpDocRule\FooBar.', + 55, + ], + [ + 'Generic type ParamClosureScopePhpDocRule\FooBar in PHPDoc tag @param-closure-scope for parameter $i does not specify all template types of class ParamClosureScopePhpDocRule\FooBar: T, TT', + 62, + ], + ]); + } + public function testGenericStatic(): void { $this->analyse([__DIR__ . '/data/incompatible-phpdoc-generic-static.php'], [ diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-closure-scope.php b/tests/PHPStan/Rules/PhpDoc/data/param-closure-scope.php new file mode 100644 index 00000000000..10857d1a50c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-closure-scope.php @@ -0,0 +1,72 @@ + $i + */ +function invalidParamClosureScopeGeneric(callable $i) { + +} + +/** + * @param-closure-scope FooBar $i + */ +function invalidParamClosureScopeWrongGenericParams(callable $i) { + +} + +/** + * @param-closure-scope FooBar $i + */ +function invalidParamClosureScopeNotAllGenericParams(callable $i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + +}