diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonDivArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonDivArithmeticOpAnalyzer.php index 32fcb32b0..4efe84713 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonDivArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonDivArithmeticOpAnalyzer.php @@ -593,7 +593,9 @@ class NonDivArithmeticOpAnalyzer } if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) { - $result_type = $always_positive ? Type::getPositiveInt() : Type::getInt(); + $result_type = $always_positive + ? new Type\Union([new Type\Atomic\TPositiveInt(), new TLiteralInt(0)]) + : Type::getInt(); } elseif (!$result_type) { $result_type = $always_positive ? Type::getPositiveInt(true) : Type::getInt(true); } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index aacaa8154..efb02ad0a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -251,22 +251,25 @@ class ArgumentsAnalyzer ); } - if ($param_storage->type !== $param_storage->signature_type) { - continue; - } + if (!$param_storage->type_inferred) { + if ($param_storage->type !== $param_storage->signature_type) { + continue; + } - $type_match_found = UnionTypeComparator::isContainedBy( - $codebase, - $replaced_type_part->params[$closure_param_offset]->type, - $param_storage->type - ); + $type_match_found = UnionTypeComparator::isContainedBy( + $codebase, + $replaced_type_part->params[$closure_param_offset]->type, + $param_storage->type + ); - if (!$type_match_found) { - continue; + if (!$type_match_found) { + continue; + } } } $param_storage->type = $replaced_type_part->params[$closure_param_offset]->type; + $param_storage->type_inferred = true; if ($method_id === 'array_map' || $method_id === 'array_filter') { ArrayFetchAnalyzer::taintArrayFetch( @@ -318,12 +321,14 @@ class ArgumentsAnalyzer [] ); + $existing_type = $statements_analyzer->node_data->getType($arg->value); + \Psalm\Internal\Type\UnionTemplateHandler::replaceTemplateTypesWithStandins( $generic_param_type, $replace_template_result, $codebase, $statements_analyzer, - $statements_analyzer->node_data->getType($arg->value), + $existing_type, $argument_offset, 'fn-' . ($context->calling_method_id ?: $context->calling_function_id) ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index a0b8e7e6c..87ea59602 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -888,7 +888,7 @@ class ArrayFunctionArgumentsAnalyzer if ($union_comparison_results->type_coerced_from_mixed) { if (IssueBuffer::accepts( new MixedArgumentTypeCoercion( - 'First parameter of closure passed to function ' . $method_id . ' expects ' . + 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id @@ -900,7 +900,7 @@ class ArrayFunctionArgumentsAnalyzer } else { if (IssueBuffer::accepts( new ArgumentTypeCoercion( - 'First parameter of closure passed to function ' . $method_id . ' expects ' . + 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id @@ -922,7 +922,7 @@ class ArrayFunctionArgumentsAnalyzer if ($union_comparison_results->scalar_type_match_found) { if (IssueBuffer::accepts( new InvalidScalarArgument( - 'First parameter of closure passed to function ' . $method_id . ' expects ' . + 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id @@ -934,7 +934,7 @@ class ArrayFunctionArgumentsAnalyzer } elseif ($types_can_be_identical) { if (IssueBuffer::accepts( new PossiblyInvalidArgument( - 'First parameter of closure passed to function ' . $method_id . ' expects ' + 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . $closure_param_type->getId() . ', possibly different type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), @@ -946,7 +946,7 @@ class ArrayFunctionArgumentsAnalyzer } } elseif (IssueBuffer::accepts( new InvalidArgument( - 'First parameter of closure passed to function ' . $method_id . ' expects ' . + 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 7f73792bd..a02cdf617 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -83,6 +83,11 @@ class FunctionLikeParameter */ public $assert_untainted = false; + /** + * @var bool + */ + public $type_inferred = false; + /** * @param string $name * @param bool $by_ref diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index 695a97598..ec4451fd8 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1041,6 +1041,22 @@ class ForeachTest extends \Psalm\Tests\TestCase echo $key . " " . $value; }' ], + 'loopClosure' => [ + ' $currentIndexes + */ + function cartesianProduct(array $currentIndexes): void { + while (rand(0, 1)) { + array_map( + function ($index) { echo $index; }, + $currentIndexes + ); + + $currentIndexes[0]++; + } + }' + ], ]; }