diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 16c0938b06..4308efe56c 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -363,7 +363,10 @@ public function check( !$parameter->passedByReference()->createsNewVariable() || (!$isBuiltin && !$argumentValueType instanceof ErrorType) ) { - $accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); + // @see https://github.com/php/php-src/issues/21568#issuecomment-4148832540 + $isStrictTypes = $scope->isDeclareStrictTypes() + && (!$isBuiltin || !$parameterType->isCallable()->yes()); + $accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $isStrictTypes); if (!$accepts->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 380c87982a..adad99a633 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -139,7 +139,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 @@ -151,7 +151,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny, bool $strictTypes = true): IsSuperTypeOfResult { $isCallable = new IsSuperTypeOfResult($type->isCallable(), []); if ($isCallable->no()) { @@ -184,7 +184,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/src/Type/ClosureType.php b/src/Type/ClosureType.php index 0a380dff93..23ee9dc162 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -225,7 +225,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult return $this->objectType->accepts($type, $strictTypes); } - return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult(); + return $this->isSuperTypeOfInternal($type, true, $strictTypes)->toAcceptsResult(); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult @@ -237,7 +237,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult return $this->isSuperTypeOfInternal($type, false); } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny, bool $strictTypes = true): IsSuperTypeOfResult { if ($type instanceof self) { $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters()); @@ -249,6 +249,7 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSup $this, $variant, $treatMixedAsAny, + $strictTypes, ); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 58faaefd2e..018d63e9f7 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2741,6 +2741,31 @@ public function testBug12363(): void $this->analyse([__DIR__ . '/data/bug-12363.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug11619(): void + { + $this->analyse([__DIR__ . '/data/bug-11619.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11619Strict(): void + { + $this->analyse([__DIR__ . '/data/bug-11619-strict.php'], [ + [ + 'Parameter #1 $string1 of function strnatcasecmp expects string, Bug11619Strict\Foo given.', + 31, + ], + [ + 'Parameter #2 $string2 of function strnatcasecmp expects string, Bug11619Strict\Foo given.', + 31, + ], + [ + 'Parameter #2 $f of function Bug11619Strict\customUsort expects callable(Stringable, Stringable): int, \'strnatcasecmp\' given.', + 58, + ], + ]); + } + public function testBug13247(): void { $this->analyse([__DIR__ . '/data/bug-13247.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11619-strict.php b/tests/PHPStan/Rules/Functions/data/bug-11619-strict.php new file mode 100644 index 0000000000..5c2277ad50 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619-strict.php @@ -0,0 +1,62 @@ += 8.1 + +namespace Bug11619Strict; + +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'); + + uasort($options, fn($a, $b) => strnatcasecmp($a, $b)); + uasort($options, fn(string $a, string $b) => strnatcasecmp($a, $b)); +} + +/** + * @param array<\Stringable> $a + * @param callable(\Stringable, \Stringable): int $f + */ +function customUsort(array &$a, callable $f): void +{ + for ($i = 1; $i < count($a); $i++) + for ($j = $i; $j > 0 && $f($a[$j-1], $a[$j]) > 0; $j--) + [$a[$j-1], $a[$j]] = [$a[$j], $a[$j-1]]; +} + +function userlandComparator(string $a, string $b): int { + return strnatcasecmp($a, $b); +} + +function test2(): void +{ + $options = [ + Foo::fromString('c'), + Foo::fromString('b'), + Foo::fromString('a'), + ]; + + customUsort($options, 'strnatcasecmp'); + + uasort($options, 'Bug11619Strict\userlandComparator'); + usort($options, 'Bug11619Strict\userlandComparator'); +} 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..984b59f042 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619.php @@ -0,0 +1,62 @@ += 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'); + + uasort($options, fn($a, $b) => strnatcasecmp($a, $b)); + uasort($options, fn(string $a, string $b) => strnatcasecmp($a, $b)); +} + +/** + * @param array<\Stringable> $a + * @param callable(\Stringable, \Stringable): int $f + */ +function customUsort(array &$a, callable $f): void +{ + for ($i = 1; $i < count($a); $i++) + for ($j = $i; $j > 0 && $f($a[$j-1], $a[$j]) > 0; $j--) + [$a[$j-1], $a[$j]] = [$a[$j], $a[$j-1]]; +} + +function userlandComparator(string $a, string $b): int { + return strnatcasecmp($a, $b); +} + +function test2(): void +{ + $options = [ + Foo::fromString('c'), + Foo::fromString('b'), + Foo::fromString('a'), + ]; + + customUsort($options, 'strnatcasecmp'); + + uasort($options, 'Bug11619\userlandComparator'); + usort($options, 'Bug11619\userlandComparator'); +}