diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index d355f211a..697e979e1 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -78,9 +78,6 @@ class ClosureAnalyzer extends FunctionLikeAnalyzer } $use_context = new Context($context->self); - $use_context->mutation_free = $context->mutation_free; - $use_context->external_mutation_free = $context->external_mutation_free; - $use_context->pure = $context->pure; $codebase = $statements_analyzer->getCodebase(); diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index 5d0da95bb..f07f5a4ac 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -709,6 +709,9 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource return false; } + /** + * @psalm-mutation-free + */ public function getFileAnalyzer() : FileAnalyzer { return $this; diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 45c13dd82..2fa39b2f8 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -95,6 +95,11 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer */ protected static $no_effects_hashes = []; + /** + * @var bool + */ + public $track_mutations = false; + /** * @var bool */ @@ -578,6 +583,17 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + if ($codebase->alter_code + && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) + || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) + ) { + $this->track_mutations = true; + } elseif ($this->function instanceof Closure + || $this->function instanceof ArrowFunction + ) { + $this->track_mutations = true; + } + $statements_analyzer->analyze($function_stmts, $context, $global_context, true); if ($codebase->alter_code @@ -717,22 +733,25 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $closure_return_type = $closure_yield_type; } - if (($storage->return_type === $storage->signature_return_type) - && (!$storage->return_type - || $storage->return_type->hasMixed() - || UnionTypeComparator::isContainedBy( - $codebase, - $closure_return_type, - $storage->return_type - )) - ) { - if ($function_type = $statements_analyzer->node_data->getType($this->function)) { - /** - * @var Type\Atomic\TFn - */ - $closure_atomic = \array_values($function_type->getAtomicTypes())[0]; + if ($function_type = $statements_analyzer->node_data->getType($this->function)) { + /** + * @var Type\Atomic\TFn + */ + $closure_atomic = \array_values($function_type->getAtomicTypes())[0]; + + if (($storage->return_type === $storage->signature_return_type) + && (!$storage->return_type + || $storage->return_type->hasMixed() + || UnionTypeComparator::isContainedBy( + $codebase, + $closure_return_type, + $storage->return_type + )) + ) { $closure_atomic->return_type = $closure_return_type; } + + $closure_atomic->is_pure = !$this->inferred_impure; } } } diff --git a/src/Psalm/Internal/Analyzer/SourceAnalyzer.php b/src/Psalm/Internal/Analyzer/SourceAnalyzer.php index 976ccf032..f8ac1ecda 100644 --- a/src/Psalm/Internal/Analyzer/SourceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/SourceAnalyzer.php @@ -195,16 +195,25 @@ abstract class SourceAnalyzer implements StatementsSource return $this->source->isStatic(); } + /** + * @psalm-mutation-free + */ public function getCodebase() : Codebase { return $this->source->getCodebase(); } + /** + * @psalm-mutation-free + */ public function getProjectAnalyzer() : ProjectAnalyzer { return $this->source->getProjectAnalyzer(); } + /** + * @psalm-mutation-free + */ public function getFileAnalyzer() : FileAnalyzer { return $this->source->getFileAnalyzer(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 3233bbd40..92633c1a0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -504,13 +504,11 @@ class ForeachAnalyzer ); if (!$context->pure) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; @@ -590,13 +588,9 @@ class ForeachAnalyzer $has_valid_iterator = true; if (!$context->pure) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; @@ -648,13 +642,9 @@ class ForeachAnalyzer } if (!$context->pure) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php index 999a66177..defdcf3e3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php @@ -113,8 +113,6 @@ class EchoAnalyzer } } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - if (!$context->collect_initializations && !$context->collect_mutations) { if ($context->mutation_free || $context->external_mutation_free) { if (IssueBuffer::accepts( @@ -126,10 +124,8 @@ class EchoAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index b912639a8..f3715ab43 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -230,8 +230,6 @@ class InstancePropertyAssignmentAnalyzer $lhs_atomic_types = $lhs_type->getAtomicTypes(); - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - while ($lhs_atomic_types) { $lhs_type_part = \array_pop($lhs_atomic_types); @@ -718,10 +716,9 @@ class InstancePropertyAssignmentAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } @@ -1131,12 +1128,10 @@ class InstancePropertyAssignmentAnalyzer $visitor->traverse($assignment_value_type); - if ($codebase->alter_code - && !$declaring_class_storage->mutation_free - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) + if (!$declaring_class_storage->mutation_free && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations && $visitor->has_mutation ) { $statements_analyzer->getSource()->inferred_has_mutation = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c75390703..22e189f48 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -209,8 +209,6 @@ class AssignmentAnalyzer : Type::getMixed(); } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) { if ($context->vars_in_scope[$array_var_id]->by_ref) { if ($context->mutation_free) { @@ -222,10 +220,8 @@ class AssignmentAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; $statements_analyzer->getSource()->inferred_has_mutation = true; @@ -812,10 +808,8 @@ class AssignmentAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { if (!$assign_var->var instanceof PhpParser\Node\Expr\Variable || $assign_var->var->name !== 'this' @@ -1090,10 +1084,6 @@ class AssignmentAnalyzer return false; } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - - $codebase = $statements_analyzer->getCodebase(); - if ($array_var_id && $stmt->var instanceof PhpParser\Node\Expr\PropertyFetch && ($stmt_var_var_type = $statements_analyzer->node_data->getType($stmt->var->var)) @@ -1109,10 +1099,8 @@ class AssignmentAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; @@ -1141,10 +1129,8 @@ class AssignmentAnalyzer // fall through } } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index 47053724e..3397dd62a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -210,7 +210,7 @@ class ConcatAnalyzer $left_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult(); $right_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult(); - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); foreach ($left_type->getAtomicTypes() as $left_type_part) { if ($left_type_part instanceof Type\Atomic\TTemplateParam) { @@ -293,11 +293,9 @@ class ConcatAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; @@ -388,11 +386,9 @@ class ConcatAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 7b83abff7..f29007a8f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -295,7 +295,6 @@ class BinaryOpAnalyzer Type\Union $stmt_right_type ) : void { $codebase = $statements_analyzer->getCodebase(); - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); if ($stmt_left_type->hasString() && $stmt_right_type->hasObjectType()) { foreach ($stmt_right_type->getAtomicTypes() as $atomic_type) { @@ -312,11 +311,9 @@ class BinaryOpAnalyzer } if (!$storage->mutation_free) { - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; @@ -350,11 +347,8 @@ class BinaryOpAnalyzer } if (!$storage->mutation_free) { - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 02084378c..82e17ca50 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -1245,7 +1245,8 @@ class FunctionCallAnalyzer extends CallAnalyzer || $context->external_mutation_free || $codebase->find_unused_variables || !$config->remember_property_assignments_after_call - || $codebase->alter_code) + || ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations)) ) { $must_use = true; @@ -1264,7 +1265,7 @@ class FunctionCallAnalyzer extends CallAnalyzer && !$function_storage->pure) || ($callmap_function_pure === false) ) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if ($context->mutation_free || $context->external_mutation_free) { if (IssueBuffer::accepts( @@ -1276,11 +1277,8 @@ class FunctionCallAnalyzer extends CallAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php index 55b944643..c08e918dc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php @@ -112,13 +112,8 @@ class MethodCallPurityAnalyzer } } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - - if ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + if ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations && !$method_storage->mutation_free && !$method_pure_compatible ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 395556fbb..7f72d54e6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -551,7 +551,7 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if (!$method_storage->external_mutation_free && !$context->inside_throw) { if ($context->pure) { @@ -564,11 +564,9 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index d4f214ca7..31e28e00e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -1119,7 +1119,7 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ return true; } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if (!$context->inside_throw) { if ($context->pure && !$method_storage->pure) { @@ -1142,11 +1142,9 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations && !$method_storage->pure ) { if (!$method_storage->mutation_free) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index f0120af6b..80c4db258 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -219,7 +219,7 @@ class InstancePropertyFetchAnalyzer && !($class_storage->external_mutation_free && $stmt_type->allow_mutations) ) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if ($context->pure) { if (IssueBuffer::accepts( @@ -231,10 +231,9 @@ class InstancePropertyFetchAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } @@ -791,7 +790,7 @@ class InstancePropertyFetchAnalyzer $property_id = $context->self . '::$' . $prop_name; } else { if ($context->inside_isset || $context->collect_initializations) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if ($context->pure) { if (IssueBuffer::accepts( @@ -804,10 +803,9 @@ class InstancePropertyFetchAnalyzer // fall through } } elseif ($context->inside_isset - && $codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) && $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } @@ -1025,7 +1023,7 @@ class InstancePropertyFetchAnalyzer && !($class_storage->external_mutation_free && $class_property_type->allow_mutations) ) { - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); + $statements_analyzer->getProjectAnalyzer(); if ($context->pure) { if (IssueBuffer::accepts( @@ -1037,10 +1035,8 @@ class InstancePropertyFetchAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index a5a239bfa..b00bafc9e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -176,8 +176,6 @@ class StaticPropertyFetchAnalyzer ); } - $project_analyzer = $statements_analyzer->getProjectAnalyzer(); - if ($context->mutation_free) { if (IssueBuffer::accepts( new \Psalm\Issue\ImpureStaticProperty( @@ -188,11 +186,9 @@ class StaticPropertyFetchAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - || isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])) - && $statements_analyzer->getSource() + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_has_mutation = true; $statements_analyzer->getSource()->inferred_impure = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index a09111f0f..925e492f6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -115,10 +115,8 @@ class VariableFetchAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } @@ -179,10 +177,8 @@ class VariableFetchAnalyzer )) { // fall through } - } elseif ($codebase->alter_code - && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) - && $statements_analyzer->getSource() - instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + } elseif ($statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations ) { $statements_analyzer->getSource()->inferred_impure = true; } diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 62ff50515..e5912e954 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -10,6 +10,7 @@ use Psalm\Internal\Provider\FileStorageProvider; use Psalm\Internal\Provider\FunctionExistenceProvider; use Psalm\Internal\Provider\FunctionParamsProvider; use Psalm\Internal\Provider\FunctionReturnTypeProvider; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\StatementsSource; use Psalm\Storage\FunctionStorage; use function strpos; @@ -450,15 +451,19 @@ class Functions || (isset($args[0]) && !$args[0]->value instanceof \PhpParser\Node\Expr\Closure); foreach ($function_callable->params as $i => $param) { - if ($param->type && $param->type->hasCallableType() && isset($args[$i])) { - foreach ($param->type->getAtomicTypes() as $possible_callable) { - $possible_callable = \Psalm\Internal\Type\Comparator\CallableTypeComparator::getCallableFromAtomic( - $codebase, - $possible_callable - ); + if ($type_provider && $param->type && $param->type->hasCallableType() && isset($args[$i])) { + $arg_type = $type_provider->getType($args[$i]->value); - if ($possible_callable && !$possible_callable->is_pure) { - return false; + if ($arg_type) { + foreach ($arg_type->getAtomicTypes() as $possible_callable) { + $possible_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $possible_callable + ); + + if ($possible_callable && !$possible_callable->is_pure) { + return false; + } } } } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 2742ebaad..8f90b90d7 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -9,6 +9,7 @@ use Psalm\Type; use Psalm\Type\Atomic\ObjectLike; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; +use Psalm\Type\Atomic\TFn; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; @@ -32,7 +33,7 @@ class CallableTypeComparator ) : bool { if ($container_type_part->is_pure && !$input_type_part->is_pure) { if ($atomic_comparison_result) { - $atomic_comparison_result->scalar_type_match_found = true; + $atomic_comparison_result->type_coerced = $input_type_part->is_pure === null; } return false; @@ -216,14 +217,18 @@ class CallableTypeComparator } /** - * @return ?TCallable + * @return TCallable|TFn|null */ public static function getCallableFromAtomic( Codebase $codebase, Type\Atomic $input_type_part, ?TCallable $container_type_part = null, ?StatementsAnalyzer $statements_analyzer = null - ) : ?TCallable { + ) { + if ($input_type_part instanceof TCallable || $input_type_part instanceof TFn) { + return $input_type_part; + } + if ($input_type_part instanceof TLiteralString && $input_type_part->value) { try { $function_storage = $codebase->functions->getStorage( diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 7fc78327f..b1e41adca 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -681,7 +681,7 @@ class TypeParser return new TFn('Closure', $params, null, $pure); } - return new TCallable($parse_tree->value, $params, null, $pure); + return new TCallable('callable', $params, null, $pure); } if ($parse_tree instanceof ParseTree\EncapsulationTree) { diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 50b7cc155..ba03bfd27 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -28,7 +28,7 @@ trait CallableTrait public $return_type; /** - * @var bool + * @var ?bool */ public $is_pure; @@ -134,7 +134,7 @@ trait CallableTrait . $param_string . $return_type_string; } - return 'callable' . $param_string . $return_type_string; + return ($this->is_pure ? 'pure-' : '') . 'callable' . $param_string . $return_type_string; } /** @@ -187,7 +187,7 @@ trait CallableTrait . $this->return_type->getId() . ($return_type_multiple ? ')' : ''); } - return $this->value . $param_string . $return_type_string; + return ($this->is_pure ? 'pure-' : '') . $this->value . $param_string . $return_type_string; } public function __toString() diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 7e061ac09..ceea1961d 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -26,10 +26,6 @@ class TCallable extends \Psalm\Type\Atomic $php_major_version, $php_minor_version ) { - if ($this->is_pure) { - return 'pure-callable'; - } - return 'callable'; } diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 2bbbbc218..dffafe70c 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -582,7 +582,7 @@ class ArrayFunctionCallTest extends TestCase ARRAY_FILTER_USE_KEY );', 'assertions' => [ - '$foo' => 'array', + '$foo' => 'array', ], ], 'ignoreFalsableCurrent' => [ diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 1319c871c..fbd444ed1 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -340,7 +340,7 @@ class ClosureTest extends TestCase $a = function() : Closure { return function() : string { return "hello"; }; }; $b = $a()();', 'assertions' => [ - '$a' => 'Closure():Closure():string(hello)', + '$a' => 'pure-Closure():pure-Closure():string(hello)', '$b' => 'string', ], ], diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index 048b556c0..b140622f3 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -160,7 +160,7 @@ class PureAnnotationTest extends TestCase } }', ], - 'sortFunction' => [ + 'sortFunctionPure' => [ ' - */ - public function providerInvalidCodeParse() - { - return [ - 'InvalidScalarArgument' => [ + 'pureCallableArgument' => [ ' $values - * @psalm-param (pure-callable(T): numeric) $num_func - * - * @psalm-return null|T + * @psalm-param array $values + * @psalm-param pure-callable(int):int $num_func * * @psalm-pure */ - function max_by(array $values, callable $num_func) - { + function max_by(array $values, callable $num_func) : ?int { $max = null; $max_num = null; foreach ($values as $value) { @@ -238,15 +224,64 @@ class PureCallableTest extends TestCase return $max; } - $c = max_by([1, 2, 3], static function(int $a): int { + $c = max_by([1, 2, 3], function(int $a): int { + return $a + 5; + }); + + echo $c;', + ], + ]; + } + + /** + * @return iterable + */ + public function providerInvalidCodeParse() + { + return [ + 'impureCallableArgument' => [ + ' $values + * @psalm-param pure-callable(int):int $num_func + * + * @psalm-pure + */ + function max_by(array $values, callable $num_func) : ?int { + $max = null; + $max_num = null; + foreach ($values as $value) { + $value_num = $num_func($value); + if (null === $max_num || $value_num >= $max_num) { + $max = $value; + $max_num = $value_num; + } + } + + return $max; + } + + $c = max_by([1, 2, 3], function(int $a): int { return $a + mt_rand(0, $a); }); - echo $c; - ', - 'error_message' => 'InvalidScalarArgument', - 'error_levels' => [], - ] + echo $c;', + 'error_message' => 'InvalidArgument', + ], + 'impureCallableReturn' => [ + ' 'InvalidReturnStatement', + ], ]; } } diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 35278ed2f..4ed32ad77 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1245,7 +1245,7 @@ class ReturnTypeTest extends TestCase $res = map(function(int $i): string { return (string) $i; })([1,2,3]); ', - 'error_message' => 'InvalidReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:28 - The inferred type \'Closure(iterable):int(1)\' does not match the declared return type \'callable(iterable):iterable\' for map', + 'error_message' => 'InvalidReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:28 - The inferred type \'pure-Closure(iterable):int(1)\' does not match the declared return type \'callable(iterable):iterable\' for map', ], 'cannotInferReturnClosureWithDifferentTypes' => [ '