diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php index 115fb81b5..9422421a3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php @@ -15,7 +15,6 @@ use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\FileManipulation\FileManipulationBuffer; -use Psalm\Issue\ImpureMethodCall; use Psalm\Issue\InvalidPropertyAssignmentValue; use Psalm\Issue\MixedMethodCall; use Psalm\Issue\MixedPropertyTypeCoercion; @@ -533,7 +532,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer if ($method_storage) { if (!$context->collect_mutations && !$context->collect_initializations) { - $can_memoize = self::checkCallPurity( + $can_memoize = MethodCallPurityAnalyzer::analyze( $statements_analyzer, $codebase, $stmt, @@ -784,126 +783,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer } } - - private static function checkCallPurity( - StatementsAnalyzer $statements_analyzer, - Codebase $codebase, - PhpParser\Node\Expr\MethodCall $stmt, - ?string $lhs_var_id, - ?string $cased_method_id, - MethodIdentifier $method_id, - \Psalm\Storage\MethodStorage $method_storage, - \Psalm\Storage\ClassLikeStorage $class_storage, - Context $context, - \Psalm\Config $config - ) : bool { - $can_memoize = false; - - $method_pure_compatible = $method_storage->external_mutation_free - && $statements_analyzer->node_data->isPureCompatible($stmt->var); - - if ($context->pure - && !$method_storage->mutation_free - && !$method_pure_compatible - ) { - if (IssueBuffer::accepts( - new ImpureMethodCall( - 'Cannot call an mutation-free method ' - . $cased_method_id . ' from a pure context', - new CodeLocation($statements_analyzer, $stmt->name) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } - } elseif ($context->mutation_free - && !$method_storage->mutation_free - && !$method_pure_compatible - ) { - if (IssueBuffer::accepts( - new ImpureMethodCall( - 'Cannot call an possibly-mutating method ' - . $cased_method_id . ' from a mutation-free context', - new CodeLocation($statements_analyzer, $stmt->name) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } - } elseif ($context->external_mutation_free - && !$method_storage->mutation_free - && $method_id->fq_class_name !== $context->self - && !$method_pure_compatible - ) { - if (IssueBuffer::accepts( - new ImpureMethodCall( - 'Cannot call an possibly-mutating method ' - . $cased_method_id . ' from a mutation-free context', - new CodeLocation($statements_analyzer, $stmt->name) - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } - } elseif (($method_storage->mutation_free - || ($method_storage->external_mutation_free - && (isset($stmt->var->external_mutation_free) || isset($stmt->var->pure)))) - && !$context->inside_unset - ) { - if ($method_storage->mutation_free && !$method_storage->mutation_free_inferred) { - if ($context->inside_conditional) { - /** @psalm-suppress UndefinedPropertyAssignment */ - $stmt->pure = true; - } - - $can_memoize = true; - } - - if ($codebase->find_unused_variables && !$context->inside_conditional) { - if (!$context->inside_assignment && !$context->inside_call) { - if (IssueBuffer::accepts( - new \Psalm\Issue\UnusedMethodCall( - 'The call to ' . $cased_method_id . ' is not used', - new CodeLocation($statements_analyzer, $stmt->name), - (string) $method_id - ), - $statements_analyzer->getSuppressedIssues() - )) { - // fall through - } - } elseif (!$method_storage->mutation_free_inferred) { - /** @psalm-suppress UndefinedPropertyAssignment */ - $stmt->pure = true; - } - } - } - - if (!$config->remember_property_assignments_after_call - && !$method_storage->mutation_free - && !$method_pure_compatible - ) { - $context->removeAllObjectVars(); - } elseif ($method_storage->this_property_mutations) { - foreach ($method_storage->this_property_mutations as $name => $_) { - $mutation_var_id = $lhs_var_id . '->' . $name; - - $this_property_didnt_exist = $lhs_var_id === '$this' - && isset($context->vars_in_scope[$mutation_var_id]) - && !isset($class_storage->declaring_property_ids[$name]); - - $context->remove($mutation_var_id); - - if ($this_property_didnt_exist) { - $context->vars_in_scope[$mutation_var_id] = Type::getMixed(); - } - } - } - - return $can_memoize; - } - - - /** * Check properties accessed with magic getters and setters. * If `@psalm-seal-properties` is set, they must be defined. diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallPurityAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallPurityAnalyzer.php new file mode 100644 index 000000000..f9212e954 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallPurityAnalyzer.php @@ -0,0 +1,132 @@ +external_mutation_free + && $statements_analyzer->node_data->isPureCompatible($stmt->var); + + if ($context->pure + && !$method_storage->mutation_free + && !$method_pure_compatible + ) { + if (IssueBuffer::accepts( + new ImpureMethodCall( + 'Cannot call an mutation-free method ' + . $cased_method_id . ' from a pure context', + new CodeLocation($statements_analyzer, $stmt->name) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif ($context->mutation_free + && !$method_storage->mutation_free + && !$method_pure_compatible + ) { + if (IssueBuffer::accepts( + new ImpureMethodCall( + 'Cannot call an possibly-mutating method ' + . $cased_method_id . ' from a mutation-free context', + new CodeLocation($statements_analyzer, $stmt->name) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif ($context->external_mutation_free + && !$method_storage->mutation_free + && $method_id->fq_class_name !== $context->self + && !$method_pure_compatible + ) { + if (IssueBuffer::accepts( + new ImpureMethodCall( + 'Cannot call an possibly-mutating method ' + . $cased_method_id . ' from a mutation-free context', + new CodeLocation($statements_analyzer, $stmt->name) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif (($method_storage->mutation_free + || ($method_storage->external_mutation_free + && (isset($stmt->var->external_mutation_free) || isset($stmt->var->pure)))) + && !$context->inside_unset + ) { + if ($method_storage->mutation_free && !$method_storage->mutation_free_inferred) { + if ($context->inside_conditional) { + /** @psalm-suppress UndefinedPropertyAssignment */ + $stmt->pure = true; + } + + $can_memoize = true; + } + + if ($codebase->find_unused_variables && !$context->inside_conditional) { + if (!$context->inside_assignment && !$context->inside_call) { + if (IssueBuffer::accepts( + new \Psalm\Issue\UnusedMethodCall( + 'The call to ' . $cased_method_id . ' is not used', + new CodeLocation($statements_analyzer, $stmt->name), + (string) $method_id + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif (!$method_storage->mutation_free_inferred) { + /** @psalm-suppress UndefinedPropertyAssignment */ + $stmt->pure = true; + } + } + } + + if (!$config->remember_property_assignments_after_call + && !$method_storage->mutation_free + && !$method_pure_compatible + ) { + $context->removeAllObjectVars(); + } elseif ($method_storage->this_property_mutations) { + foreach ($method_storage->this_property_mutations as $name => $_) { + $mutation_var_id = $lhs_var_id . '->' . $name; + + $this_property_didnt_exist = $lhs_var_id === '$this' + && isset($context->vars_in_scope[$mutation_var_id]) + && !isset($class_storage->declaring_property_ids[$name]); + + $context->remove($mutation_var_id); + + if ($this_property_didnt_exist) { + $context->vars_in_scope[$mutation_var_id] = Type::getMixed(); + } + } + } + + return $can_memoize; + } +}