From cdaff5f00d621c3a94e048b8bd9fa864d47b3228 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 7 Apr 2024 14:11:31 +0200 Subject: [PATCH] pure-callable and pure-Closure --- src/PhpDoc/TypeNodeResolver.php | 22 ++++- .../Callables/CallableParametersAcceptor.php | 3 + .../Callables/FunctionCallableVariant.php | 21 +++++ src/Reflection/InaccessibleMethod.php | 15 +++- src/Reflection/TrivialParametersAcceptor.php | 6 ++ src/Rules/MissingTypehintCheck.php | 7 +- src/Rules/RuleLevelHelper.php | 10 ++- src/Type/CallableType.php | 42 ++++++++- src/Type/CallableTypeHelper.php | 10 ++- src/Type/ClosureType.php | 58 +++++++++++- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/more-types.php | 2 +- tests/PHPStan/Analyser/data/pure-callable.php | 18 ++++ .../Rules/Methods/CallMethodsRuleTest.php | 31 +++++++ ...MissingMethodParameterTypehintRuleTest.php | 4 + .../missing-method-parameter-typehint.php | 12 +++ .../Methods/data/pure-callable-accepts.php | 68 ++++++++++++++ .../PhpDoc/IncompatiblePhpDocTypeRuleTest.php | 8 ++ .../Rules/PhpDoc/data/incompatible-types.php | 9 ++ .../PHPStan/Rules/Pure/data/pure-function.php | 12 +++ tests/PHPStan/Type/CallableTypeTest.php | 40 +++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 90 +++++++++++++++++++ 22 files changed, 471 insertions(+), 18 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/pure-callable.php create mode 100644 tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 5c01c0a8b..e080c998e 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -43,6 +43,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -352,9 +353,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new IterableType(new MixedType(), new MixedType()); case 'callable': - case 'pure-callable': return new CallableType(); + case 'pure-callable': + return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()); + + case 'pure-closure': + return new ClosureType(); + case 'resource': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -928,7 +934,12 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi $returnType = $this->resolve($typeNode->returnType, $nameScope); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags); + $pure = $mainType->isPure(); + if ($pure->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure); } elseif ( $mainType instanceof ObjectType @@ -941,6 +952,13 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi false, ), ]); + } elseif ($mainType instanceof ClosureType) { + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints()); + if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return $closure; } return new ErrorType(); diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index 2dfadf522..62371a0e8 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -4,6 +4,7 @@ use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\TrinaryLogic; /** * @api @@ -16,6 +17,8 @@ interface CallableParametersAcceptor extends ParametersAcceptor */ public function getThrowPoints(): array; + public function isPure(): TrinaryLogic; + /** * @return SimpleImpurePoint[] */ diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 756f4bb84..550e00426 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\NeverType; @@ -13,6 +14,7 @@ use PHPStan\Type\Type; use Throwable; use function array_map; +use function count; use function sprintf; class FunctionCallableVariant implements CallableParametersAcceptor, ParametersAcceptorWithPhpDocs @@ -115,6 +117,25 @@ public function getThrowPoints(): array return $this->throwPoints = $throwPoints; } + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + public function getImpurePoints(): array { if ($this->impurePoints !== null) { diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 9b0daa3e8..5b8504a7d 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -3,6 +3,8 @@ namespace PHPStan\Reflection; use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; @@ -58,9 +60,20 @@ public function getThrowPoints(): array return []; } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getImpurePoints(): array { - return []; + return [ + new SimpleImpurePoint( + 'methodCall', + 'call to unknown method', + false, + ), + ]; } public function getInvalidateExpressions(): array diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index ecf8a937f..3f663f790 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; @@ -64,6 +65,11 @@ public function getThrowPoints(): array return []; } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getImpurePoints(): array { return [ diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 489ed1469..ab88049e9 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -9,6 +9,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\ConditionalType; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\GenericObjectType; @@ -162,8 +163,10 @@ public function getCallablesWithMissingSignature(Type $type): array $result = []; TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( - ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === Closure::class)) { + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 3bc0251fb..f3076392f 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -95,17 +95,25 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { if ($acceptedType instanceof CallableType) { if ($acceptedType->isCommonCallable()) { - return new CallableType(null, null, $acceptedType->isVariadic()); + return $acceptedType; } return new CallableType( $acceptedType->getParameters(), $traverse($this->transformCommonType($acceptedType->getReturnType())), $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), ); } if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } + return new ClosureType( $acceptedType->getParameters(), $traverse($this->transformCommonType($acceptedType->getReturnType())), diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index c4e531a3c..c1cfe1a71 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -63,6 +63,8 @@ class CallableType implements CompoundType, CallableParametersAcceptor private TemplateTypeMap $resolvedTemplateTypeMap; + private TrinaryLogic $isPure; + /** * @api * @param array|null $parameters @@ -75,6 +77,7 @@ public function __construct( ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, private array $templateTags = [], + ?TrinaryLogic $isPure = null, ) { $this->parameters = $parameters ?? []; @@ -82,6 +85,7 @@ public function __construct( $this->isCommonCallable = $parameters === null && $returnType === null; $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->isPure = $isPure ?? TrinaryLogic::createMaybe(); } /** @@ -92,6 +96,11 @@ public function getTemplateTags(): array return $this->templateTags; } + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + /** * @return string[] */ @@ -146,7 +155,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult { $isCallable = new AcceptsResult($type->isCallable(), []); - if ($isCallable->no() || $this->isCommonCallable) { + if ($isCallable->no()) { return $isCallable; } @@ -155,6 +164,19 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Accep $scope = new OutOfClassScope(); } + if ($this->isCommonCallable) { + if ($this->isPure()->yes()) { + $typePure = TrinaryLogic::createYes(); + foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $typePure = $typePure->and($variant->isPure()); + } + + return $isCallable->and(new AcceptsResult($typePure, [])); + } + + return $isCallable; + } + $variantsResult = null; foreach ($type->getCallableParametersAcceptors($scope) as $variant) { $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); @@ -221,6 +243,7 @@ function (): string { $this->templateTypeMap, $this->resolvedTemplateTypeMap, $this->templateTags, + $this->isPure, ); return $printer->print($selfWithoutParameterNames->toPhpDocNode()); @@ -247,11 +270,16 @@ public function getThrowPoints(): array public function getImpurePoints(): array { + $pure = $this->isPure(); + if ($pure->yes()) { + return []; + } + return [ new SimpleImpurePoint( 'functionCall', 'call to a callable', - false, + $pure->no(), ), ]; } @@ -414,6 +442,7 @@ public function traverse(callable $cb): Type $this->templateTypeMap, $this->resolvedTemplateTypeMap, $this->templateTags, + $this->isPure, ); } @@ -463,6 +492,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->templateTypeMap, $this->resolvedTemplateTypeMap, $this->templateTags, + $this->isPure, ); } @@ -599,7 +629,7 @@ public function getFiniteTypes(): array public function toPhpDocNode(): TypeNode { if ($this->isCommonCallable) { - return new IdentifierTypeNode('callable'); + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); } $parameters = []; @@ -623,7 +653,7 @@ public function toPhpDocNode(): TypeNode } return new CallableTypeNode( - new IdentifierTypeNode('callable'), + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), $parameters, $this->returnType->toPhpDocNode(), $templateTags, @@ -639,6 +669,10 @@ public static function __set_state(array $properties): Type (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], $properties['variadic'], + $properties['templateTypeMap'], + $properties['resolvedTemplateTypeMap'], + $properties['templateTags'], + $properties['isPure'], ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 102379644..8e91be2e8 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\TrinaryLogic; use function array_key_exists; use function array_merge; @@ -13,8 +13,8 @@ class CallableTypeHelper { public static function isParametersAcceptorSuperTypeOf( - ParametersAcceptor $ours, - ParametersAcceptor $theirs, + CallableParametersAcceptor $ours, + CallableParametersAcceptor $theirs, bool $treatMixedAsAny, ): AcceptsResult { @@ -103,6 +103,10 @@ public static function isParametersAcceptorSuperTypeOf( $isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []); } + if ($ours->isPure()->yes()) { + $result = $result->and(new AcceptsResult($theirs->isPure(), [])); + } + return $result->and($isReturnTypeSuperType); } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index d0f39a163..d3498921e 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -60,6 +60,13 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; + /** @var array */ + private array $parameters; + + private Type $returnType; + + private bool $isCommonCallable; + private ObjectType $objectType; private TemplateTypeMap $templateTypeMap; @@ -70,7 +77,7 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor /** * @api - * @param array $parameters + * @param array|null $parameters * @param array $templateTags * @param SimpleThrowPoint[] $throwPoints * @param SimpleImpurePoint[] $impurePoints @@ -78,9 +85,9 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor * @param string[] $usedVariables */ public function __construct( - private array $parameters, - private Type $returnType, - private bool $variadic, + ?array $parameters = null, + ?Type $returnType = null, + private bool $variadic = true, ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, ?TemplateTypeVarianceMap $callSiteVarianceMap = null, @@ -91,6 +98,9 @@ public function __construct( private array $usedVariables = [], ) { + $this->parameters = $parameters ?? []; + $this->returnType = $returnType ?? new MixedType(); + $this->isCommonCallable = $parameters === null && $returnType === null; $this->objectType = new ObjectType(Closure::class); $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); @@ -105,6 +115,25 @@ public function getTemplateTags(): array return $this->templateTags; } + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + public function getClassName(): string { return $this->objectType->getClassName(); @@ -201,6 +230,10 @@ 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( @@ -330,6 +363,11 @@ public function getEnumCases(): array return []; } + public function isCommonCallable(): bool + { + return $this->isCommonCallable; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; @@ -479,6 +517,10 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para public function traverse(callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + return new self( array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); @@ -506,6 +548,10 @@ public function traverse(callable $cb): Type public function traverseSimultaneously(Type $right, callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + if (!$right instanceof self) { return $this; } @@ -666,6 +712,10 @@ public function getFiniteTypes(): array public function toPhpDocNode(): TypeNode { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-Closure' : 'Closure'); + } + $parameters = []; foreach ($this->parameters as $parameter) { $parameters[] = new CallableTypeParameterNode( diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 8a0dd4621..3dc28c01d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -330,6 +330,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4343.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-method.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-constructor.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/pure-callable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4351.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-use.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php'); diff --git a/tests/PHPStan/Analyser/data/more-types.php b/tests/PHPStan/Analyser/data/more-types.php index 9f646300c..f1b742f17 100644 --- a/tests/PHPStan/Analyser/data/more-types.php +++ b/tests/PHPStan/Analyser/data/more-types.php @@ -30,7 +30,7 @@ public function doFoo( $nonEmptyMixed ): void { - assertType('callable(): mixed', $pureCallable); + assertType('pure-callable(): mixed', $pureCallable); assertType('array&callable(): mixed', $callableArray); assertType('resource', $closedResource); assertType('resource', $openResource); diff --git a/tests/PHPStan/Analyser/data/pure-callable.php b/tests/PHPStan/Analyser/data/pure-callable.php new file mode 100644 index 000000000..39ef17228 --- /dev/null +++ b/tests/PHPStan/Analyser/data/pure-callable.php @@ -0,0 +1,18 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/pure-callable-accepts.php'], [ + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, callable(): mixed given.', + 33, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 35, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 36, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, Closure(): 1 given.', + 41, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureClosure() expects pure-Closure, Closure(): 1 given.', + 61, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 7d02ecaf3..2d4cf56a1 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -84,6 +84,10 @@ public function testRule(): void 226, 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], + [ + 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', + 238, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index dfe059bb2..d5a333491 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -229,3 +229,15 @@ function generics(callable $cb): void } } + +class MissingPureClosureSignatureType { + + /** + * @param pure-Closure $cb + */ + function doFoo(\Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php new file mode 100644 index 000000000..3c801d209 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php @@ -0,0 +1,68 @@ +acceptsCallable($cb); + $this->acceptsCallable($pureCb); + $this->acceptsPureCallable($cb); + $this->acceptsPureCallable($pureCb); + $this->acceptsInt($cb); + $this->acceptsInt($pureCb); + + $this->acceptsPureCallable(function (): int { + return 1; + }); + $this->acceptsPureCallable(function (): int { + sleep(1); + + return 1; + }); + } + + /** + * @param pure-Closure $cb + */ + public function acceptsPureClosure(\Closure $cb): void + { + + } + + public function doFoo2(): void + { + $this->acceptsPureClosure(function (): int { + return 1; + }); + $this->acceptsPureClosure(function (): int { + sleep(1); + + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index e73882613..e0d889995 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -207,6 +207,14 @@ public function testRule(): void 'PHPDoc tag @param-closure-this for parameter $cb contains unresolvable type.', 357, ], + [ + 'PHPDoc tag @param for parameter $cb contains unresolvable type.', + 366, + ], + [ + 'PHPDoc tag @param for parameter $cl contains unresolvable type.', + 366, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 29a581539..3ce9209f9 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -358,3 +358,12 @@ function paramClosureThisWithNonObject(callable $cb): void { } + +/** + * @param pure-callable(): void $cb + * @param pure-Closure(): void $cl + */ +function pureCallableCannotReturnVoid(callable $cb, \Closure $cl): void +{ + +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php index d7b182b44..1d526ec08 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-function.php +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -139,3 +139,15 @@ function callsClosures(\Closure $closure1, \Closure $closure2): int $closure1(); return $closure2(); } + +/** + * @phpstan-pure + * @param pure-callable $cb + * @param pure-Closure $closure + * @return int + */ +function callsPureCallableIdentifierTypeNode(callable $cb, \Closure $closure): int +{ + $cb(); + $closure(); +} diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index eec339aea..f4068638d 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -354,6 +354,46 @@ public function dataAccepts(): array new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], ]; } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 76b007490..79b72c75a 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -15,7 +15,9 @@ use Iterator; use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -2522,6 +2524,50 @@ public function dataUnion(): iterable ObjectWithoutClassType::class, 'object', ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new ClosureType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + new ClosureType(), + ], + ClosureType::class, + 'Closure(): mixed', // different result might be okay too + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + new ClosureType(), + ], + ClosureType::class, + 'Closure(): mixed', // different result might be okay too + ]; } /** @@ -4162,6 +4208,50 @@ public function dataIntersect(): iterable IntersectionType::class, 'array{a?: true, c?: true}&non-empty-array', ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new ClosureType(), + ], + ClosureType::class, + 'pure-Closure', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + new ClosureType(), + ], + ClosureType::class, + 'pure-Closure', // different result might be okay too + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + new ClosureType(), + ], + ClosureType::class, + 'pure-Closure', // different result might be okay too + ]; } /**