diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 2ad962c8d32..598ac1a4a36 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 0af76250814..dcd57170eb1 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 7d1ed9047b5..2ae27db2962 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 0ba5e1a7a57..5e32e03f280 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; @@ -35,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; @@ -63,6 +65,8 @@ public function __construct( private bool $checkExtraArguments, #[AutowiredParameter] private bool $checkMissingTypehints, + #[AutowiredParameter(ref: '%featureToggles.reportMixedTernaryAndCoalesce%')] + private bool $reportMixedTernaryAndCoalesce, ) { } @@ -412,6 +416,29 @@ public function check( ->line($argumentLine) ->acceptsReasonsTip($accepts->reasons) ->build(); + } 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; + } + + $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,52 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + /** + * 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 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 getInlineBranchTypes(Expr $expr, Scope $scope): array + { + if ($expr instanceof Expr\Ternary) { + $truthyScope = $scope->filterByTruthyValue($expr->cond); + $falseyScope = $scope->filterByTruthyValue(new Expr\BooleanNot($expr->cond)); + + 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 ($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 [$scope->getType($expr)]; + } + /** * @return list|null Null when the expression is not a constant or bitmask of constants */ diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index 3cf21124155..ec2451f4e55 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: true, ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 79ce5dc0fb3..f6c89819ff0 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: 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 82d35191085..45f362d4b74 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: 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 a075fb6bb42..34dd31c1688 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: 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 f665cbe2f1c..8d79e869a14 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: 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 7d3e86139e9..093c445dd8f 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: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 2e4dc4462ea..e08eae0d7ca 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: 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 48384cb74da..3e2fd91a0d5 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: 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 0dc44d2ba63..351c3a4e31f 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: true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index cb7cc1558df..577fe2acf86 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: true, ), $ruleLevelHelper, reportMaybes: true, diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 1d4dd576aed..a191ab94771 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, ), ); } @@ -2978,4 +2979,39 @@ public function testBug11494(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug11281(): void + { + $this->analyse([__DIR__ . '/data/bug-11281.php'], [ + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, string given.', + 18, + ], + [ + '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 $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.', + 99, + ], + [ + 'Parameter #1 $i of function Bug11281Functions\takesInt expects int, bool|int|string given.', + 118, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index a6181d6732c..dc8d8722e1a 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: true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 661cf08dc5e..986d5554a64 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: 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 0025c2d6658..997899bed16 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: 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 d7a77b3d814..2005b845712 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: true, ), 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 new file mode 100644 index 00000000000..95de5cf9bfa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11281.php @@ -0,0 +1,119 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11281Functions; + +function takesInt(int $i): void +{ +} + +/** + * @param array $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. + takesInt(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. + takesInt(array_key_exists('key', $values) ? $values['key'] : 5); +} + +/** + * @param array $values + */ +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'); +} + +/** + * @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 +{ +} + +function benevolentBranchNotReported(mixed $value): void +{ + // 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, + ); +} + +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); +} + +function testMaybeError(mixed $mixed, string|bool|int $maybeInt): void +{ + takesInt( $mixed ?? $maybeInt); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index a6e55ecbd30..f00ae971b13 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 be9dde7dd3e..8d13a9367ff 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/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 517bed39133..d409b4ca80d 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: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck( diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php index 5cad631977b..5ed53d1459d 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/Methods/data/bug-11281-static.php b/tests/PHPStan/Rules/Methods/data/bug-11281-static.php new file mode 100644 index 00000000000..54f12a8f9e4 --- /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 00000000000..0f2d63ca8c9 --- /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'); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 78de39db51d..73490d5c598 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: 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 d007f02c39e..beb5d49acfc 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: 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 68caade88a1..34ac27ad1d5 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: true, ), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: false),