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
3 changes: 3 additions & 0 deletions src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,15 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
usedVariables: $cachedClosureData['usedVariables'],
acceptsNamedArguments: TrinaryLogic::createYes(),
mustUseReturnValue: $mustUseReturnValue,
isStatic: TrinaryLogic::createFromBoolean($expr->static),
);
}
if (self::$resolveClosureTypeDepth >= 2) {
return new ClosureType(
$parameters,
$scope->getFunctionType($expr->returnType, false, false),
$isVariadic,
isStatic: TrinaryLogic::createFromBoolean($expr->static),
);
}

Expand Down Expand Up @@ -453,6 +455,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
usedVariables: $usedVariables,
acceptsNamedArguments: TrinaryLogic::createYes(),
mustUseReturnValue: $mustUseReturnValue,
isStatic: TrinaryLogic::createFromBoolean($expr->static),
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
),
]);
} elseif ($mainType instanceof ClosureType) {
$closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments(), mustUseReturnValue: $mainType->mustUseReturnValue());
$closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments(), mustUseReturnValue: $mainType->mustUseReturnValue(), isStatic: $mainType->isStaticClosure());
if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) {
return new ErrorType();
}
Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/Callables/CallableParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ public function mustUseReturnValue(): TrinaryLogic;

public function getAsserts(): Assertions;

public function isStaticClosure(): TrinaryLogic;

}
5 changes: 5 additions & 0 deletions src/Reflection/Callables/FunctionCallableVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,9 @@ public function getAsserts(): Assertions
return $this->function->getAsserts();
}

public function isStaticClosure(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

}
6 changes: 6 additions & 0 deletions src/Reflection/ExtendedCallableFunctionVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function __construct(
private TrinaryLogic $acceptsNamedArguments,
private TrinaryLogic $mustUseReturnValue,
private ?Assertions $assertions = null,
private ?TrinaryLogic $isStatic = null,
)
{
parent::__construct(
Expand Down Expand Up @@ -92,4 +93,9 @@ public function getAsserts(): Assertions
return $this->assertions ?? Assertions::createEmpty();
}

public function isStaticClosure(): TrinaryLogic
{
return $this->isStatic ?? TrinaryLogic::createMaybe();
}

}
1 change: 1 addition & 0 deletions src/Reflection/GenericParametersAcceptorResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
$originalParametersAcceptor->acceptsNamedArguments(),
$originalParametersAcceptor->mustUseReturnValue(),
$originalParametersAcceptor->getAsserts(),
$originalParametersAcceptor->isStaticClosure(),
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/Reflection/InaccessibleMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,9 @@ public function getAsserts(): Assertions
return Assertions::createEmpty();
}

public function isStaticClosure(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

}
1 change: 1 addition & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
TemplateTypeMap::createEmpty(),
TemplateTypeVarianceMap::createEmpty(),
acceptsNamedArguments: TrinaryLogic::createYes(),
isStatic: TrinaryLogic::createYes(),
);
}
if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
Expand Down
4 changes: 4 additions & 0 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
$usedVariables = [];
$acceptsNamedArguments = TrinaryLogic::createNo();
$mustUseReturnValue = TrinaryLogic::createMaybe();
$isStaticClosure = TrinaryLogic::createMaybe();

foreach ($acceptors as $acceptor) {
$returnTypes[] = $acceptor->getReturnType();
Expand All @@ -747,6 +748,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
$usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables());
$acceptsNamedArguments = $acceptsNamedArguments->or($acceptor->acceptsNamedArguments());
$mustUseReturnValue = $mustUseReturnValue->or($acceptor->mustUseReturnValue());
$isStaticClosure = $isStaticClosure->or($acceptor->isStaticClosure());
}
$isVariadic = $isVariadic || $acceptor->isVariadic();

Expand Down Expand Up @@ -854,6 +856,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
$usedVariables,
$acceptsNamedArguments,
$mustUseReturnValue,
isStatic: $isStaticClosure,
);
}

Expand Down Expand Up @@ -892,6 +895,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara
$acceptor->acceptsNamedArguments(),
$acceptor->mustUseReturnValue(),
$acceptor->getAsserts(),
$acceptor->isStaticClosure(),
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/Reflection/ResolvedFunctionVariantWithCallable.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function __construct(
private TrinaryLogic $acceptsNamedArguments,
private TrinaryLogic $mustUseReturnValue,
private ?Assertions $assertions = null,
private ?TrinaryLogic $isStatic = null,
)
{
}
Expand Down Expand Up @@ -124,4 +125,9 @@ public function getAsserts(): Assertions
return $this->assertions ?? Assertions::createEmpty();
}

public function isStaticClosure(): TrinaryLogic
{
return $this->isStatic ?? TrinaryLogic::createMaybe();
}

}
5 changes: 5 additions & 0 deletions src/Reflection/TrivialParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,9 @@ public function getAsserts(): Assertions
return Assertions::createEmpty();
}

public function isStaticClosure(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}

}
1 change: 1 addition & 0 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
$acceptedType->getUsedVariables(),
$acceptedType->acceptsNamedArguments(),
$acceptedType->mustUseReturnValue(),
isStatic: $acceptedType->isStaticClosure(),
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,11 @@ public function getAsserts(): Assertions
return Assertions::createEmpty();
}

public function isStaticClosure(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}

