Navigation Menu

Skip to content

Commit

Permalink
Allow adding pure annotations to functions
Browse files Browse the repository at this point in the history
Ref #4036
  • Loading branch information
muglug committed Aug 23, 2020
1 parent 3538fe1 commit 67f9adb
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 78 deletions.
24 changes: 24 additions & 0 deletions src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -95,6 +95,16 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
*/
protected static $no_effects_hashes = [];

/**
* @var bool
*/
public $inferred_impure = false;

/**
* @var bool
*/
public $inferred_has_mutation = false;

/**
* @var FunctionLikeStorage
*/
Expand Down Expand Up @@ -569,6 +579,20 @@ function (FunctionLikeParameter $p) {

$statements_analyzer->analyze($function_stmts, $context, $global_context, true);

if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& !$this->inferred_impure
) {
$manipulator = FunctionDocblockManipulator::getForFunction(
$project_analyzer,
$this->source->getFilePath(),
$this->getId(),
$this->function
);

$manipulator->makePure();
}

if (!$context->collect_initializations
&& !$context->collect_mutations
&& $project_analyzer->debug_performance
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Expand Up @@ -1349,6 +1349,8 @@ public function setIssuesToFix(array $issues)
{
$supported_issues_to_fix = static::getSupportedIssuesToFix();

$supported_issues_to_fix[] = 'MissingPureAnnotation';

$unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix);

if (! empty($unsupportedIssues)) {
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Analyzer/SourceAnalyzer.php
Expand Up @@ -141,6 +141,7 @@ public function getRequireNesting()
}

/**
* @psalm-mutation-free
* @return StatementsSource
*/
public function getSource()
Expand Down
31 changes: 18 additions & 13 deletions src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php
Expand Up @@ -113,19 +113,24 @@ public static function analyze(
}
}

if (!$context->collect_initializations
&& !$context->collect_mutations
&& ($context->mutation_free
|| $context->external_mutation_free)
) {
if (IssueBuffer::accepts(
new ImpureFunctionCall(
'Cannot call echo from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
$project_analyzer = $statements_analyzer->getProjectAnalyzer();

if (!$context->collect_initializations && !$context->collect_mutations) {
if ($context->mutation_free || $context->external_mutation_free) {
if (IssueBuffer::accepts(
new ImpureFunctionCall(
'Cannot call echo from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}

Expand Down
Expand Up @@ -230,6 +230,8 @@ public static function analyze(

$lhs_atomic_types = $lhs_type->getAtomicTypes();

$project_analyzer = $statements_analyzer->getProjectAnalyzer();

while ($lhs_atomic_types) {
$lhs_type_part = \array_pop($lhs_atomic_types);

Expand Down Expand Up @@ -724,29 +726,48 @@ public static function analyze(
)) {
// fall through
}
} elseif ($declaring_class_storage->mutation_free) {
} elseif ($declaring_class_storage->mutation_free
|| $codebase->alter_code
) {
$visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor(
$statements_analyzer,
$stmt
);

$visitor->traverse($assignment_value_type);

if ($codebase->alter_code
&& !$declaring_class_storage->mutation_free
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
&& $visitor->has_mutation
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
}
} elseif ($context->mutation_free
&& !$context->collect_mutations
} elseif (!$context->collect_mutations
&& !$context->collect_initializations
&& isset($context->vars_in_scope[$lhs_var_id])
&& !$context->vars_in_scope[$lhs_var_id]->allow_mutations
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if ($context->mutation_free) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}

Expand Down
Expand Up @@ -209,6 +209,8 @@ public static function analyze(
: 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) {
Expand All @@ -220,6 +222,11 @@ public static function analyze(
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}

$assign_value_type->by_ref = true;
Expand Down Expand Up @@ -789,19 +796,25 @@ public static function analyze(
$property_var_pure_compatible = $statements_analyzer->node_data->isPureCompatible($assign_var->var);

// prevents writing to any properties in a mutation-free context
if (($context->mutation_free || $context->external_mutation_free)
&& !$property_var_pure_compatible
if (!$property_var_pure_compatible
&& !$context->collect_mutations
&& !$context->collect_initializations
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $assign_var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if ($context->mutation_free || $context->external_mutation_free) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $assign_var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
} elseif ($assign_var instanceof PhpParser\Node\Expr\StaticPropertyFetch &&
Expand Down Expand Up @@ -964,14 +977,14 @@ public static function assignTypeFromVarDocblock(
return;
}

$project_analyzer = $statements_analyzer->getProjectAnalyzer();

if ($codebase->find_unused_variables
&& $type_location
&& isset($context->vars_in_scope[$var_comment->var_id])
&& $context->vars_in_scope[$var_comment->var_id]->getId() === $var_comment_type->getId()
&& !$var_comment_type->isMixed()
) {
$project_analyzer = $statements_analyzer->getProjectAnalyzer();

if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
) {
Expand Down Expand Up @@ -1068,23 +1081,32 @@ public static function analyzeAssignmentOperation(
return false;
}

$project_analyzer = $statements_analyzer->getProjectAnalyzer();

$codebase = $statements_analyzer->getCodebase();

if ($array_var_id
&& $context->mutation_free
&& $stmt->var instanceof PhpParser\Node\Expr\PropertyFetch
&& ($stmt_var_var_type = $statements_analyzer->node_data->getType($stmt->var->var))
&& !$stmt_var_var_type->reference_free
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt->var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if ($context->mutation_free) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt->var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
} elseif ($context->mutation_free
&& !$context->collect_mutations
} elseif (!$context->collect_mutations
&& !$context->collect_initializations
&& $stmt->var instanceof PhpParser\Node\Expr\PropertyFetch
) {
Expand All @@ -1094,18 +1116,25 @@ public static function analyzeAssignmentOperation(
$statements_analyzer
);

if (isset($context->vars_in_scope[$lhs_var_id])
&& !$context->vars_in_scope[$lhs_var_id]->allow_mutations
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if ($context->mutation_free) {
if (isset($context->vars_in_scope[$lhs_var_id])
&& !$context->vars_in_scope[$lhs_var_id]->allow_mutations
) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a mutation-free context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}

Expand Down
Expand Up @@ -210,6 +210,8 @@ public static function analyze(
$left_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
$right_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();

$project_analyzer = $statements_analyzer->getProjectAnalyzer();

foreach ($left_type->getAtomicTypes() as $left_type_part) {
if ($left_type_part instanceof Type\Atomic\TTemplateParam) {
if (IssueBuffer::accepts(
Expand Down Expand Up @@ -291,6 +293,12 @@ public static function analyze(
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
}
Expand Down Expand Up @@ -378,6 +386,12 @@ public static function analyze(
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
}
Expand Down

0 comments on commit 67f9adb

Please sign in to comment.