diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php index 0a3ac3b3b62..eb5076fa4c2 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php @@ -161,17 +161,20 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getString(''); } - // these placeholders are too complex to handle for now - if (preg_match( - '/%(?:\d+\$)?[-+]?(?:\d+|\*)(?:\.(?:\d+|\*))?[bcdouxXeEfFgGhHs]/', + // these placeholders still fall back to a generic return type, + // but we validate their format and argument count first + $has_complex_placeholder = preg_match( + '/%(?:\d+\$)?[-+]?(?:' + . '(?:\d+|\*(?:\d+\$)?)(?:\.(?:\d+|\*(?:\d+\$)?))?' + . '|\.\*(?:\d+\$)?' + . ')[bcdouxXeEfFgGhHs]/', $type->getSingleStringLiteral()->value, - ) === 1) { - return null; - } + ) === 1; // assume a random, high number for tests $provided_placeholders_count = $has_splat_args === true ? 100 : count($call_args) - 1; - $dummy = array_fill(0, $provided_placeholders_count, ''); + // Use integer dummies for * width/precision so PHP validates arity without raising a false ValueError. + $dummy = array_fill(0, $provided_placeholders_count, $has_complex_placeholder ? 0 : ''); // check if we have enough/too many arguments and a valid format $initial_result = null; @@ -287,6 +290,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } } + if ($has_complex_placeholder) { + return null; + } + if ($event->getFunctionId() === 'printf') { // printf only has the format validated above // don't change the return type diff --git a/tests/ReturnTypeProvider/SprintfTest.php b/tests/ReturnTypeProvider/SprintfTest.php index f6ece62f672..e1b9fda7513 100644 --- a/tests/ReturnTypeProvider/SprintfTest.php +++ b/tests/ReturnTypeProvider/SprintfTest.php @@ -195,6 +195,85 @@ public function providerValidCodeParse(): iterable 'php_version' => '8.0', ]; + yield 'sprintfComplexPlaceholderNotYetSupported4' => [ + 'code' => ' [ + '$val===' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + + yield 'printfComplexPlaceholderNotYetSupported4' => [ + 'code' => ' [ + '$val===' => 'int<0, max>', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + + yield 'sprintfComplexPlaceholderNotYetSupported5' => [ + 'code' => ' [ + '$val===' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + + yield 'sprintfComplexPlaceholderNotYetSupported6' => [ + 'code' => ' [ + '$val===' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + + yield 'sprintfComplexPlaceholderNotYetSupported7' => [ + 'code' => ' [ + '$val===' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + + yield 'sprintfComplexPlaceholderNotYetSupported8' => [ + 'code' => ' [ + '$val===' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; + yield 'sprintfSplatUnpackingArray' => [ 'code' => ' 'InvalidArgument', ], + 'sprintfEscapedPercentLiteralStillReportsTooManyArguments' => [ + 'code' => ' 'TooManyArguments', + ], + 'sprintfEscapedPercentThenPrecisionStarStillReportsTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfPositionalEscapedPercentThenPrecisionStarStillReportsTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfPrecisionStarTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfPrecisionStarTooManyArguments' => [ + 'code' => ' 'TooManyArguments', + ], + 'sprintfPositionalPrecisionStarTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfPositionalPrecisionStarTooManyArguments' => [ + 'code' => ' 'TooManyArguments', + ], + 'sprintfWidthAndPositionalPrecisionStarTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfWidthAndPositionalPrecisionStarTooManyArguments' => [ + 'code' => ' 'TooManyArguments', + ], + 'sprintfPositionalWidthAndPrecisionStarTooFewArguments' => [ + 'code' => ' 'TooFewArguments', + ], + 'sprintfPositionalWidthAndPrecisionStarTooManyArguments' => [ + 'code' => ' 'TooManyArguments', + ], 'printfVariableFormat' => [ 'code' => '