From befcc3e1d6d8dacf24b636711bae8ddf42894f20 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:19:59 +0000 Subject: [PATCH] Fix callable parameter checking for Stringable objects in non-strict mode - Propagated $strictTypes from CallableType::accepts() through to CallableTypeHelper - In non-strict mode, Stringable objects are now accepted where string is expected in callable parameters - This fixes false positives for patterns like uasort($stringableArray, 'strnatcasecmp') - In strict_types mode, the stricter behavior is preserved (e.g. closures with explicit string params) - New regression test in tests/PHPStan/Rules/Functions/data/bug-11619.php Closes https://github.com/phpstan/phpstan/issues/11619 --- src/Type/CallableType.php | 8 ++--- src/Type/CallableTypeHelper.php | 3 +- .../CallToFunctionParametersRuleTest.php | 6 ++++ .../Rules/Functions/data/bug-11619.php | 30 +++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-11619.php diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 9d9cfb65e4..4709fe5c61 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -138,7 +138,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult return $type->isAcceptedBy($this, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); + return $this->isSuperTypeOfInternal($type, true, $strictTypes)->toAcceptsResult(); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult @@ -147,10 +147,10 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false); + return $this->isSuperTypeOfInternal($type, false, true); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny, bool $strictTypes): IsSuperTypeOfResult { $isCallable = new IsSuperTypeOfResult($type->isCallable(), []); if ($isCallable->no()) { @@ -183,7 +183,7 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSup if (!$variant instanceof CallableParametersAcceptor) { return IsSuperTypeOfResult::createNo([]); } - $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); + $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny, $strictTypes); if ($variantsResult === null) { $variantsResult = $isSuperType; } else { diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 4e99e94cc9..2f5b9f89b2 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -16,6 +16,7 @@ public static function isParametersAcceptorSuperTypeOf( CallableParametersAcceptor $ours, CallableParametersAcceptor $theirs, bool $treatMixedAsAny, + bool $strictTypes = true, ): IsSuperTypeOfResult { $theirParameters = $theirs->getParameters(); @@ -72,7 +73,7 @@ public static function isParametersAcceptorSuperTypeOf( } if ($treatMixedAsAny) { - $isSuperType = $theirParameter->getType()->accepts($ourParameterType, true); + $isSuperType = $theirParameter->getType()->accepts($ourParameterType, $strictTypes); $isSuperType = new IsSuperTypeOfResult($isSuperType->result, $isSuperType->reasons); } else { $isSuperType = $theirParameter->getType()->isSuperTypeOf($ourParameterType); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 6bb50169fe..6cb5815c53 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2746,4 +2746,10 @@ public function testBug13247(): void $this->analyse([__DIR__ . '/data/bug-13247.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug11619(): void + { + $this->analyse([__DIR__ . '/data/bug-11619.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11619.php b/tests/PHPStan/Rules/Functions/data/bug-11619.php new file mode 100644 index 0000000000..ca66d37b56 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619.php @@ -0,0 +1,30 @@ += 8.1 + +namespace Bug11619; + +final class Foo implements \Stringable { + + private function __construct(public readonly string $value) { + } + + public static function fromString(string $string): self { + return new self($string); + } + + public function __toString(): string { + return $this->value; + } + +} + +function test(): void +{ + $options = [ + Foo::fromString('c'), + Foo::fromString('b'), + Foo::fromString('a'), + ]; + + uasort($options, 'strnatcasecmp'); + usort($options, 'strnatcasecmp'); +}