diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index e996bafbdda..09a24a1249b 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -220,6 +220,7 @@ 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) { @@ -227,6 +228,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $parameters, $scope->getFunctionType($expr->returnType, false, false), $isVariadic, + isStatic: TrinaryLogic::createFromBoolean($expr->static), ); } @@ -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), ); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index bd243ef7d26..c9194768d4c 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -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(); } diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index bcef9878eee..df55a609c39 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -60,4 +60,6 @@ public function mustUseReturnValue(): TrinaryLogic; public function getAsserts(): Assertions; + public function isStaticClosure(): TrinaryLogic; + } diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 6c48e4b0102..2ddf2c1c716 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -179,4 +179,9 @@ public function getAsserts(): Assertions return $this->function->getAsserts(); } + public function isStaticClosure(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php index 389893394e3..2a4fd692a0f 100644 --- a/src/Reflection/ExtendedCallableFunctionVariant.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -38,6 +38,7 @@ public function __construct( private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, private ?Assertions $assertions = null, + private ?TrinaryLogic $isStatic = null, ) { parent::__construct( @@ -92,4 +93,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function isStaticClosure(): TrinaryLogic + { + return $this->isStatic ?? TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index c72afeedbb5..63d9c2b2513 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -134,6 +134,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->acceptsNamedArguments(), $originalParametersAcceptor->mustUseReturnValue(), $originalParametersAcceptor->getAsserts(), + $originalParametersAcceptor->isStaticClosure(), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 68fce995f82..f0f1b44ca12 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -98,4 +98,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function isStaticClosure(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 495b8266664..08d8964fd8c 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -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) { diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index b4c9b3a3821..c802cf334b1 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -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(); @@ -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(); @@ -860,6 +862,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $usedVariables, $acceptsNamedArguments, $mustUseReturnValue, + isStatic: $isStaticClosure, ); } @@ -898,6 +901,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara $acceptor->acceptsNamedArguments(), $acceptor->mustUseReturnValue(), $acceptor->getAsserts(), + $acceptor->isStaticClosure(), ); } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 6f816fa0ac9..7e57be7a952 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -30,6 +30,7 @@ public function __construct( private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, private ?Assertions $assertions = null, + private ?TrinaryLogic $isStatic = null, ) { } @@ -124,4 +125,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function isStaticClosure(): TrinaryLogic + { + return $this->isStatic ?? TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 157368d4c02..1aaefa6a33d 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -108,4 +108,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function isStaticClosure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 9c055399356..1c1bd926f90 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -123,6 +123,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->getUsedVariables(), $acceptedType->acceptsNamedArguments(), $acceptedType->mustUseReturnValue(), + isStatic: $acceptedType->isStaticClosure(), ); } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index c2760ef583e..bea8c783ba0 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -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(); diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 2f5b9f89b29..709ed1fb610 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -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); } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index a819526021f..e2a25985190 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -88,6 +88,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor private Assertions $assertions; + private TrinaryLogic $isStatic; + /** * @api * @param list|null $parameters @@ -112,6 +114,7 @@ public function __construct( ?TrinaryLogic $acceptsNamedArguments = null, ?TrinaryLogic $mustUseReturnValue = null, ?Assertions $assertions = null, + ?TrinaryLogic $isStatic = null, ) { if ($acceptsNamedArguments === null) { @@ -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 @@ -268,45 +272,53 @@ 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 { return $level->handle( static fn (): string => 'Closure', - function (): string { - if ($this->isCommonCallable) { - return $this->isPure()->yes() ? 'pure-Closure' : 'Closure'; - } - - $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, - ); + fn (): string => $this->describeBody(true, false), + fn (): string => $this->describeBody(true, true), + ); + } + + private function describeBody(bool $showPure, bool $showStatic): string + { + $prefix = $showStatic && $this->isStatic->yes() ? 'static-' : ''; - return $printer->print($selfWithoutParameterNames->toPhpDocNode()); - }, + if ($this->isCommonCallable) { + $name = $showPure && $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()); } public function isOffsetAccessLegal(): TrinaryLogic @@ -496,6 +508,11 @@ public function mustUseReturnValue(): TrinaryLogic return $this->mustUseReturnValue; } + public function isStaticClosure(): TrinaryLogic + { + return $this->isStatic; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -709,6 +726,7 @@ public function traverse(callable $cb): Type $this->acceptsNamedArguments, $this->mustUseReturnValue, $this->assertions, + $this->isStatic, ); } @@ -761,6 +779,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->acceptsNamedArguments, $this->mustUseReturnValue, $this->assertions, + $this->isStatic, ); } diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index df0e2d54aa2..1bccae3ac1a 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -56,6 +56,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, usedVariables: $variant->getUsedVariables(), acceptsNamedArguments: $variant->acceptsNamedArguments(), mustUseReturnValue: $variant->mustUseReturnValue(), + isStatic: $variant->isStaticClosure(), ); } diff --git a/tests/PHPStan/Analyser/data/bug-14324.php b/tests/PHPStan/Analyser/data/bug-14324.php index 20f97172fb9..37f7a1db2f7 100644 --- a/tests/PHPStan/Analyser/data/bug-14324.php +++ b/tests/PHPStan/Analyser/data/bug-14324.php @@ -50,7 +50,7 @@ public function createMap(): void 'bar2' => static fn() => 'bar2', 'baz2' => static fn() => 'baz2', ]; - assertType("array{foo: Closure(): 'foo', bar: Closure(): 'bar', baz: Closure(): 'baz', qux: Closure(): 'qux', quux: Closure(): 'quux', corge: Closure(): 'corge', grault: Closure(): 'grault', garply: Closure(): 'garply', waldo: Closure(): 'waldo', fred: Closure(): 'fred', plugh: Closure(): 'plugh', xyzzy: Closure(): 'xyzzy', thud: Closure(): 'thud', foo1: Closure(): 'foo1', bar1: Closure(): 'bar1', baz1: Closure(): 'baz1', qux1: Closure(): 'qux1', quux1: Closure(): 'quux1', corge1: Closure(): 'corge1', grault1: Closure(): 'grault1', garply1: Closure(): 'garply1', waldo1: Closure(): 'waldo1', fred1: Closure(): 'fred1', plugh1: Closure(): 'plugh1', xyzzy1: Closure(): 'xyzzy1', thud1: Closure(): 'thud1', foo2: Closure(): 'foo2', bar2: Closure(): 'bar2', baz2: Closure(): 'baz2'}", self::$map); + assertType("array{foo: static-Closure(): 'foo', bar: static-Closure(): 'bar', baz: static-Closure(): 'baz', qux: static-Closure(): 'qux', quux: static-Closure(): 'quux', corge: static-Closure(): 'corge', grault: static-Closure(): 'grault', garply: static-Closure(): 'garply', waldo: static-Closure(): 'waldo', fred: static-Closure(): 'fred', plugh: static-Closure(): 'plugh', xyzzy: static-Closure(): 'xyzzy', thud: static-Closure(): 'thud', foo1: static-Closure(): 'foo1', bar1: static-Closure(): 'bar1', baz1: static-Closure(): 'baz1', qux1: static-Closure(): 'qux1', quux1: static-Closure(): 'quux1', corge1: static-Closure(): 'corge1', grault1: static-Closure(): 'grault1', garply1: static-Closure(): 'garply1', waldo1: static-Closure(): 'waldo1', fred1: static-Closure(): 'fred1', plugh1: static-Closure(): 'plugh1', xyzzy1: static-Closure(): 'xyzzy1', thud1: static-Closure(): 'thud1', foo2: static-Closure(): 'foo2', bar2: static-Closure(): 'bar2', baz2: static-Closure(): 'baz2'}", self::$map); foreach (self::ADDITIONAL_MAPS as $map) { // added with 3 entries, breaching the closure limit of 32 entries diff --git a/tests/PHPStan/Analyser/nsrt/bug-7031.php b/tests/PHPStan/Analyser/nsrt/bug-7031.php index a325a67d1f2..598dd24be92 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7031.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7031.php @@ -7,6 +7,6 @@ class SomeKey {} function () { - assertType('Closure(int): Generator', static fn(int $value): iterable => yield new SomeKey); - assertType('Closure(int): Generator', static function (int $value): iterable { yield new SomeKey; }); + assertType('static-Closure(int): Generator', static fn(int $value): iterable => yield new SomeKey); + assertType('static-Closure(int): Generator', static function (int $value): iterable { yield new SomeKey; }); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9764.php b/tests/PHPStan/Analyser/nsrt/bug-9764.php index f24b810fe8d..a75472594eb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9764.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9764.php @@ -18,7 +18,7 @@ function (): void { /** @var array $a */ $a = []; $c = static fn (): array => $a; - assertType('Closure(): array', $c); + assertType('static-Closure(): array', $c); $r = result($c); assertType('array', $r); diff --git a/tests/PHPStan/Analyser/nsrt/closure-static-type.php b/tests/PHPStan/Analyser/nsrt/closure-static-type.php new file mode 100644 index 00000000000..6b84bd967df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-static-type.php @@ -0,0 +1,62 @@ + 1; + assertType('static-Closure(): 1', $staticArrow); + + $nonStaticArrow = fn (): int => 1; + assertType('Closure(): 1', $nonStaticArrow); + } + + public function doBindTo(): void + { + $static = static function (): void {}; + assertType('static-Closure(): void', $static->bindTo($this)); + + $nonStatic = function (): void {}; + assertType('Closure(): void', $nonStatic->bindTo($this)); + } + + public function doBind(): void + { + $static = static function (): void {}; + assertType('static-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('static-Closure(): void', $fn->bindTo($this)); + + $fn2 = Closure::fromCallable(function (): void {}); + assertType('Closure(): void', $fn2->bindTo($this)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/degrade-closures.php b/tests/PHPStan/Analyser/nsrt/degrade-closures.php index a7be72480e7..a30e0e69b21 100644 --- a/tests/PHPStan/Analyser/nsrt/degrade-closures.php +++ b/tests/PHPStan/Analyser/nsrt/degrade-closures.php @@ -36,7 +36,7 @@ $arr[] = static function () {}; $arr[] = static function () {}; $arr[] = static function () {}; -assertType('array{Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void, Closure(): void}', $arr); +assertType('array{static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void, static-Closure(): void}', $arr); $arr[] = static function () {}; assertType('non-empty-list&oversized-array', $arr); diff --git a/tests/PHPStan/Reflection/AttributeReflectionTest.php b/tests/PHPStan/Reflection/AttributeReflectionTest.php index ab907f196f3..b469da422d0 100644 --- a/tests/PHPStan/Reflection/AttributeReflectionTest.php +++ b/tests/PHPStan/Reflection/AttributeReflectionTest.php @@ -179,7 +179,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback', [ - 'callback' => 'Closure(int): mixed', + 'callback' => 'static-Closure(int): mixed', ], ], ], @@ -190,7 +190,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback', [ - 'callback' => 'Closure(int): mixed', + 'callback' => 'static-Closure(int): mixed', ], ], ], @@ -201,7 +201,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback', [ - 'callback' => 'Closure(int): string', + 'callback' => 'static-Closure(int): string', ], ], ], @@ -215,7 +215,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback2', [ - 'callback' => 'Closure(mixed): mixed', + 'callback' => 'static-Closure(mixed): mixed', ], ], ], @@ -226,7 +226,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback2', [ - 'callback' => 'Closure(int=): mixed', + 'callback' => 'static-Closure(int=): mixed', ], ], ], @@ -237,7 +237,7 @@ public static function dataAttributeReflections(): iterable [ 'ClosureInAttribute\\AttrWithCallback2', [ - 'callback' => 'Closure(int ...): mixed', + 'callback' => 'static-Closure(int ...): mixed', ], ], ], diff --git a/tests/PHPStan/Type/ClosureTypeTest.php b/tests/PHPStan/Type/ClosureTypeTest.php index f5ef07239e4..440d7cd17ea 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -89,6 +89,41 @@ public static function dataIsSuperTypeOf(): array new ObjectWithoutClassType(new ObjectType(Closure::class)), TrinaryLogic::createNo(), ], + 'static closure is supertype of static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + 'static closure is not supertype of non-static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + 'non-static closure is not supertype of static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + TrinaryLogic::createNo(), + ], + 'non-static closure is supertype of non-static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + 'maybe-static closure is supertype of static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + 'maybe-static closure is supertype of non-static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + 'static closure is maybe supertype of maybe-static closure' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + TrinaryLogic::createMaybe(), + ], ]; } @@ -107,4 +142,121 @@ public function testIsSuperTypeOf( ); } + public static function dataEquals(): array + { + return [ + 'static equals static' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + true, + ], + 'static does not equal non-static' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + false, + ], + 'static does not equal maybe-static' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + false, + ], + 'maybe-static equals maybe-static' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + true, + ], + ]; + } + + #[DataProvider('dataEquals')] + public function testEquals( + ClosureType $type, + ClosureType $otherType, + bool $expectedResult, + ): void + { + $this->assertSame($expectedResult, $type->equals($otherType)); + } + + public static function dataDescribe(): array + { + return [ + 'static closure at typeOnly' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + VerbosityLevel::typeOnly(), + 'Closure', + ], + 'static closure at value' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + VerbosityLevel::value(), + 'Closure(): mixed', + ], + 'static closure at precise' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + VerbosityLevel::precise(), + 'static-Closure(): mixed', + ], + 'static closure at cache' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()), + VerbosityLevel::cache(), + 'static-Closure(): mixed', + ], + 'non-static closure at precise' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + VerbosityLevel::precise(), + 'Closure(): mixed', + ], + 'non-static closure at cache' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()), + VerbosityLevel::cache(), + 'Closure(): mixed', + ], + 'maybe-static closure at precise' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + VerbosityLevel::precise(), + 'Closure(): mixed', + ], + 'maybe-static closure at cache' => [ + new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createMaybe()), + VerbosityLevel::cache(), + 'Closure(): mixed', + ], + 'static common closure at precise' => [ + new ClosureType(isStatic: TrinaryLogic::createYes()), + VerbosityLevel::precise(), + 'static-Closure', + ], + 'static common closure at cache' => [ + new ClosureType(isStatic: TrinaryLogic::createYes()), + VerbosityLevel::cache(), + 'static-Closure', + ], + ]; + } + + #[DataProvider('dataDescribe')] + public function testDescribe( + ClosureType $type, + VerbosityLevel $level, + string $expectedDescription, + ): void + { + $this->assertSame($expectedDescription, $type->describe($level)); + } + + public function testIsStaticClosure(): void + { + $staticClosure = new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createYes()); + $this->assertTrue($staticClosure->isStaticClosure()->yes()); + + $nonStaticClosure = new ClosureType([], new MixedType(), false, isStatic: TrinaryLogic::createNo()); + $this->assertTrue($nonStaticClosure->isStaticClosure()->no()); + + $maybeClosure = new ClosureType([], new MixedType(), false); + $this->assertTrue($maybeClosure->isStaticClosure()->maybe()); + + $defaultClosure = new ClosureType(); + $this->assertTrue($defaultClosure->isStaticClosure()->maybe()); + } + }