Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions src/Analyser/ArgumentsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,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) {
Expand Down Expand Up @@ -531,13 +525,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) {
Expand Down Expand Up @@ -1230,7 +1219,7 @@ private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflec
/**
* @param FunctionReflection|MethodReflection|null $calleeReflection
*/
private function resolveClosureThisType(
private function resolveClosureThisTypeFromExtensions(
?CallLike $call,
$calleeReflection,
ParameterReflection $parameter,
Expand Down Expand Up @@ -1269,11 +1258,52 @@ private function resolveClosureThisType(
}
}

if ($parameter instanceof ExtendedParameterReflection) {
return $parameter->getClosureThisType();
return null;
}

/**
* A closure-this type from an extension also binds the closure scope;
* from @param-closure-this it binds only $this, the scope comes from @param-closure-scope.
*
* @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];
}

return null;
$closureThisType = null;
$closureScopeType = $parameter->getClosureScopeType();
if (!$isStatic) {
$closureThisType = $this->resolveClosureThisTypeFromExtensions($call, $calleeReflection, $parameter, $scopeToPass);
if ($closureThisType !== null) {
$closureScopeType ??= $closureThisType;
} else {
$closureThisType = $parameter->getClosureThisType();
}
}

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];
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,7 @@ public function enterTrait(ClassReflection $traitReflection): self
* @param array<string, Type> $phpDocClosureThisTypeParameters
* @param array<string, bool> $phpDocPureUnlessCallableIsImpureParameters
* @param array<string, bool> $phpDocPureUnlessParameterPassedParameters
* @param array<string, Type> $phpDocClosureScopeTypeParameters
*/
public function enterClassMethod(
Node\Stmt\ClassMethod $classMethod,
Expand All @@ -1921,6 +1922,7 @@ public function enterClassMethod(
?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
array $phpDocPureUnlessCallableIsImpureParameters = [],
array $phpDocPureUnlessParameterPassedParameters = [],
array $phpDocClosureScopeTypeParameters = [],
): self
{
if (!$this->isInClass()) {
Expand Down Expand Up @@ -1958,6 +1960,7 @@ public function enterClassMethod(
$this->services->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
$phpDocPureUnlessCallableIsImpureParameters,
$phpDocPureUnlessParameterPassedParameters,
array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureScopeTypeParameters),
),
!$classMethod->isStatic(),
);
Expand Down Expand Up @@ -2128,6 +2131,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func
* @param array<string, Type> $phpDocClosureThisTypeParameters
* @param array<string, bool> $pureUnlessCallableIsImpureParameters
* @param array<string, bool> $pureUnlessParameterPassedParameters
* @param array<string, Type> $phpDocClosureScopeTypeParameters
*/
public function enterFunction(
Node\Stmt\Function_ $function,
Expand All @@ -2147,6 +2151,7 @@ public function enterFunction(
array $phpDocClosureThisTypeParameters = [],
array $pureUnlessCallableIsImpureParameters = [],
array $pureUnlessParameterPassedParameters = [],
array $phpDocClosureScopeTypeParameters = [],
): self
{
return $this->enterFunctionLike(
Expand Down Expand Up @@ -2174,6 +2179,7 @@ public function enterFunction(
$this->services->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
$phpDocClosureScopeTypeParameters,
),
false,
);
Expand Down
15 changes: 13 additions & 2 deletions src/Analyser/PhpDocsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ public function __construct(
}

/**
* @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array<string, bool>, array<string, bool>}
* @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array<string, bool>, array<string, bool>, array<string, Type>}
*/
public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
{
$templateTypeMap = TemplateTypeMap::createEmpty();
$phpDocParameterTypes = [];
$phpDocImmediatelyInvokedCallableParameters = [];
$phpDocClosureThisTypeParameters = [];
$phpDocClosureScopeTypeParameters = [];
$phpDocReturnType = null;
$phpDocThrowType = null;
$deprecatedDescription = null;
Expand Down Expand Up @@ -184,6 +185,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();
Expand Down Expand Up @@ -236,7 +247,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, $phpDocPureUnlessParameterPassedParameters];
return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters, $phpDocPureUnlessParameterPassedParameters, $phpDocClosureScopeTypeParameters];
}

private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/StmtHandler/ClassMethodHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function processStmt(
{
$dependencies = [];
$dependencies[] = $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters, $phpDocClosureScopeTypeParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt);

$dependencies[] = $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback);

Expand Down Expand Up @@ -113,6 +113,7 @@ public function processStmt(
null,
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
$phpDocClosureScopeTypeParameters,
);

if (!$scope->isInClass()) {
Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/StmtHandler/FunctionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function processStmt(
{
$dependencies = [];
$dependencies[] = $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters, $phpDocClosureScopeTypeParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt);

$dependencies[] = $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback);

Expand Down Expand Up @@ -97,6 +97,7 @@ public function processStmt(
$phpDocClosureThisTypeParameters,
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
$phpDocClosureScopeTypeParameters,
);
$functionReflection = $functionScope->getFunction();
if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
Expand Down
2 changes: 2 additions & 0 deletions src/Dependency/DependencyTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static function ofCalledParameters(array $parameters): array

$types[] = $parameter->getOutType();
$types[] = $parameter->getClosureThisType();
$types[] = $parameter->getClosureScopeType();
}

return $types;
Expand Down Expand Up @@ -141,6 +142,7 @@ public static function ofDeclaration(PhpFunctionFromParserNodeReflection $reflec
$types[] = $parameter->getPhpDocType();
$types[] = $parameter->getOutType();
$types[] = $parameter->getClosureThisType();
$types[] = $parameter->getClosureScopeType();
}
$types[] = $reflection->getNativeReturnType();
$types[] = $reflection->getPhpDocReturnType();
Expand Down
22 changes: 22 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -440,6 +441,27 @@ public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $n
return $closureThisTypes;
}

/**
* @return array<string, ParamClosureScopeTag>
*/
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;
Expand Down
67 changes: 67 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,6 +109,9 @@ final class ResolvedPhpDocBlock
/** @var array<string, ParamClosureThisTag>|false */
private array|false $paramClosureThisTags = false;

/** @var array<string, ParamClosureScopeTag>|false */
private array|false $paramClosureScopeTags = false;

private ReturnTag|false|null $returnTag = false;

private ThrowsTag|false|null $throwsTag = false;
Expand Down Expand Up @@ -232,6 +236,7 @@ public static function createEmpty(): self
$self->paramsPureUnlessCallableIsImpure = [];
$self->paramsPureUnlessParameterPassed = [];
$self->paramClosureThisTags = [];
$self->paramClosureScopeTags = [];
$self->returnTag = null;
$self->throwsTag = null;
$self->mixinTags = [];
Expand Down Expand Up @@ -290,6 +295,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $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, $parameterMapping);
$result->mixinTags = $this->getMixinTags();
Expand Down Expand Up @@ -376,6 +382,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);
Expand Down Expand Up @@ -413,6 +430,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;
Expand Down Expand Up @@ -640,6 +658,21 @@ public function getParamClosureThisTags(): array
return $this->paramClosureThisTags;
}

/**
* @return array<string, ParamClosureScopeTag>
*/
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)) {
Expand Down Expand Up @@ -1223,6 +1256,40 @@ private static function mergeOneParentParamClosureThisTag(array $paramsClosureTh
return $paramsClosureThisTags;
}

/**
* @param array<string, ParamClosureScopeTag> $paramsClosureScopeTags
* @return array<string, ParamClosureScopeTag>
*/
private static function mergeParamClosureScopeTags(array $paramsClosureScopeTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array
{
return self::mergeOneParentParamClosureScopeTag($paramsClosureScopeTags, $parent, $parameterMapping, $parentClass);
}

/**
* @param array<string, ParamClosureScopeTag> $paramsClosureScopeTags
* @return array<string, ParamClosureScopeTag>
*/
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) {
Expand Down
Loading
Loading