diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index c044f3470..ae5d33c89 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -1070,7 +1070,13 @@ class FunctionCallAnalyzer extends CallAnalyzer $must_use = true; $callmap_function_pure = $function_id && $in_call_map - ? $codebase->functions->isCallMapFunctionPure($codebase, $function_id, $stmt->args, $must_use) + ? $codebase->functions->isCallMapFunctionPure( + $codebase, + $statements_analyzer->node_data, + $function_id, + $stmt->args, + $must_use + ) : null; if ((!$in_call_map diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 2d41e0c8a..b17f7a81d 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -1973,6 +1973,7 @@ class TypeAnalyzer $matching_callable->is_pure = $codebase->functions->isCallMapFunctionPure( $codebase, + $statements_analyzer ? $statements_analyzer->node_data : null, $input_type_part->value, null, $must_use diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index f4a5cd0aa..b6c1c67f8 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -16,6 +16,8 @@ use function strpos; use function strtolower; use function substr; use Closure; +use Psalm\Type\Atomic\TNamedObject; +use Psalm\Internal\MethodIdentifier; /** * @internal @@ -290,6 +292,7 @@ class Functions */ public function isCallMapFunctionPure( Codebase $codebase, + ?\Psalm\NodeTypeProvider $type_provider, string $function_id, ?array $args, bool &$must_use = true @@ -403,6 +406,28 @@ class Functions return true; } + if ($function_id === 'count' && isset($args[0]) && $type_provider) { + $count_type = $type_provider->getType($args[0]->value); + + if ($count_type) { + foreach ($count_type->getAtomicTypes() as $atomic_count_type) { + if ($atomic_count_type instanceof TNamedObject) { + $count_method_id = new MethodIdentifier( + $atomic_count_type->value, + 'count' + ); + + try { + $method_storage = $codebase->methods->getStorage($count_method_id); + return $method_storage->mutation_free; + } catch (\Exception $e) { + // do nothing + } + } + } + } + } + $function_callable = \Psalm\Internal\Codebase\InternalCallMapHandler::getCallableFromCallMapById( $codebase, $function_id, diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index c3527cc43..b35910e17 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -301,6 +301,22 @@ class PureAnnotationTest extends TestCase return $sum; }' ], + 'countMethodCanBePure' => [ + ' 'ImpureMethodCall' ], + 'countCanBeImpure' => [ + ' 'ImpureFunctionCall', + ], ]; } }