From 80082797b37561d1c1b8f517505d5eefa99f89d4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 21 Oct 2025 11:23:56 +0200 Subject: [PATCH 1/5] Fix strtr inferences --- ...aceFunctionsDynamicReturnTypeExtension.php | 6 +++++ .../Rules/Methods/CallMethodsRuleTest.php | 10 ++++++++ .../PHPStan/Rules/Methods/data/bug-13708.php | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-13708.php diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 57e3b76710..1c75b27692 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -99,6 +99,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( if ($replaceArgumentType->isArray()->yes()) { $replaceArgumentType = $replaceArgumentType->getIterableValueType(); } + } elseif ($functionReflection->getName() === 'strtr' && isset($functionCall->getArgs()[1])) { + // `strtr` has two signatures: `strtr($string1, $string2, $string3)` and `strtr($string1, $array)` + $secondArgumentType = $scope->getType($functionCall->getArgs()[1]->value); + if ($secondArgumentType->isArray()->yes()) { + $replaceArgumentType = $secondArgumentType->getIterableValueType(); + } } } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 0dbebf0da3..294a72488e 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3656,6 +3656,16 @@ public function testBug5642(): void ]); } + public function testBug13708(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-13708.php'], []); + } + public function testBug3396(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-13708.php b/tests/PHPStan/Rules/Methods/data/bug-13708.php new file mode 100644 index 0000000000..867a56ff52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-13708.php @@ -0,0 +1,23 @@ +takeNonEmpty( + strtr('change {me}', ['{me}' => 'me']) + ); + } +} From 6dbf9e460239604cf523e5d3fe020d739cf3b65d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 21 Oct 2025 21:00:27 +0200 Subject: [PATCH 2/5] Add assertion --- tests/PHPStan/Analyser/nsrt/strtr.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/strtr.php b/tests/PHPStan/Analyser/nsrt/strtr.php index 5bc9fd6679..44dc4981c2 100644 --- a/tests/PHPStan/Analyser/nsrt/strtr.php +++ b/tests/PHPStan/Analyser/nsrt/strtr.php @@ -24,4 +24,16 @@ function doFoo(string $s, $nonEmptyString, $nonFalseyString) { assertType('non-empty-string', strtr($nonFalseyString, $s, $nonEmptyString)); assertType('non-falsy-string', strtr($nonFalseyString, $nonEmptyString, $nonFalseyString)); assertType('non-falsy-string', strtr($nonFalseyString, $nonFalseyString, $nonFalseyString)); + + assertType('string', strtr($s, [$s => $nonEmptyString])); + assertType('string', strtr($s, [$nonEmptyString => $nonEmptyString])); + assertType('string', strtr($s, [$nonFalseyString => $nonFalseyString])); + + assertType('non-empty-string', strtr($nonEmptyString, [$s => $nonEmptyString])); + assertType('non-empty-string', strtr($nonEmptyString, [$nonEmptyString => $nonEmptyString])); + assertType('non-empty-string', strtr($nonEmptyString, [$nonFalseyString => $nonFalseyString])); + + assertType('non-empty-string', strtr($nonFalseyString, [$s => $nonEmptyString])); + assertType('non-falsy-string', strtr($nonFalseyString, [$nonEmptyString => $nonFalseyString])); + assertType('non-falsy-string', strtr($nonFalseyString, [$nonFalseyString => $nonFalseyString])); } From 84f051411227ac30f75f9d3237ac55da68508187 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 10:55:03 +0100 Subject: [PATCH 3/5] Improve maybe inferences --- .../ReplaceFunctionsDynamicReturnTypeExtension.php | 5 ++++- tests/PHPStan/Analyser/nsrt/strtr.php | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 1c75b27692..202288dfe6 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -101,7 +101,10 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( } } elseif ($functionReflection->getName() === 'strtr' && isset($functionCall->getArgs()[1])) { // `strtr` has two signatures: `strtr($string1, $string2, $string3)` and `strtr($string1, $array)` - $secondArgumentType = $scope->getType($functionCall->getArgs()[1]->value); + $secondArgumentType = TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + $scope->getType($functionCall->getArgs()[1]->value) + ); if ($secondArgumentType->isArray()->yes()) { $replaceArgumentType = $secondArgumentType->getIterableValueType(); } diff --git a/tests/PHPStan/Analyser/nsrt/strtr.php b/tests/PHPStan/Analyser/nsrt/strtr.php index 44dc4981c2..0ac591b546 100644 --- a/tests/PHPStan/Analyser/nsrt/strtr.php +++ b/tests/PHPStan/Analyser/nsrt/strtr.php @@ -7,8 +7,9 @@ /** * @param non-empty-string $nonEmptyString * @param non-falsy-string $nonFalseyString + * @param mixed $mixed */ -function doFoo(string $s, $nonEmptyString, $nonFalseyString) { +function doFoo(string $s, $nonEmptyString, $nonFalseyString, $mixed) { assertType('string', strtr($s, 'f', 'b')); assertType('string', strtr($s, ['f' => 'b'])); assertType('string', strtr($s, ['f' => 'b', 'o' => 'a'])); @@ -36,4 +37,15 @@ function doFoo(string $s, $nonEmptyString, $nonFalseyString) { assertType('non-empty-string', strtr($nonFalseyString, [$s => $nonEmptyString])); assertType('non-falsy-string', strtr($nonFalseyString, [$nonEmptyString => $nonFalseyString])); assertType('non-falsy-string', strtr($nonFalseyString, [$nonFalseyString => $nonFalseyString])); + + assertType('non-empty-string', strtr($nonEmptyString, rand(0, 1) ? [$s => $nonEmptyString] : null)); + assertType('non-empty-string', strtr($nonEmptyString, rand(0, 1) ? [$nonEmptyString => $nonEmptyString] : null)); + assertType('non-empty-string', strtr($nonEmptyString, rand(0, 1) ? [$nonFalseyString => $nonFalseyString] : null)); + + assertType('non-empty-string', strtr($nonFalseyString, rand(0, 1) ? [$s => $nonEmptyString] : null)); + assertType('non-falsy-string', strtr($nonFalseyString, rand(0, 1) ? [$nonEmptyString => $nonFalseyString] : null)); + assertType('non-falsy-string', strtr($nonFalseyString, rand(0, 1) ? [$nonFalseyString => $nonFalseyString] : null)); + + assertType('string', strtr($nonEmptyString, $mixed)); + assertType('string', strtr($nonFalseyString, $mixed)); } From 3a569972189d8f253bddeae9b399adbb9bdad32e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 11:17:51 +0100 Subject: [PATCH 4/5] Fix cs --- src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 202288dfe6..b87b7d97fc 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -103,7 +103,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( // `strtr` has two signatures: `strtr($string1, $string2, $string3)` and `strtr($string1, $array)` $secondArgumentType = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - $scope->getType($functionCall->getArgs()[1]->value) + $scope->getType($functionCall->getArgs()[1]->value), ); if ($secondArgumentType->isArray()->yes()) { $replaceArgumentType = $secondArgumentType->getIterableValueType(); From 60e714b572ae2a5c100087b9dd3ef146c92e3fef Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Dec 2025 11:19:58 +0100 Subject: [PATCH 5/5] Try --- src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index b87b7d97fc..b5a24cdcf9 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -105,9 +105,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( new ArrayType(new MixedType(), new MixedType()), $scope->getType($functionCall->getArgs()[1]->value), ); - if ($secondArgumentType->isArray()->yes()) { - $replaceArgumentType = $secondArgumentType->getIterableValueType(); - } + $replaceArgumentType = $secondArgumentType->getIterableValueType(); } }