diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index f852b5a2a..d093dedbb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -897,9 +897,15 @@ class FunctionCallAnalyzer extends CallAnalyzer if ($function_storage && $function_storage->template_types) { foreach ($function_storage->template_types as $template_name => $_) { if (!isset($template_result->upper_bounds[$template_name])) { - $template_result->upper_bounds[$template_name] = [ - 'fn-' . $function_id => [Type::getEmpty(), 0] - ]; + if ($template_name === 'TFunctionArgCount') { + $template_result->upper_bounds[$template_name] = [ + 'fn-' . $function_id => [Type::getInt(false, count($stmt->args)), 0] + ]; + } else { + $template_result->upper_bounds[$template_name] = [ + 'fn-' . $function_id => [Type::getEmpty(), 0] + ]; + } } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallReturnTypeFetcher.php index c28b86397..c20e07b69 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallReturnTypeFetcher.php @@ -140,9 +140,15 @@ class MethodCallReturnTypeFetcher [$template_type->defining_class] ) ) { - $template_result->upper_bounds[$template_type->param_name] = [ - ($template_type->defining_class) => [Type::getEmpty(), 0] - ]; + if ($template_type->param_name === 'TFunctionArgCount') { + $template_result->upper_bounds[$template_type->param_name] = [ + 'fn-' . $method_id => [Type::getInt(false, \count($stmt->args)), 0] + ]; + } else { + $template_result->upper_bounds[$template_type->param_name] = [ + ($template_type->defining_class) => [Type::getEmpty(), 0] + ]; + } } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 70d4ae72b..0608ed533 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -916,9 +916,15 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ [$template_type->param_name] [$template_type->defining_class] )) { - $template_result->upper_bounds[$template_type->param_name] = [ - ($template_type->defining_class) => [Type::getEmpty(), 0] - ]; + if ($template_type->param_name === 'TFunctionArgCount') { + $template_result->upper_bounds[$template_type->param_name] = [ + 'fn-' . $method_id => [Type::getInt(false, count($stmt->args)), 0] + ]; + } else { + $template_result->upper_bounds[$template_type->param_name] = [ + ($template_type->defining_class) => [Type::getEmpty(), 0] + ]; + } } } } diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index e246fe021..34a90db90 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -2513,16 +2513,15 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse // This checks for param references in the return type tokens // If found, the param is replaced with a generated template param foreach ($fixed_type_tokens as $i => $type_token) { - if ($type_token[0][0] === '$') { - $token_body = $type_token[0]; + $token_body = $type_token[0]; + $template_function_id = 'fn-' . strtolower($cased_function_id); + if ($token_body[0] === '$') { foreach ($storage->params as $j => $param_storage) { if ('$' . $param_storage->name === $token_body) { if (!isset($param_type_mapping[$token_body])) { $template_name = 'TGeneratedFromParam' . $j; - $template_function_id = 'fn-' . strtolower($cased_function_id); - $template_as_type = $param_storage->type ? clone $param_storage->type : Type::getMixed(); @@ -2556,9 +2555,26 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse } $fixed_type_tokens[$i][0] = $param_type_mapping[$token_body]; + + continue 2; } } } + + if ($token_body === 'func_num_args()') { + $template_name = 'TFunctionArgCount'; + + $storage->template_types[$template_name] = [ + $template_function_id => [ + Type::getInt() + ], + ]; + + $this->function_template_types[$template_name] + = $storage->template_types[$template_name]; + + $fixed_type_tokens[$i][0] = $template_name; + } } $storage->return_type = Type::parseTokens( diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index e9972d1f9..867f2c195 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -1147,6 +1147,17 @@ abstract class Type || $char === '&' || $char === '=' ) { + if ($char === '(' + && $type_tokens[$rtc][0] === 'func_num_args' + && isset($chars[$i + 1]) + && $chars[$i + 1] === ')' + ) { + $type_tokens[$rtc][0] = 'func_num_args()'; + ++$i; + + continue; + } + if ($type_tokens[$rtc][0] === '') { $type_tokens[$rtc] = [$char, $i]; } else { diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 062d9bf44..b2a7b7864 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -439,6 +439,28 @@ class ConditionalReturnTypeTest extends TestCase } }' ], + 'conditionalOnArgCount' => [ + ' 'false', + '$b' => 'string', + '$c' => 'string', + ] + ], ]; } }