mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Fix #4077 - always track closure purity
This commit is contained in:
parent
7beb274671
commit
02255ae26b
@ -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();
|
||||
|
||||
|
@ -709,6 +709,9 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getFileAnalyzer() : FileAnalyzer
|
||||
{
|
||||
return $this;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -582,7 +582,7 @@ class ArrayFunctionCallTest extends TestCase
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);',
|
||||
'assertions' => [
|
||||
'$foo' => 'array<string, Closure():string(baz)>',
|
||||
'$foo' => 'array<string, pure-Closure():string(baz)>',
|
||||
],
|
||||
],
|
||||
'ignoreFalsableCurrent' => [
|
||||
|
@ -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',
|
||||
],
|
||||
],
|
||||
|
@ -160,7 +160,7 @@ class PureAnnotationTest extends TestCase
|
||||
}
|
||||
}',
|
||||
],
|
||||
'sortFunction' => [
|
||||
'sortFunctionPure' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-pure
|
||||
|
@ -202,29 +202,15 @@ class PureCallableTest extends TestCase
|
||||
return $c;
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
|
||||
*/
|
||||
public function providerInvalidCodeParse()
|
||||
{
|
||||
return [
|
||||
'InvalidScalarArgument' => [
|
||||
'pureCallableArgument' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-template T
|
||||
*
|
||||
* @psalm-param array<int, T> $values
|
||||
* @psalm-param (pure-callable(T): numeric) $num_func
|
||||
*
|
||||
* @psalm-return null|T
|
||||
* @psalm-param array<int, int> $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<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
|
||||
*/
|
||||
public function providerInvalidCodeParse()
|
||||
{
|
||||
return [
|
||||
'impureCallableArgument' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-param array<int, int> $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' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @return pure-callable():int
|
||||
*/
|
||||
function foo(): callable {
|
||||
return function() {
|
||||
echo "bar";
|
||||
return 1;
|
||||
};
|
||||
}',
|
||||
'error_message' => 'InvalidReturnStatement',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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<mixed, T:fn-map as mixed>):int(1)\' does not match the declared return type \'callable(iterable<mixed, T:fn-map as mixed>):iterable<mixed, U:fn-map as mixed>\' for map',
|
||||
'error_message' => 'InvalidReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:28 - The inferred type \'pure-Closure(iterable<mixed, T:fn-map as mixed>):int(1)\' does not match the declared return type \'callable(iterable<mixed, T:fn-map as mixed>):iterable<mixed, U:fn-map as mixed>\' for map',
|
||||
],
|
||||
'cannotInferReturnClosureWithDifferentTypes' => [
|
||||
'<?php
|
||||
|
Loading…
x
Reference in New Issue
Block a user