public function toNumber(): Type
{
return new ErrorType();
Expand Down
7 changes: 7 additions & 0 deletions src/Type/CallableTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ public static function isParametersAcceptorSuperTypeOf(
$result = $result->and(new IsSuperTypeOfResult($theirs->isPure()->negate(), []));
}

$ourStatic = $ours->isStaticClosure();
if ($ourStatic->yes()) {
$result = $result->and(new IsSuperTypeOfResult($theirs->isStaticClosure(), []));
} elseif ($ourStatic->no()) {
$result = $result->and(new IsSuperTypeOfResult($theirs->isStaticClosure()->negate(), []));
}

return $result->and($isReturnTypeSuperType);
}

Expand Down
18 changes: 16 additions & 2 deletions src/Type/ClosureType.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor

private Assertions $assertions;

private TrinaryLogic $isStatic;

/**
* @api
* @param list<ParameterReflection>|null $parameters
Expand All @@ -112,6 +114,7 @@ public function __construct(
?TrinaryLogic $acceptsNamedArguments = null,
?TrinaryLogic $mustUseReturnValue = null,
?Assertions $assertions = null,
?TrinaryLogic $isStatic = null,
)
{
if ($acceptsNamedArguments === null) {
Expand All @@ -132,6 +135,7 @@ public function __construct(
$this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty();
$this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)];
$this->assertions = $assertions ?? Assertions::createEmpty();
$this->isStatic = $isStatic ?? TrinaryLogic::createMaybe();
}

public function getAsserts(): Assertions
Expand Down Expand Up @@ -268,7 +272,8 @@ public function equals(Type $type): bool
}

return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise())
&& $this->isPure()->equals($type->isPure());
&& $this->isPure()->equals($type->isPure())
&& $this->isStatic->equals($type->isStatic);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this addition.

I think its common to pass a static closure into a function expecting a non-static one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch is clean and up to date. No code changes are needed for this review comment. Here's the analysis:

The isStatic check in equals() is correct and consistent with the existing pattern. The equals() method checks type identity (are these the exact same type?), not assignability (can one be used where the other is expected?). The isPure() check — already present before this PR — follows the exact same pattern: two closures with different purity are not equal types, even though a pure closure can be passed where an impure one is expected.

The reviewer's concern about assignability is already handled by isSuperTypeOf(). The CallableTypeHelper::isParametersAcceptorSuperTypeOf() logic and the isSuperTypeOf tests show:

  • maybe-static closure (the default for PHPDoc Closure types) accepts both static and non-static closures
  • Only explicitly non-static closure types reject static closures, which is correct: a function that explicitly requires a non-static closure may intend to bindTo() it, and bindTo() returns null for static closures

In practice, function parameters typed via PHPDoc as Closure(): void are always maybe-static, so passing a static closure is accepted. The strict isSuperTypeOf() check only triggers between two code-inferred closures with known static-ness.

}

public function describe(VerbosityLevel $level): string
Expand All @@ -277,7 +282,9 @@ public function describe(VerbosityLevel $level): string
static fn (): string => 'Closure',
function (): string {
if ($this->isCommonCallable) {
return $this->isPure()->yes() ? 'pure-Closure' : 'Closure';
$prefix = $this->isStatic->yes() ? 'static-' : '';
$name = $this->isPure()->yes() ? 'pure-Closure' : 'Closure';
return $prefix . $name;
}

$printer = new Printer();
Expand Down Expand Up @@ -496,6 +503,11 @@ public function mustUseReturnValue(): TrinaryLogic
return $this->mustUseReturnValue;
}

public function isStaticClosure(): TrinaryLogic
{
return $this->isStatic;
}

public function isCloneable(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down Expand Up @@ -709,6 +721,7 @@ public function traverse(callable $cb): Type
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
$this->assertions,
$this->isStatic,
);
}

Expand Down Expand Up @@ -761,6 +774,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
$this->assertions,
$this->isStatic,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
usedVariables: $variant->getUsedVariables(),
acceptsNamedArguments: $variant->acceptsNamedArguments(),
mustUseReturnValue: $variant->mustUseReturnValue(),
isStatic: $variant->isStaticClosure(),
);
}

Expand Down
62 changes: 62 additions & 0 deletions tests/PHPStan/Analyser/nsrt/closure-static-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace ClosureStaticType;

use Closure;
use function PHPStan\Testing\assertType;

final class Foo
{

public function doFoo(): void
{
$static = static function (): void {};
assertType('Closure(): void', $static);

$nonStatic = function (): void {};
assertType('Closure(): void', $nonStatic);

$staticArrow = static fn (): int => 1;
assertType('Closure(): 1', $staticArrow);

$nonStaticArrow = fn (): int => 1;
assertType('Closure(): 1', $nonStaticArrow);
}

public function doBindTo(): void
{
$static = static function (): void {};
assertType('Closure(): void', $static->bindTo($this));

$nonStatic = function (): void {};
assertType('Closure(): void', $nonStatic->bindTo($this));
}

public function doBind(): void
{
$static = static function (): void {};
assertType('Closure(): void', Closure::bind($static, $this));

$nonStatic = function (): void {};
assertType('Closure(): void', Closure::bind($nonStatic, $this));
}

/**
* @param Closure(): void $unknownClosure
*/
public function doUnknown(Closure $unknownClosure): void
{
assertType('Closure(): void', $unknownClosure->bindTo($this));
assertType('Closure(): void', Closure::bind($unknownClosure, $this));
}

public function doFromCallable(): void
{
$fn = Closure::fromCallable(static function (): void {});
assertType('Closure(): void', $fn->bindTo($this));

$fn2 = Closure::fromCallable(function (): void {});
assertType('Closure(): void', $fn2->bindTo($this));
}

}
Loading
Loading