From 6324ede9bc7735c66ff1adb42b6f744e8c1d31ab Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:22:49 +0000 Subject: [PATCH 01/15] Cover subtype-absorbed try/catch results surviving as conditional targets under `if`, `&&`, and ternary guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The reported bug (a try-branch-assigned `array` collapsing to `mixed` inside `if (!$hasError)`) was already fixed by MutatingScope::createConditionalExpressions keeping subtype-absorbed variables as conditional targets instead of dropping them (commit c53a7b8a5 / PR #5876, already on this branch). - Extends tests/PHPStan/Analyser/nsrt/bug-11281.php with analogous control-flow forms that exercise the same branch-merge path: - positive guard `if ($ok)` - guard combined with `&&` (`if ($other && $ok)`) - guard read through a ternary (`$ok ? $values : []`) - Probed further siblings (switch/while/do-while, nested ifs, elseif chains, property targets, non-bool guards, super/subtype directions, and the array_key_exists/isset/`??` ternary family) — all already infer the precise type, so no source change was needed. --- tests/PHPStan/Analyser/nsrt/bug-11281.php | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11281.php b/tests/PHPStan/Analyser/nsrt/bug-11281.php index fa9d543098..3d9fc067dd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11281.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11281.php @@ -25,6 +25,48 @@ function hello2(string $values): void } } +/** + * The merged subtype-absorbed variable must survive as a conditional target + * regardless of which control-flow form reads the guard afterwards. + */ +function positiveGuard(string $values): void +{ + $ok = false; + try { + $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); + $ok = true; + } catch (\Throwable) { + } + if ($ok) { + assertType('array', $values); + } +} + +function nestedGuard(string $values, bool $other): void +{ + $ok = false; + try { + $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); + $ok = true; + } catch (\Throwable) { + } + if ($other && $ok) { + assertType('array', $values); + } +} + +function ternaryGuard(string $values): void +{ + $ok = false; + try { + $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); + $ok = true; + } catch (\Throwable) { + } + $result = $ok ? $values : []; + assertType('array', $result); +} + final class Hello { From 91c44f7ffec9e99b2277f41132c005862e12f8da Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 26 Jun 2026 08:40:58 +0000 Subject: [PATCH 02/15] Report ternary argument branches whose type is hidden by mixed normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a ternary is passed as a function/method argument and its resulting type normalizes to `mixed` (e.g. `mixed|string` collapses to `mixed`), a branch carrying a definitely-wrong value — such as a `string` passed to an `int` parameter — was silently accepted. Inspect the ternary's branch types separately in their narrowed scopes when the whole argument collapsed to `mixed`, so the offending branch is still reported. The branch inspection is gated on the mixed collapse, since a non-mixed resulting type keeps enough information for the regular argument check and branch-by-branch checking would otherwise flag legitimate normalizations (e.g. an `ArrayIterator` branch absorbed into `Traversable`). Closes https://github.com/phpstan/phpstan/issues/11281 Co-Authored-By: Claude Opus 4.8 --- src/Rules/FunctionCallParametersCheck.php | 56 +++++++++++++++++++ .../CallToFunctionParametersRuleTest.php | 14 +++++ .../Rules/Functions/data/bug-11281.php | 34 +++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-11281.php diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 0ba5e1a7a5..ac0b1bda59 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -26,6 +26,7 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -412,6 +413,32 @@ public function check( ->line($argumentLine) ->acceptsReasonsTip($accepts->reasons) ->build(); + } elseif ($argumentValue instanceof Expr\Ternary && $argumentValueType instanceof MixedType) { + // Type normalization can collapse a ternary's resulting type to + // mixed (e.g. mixed|string becomes mixed), hiding a branch whose + // type is not accepted. When the whole argument collapsed to mixed, + // inspect the branch types separately so such a passed value is + // still reported. A non-mixed resulting type keeps enough + // information for the regular check above, so branch inspection + // would only introduce false positives there. + foreach ($this->getTernaryBranchTypes($argumentValue, $scope) as $branchType) { + $branchAccepts = $this->ruleLevelHelper->accepts($parameterType, $branchType, $isStrictTypes); + if ($branchAccepts->result) { + continue; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $branchType); + $errors[] = RuleErrorBuilder::message(sprintf( + $wrongArgumentTypeMessage, + $this->describeParameter($parameter, $argumentName ?? $i + 1), + $parameterType->describe($verbosityLevel), + $branchType->describe($verbosityLevel), + )) + ->identifier('argument.type') + ->line($argumentLine) + ->acceptsReasonsTip($branchAccepts->reasons) + ->build(); + } } } @@ -801,6 +828,35 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + /** + * Collects the leaf types of a ternary's branches, each resolved in the scope + * narrowed by the controlling condition. Nested ternaries are flattened so every + * value the expression can produce is represented by its own (un-normalized) type. + * + * @return list + */ + private function getTernaryBranchTypes(Expr\Ternary $ternary, Scope $scope): array + { + $truthyScope = $scope->filterByTruthyValue($ternary->cond); + $falseyScope = $scope->filterByFalseyValue($ternary->cond); + + if ($ternary->if === null) { + $ifTypes = [TypeCombinator::removeFalsey($truthyScope->getType($ternary->cond))]; + } elseif ($ternary->if instanceof Expr\Ternary) { + $ifTypes = $this->getTernaryBranchTypes($ternary->if, $truthyScope); + } else { + $ifTypes = [$truthyScope->getType($ternary->if)]; + } + + if ($ternary->else instanceof Expr\Ternary) { + $elseTypes = $this->getTernaryBranchTypes($ternary->else, $falseyScope); + } else { + $elseTypes = [$falseyScope->getType($ternary->else)]; + } + + return array_merge($ifTypes, $elseTypes); + } + /** * @return list|null Null when the expression is not a constant or bitmask of constants */ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 1d4dd576ae..a2a25e1fd4 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2978,4 +2978,18 @@ public function testBug11494(): void ]); } + public function testBug11281(): void + { + $this->analyse([__DIR__ . '/data/bug-11281.php'], [ + [ + 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', + 16, + ], + [ + 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php new file mode 100644 index 0000000000..69ee17fb57 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -0,0 +1,34 @@ + $values + */ +function test(array $values): void +{ + // The ternary's resulting type normalizes to mixed (mixed|string), + // but the else branch is definitely a string passed to an int parameter. + sayHello(array_key_exists('key', $values) ? $values['key'] : ' a string'); +} + +/** + * @param array $values + */ +function noError(array $values): void +{ + // Numeric-ish coercible branches must not be flagged. + sayHello(array_key_exists('key', $values) ? $values['key'] : 5); +} + +/** + * @param array $values + */ +function nested(array $values, bool $other, bool $another): void +{ + sayHello($other ? $values['key'] : ($another ? 1 : ' nested string')); +} From d6764511808be0c8431f125232b5f362b20ac779 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 26 Jun 2026 13:40:36 +0000 Subject: [PATCH 03/15] Narrow ternary else branch via negated condition in branch-type inspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getTernaryBranchTypes resolved the else branch with filterByFalseyValue($cond), which narrows asymmetrically for conditions whose stub only declares @phpstan-assert-if-true (e.g. is_resource()). The falsey scope then diverged from the type the ternary actually produces — is_resource($value) ? stream_get_contents($value) : $value yielded a spurious `resource` for $value instead of the `mixed` the ternary really produces, triggering a false-positive argument.type error when passed to a string parameter. Resolve the else branch with filterByTruthyValue(!cond) instead, mirroring how TernaryHandler::specifyTypes models the else branch (BooleanNot of the condition). This keeps the #11281 fix intact while removing the false positive. Co-Authored-By: Claude Opus 4.8 --- src/Rules/FunctionCallParametersCheck.php | 9 ++++++++- .../CallToFunctionParametersRuleTest.php | 4 ++-- .../Rules/Functions/data/bug-11281.php | 20 ++++++++++++++++++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index ac0b1bda59..3ea9223f7c 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -833,12 +833,19 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu * narrowed by the controlling condition. Nested ternaries are flattened so every * value the expression can produce is represented by its own (un-normalized) type. * + * The else branch is narrowed by the negated condition (`filterByTruthyValue` of + * `!cond`) rather than `filterByFalseyValue($cond)`, mirroring how + * TernaryHandler::specifyTypes models the else branch. Some conditions (e.g. + * `is_resource()`, whose stub only declares `@phpstan-assert-if-true`) narrow + * asymmetrically, so the falsey scope would otherwise diverge from the type the + * ternary actually produces and report spurious branch types. + * * @return list */ private function getTernaryBranchTypes(Expr\Ternary $ternary, Scope $scope): array { $truthyScope = $scope->filterByTruthyValue($ternary->cond); - $falseyScope = $scope->filterByFalseyValue($ternary->cond); + $falseyScope = $scope->filterByTruthyValue(new Expr\BooleanNot($ternary->cond)); if ($ternary->if === null) { $ifTypes = [TypeCombinator::removeFalsey($truthyScope->getType($ternary->cond))]; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a2a25e1fd4..e324e3c758 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2983,11 +2983,11 @@ public function testBug11281(): void $this->analyse([__DIR__ . '/data/bug-11281.php'], [ [ 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', - 16, + 18, ], [ 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', - 33, + 35, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index 69ee17fb57..c6a8d0e650 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug11281Functions; @@ -32,3 +34,19 @@ function nested(array $values, bool $other, bool $another): void { sayHello($other ? $values['key'] : ($another ? 1 : ' nested string')); } + +function expectsString(string $s): void +{ +} + +function falsePositive(mixed $value): void +{ + // is_resource() only narrows asymmetrically (@phpstan-assert-if-true), so the + // else branch must keep the type the ternary actually produces (mixed, accepted), + // not a spurious narrowing. No error should be reported here. + expectsString( + is_resource($value) + ? stream_get_contents($value) + : $value, + ); +} From 6f6c5619388e9fe1ef46d849eb0810ebabcd9b0e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 26 Jun 2026 13:55:09 +0000 Subject: [PATCH 04/15] Clarify ternary branch-inspection test: benevolent vs strict string|false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous `falsePositive` case used `stream_get_contents()`, which returns a *benevolent* string|false. PHPStan intentionally accepts benevolent unions for a string parameter, so the equivalent direct call is error-free too — and reporting the branch would re-introduce the very pg_escape_bytea false positive this branch inspection guards against (its second `string` parameter is fed the same `stream_get_contents()` benevolent union). Document that no-error case precisely and add a companion `strictFalseBranchReported` showing that a *strict* (non-benevolent) string|false branch under the same asymmetric `is_resource()` narrowing IS reported. This makes branch inspection's consistency with PHPStan's standard accept semantics explicit. Co-Authored-By: Claude Opus 4.8 --- .../CallToFunctionParametersRuleTest.php | 4 ++++ .../Rules/Functions/data/bug-11281.php | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index e324e3c758..7df47077e5 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2989,6 +2989,10 @@ public function testBug11281(): void 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', 35, ], + [ + 'Parameter #1 $s of function Bug11281Functions\expectsString expects string, string|false given.', + 64, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index c6a8d0e650..78cd18e10e 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -39,14 +39,30 @@ function expectsString(string $s): void { } -function falsePositive(mixed $value): void +function benevolentBranchNotReported(mixed $value): void { - // is_resource() only narrows asymmetrically (@phpstan-assert-if-true), so the - // else branch must keep the type the ternary actually produces (mixed, accepted), - // not a spurious narrowing. No error should be reported here. + // stream_get_contents() returns a *benevolent* string|false. PHPStan intentionally + // accepts benevolent unions for a string parameter, so the equivalent direct call + // expectsString(stream_get_contents($r)) is error-free too — reporting it here would + // re-introduce the pg_escape_bytea false positive this branch inspection guards + // against. is_resource() also narrows asymmetrically (@phpstan-assert-if-true only), + // so the else branch must keep the type the ternary actually produces (mixed, + // accepted) instead of a spurious narrowing. No error should be reported here. expectsString( is_resource($value) ? stream_get_contents($value) : $value, ); } + +function strictFalseBranchReported(mixed $value, string|false $sf): void +{ + // A *strict* (non-benevolent) string|false branch is not accepted by the string + // parameter, so branch inspection reports it even though is_resource() narrows + // asymmetrically and the ternary's resulting type normalizes to mixed. + expectsString( + is_resource($value) + ? $sf + : $value, + ); +} From d0c9f4dff03117bbcb13c81302fec0f150b41908 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 10:46:27 +0200 Subject: [PATCH 05/15] revert nsrt tests.. this is a rule only change without impact on inference --- tests/PHPStan/Analyser/nsrt/bug-11281.php | 42 ----------------------- 1 file changed, 42 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11281.php b/tests/PHPStan/Analyser/nsrt/bug-11281.php index 3d9fc067dd..fa9d543098 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11281.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11281.php @@ -25,48 +25,6 @@ function hello2(string $values): void } } -/** - * The merged subtype-absorbed variable must survive as a conditional target - * regardless of which control-flow form reads the guard afterwards. - */ -function positiveGuard(string $values): void -{ - $ok = false; - try { - $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); - $ok = true; - } catch (\Throwable) { - } - if ($ok) { - assertType('array', $values); - } -} - -function nestedGuard(string $values, bool $other): void -{ - $ok = false; - try { - $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); - $ok = true; - } catch (\Throwable) { - } - if ($other && $ok) { - assertType('array', $values); - } -} - -function ternaryGuard(string $values): void -{ - $ok = false; - try { - $values = array_map(static fn ($item) => Hello::fromObject($item), json_decode($values)); - $ok = true; - } catch (\Throwable) { - } - $result = $ok ? $values : []; - assertType('array', $result); -} - final class Hello { From 884b3cd53fb503590b6334164ee460777316b6f5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 10:50:39 +0200 Subject: [PATCH 06/15] Update CallToFunctionParametersRuleTest.php --- .../PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 7df47077e5..1dd911557f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2978,6 +2978,7 @@ public function testBug11494(): void ]); } + #[RequiresPhp('>= 8.0.0')] public function testBug11281(): void { $this->analyse([__DIR__ . '/data/bug-11281.php'], [ From aeeab6b33b355dce4700e822b2134649cfb5bf50 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 10:51:37 +0200 Subject: [PATCH 07/15] Update FunctionCallParametersCheck.php --- src/Rules/FunctionCallParametersCheck.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 3ea9223f7c..b28c50f849 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -414,13 +414,6 @@ public function check( ->acceptsReasonsTip($accepts->reasons) ->build(); } elseif ($argumentValue instanceof Expr\Ternary && $argumentValueType instanceof MixedType) { - // Type normalization can collapse a ternary's resulting type to - // mixed (e.g. mixed|string becomes mixed), hiding a branch whose - // type is not accepted. When the whole argument collapsed to mixed, - // inspect the branch types separately so such a passed value is - // still reported. A non-mixed resulting type keeps enough - // information for the regular check above, so branch inspection - // would only introduce false positives there. foreach ($this->getTernaryBranchTypes($argumentValue, $scope) as $branchType) { $branchAccepts = $this->ruleLevelHelper->accepts($parameterType, $branchType, $isStrictTypes); if ($branchAccepts->result) { From 5620a89ce7d16ef09cb7238d8d6ac90ed1d50bf4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 10:55:50 +0200 Subject: [PATCH 08/15] Update bug-11281.php --- tests/PHPStan/Rules/Functions/data/bug-11281.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index 78cd18e10e..e3df13314c 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -66,3 +66,14 @@ function strictFalseBranchReported(mixed $value, string|false $sf): void : $value, ); } + +function ternaryInVariable(mixed $value, string|false $sf): void +{ + // no error because the ternary is not used inline as function argument, + // but stored in a variable, which kicks in type normalization. + $result = is_resource($value) + ? $sf + : $value; + + expectsString($result); +} From fe09fa42c4aac4a1cf10d7e73beb260625b6a324 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 10:59:38 +0200 Subject: [PATCH 09/15] Update bug-11281.php --- tests/PHPStan/Rules/Functions/data/bug-11281.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index e3df13314c..667fa07962 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -4,7 +4,7 @@ namespace Bug11281Functions; -function sayHello(int $i): void +function takesInt(int $i): void { } @@ -15,7 +15,7 @@ function test(array $values): void { // The ternary's resulting type normalizes to mixed (mixed|string), // but the else branch is definitely a string passed to an int parameter. - sayHello(array_key_exists('key', $values) ? $values['key'] : ' a string'); + takesInt(array_key_exists('key', $values) ? $values['key'] : ' a string'); } /** @@ -24,7 +24,7 @@ function test(array $values): void function noError(array $values): void { // Numeric-ish coercible branches must not be flagged. - sayHello(array_key_exists('key', $values) ? $values['key'] : 5); + takesInt(array_key_exists('key', $values) ? $values['key'] : 5); } /** @@ -32,7 +32,7 @@ function noError(array $values): void */ function nested(array $values, bool $other, bool $another): void { - sayHello($other ? $values['key'] : ($another ? 1 : ' nested string')); + takesInt($other ? $values['key'] : ($another ? 1 : ' nested string')); } function expectsString(string $s): void From 4043254a30078534438ceb1bd6ba30fcf021f3d8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 11:06:06 +0200 Subject: [PATCH 10/15] Update CallToFunctionParametersRuleTest.php --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 1dd911557f..92ff28f323 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2983,11 +2983,11 @@ public function testBug11281(): void { $this->analyse([__DIR__ . '/data/bug-11281.php'], [ [ - 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', 18, ], [ - 'Parameter #1 $i of function Bug11281Functions\sayHello expects int, string given.', + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', 35, ], [ From 68c73eaadea23f56cea32d2a6fab1ee9822ac511 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 27 Jun 2026 09:12:34 +0000 Subject: [PATCH 11/15] Cover short ternary (?:) in ternary branch inspection test The short-ternary form `$mixed ?: ' a string'` already flows through getTernaryBranchTypes() via the `$ternary->if === null` branch, so the else branch is reported when it doesn't match the parameter type. Add a regression case locking in this coverage. Co-Authored-By: Claude Opus 4.8 --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 6 +++++- tests/PHPStan/Rules/Functions/data/bug-11281.php | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 92ff28f323..67c7e7e869 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2990,9 +2990,13 @@ public function testBug11281(): void 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', 35, ], + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', + 42, + ], [ 'Parameter #1 $s of function Bug11281Functions\expectsString expects string, string|false given.', - 64, + 71, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index 667fa07962..a1e70eccc9 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -35,6 +35,13 @@ function nested(array $values, bool $other, bool $another): void takesInt($other ? $values['key'] : ($another ? 1 : ' nested string')); } +function testShortTernary(mixed $mixed): void +{ + // Short ternary (?:): the if-branch reuses the (truthy-narrowed) condition value, + // the else branch is a string passed to an int parameter, so it must be reported. + takesInt($mixed ?: ' a string'); +} + function expectsString(string $s): void { } From 69b5b4afb4baa515b7a348c84777a77c430a2a6f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 27 Jun 2026 09:55:39 +0000 Subject: [PATCH 12/15] Support null-coalesce in inline mixed argument check, gate behind bleedingEdge Extends the inline ternary argument inspection to the null-coalesce operator (??) and moves the whole feature behind a new bleedingEdge feature toggle (reportMixedTernaryAndCoalesce). When an inline ternary or coalesce argument collapses to plain mixed, each branch is now inspected against the parameter type so obvious errors surface on lower levels. Co-Authored-By: Claude Opus 4.8 --- conf/bleedingEdge.neon | 1 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/FunctionCallParametersCheck.php | 59 ++++++++++++------- .../Analyser/Bug9307CallMethodsRuleTest.php | 1 + .../Rules/Classes/ClassAttributesRuleTest.php | 1 + .../ClassConstantAttributesRuleTest.php | 1 + .../ForbiddenNameCheckExtensionRuleTest.php | 1 + .../Rules/Classes/InstantiationRuleTest.php | 1 + .../Constants/ConstantAttributesRuleTest.php | 1 + .../EnumCases/EnumCaseAttributesRuleTest.php | 1 + .../ArrowFunctionAttributesRuleTest.php | 1 + .../PHPStan/Rules/Functions/Bug14844Test.php | 1 + .../Rules/Functions/CallCallablesRuleTest.php | 1 + .../CallToFunctionParametersRuleTest.php | 11 +++- .../Rules/Functions/CallUserFuncRuleTest.php | 1 + .../Functions/ClosureAttributesRuleTest.php | 1 + .../Functions/FunctionAttributesRuleTest.php | 1 + .../Functions/ParamAttributesRuleTest.php | 1 + .../Rules/Functions/data/bug-11281.php | 28 +++++++++ .../Methods/MethodAttributesRuleTest.php | 1 + ...thPossiblyRenamedNamedArgumentRuleTest.php | 2 +- .../Properties/PropertyAttributesRuleTest.php | 1 + .../PropertyHookAttributesRuleTest.php | 1 + .../Rules/Traits/TraitAttributesRuleTest.php | 1 + 25 files changed, 98 insertions(+), 23 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 2ad962c8d3..598ac1a4a3 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -22,3 +22,4 @@ parameters: checkDynamicConstantNameValues: true unusedLabel: true newOnNonObject: true + reportMixedTernaryAndCoalesce: true diff --git a/conf/config.neon b/conf/config.neon index 0af7625081..dcd57170eb 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -49,6 +49,7 @@ parameters: checkDynamicConstantNameValues: false unusedLabel: false newOnNonObject: false + reportMixedTernaryAndCoalesce: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 7d1ed9047b..2ae27db296 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -51,6 +51,7 @@ parametersSchema: checkDynamicConstantNameValues: bool() unusedLabel: bool() newOnNonObject: bool() + reportMixedTernaryAndCoalesce: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index b28c50f849..5e32e03f28 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -36,6 +36,7 @@ use function array_fill; use function array_key_exists; use function array_last; +use function array_map; use function array_merge; use function count; use function implode; @@ -64,6 +65,8 @@ public function __construct( private bool $checkExtraArguments, #[AutowiredParameter] private bool $checkMissingTypehints, + #[AutowiredParameter(ref: '%featureToggles.reportMixedTernaryAndCoalesce%')] + private bool $reportMixedTernaryAndCoalesce, ) { } @@ -413,8 +416,12 @@ public function check( ->line($argumentLine) ->acceptsReasonsTip($accepts->reasons) ->build(); - } elseif ($argumentValue instanceof Expr\Ternary && $argumentValueType instanceof MixedType) { - foreach ($this->getTernaryBranchTypes($argumentValue, $scope) as $branchType) { + } elseif ( + $this->reportMixedTernaryAndCoalesce + && ($argumentValue instanceof Expr\Ternary || $argumentValue instanceof Expr\BinaryOp\Coalesce) + && $argumentValueType instanceof MixedType + ) { + foreach ($this->getInlineBranchTypes($argumentValue, $scope) as $branchType) { $branchAccepts = $this->ruleLevelHelper->accepts($parameterType, $branchType, $isStrictTypes); if ($branchAccepts->result) { continue; @@ -822,39 +829,49 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu } /** - * Collects the leaf types of a ternary's branches, each resolved in the scope - * narrowed by the controlling condition. Nested ternaries are flattened so every - * value the expression can produce is represented by its own (un-normalized) type. + * Collects the leaf types an inline ternary (`? :`, `?:`) or null-coalesce (`??`) + * expression can produce, each resolved in the scope narrowed by the controlling + * condition. Nested ternaries and coalesces (including mixed nesting) are flattened + * so every value the expression can produce is represented by its own + * (un-normalized) type. Plain leaf expressions resolve to their scope type. * - * The else branch is narrowed by the negated condition (`filterByTruthyValue` of - * `!cond`) rather than `filterByFalseyValue($cond)`, mirroring how + * The ternary else branch is narrowed by the negated condition (`filterByTruthyValue` + * of `!cond`) rather than `filterByFalseyValue($cond)`, mirroring how * TernaryHandler::specifyTypes models the else branch. Some conditions (e.g. * `is_resource()`, whose stub only declares `@phpstan-assert-if-true`) narrow * asymmetrically, so the falsey scope would otherwise diverge from the type the * ternary actually produces and report spurious branch types. * + * The coalesce left operand contributes its non-null type, since `$a ?? $b` only + * yields `$a` when it is set and not null. + * * @return list */ - private function getTernaryBranchTypes(Expr\Ternary $ternary, Scope $scope): array + private function getInlineBranchTypes(Expr $expr, Scope $scope): array { - $truthyScope = $scope->filterByTruthyValue($ternary->cond); - $falseyScope = $scope->filterByTruthyValue(new Expr\BooleanNot($ternary->cond)); + if ($expr instanceof Expr\Ternary) { + $truthyScope = $scope->filterByTruthyValue($expr->cond); + $falseyScope = $scope->filterByTruthyValue(new Expr\BooleanNot($expr->cond)); - if ($ternary->if === null) { - $ifTypes = [TypeCombinator::removeFalsey($truthyScope->getType($ternary->cond))]; - } elseif ($ternary->if instanceof Expr\Ternary) { - $ifTypes = $this->getTernaryBranchTypes($ternary->if, $truthyScope); - } else { - $ifTypes = [$truthyScope->getType($ternary->if)]; + if ($expr->if === null) { + $ifTypes = [TypeCombinator::removeFalsey($truthyScope->getType($expr->cond))]; + } else { + $ifTypes = $this->getInlineBranchTypes($expr->if, $truthyScope); + } + + return array_merge($ifTypes, $this->getInlineBranchTypes($expr->else, $falseyScope)); } - if ($ternary->else instanceof Expr\Ternary) { - $elseTypes = $this->getTernaryBranchTypes($ternary->else, $falseyScope); - } else { - $elseTypes = [$falseyScope->getType($ternary->else)]; + if ($expr instanceof Expr\BinaryOp\Coalesce) { + $leftTypes = array_map( + static fn (Type $type): Type => TypeCombinator::removeNull($type), + $this->getInlineBranchTypes($expr->left, $scope), + ); + + return array_merge($leftTypes, $this->getInlineBranchTypes($expr->right, $scope)); } - return array_merge($ifTypes, $elseTypes); + return [$scope->getType($expr)]; } /** diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index 3cf2112415..6a7a97ea68 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -48,6 +48,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 79ce5dc0fb..8c5b89b8ea 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -51,6 +51,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index 82d3519108..f8e450ab20 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index a075fb6bb4..eacffdb19a 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -47,6 +47,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index f665cbe2f1..0bc4ab8525 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -49,6 +49,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true), diff --git a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php index 7d3e86139e..dd3632032d 100644 --- a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php @@ -52,6 +52,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 2e4dc4462e..7b8397596a 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -47,6 +47,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 48384cb74d..64656b824c 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/Bug14844Test.php b/tests/PHPStan/Rules/Functions/Bug14844Test.php index 0dc44d2ba6..2ee2ab921d 100644 --- a/tests/PHPStan/Rules/Functions/Bug14844Test.php +++ b/tests/PHPStan/Rules/Functions/Bug14844Test.php @@ -41,6 +41,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), ); } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index cb7cc1558d..5216507a50 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -45,6 +45,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), $ruleLevelHelper, reportMaybes: true, diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 67c7e7e869..bd74373218 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -49,6 +49,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: true, ), ); } @@ -2994,9 +2995,17 @@ public function testBug11281(): void 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', 42, ], + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', + 52, + ], + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', + 70, + ], [ 'Parameter #1 $s of function Bug11281Functions\expectsString expects string, string|false given.', - 71, + 99, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index a6181d6732..28455affe7 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -41,6 +41,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), ); } diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 661cf08dc5..5bf778dca8 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 0025c2d665..97e4d13fec 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index d7a77b3d81..b8b39388e2 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index a1e70eccc9..76600165f6 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -42,6 +42,34 @@ function testShortTernary(mixed $mixed): void takesInt($mixed ?: ' a string'); } +/** + * @param array $values + */ +function testCoalesce(array $values): void +{ + // The coalesce's resulting type normalizes to mixed (mixed|string), + // but the else branch is definitely a string passed to an int parameter. + takesInt($values['key'] ?? ' a string'); +} + +/** + * @param array $values + */ +function noErrorCoalesce(array $values): void +{ + // Numeric-ish coercible branch must not be flagged. + takesInt($values['key'] ?? 5); +} + +/** + * @param array $values + * @param array $other + */ +function nestedCoalesce(array $values, array $other): void +{ + takesInt($values['key'] ?? $other['key'] ?? ' a string'); +} + function expectsString(string $s): void { } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 517bed3913..39595a95b5 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php index 5cad631977..5ed53d1459 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php @@ -32,7 +32,7 @@ protected function getRule(): Rule return new CompositeRule([ new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true, false), ), new OverridingMethodRule( $phpVersion, diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 78de39db51..f7a4dee958 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -48,6 +48,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index d007f02c39..943fe85258 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -46,6 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index 68caade88a..0c983a948d 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -53,6 +53,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: false, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), From 30189071919bdcbc5926a02704b7c23aca44891d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 27 Jun 2026 09:55:44 +0000 Subject: [PATCH 13/15] Add method-call and static-call coverage for inline mixed argument check Co-Authored-By: Claude Opus 4.8 --- .../Rules/Methods/CallMethodsRuleTest.php | 22 +++++++++++++ .../Methods/CallStaticMethodsRuleTest.php | 20 ++++++++++++ .../Rules/Methods/data/bug-11281-static.php | 32 +++++++++++++++++++ .../PHPStan/Rules/Methods/data/bug-11281.php | 32 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-11281-static.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-11281.php diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index a6e55ecbd3..f00ae971b1 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -29,6 +29,8 @@ class CallMethodsRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; + private bool $reportMixedTernaryAndCoalesce = false; + protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); @@ -59,6 +61,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: $this->reportMixedTernaryAndCoalesce, ), ); } @@ -4235,4 +4238,23 @@ public function testBug14808(): void $this->analyse([__DIR__ . '/data/bug-14808.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug11281(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->reportMixedTernaryAndCoalesce = true; + $this->analyse([__DIR__ . '/data/bug-11281.php'], [ + [ + 'Parameter #1 $i of method Bug11281Methods\Foo::takesInt() expects int, string given.', + 21, + ], + [ + 'Parameter #1 $i of method Bug11281Methods\Foo::takesInt() expects int, string given.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index be9dde7dd3..8d13a9367f 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -29,6 +29,8 @@ class CallStaticMethodsRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; + private bool $reportMixedTernaryAndCoalesce = false; + protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); @@ -70,6 +72,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, + reportMixedTernaryAndCoalesce: $this->reportMixedTernaryAndCoalesce, ), ); } @@ -1057,4 +1060,21 @@ public function testClassExistOnCall(): void $this->analyse([__DIR__ . '/data/class-exists-on-static-call.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug11281(): void + { + $this->checkThisOnly = false; + $this->reportMixedTernaryAndCoalesce = true; + $this->analyse([__DIR__ . '/data/bug-11281-static.php'], [ + [ + 'Parameter #1 $i of static method Bug11281StaticMethods\Foo::takesInt() expects int, string given.', + 21, + ], + [ + 'Parameter #1 $i of static method Bug11281StaticMethods\Foo::takesInt() expects int, string given.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11281-static.php b/tests/PHPStan/Rules/Methods/data/bug-11281-static.php new file mode 100644 index 0000000000..54f12a8f9e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11281-static.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11281StaticMethods; + +class Foo +{ + + public static function takesInt(int $i): void + { + } + + /** + * @param array $values + */ + public static function ternary(array $values): void + { + // The ternary's resulting type normalizes to mixed (mixed|string), + // but the else branch is definitely a string passed to an int parameter. + self::takesInt(array_key_exists('key', $values) ? $values['key'] : ' a string'); + } + + /** + * @param array $values + */ + public static function coalesce(array $values): void + { + self::takesInt($values['key'] ?? ' a string'); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-11281.php b/tests/PHPStan/Rules/Methods/data/bug-11281.php new file mode 100644 index 0000000000..0f2d63ca8c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11281.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11281Methods; + +class Foo +{ + + public function takesInt(int $i): void + { + } + + /** + * @param array $values + */ + public function ternary(array $values): void + { + // The ternary's resulting type normalizes to mixed (mixed|string), + // but the else branch is definitely a string passed to an int parameter. + $this->takesInt(array_key_exists('key', $values) ? $values['key'] : ' a string'); + } + + /** + * @param array $values + */ + public function coalesce(array $values): void + { + $this->takesInt($values['key'] ?? ' a string'); + } + +} From ef860ba07348da40699863cf8257a5c7f6ebee88 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 12:11:59 +0200 Subject: [PATCH 14/15] enable reportMixedTernaryAndCoalesce by default in tests --- tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php | 2 +- tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php | 2 +- .../Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php | 2 +- tests/PHPStan/Rules/Classes/InstantiationRuleTest.php | 2 +- tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php | 2 +- .../PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Functions/Bug14844Test.php | 2 +- tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php | 2 +- tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php | 2 +- tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php | 2 +- .../PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php | 2 +- tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index 6a7a97ea68..ec2451f4e5 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -48,7 +48,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 8c5b89b8ea..f6c89819ff 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -51,7 +51,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index f8e450ab20..45f362d4b7 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index eacffdb19a..34dd31c168 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -47,7 +47,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 0bc4ab8525..8d79e869a1 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -49,7 +49,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true), diff --git a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php index dd3632032d..093c445dd8 100644 --- a/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Constants/ConstantAttributesRuleTest.php @@ -52,7 +52,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 7b8397596a..e08eae0d7c 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -47,7 +47,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 64656b824c..3e2fd91a0d 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/Bug14844Test.php b/tests/PHPStan/Rules/Functions/Bug14844Test.php index 2ee2ab921d..351c3a4e31 100644 --- a/tests/PHPStan/Rules/Functions/Bug14844Test.php +++ b/tests/PHPStan/Rules/Functions/Bug14844Test.php @@ -41,7 +41,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 5216507a50..577fe2acf8 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -45,7 +45,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), $ruleLevelHelper, reportMaybes: true, diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index 28455affe7..dc8d8722e1 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -41,7 +41,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 5bf778dca8..986d5554a6 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 97e4d13fec..997899bed1 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index b8b39388e2..2005b84571 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 39595a95b5..d409b4ca80 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index f7a4dee958..73490d5c59 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -48,7 +48,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index 943fe85258..beb5d49acf 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -46,7 +46,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index 0c983a948d..34ac27ad1d 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -53,7 +53,7 @@ protected function getRule(): Rule checkArgumentsPassedByReference: true, checkExtraArguments: true, checkMissingTypehints: true, - reportMixedTernaryAndCoalesce: false, + reportMixedTernaryAndCoalesce: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false), From a6b7c9da7c54bdbdfa42509ce7a806dfdb92d4ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 27 Jun 2026 12:25:20 +0200 Subject: [PATCH 15/15] test a maybe error --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 4 ++++ tests/PHPStan/Rules/Functions/data/bug-11281.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index bd74373218..a191ab9477 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3007,6 +3007,10 @@ public function testBug11281(): void 'Parameter #1 $s of function Bug11281Functions\expectsString expects string, string|false given.', 99, ], + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, bool|int|string given.', + 118, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11281.php b/tests/PHPStan/Rules/Functions/data/bug-11281.php index 76600165f6..95de5cf9bf 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-11281.php +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -112,3 +112,8 @@ function ternaryInVariable(mixed $value, string|false $sf): void expectsString($result); } + +function testMaybeError(mixed $mixed, string|bool|int $maybeInt): void +{ + takesInt( $mixed ?? $maybeInt); +}