1
0
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:
Brown 2020-08-28 12:42:55 -04:00 committed by Daniil Gentili
parent 7beb274671
commit 02255ae26b
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
27 changed files with 190 additions and 187 deletions

View File

@ -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();

View File

@ -709,6 +709,9 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
return false;
}
/**
* @psalm-mutation-free
*/
public function getFileAnalyzer() : FileAnalyzer
{
return $this;

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
) {

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}
}
}
}

View File

@ -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(

View File

@ -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) {

View File

@ -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()

View File

@ -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';
}

View File

@ -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' => [

View File

@ -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',
],
],

View File

@ -160,7 +160,7 @@ class PureAnnotationTest extends TestCase
}
}',
],
'sortFunction' => [
'sortFunctionPure' => [
'<?php
/**
* @psalm-pure

View File

@ -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',
],
];
}
}

View File

@ -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