mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Break out method call purity checks
This commit is contained in:
parent
bfb919d26a
commit
9732697e45
@ -15,7 +15,6 @@ use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Issue\ImpureMethodCall;
|
||||
use Psalm\Issue\InvalidPropertyAssignmentValue;
|
||||
use Psalm\Issue\MixedMethodCall;
|
||||
use Psalm\Issue\MixedPropertyTypeCoercion;
|
||||
@ -533,7 +532,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
if ($method_storage) {
|
||||
if (!$context->collect_mutations && !$context->collect_initializations) {
|
||||
$can_memoize = self::checkCallPurity(
|
||||
$can_memoize = MethodCallPurityAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$stmt,
|
||||
@ -784,126 +783,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function checkCallPurity(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
PhpParser\Node\Expr\MethodCall $stmt,
|
||||
?string $lhs_var_id,
|
||||
?string $cased_method_id,
|
||||
MethodIdentifier $method_id,
|
||||
\Psalm\Storage\MethodStorage $method_storage,
|
||||
\Psalm\Storage\ClassLikeStorage $class_storage,
|
||||
Context $context,
|
||||
\Psalm\Config $config
|
||||
) : bool {
|
||||
$can_memoize = false;
|
||||
|
||||
$method_pure_compatible = $method_storage->external_mutation_free
|
||||
&& $statements_analyzer->node_data->isPureCompatible($stmt->var);
|
||||
|
||||
if ($context->pure
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an mutation-free method '
|
||||
. $cased_method_id . ' from a pure context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($context->mutation_free
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an possibly-mutating method '
|
||||
. $cased_method_id . ' from a mutation-free context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($context->external_mutation_free
|
||||
&& !$method_storage->mutation_free
|
||||
&& $method_id->fq_class_name !== $context->self
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an possibly-mutating method '
|
||||
. $cased_method_id . ' from a mutation-free context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif (($method_storage->mutation_free
|
||||
|| ($method_storage->external_mutation_free
|
||||
&& (isset($stmt->var->external_mutation_free) || isset($stmt->var->pure))))
|
||||
&& !$context->inside_unset
|
||||
) {
|
||||
if ($method_storage->mutation_free && !$method_storage->mutation_free_inferred) {
|
||||
if ($context->inside_conditional) {
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$stmt->pure = true;
|
||||
}
|
||||
|
||||
$can_memoize = true;
|
||||
}
|
||||
|
||||
if ($codebase->find_unused_variables && !$context->inside_conditional) {
|
||||
if (!$context->inside_assignment && !$context->inside_call) {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\UnusedMethodCall(
|
||||
'The call to ' . $cased_method_id . ' is not used',
|
||||
new CodeLocation($statements_analyzer, $stmt->name),
|
||||
(string) $method_id
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif (!$method_storage->mutation_free_inferred) {
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$stmt->pure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$config->remember_property_assignments_after_call
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
$context->removeAllObjectVars();
|
||||
} elseif ($method_storage->this_property_mutations) {
|
||||
foreach ($method_storage->this_property_mutations as $name => $_) {
|
||||
$mutation_var_id = $lhs_var_id . '->' . $name;
|
||||
|
||||
$this_property_didnt_exist = $lhs_var_id === '$this'
|
||||
&& isset($context->vars_in_scope[$mutation_var_id])
|
||||
&& !isset($class_storage->declaring_property_ids[$name]);
|
||||
|
||||
$context->remove($mutation_var_id);
|
||||
|
||||
if ($this_property_didnt_exist) {
|
||||
$context->vars_in_scope[$mutation_var_id] = Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $can_memoize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check properties accessed with magic getters and setters.
|
||||
* If `@psalm-seal-properties` is set, they must be defined.
|
||||
|
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Issue\ImpureMethodCall;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
|
||||
class MethodCallPurityAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
PhpParser\Node\Expr\MethodCall $stmt,
|
||||
?string $lhs_var_id,
|
||||
?string $cased_method_id,
|
||||
MethodIdentifier $method_id,
|
||||
\Psalm\Storage\MethodStorage $method_storage,
|
||||
\Psalm\Storage\ClassLikeStorage $class_storage,
|
||||
Context $context,
|
||||
\Psalm\Config $config
|
||||
) : bool {
|
||||
$can_memoize = false;
|
||||
|
||||
$method_pure_compatible = $method_storage->external_mutation_free
|
||||
&& $statements_analyzer->node_data->isPureCompatible($stmt->var);
|
||||
|
||||
if ($context->pure
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an mutation-free method '
|
||||
. $cased_method_id . ' from a pure context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($context->mutation_free
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an possibly-mutating method '
|
||||
. $cased_method_id . ' from a mutation-free context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($context->external_mutation_free
|
||||
&& !$method_storage->mutation_free
|
||||
&& $method_id->fq_class_name !== $context->self
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call an possibly-mutating method '
|
||||
. $cased_method_id . ' from a mutation-free context',
|
||||
new CodeLocation($statements_analyzer, $stmt->name)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif (($method_storage->mutation_free
|
||||
|| ($method_storage->external_mutation_free
|
||||
&& (isset($stmt->var->external_mutation_free) || isset($stmt->var->pure))))
|
||||
&& !$context->inside_unset
|
||||
) {
|
||||
if ($method_storage->mutation_free && !$method_storage->mutation_free_inferred) {
|
||||
if ($context->inside_conditional) {
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$stmt->pure = true;
|
||||
}
|
||||
|
||||
$can_memoize = true;
|
||||
}
|
||||
|
||||
if ($codebase->find_unused_variables && !$context->inside_conditional) {
|
||||
if (!$context->inside_assignment && !$context->inside_call) {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\UnusedMethodCall(
|
||||
'The call to ' . $cased_method_id . ' is not used',
|
||||
new CodeLocation($statements_analyzer, $stmt->name),
|
||||
(string) $method_id
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif (!$method_storage->mutation_free_inferred) {
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$stmt->pure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$config->remember_property_assignments_after_call
|
||||
&& !$method_storage->mutation_free
|
||||
&& !$method_pure_compatible
|
||||
) {
|
||||
$context->removeAllObjectVars();
|
||||
} elseif ($method_storage->this_property_mutations) {
|
||||
foreach ($method_storage->this_property_mutations as $name => $_) {
|
||||
$mutation_var_id = $lhs_var_id . '->' . $name;
|
||||
|
||||
$this_property_didnt_exist = $lhs_var_id === '$this'
|
||||
&& isset($context->vars_in_scope[$mutation_var_id])
|
||||
&& !isset($class_storage->declaring_property_ids[$name]);
|
||||
|
||||
$context->remove($mutation_var_id);
|
||||
|
||||
if ($this_property_didnt_exist) {
|
||||
$context->vars_in_scope[$mutation_var_id] = Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $can_memoize;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user