Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -220,13 +220,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 @@ -446,6 +448,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 @@ -736,6 +736,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 @@ -753,6 +754,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 @@ -860,6 +862,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
$usedVariables,
$acceptsNamedArguments,
$mustUseReturnValue,
isStatic: $isStaticClosure,
);
}

Expand Down Expand Up @@ -898,6 +901,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
80 changes: 79 additions & 1 deletion 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);
}

public function describe(VerbosityLevel $level): string
Expand Down Expand Up @@ -306,6 +311,72 @@ function (): string {

return $printer->print($selfWithoutParameterNames->toPhpDocNode());
},
function (): string {
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.

Maybe a private methode could be created to refactor the value and precise level

$prefix = $this->isStatic->yes() ? 'static ' : '';
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.

prefix should be with a -

if ($this->isCommonCallable) {
$name = $this->isPure()->yes() ? 'pure-Closure' : 'Closure';
return $prefix . $name;
}

$printer = new Printer();
$selfWithoutParameterNames = new self(
array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter(
'',
$p->getType(),
optional: $p->isOptional() && !$p->isVariadic(),
passedByReference: PassedByReference::createNo(),
variadic: $p->isVariadic(),
defaultValue: $p->getDefaultValue(),
), $this->parameters),
$this->returnType,
$this->variadic,
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
$this->callSiteVarianceMap,
$this->templateTags,
$this->throwPoints,
$this->impurePoints,
$this->invalidateExpressions,
$this->usedVariables,
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
);

return $prefix . $printer->print($selfWithoutParameterNames->toPhpDocNode());
},
function (): string {
$prefix = !$this->isStatic->maybe() ? ($this->isStatic->yes() ? 'static ' : 'non-static ') : '';
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.

There is no reason to use non-static here, we don't use non-pure.

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.

Both commits are pushed. Here's a summary of the changes:

Commit 1: Revert Closure::bindTo()/Closure::bind() return type narrowing

  • Reverted ClosureBindDynamicReturnTypeExtension and ClosureBindToDynamicReturnTypeExtension to their original state (no static closure checks)
  • Updated closure-static-type.php test assertions to expect the closure type (not null) from bind/bindTo
  • Reverted bug-4734 integration test to original 5-error expectation

Commit 2: Use hyphen prefix and extract helper method

  • Changed prefix from 'static ' to 'static-' (matching pure-Closure convention)
  • Removed non-static prefix at cache level (consistent with not using non-pure)
  • Extracted describeWithPrefix() private method, eliminating the duplicated closure body across value, precise, and cache callbacks (cache now falls through to precise)
  • Updated all test assertion strings from 'static Closure' to 'static-Closure'

if ($this->isCommonCallable) {
$name = $this->isPure()->yes() ? 'pure-Closure' : 'Closure';
return $prefix . $name;
}

$printer = new Printer();
$selfWithoutParameterNames = new self(
array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter(
'',
$p->getType(),
optional: $p->isOptional() && !$p->isVariadic(),
passedByReference: PassedByReference::createNo(),
variadic: $p->isVariadic(),
defaultValue: $p->getDefaultValue(),
), $this->parameters),
$this->returnType,
$this->variadic,
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
$this->callSiteVarianceMap,
$this->templateTags,
$this->throwPoints,
$this->impurePoints,
$this->invalidateExpressions,
$this->usedVariables,
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
);

return $prefix . $printer->print($selfWithoutParameterNames->toPhpDocNode());
},
);
}

Expand Down Expand Up @@ -496,6 +567,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 +785,7 @@ public function traverse(callable $cb): Type
$this->acceptsNamedArguments,
$this->mustUseReturnValue,
$this->assertions,
$this->isStatic,
);
}

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

Expand Down
10 changes: 10 additions & 0 deletions src/Type/Php/ClosureBindDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ClosureType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

#[AutowiredService]
final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
Expand All @@ -32,6 +34,14 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
return null;
}

if ($closureType->isStaticClosure()->yes()) {
return new NullType();
}

if ($closureType->isStaticClosure()->maybe()) {
return TypeCombinator::union($closureType, new NullType());
}

return $closureType;
}

Expand Down
10 changes: 10 additions & 0 deletions src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ClosureType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

#[AutowiredService]
final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
Expand All @@ -32,6 +34,14 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
return null;
}

if ($closureType->isStaticClosure()->yes()) {
return new NullType();
}

if ($closureType->isStaticClosure()->maybe()) {
return TypeCombinator::union($closureType, new NullType());
}

return $closureType;
}

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
Loading
Loading