diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index bcef9878ee..bfc3292b0f 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 isBuiltin(): TrinaryLogic; + } diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 6c48e4b010..49eb7c066e 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -179,4 +179,14 @@ public function getAsserts(): Assertions return $this->function->getAsserts(); } + public function isBuiltin(): TrinaryLogic + { + $isBuiltin = $this->function->isBuiltin(); + if ($isBuiltin instanceof TrinaryLogic) { + return $isBuiltin; + } + + return TrinaryLogic::createFromBoolean($isBuiltin); + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php index 389893394e..322df831ab 100644 --- a/src/Reflection/ExtendedCallableFunctionVariant.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -37,6 +37,7 @@ public function __construct( private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, + private TrinaryLogic $isBuiltinCallable, private ?Assertions $assertions = null, ) { @@ -92,4 +93,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function isBuiltin(): TrinaryLogic + { + return $this->isBuiltinCallable; + } + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index d9b75bf3e0..1187a1b14a 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -130,6 +130,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->getUsedVariables(), $originalParametersAcceptor->acceptsNamedArguments(), $originalParametersAcceptor->mustUseReturnValue(), + $originalParametersAcceptor->isBuiltin(), $originalParametersAcceptor->getAsserts(), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 68fce995f8..5296f39e52 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -98,4 +98,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index b4c9b3a382..2178b2ca8c 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(); + $isBuiltin = 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()); + $isBuiltin = $isBuiltin->or($acceptor->isBuiltin()); } $isVariadic = $isVariadic || $acceptor->isVariadic(); @@ -860,6 +862,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $usedVariables, $acceptsNamedArguments, $mustUseReturnValue, + $isBuiltin, ); } @@ -897,6 +900,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara $acceptor->getUsedVariables(), $acceptor->acceptsNamedArguments(), $acceptor->mustUseReturnValue(), + $acceptor->isBuiltin(), $acceptor->getAsserts(), ); } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 6f816fa0ac..4a12f23a41 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -29,6 +29,7 @@ public function __construct( private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, + private TrinaryLogic $isBuiltinCallable, private ?Assertions $assertions = null, ) { @@ -124,4 +125,9 @@ public function getAsserts(): Assertions return $this->assertions ?? Assertions::createEmpty(); } + public function isBuiltin(): TrinaryLogic + { + return $this->isBuiltinCallable; + } + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 157368d4c0..e789dc4776 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -108,4 +108,9 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 380c87982a..0b3b293c29 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -180,11 +180,12 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSup $variantsResult = null; foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $isBuiltinCallable = $variant->isBuiltin()->yes(); $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$variant], false); if (!$variant instanceof CallableParametersAcceptor) { return IsSuperTypeOfResult::createNo([]); } - $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); + $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny, !$isBuiltinCallable); if ($variantsResult === null) { $variantsResult = $isSuperType; } else { @@ -404,6 +405,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function toNumber(): Type { return new ErrorType(); 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..97a9c24dff 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -138,6 +138,11 @@ public function getAsserts(): Assertions return $this->assertions; } + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + /** * @return array */ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 58faaefd2e..2d3d2d71fa 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2741,6 +2741,39 @@ 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'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11619Error(): void + { + $this->analyse([__DIR__ . '/data/bug-11619-error.php'], [ + [ + 'Parameter #1 $string1 of function strnatcasecmp expects string, Bug11619Error\Foo given.', + 32, + ], + [ + 'Parameter #2 $string2 of function strnatcasecmp expects string, Bug11619Error\Foo given.', + 32, + ], + ]); + } + + #[RequiresPhp('>= 8.1')] + public function testBug11619Typed(): void + { + $this->analyse([__DIR__ . '/data/bug-11619-typed.php'], []); + } + public function testBug13247(): void { $this->analyse([__DIR__ . '/data/bug-13247.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11619-error.php b/tests/PHPStan/Rules/Functions/data/bug-11619-error.php new file mode 100644 index 0000000000..19597f851d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619-error.php @@ -0,0 +1,34 @@ +value; + } + +} + +$options = [ + Foo::fromString('c'), + Foo::fromString('b'), + Foo::fromString('a'), + Foo::fromString('ccc'), + Foo::fromString('bcc'), +]; + + +uasort($options, fn($a, $b) => strnatcasecmp($a, $b)); + +var_export($options); 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..98108e0395 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619-strict.php @@ -0,0 +1,30 @@ += 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'); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11619-typed.php b/tests/PHPStan/Rules/Functions/data/bug-11619-typed.php new file mode 100644 index 0000000000..4340818336 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11619-typed.php @@ -0,0 +1,34 @@ +value; + } + +} + +$options = [ + Foo::fromString('c'), + Foo::fromString('b'), + Foo::fromString('a'), + Foo::fromString('ccc'), + Foo::fromString('bcc'), +]; + + +uasort($options, fn(string $a, string $b) => strnatcasecmp($a, $b)); + +var_export($options); 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'); +}