mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Fix #4036 - add immutable annotations automatically too
This commit is contained in:
parent
1ac27d6d22
commit
91e1e5f0f6
@ -1699,7 +1699,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
self::addOrUpdatePropertyType(
|
||||
$project_analyzer,
|
||||
$stmt,
|
||||
$property_id,
|
||||
$suggested_type,
|
||||
$this,
|
||||
$suggested_type->from_docblock
|
||||
@ -1723,7 +1722,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
private static function addOrUpdatePropertyType(
|
||||
ProjectAnalyzer $project_analyzer,
|
||||
PhpParser\Node\Stmt\Property $property,
|
||||
string $property_id,
|
||||
Type\Union $inferred_type,
|
||||
StatementsSource $source,
|
||||
bool $docblock_only = false
|
||||
@ -1731,7 +1729,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$manipulator = PropertyDocblockManipulator::getForProperty(
|
||||
$project_analyzer,
|
||||
$source->getFilePath(),
|
||||
$property_id,
|
||||
$property
|
||||
);
|
||||
|
||||
|
@ -181,6 +181,12 @@ class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
|
||||
if ($closure_analyzer->inferred_has_mutation
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
}
|
||||
|
||||
if (!$statements_analyzer->node_data->getType($stmt)) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getClosure());
|
||||
}
|
||||
|
@ -623,6 +623,10 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->inferred_has_mutation && $context->self) {
|
||||
$this->codebase->analyzer->addMutableClass($context->self);
|
||||
}
|
||||
|
||||
if (!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
&& $project_analyzer->debug_performance
|
||||
|
@ -1360,6 +1360,7 @@ class ProjectAnalyzer
|
||||
{
|
||||
$supported_issues_to_fix = static::getSupportedIssuesToFix();
|
||||
|
||||
$supported_issues_to_fix[] = 'MissingImmutableAnnotation';
|
||||
$supported_issues_to_fix[] = 'MissingPureAnnotation';
|
||||
|
||||
$unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix);
|
||||
|
@ -127,9 +127,11 @@ class EchoAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
|
@ -692,31 +692,32 @@ class InstancePropertyAssignmentAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
// prevents writing to readonly properties
|
||||
if ($property_storage->readonly) {
|
||||
$appearing_property_class = $codebase->properties->getAppearingClassForProperty(
|
||||
$property_id,
|
||||
true
|
||||
);
|
||||
$appearing_property_class = $codebase->properties->getAppearingClassForProperty(
|
||||
$property_id,
|
||||
true
|
||||
);
|
||||
|
||||
$stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
|
||||
$stmt_var_type = $statements_analyzer->node_data->getType($stmt->var);
|
||||
|
||||
$property_var_pure_compatible = $stmt_var_type
|
||||
&& $stmt_var_type->reference_free
|
||||
&& $stmt_var_type->allow_mutations;
|
||||
$property_var_pure_compatible = $stmt_var_type
|
||||
&& $stmt_var_type->reference_free
|
||||
&& $stmt_var_type->allow_mutations;
|
||||
|
||||
if ($appearing_property_class) {
|
||||
$can_set_property = $context->self
|
||||
&& $context->calling_method_id
|
||||
&& ($appearing_property_class === $context->self
|
||||
|| $codebase->classExtends($context->self, $appearing_property_class))
|
||||
&& (\strpos($context->calling_method_id, '::__construct')
|
||||
|| \strpos($context->calling_method_id, '::unserialize')
|
||||
|| \strpos($context->calling_method_id, '::__unserialize')
|
||||
|| $property_storage->allow_private_mutation
|
||||
|| $property_var_pure_compatible);
|
||||
if ($appearing_property_class
|
||||
&& ($property_storage->readonly || $codebase->alter_code)
|
||||
) {
|
||||
$can_set_readonly_property = $context->self
|
||||
&& $context->calling_method_id
|
||||
&& ($appearing_property_class === $context->self
|
||||
|| $codebase->classExtends($context->self, $appearing_property_class))
|
||||
&& (\strpos($context->calling_method_id, '::__construct')
|
||||
|| \strpos($context->calling_method_id, '::unserialize')
|
||||
|| \strpos($context->calling_method_id, '::__unserialize')
|
||||
|| $property_storage->allow_private_mutation
|
||||
|| $property_var_pure_compatible);
|
||||
|
||||
if (!$can_set_property) {
|
||||
if (!$can_set_readonly_property) {
|
||||
if ($property_storage->readonly) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InaccessibleProperty(
|
||||
$property_id . ' is marked readonly',
|
||||
@ -726,28 +727,39 @@ class InstancePropertyAssignmentAnalyzer
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($declaring_class_storage->mutation_free
|
||||
|| $codebase->alter_code
|
||||
} elseif (!$declaring_class_storage->mutation_free
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor(
|
||||
$statements_analyzer,
|
||||
$stmt
|
||||
);
|
||||
$codebase->analyzer->addMutableClass($declaring_property_class);
|
||||
}
|
||||
} elseif ($declaring_class_storage->mutation_free
|
||||
|| $codebase->alter_code
|
||||
) {
|
||||
$visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor(
|
||||
$statements_analyzer,
|
||||
$stmt
|
||||
);
|
||||
|
||||
$visitor->traverse($assignment_value_type);
|
||||
$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;
|
||||
}
|
||||
if ($codebase->alter_code
|
||||
&& !$declaring_class_storage->mutation_free
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
&& $visitor->has_mutation
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
} elseif (!$context->collect_mutations
|
||||
}
|
||||
|
||||
if (!$property_storage->readonly
|
||||
&& !$context->collect_mutations
|
||||
&& !$context->collect_initializations
|
||||
&& isset($context->vars_in_scope[$lhs_var_id])
|
||||
&& !$context->vars_in_scope[$lhs_var_id]->allow_mutations
|
||||
|
@ -223,10 +223,12 @@ class AssignmentAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
}
|
||||
|
||||
$assign_value_type->by_ref = true;
|
||||
@ -811,9 +813,16 @@ class AssignmentAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
if (!$assign_var->var instanceof PhpParser\Node\Expr\Variable
|
||||
|| $assign_var->var->name !== 'this'
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
}
|
||||
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
@ -1101,9 +1110,11 @@ class AssignmentAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
} elseif (!$context->collect_mutations
|
||||
@ -1131,9 +1142,11 @@ class AssignmentAnalyzer
|
||||
}
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource() instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
|
@ -294,10 +294,12 @@ class ConcatAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
@ -387,10 +389,12 @@ class ConcatAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
|
@ -313,10 +313,12 @@ class BinaryOpAnalyzer
|
||||
|
||||
if (!$storage->mutation_free) {
|
||||
if ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
@ -349,10 +351,12 @@ class BinaryOpAnalyzer
|
||||
|
||||
if (!$storage->mutation_free) {
|
||||
if ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
|
@ -1262,10 +1262,12 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,8 @@ class MethodCallPurityAnalyzer
|
||||
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
|
||||
|
||||
if ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
&& !$method_storage->mutation_free
|
||||
|
@ -565,10 +565,12 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
|
@ -1143,7 +1143,8 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
&& !$method_storage->pure
|
||||
|
@ -189,10 +189,12 @@ class StaticPropertyFetchAnalyzer
|
||||
// fall through
|
||||
}
|
||||
} elseif ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|
||||
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
|
||||
&& $statements_analyzer->getSource()
|
||||
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Analyzer\IssueData;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\ClassDocblockManipulator;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\FileManipulation\FunctionDocblockManipulator;
|
||||
use Psalm\Internal\FileManipulation\PropertyDocblockManipulator;
|
||||
@ -60,7 +61,8 @@ use function array_values;
|
||||
* taint_data: ?\Psalm\Internal\Codebase\Taint,
|
||||
* unused_suppressions: array<string, array<int, int>>,
|
||||
* used_suppressions: array<string, array<int, bool>>,
|
||||
* manipulators: array<string, array<int, FunctionDocblockManipulator>>,
|
||||
* function_docblock_manipulators: array<string, array<int, FunctionDocblockManipulator>>,
|
||||
* mutable_classes: array<string, bool>,
|
||||
* }
|
||||
*/
|
||||
|
||||
@ -169,6 +171,11 @@ class Analyzer
|
||||
*/
|
||||
public $possible_method_param_types = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public $mutable_classes = [];
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
FileProvider $file_provider,
|
||||
@ -467,7 +474,8 @@ class Analyzer
|
||||
'taint_data' => $codebase->taint,
|
||||
'unused_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUnusedSuppressions() : [],
|
||||
'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [],
|
||||
'manipulators' => FunctionDocblockManipulator::getManipulators(),
|
||||
'function_docblock_manipulators' => FunctionDocblockManipulator::getManipulators(),
|
||||
'mutable_classes' => $codebase->analyzer->mutable_classes,
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
},
|
||||
@ -532,7 +540,9 @@ class Analyzer
|
||||
$pool_data['class_property_locations']
|
||||
);
|
||||
|
||||
FunctionDocblockManipulator::addManipulators($pool_data['manipulators']);
|
||||
$this->mutable_classes = array_merge($this->mutable_classes, $pool_data['mutable_classes']);
|
||||
|
||||
FunctionDocblockManipulator::addManipulators($pool_data['function_docblock_manipulators']);
|
||||
|
||||
$this->analyzed_methods = array_merge($pool_data['analyzed_methods'], $this->analyzed_methods);
|
||||
|
||||
@ -1364,6 +1374,11 @@ class Analyzer
|
||||
PropertyDocblockManipulator::getManipulationsForFile($file_path)
|
||||
);
|
||||
|
||||
FileManipulationBuffer::add(
|
||||
$file_path,
|
||||
ClassDocblockManipulator::getManipulationsForFile($file_path)
|
||||
);
|
||||
|
||||
$file_manipulations = FileManipulationBuffer::getManipulationsForFile($file_path);
|
||||
|
||||
if (!$file_manipulations) {
|
||||
@ -1546,6 +1561,11 @@ class Analyzer
|
||||
return $this->possible_method_param_types;
|
||||
}
|
||||
|
||||
public function addMutableClass(string $fqcln) : void
|
||||
{
|
||||
$this->mutable_classes[\strtolower($fqcln)] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_path
|
||||
* @param string $method_id
|
||||
|
@ -19,6 +19,7 @@ use Psalm\Exception\UnpopulatedClasslikeException;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\FileManipulation\ClassDocblockManipulator;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Internal\Provider\FileReferenceProvider;
|
||||
use Psalm\Internal\Provider\StatementsProvider;
|
||||
@ -784,6 +785,9 @@ class ClassLikes
|
||||
|
||||
$progress->debug('Checking class references' . PHP_EOL);
|
||||
|
||||
$project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
foreach ($this->existing_classlikes_lc as $fq_class_name_lc => $_) {
|
||||
try {
|
||||
$classlike_storage = $this->classlike_storage_provider->get($fq_class_name_lc);
|
||||
@ -814,10 +818,61 @@ class ClassLikes
|
||||
}
|
||||
|
||||
$this->findPossibleMethodParamTypes($classlike_storage);
|
||||
|
||||
if ($codebase->alter_code
|
||||
&& isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation'])
|
||||
&& !isset($codebase->analyzer->mutable_classes[$fq_class_name_lc])
|
||||
&& !$classlike_storage->external_mutation_free
|
||||
&& $classlike_storage->properties
|
||||
&& isset($classlike_storage->methods['__construct'])
|
||||
) {
|
||||
$stmts = $codebase->getStatementsForFile(
|
||||
$classlike_storage->location->file_path
|
||||
);
|
||||
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Namespace_) {
|
||||
foreach ($stmt->stmts as $namespace_stmt) {
|
||||
if ($namespace_stmt instanceof PhpParser\Node\Stmt\Class_
|
||||
&& \strtolower((string) $stmt->name . '\\' . (string) $namespace_stmt->name)
|
||||
=== $fq_class_name_lc
|
||||
) {
|
||||
self::makeImmutable(
|
||||
$namespace_stmt,
|
||||
$project_analyzer,
|
||||
$classlike_storage->location->file_path
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_
|
||||
&& \strtolower((string) $stmt->name) === $fq_class_name_lc
|
||||
) {
|
||||
self::makeImmutable(
|
||||
$stmt,
|
||||
$project_analyzer,
|
||||
$classlike_storage->location->file_path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function makeImmutable(
|
||||
PhpParser\Node\Stmt\Class_ $class_stmt,
|
||||
\Psalm\Internal\Analyzer\ProjectAnalyzer $project_analyzer,
|
||||
string $file_path
|
||||
) : void {
|
||||
$manipulator = ClassDocblockManipulator::getForClass(
|
||||
$project_analyzer,
|
||||
$file_path,
|
||||
$class_stmt
|
||||
);
|
||||
|
||||
$manipulator->makeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
146
src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php
Normal file
146
src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace Psalm\Internal\FileManipulation;
|
||||
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function ltrim;
|
||||
use PhpParser\Node\Stmt\Class_;
|
||||
use Psalm\DocComment;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ClassDocblockManipulator
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<int, self>>
|
||||
*/
|
||||
private static $manipulators = [];
|
||||
|
||||
/** @var Class_ */
|
||||
private $stmt;
|
||||
|
||||
/** @var int */
|
||||
private $docblock_start;
|
||||
|
||||
/** @var int */
|
||||
private $docblock_end;
|
||||
|
||||
/** @var bool */
|
||||
private $immutable = false;
|
||||
|
||||
/** @var string */
|
||||
private $indentation;
|
||||
|
||||
public static function getForClass(
|
||||
ProjectAnalyzer $project_analyzer,
|
||||
string $file_path,
|
||||
Class_ $stmt
|
||||
) : self {
|
||||
if (isset(self::$manipulators[$file_path][$stmt->getLine()])) {
|
||||
return self::$manipulators[$file_path][$stmt->getLine()];
|
||||
}
|
||||
|
||||
$manipulator
|
||||
= self::$manipulators[$file_path][$stmt->getLine()]
|
||||
= new self($project_analyzer, $stmt, $file_path);
|
||||
|
||||
return $manipulator;
|
||||
}
|
||||
|
||||
private function __construct(
|
||||
ProjectAnalyzer $project_analyzer,
|
||||
Class_ $stmt,
|
||||
string $file_path
|
||||
) {
|
||||
$this->stmt = $stmt;
|
||||
$docblock = $stmt->getDocComment();
|
||||
$this->docblock_start = $docblock ? $docblock->getFilePos() : (int)$stmt->getAttribute('startFilePos');
|
||||
$this->docblock_end = (int)$stmt->getAttribute('startFilePos');
|
||||
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
$file_contents = $codebase->getFileContents($file_path);
|
||||
|
||||
$preceding_newline_pos = (int) strrpos($file_contents, "\n", $this->docblock_end - strlen($file_contents));
|
||||
|
||||
$first_line = substr($file_contents, $preceding_newline_pos + 1, $this->docblock_end - $preceding_newline_pos);
|
||||
|
||||
$this->indentation = str_replace(ltrim($first_line), '', $first_line);
|
||||
}
|
||||
|
||||
public function makeImmutable() : void
|
||||
{
|
||||
$this->immutable = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new docblock given the existing docblock, if one exists, and the updated return types
|
||||
* and/or parameters
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getDocblock()
|
||||
{
|
||||
$docblock = $this->stmt->getDocComment();
|
||||
|
||||
if ($docblock) {
|
||||
$parsed_docblock = DocComment::parsePreservingLength($docblock);
|
||||
} else {
|
||||
$parsed_docblock = new \Psalm\Internal\Scanner\ParsedDocblock('', []);
|
||||
}
|
||||
|
||||
$modified_docblock = false;
|
||||
|
||||
if ($this->immutable) {
|
||||
$modified_docblock = true;
|
||||
$parsed_docblock->tags['psalm-immutable'] = [''];
|
||||
}
|
||||
|
||||
if (!$modified_docblock) {
|
||||
return (string)$docblock . "\n" . $this->indentation;
|
||||
}
|
||||
|
||||
return $parsed_docblock->render($this->indentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_path
|
||||
*
|
||||
* @return array<int, FileManipulation>
|
||||
*/
|
||||
public static function getManipulationsForFile($file_path)
|
||||
{
|
||||
if (!isset(self::$manipulators[$file_path])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$file_manipulations = [];
|
||||
|
||||
foreach (self::$manipulators[$file_path] as $manipulator) {
|
||||
if ($manipulator->immutable) {
|
||||
$file_manipulations[$manipulator->docblock_start] = new FileManipulation(
|
||||
$manipulator->docblock_start,
|
||||
$manipulator->docblock_end,
|
||||
$manipulator->getDocblock()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $file_manipulations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function clearCache()
|
||||
{
|
||||
self::$manipulators = [];
|
||||
}
|
||||
}
|
@ -18,15 +18,10 @@ use function substr;
|
||||
*/
|
||||
class PropertyDocblockManipulator
|
||||
{
|
||||
/** @var array<string, array<string, self>> */
|
||||
private static $manipulators = [];
|
||||
|
||||
/**
|
||||
* Manipulators ordered by line number
|
||||
*
|
||||
* @var array<string, array<int, self>>
|
||||
*/
|
||||
private static $ordered_manipulators = [];
|
||||
private static $manipulators = [];
|
||||
|
||||
/** @var Property */
|
||||
private $stmt;
|
||||
@ -70,16 +65,14 @@ class PropertyDocblockManipulator
|
||||
public static function getForProperty(
|
||||
ProjectAnalyzer $project_analyzer,
|
||||
string $file_path,
|
||||
string $property_id,
|
||||
Property $stmt
|
||||
) : self {
|
||||
if (isset(self::$manipulators[$file_path][$property_id])) {
|
||||
return self::$manipulators[$file_path][$property_id];
|
||||
if (isset(self::$manipulators[$file_path][$stmt->getLine()])) {
|
||||
return self::$manipulators[$file_path][$stmt->getLine()];
|
||||
}
|
||||
|
||||
$manipulator
|
||||
= self::$manipulators[$file_path][$property_id]
|
||||
= self::$ordered_manipulators[$file_path][$stmt->getLine()]
|
||||
= self::$manipulators[$file_path][$stmt->getLine()]
|
||||
= new self($project_analyzer, $stmt, $file_path);
|
||||
|
||||
return $manipulator;
|
||||
@ -227,7 +220,7 @@ class PropertyDocblockManipulator
|
||||
|
||||
$file_manipulations = [];
|
||||
|
||||
foreach (self::$ordered_manipulators[$file_path] as $manipulator) {
|
||||
foreach (self::$manipulators[$file_path] as $manipulator) {
|
||||
if ($manipulator->new_php_type) {
|
||||
if ($manipulator->typehint_start && $manipulator->typehint_end) {
|
||||
$file_manipulations[$manipulator->typehint_start] = new FileManipulation(
|
||||
@ -277,6 +270,5 @@ class PropertyDocblockManipulator
|
||||
public static function clearCache()
|
||||
{
|
||||
self::$manipulators = [];
|
||||
self::$ordered_manipulators = [];
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ abstract class RuntimeCaches
|
||||
\Psalm\Internal\Type\TypeTokenizer::clearCache();
|
||||
\Psalm\Internal\Provider\FileReferenceProvider::clearCache();
|
||||
\Psalm\Internal\FileManipulation\FileManipulationBuffer::clearCache();
|
||||
\Psalm\Internal\FileManipulation\ClassDocblockManipulator::clearCache();
|
||||
\Psalm\Internal\FileManipulation\FunctionDocblockManipulator::clearCache();
|
||||
\Psalm\Internal\FileManipulation\PropertyDocblockManipulator::clearCache();
|
||||
\Psalm\Internal\Analyzer\FileAnalyzer::clearCache();
|
||||
|
173
tests/FileManipulation/ImmutableAnnotationAdditionTest.php
Normal file
173
tests/FileManipulation/ImmutableAnnotationAdditionTest.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
namespace Psalm\Tests\FileManipulation;
|
||||
|
||||
class ImmutableAnnotationAdditionTest extends FileManipulationTest
|
||||
{
|
||||
/**
|
||||
* @return array<string,array{string,string,string,string[],bool}>
|
||||
*/
|
||||
public function providerValidCodeParse()
|
||||
{
|
||||
return [
|
||||
'addPureAnnotationToFunction' => [
|
||||
'<?php
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'7.4',
|
||||
['MissingImmutableAnnotation'],
|
||||
true,
|
||||
],
|
||||
'addPureAnnotationToFunctionWithExistingDocblock' => [
|
||||
'<?php
|
||||
/**
|
||||
* This is a class
|
||||
* that is cool
|
||||
*/
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
/**
|
||||
* This is a class
|
||||
* that is cool
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'7.4',
|
||||
['MissingImmutableAnnotation'],
|
||||
true,
|
||||
],
|
||||
'dontAddPureAnnotationWhenMethodHasImpurity' => [
|
||||
'<?php
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
echo $this->i;
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
echo $this->i;
|
||||
return $this->i + 5;
|
||||
}
|
||||
}',
|
||||
'7.4',
|
||||
['MissingImmutableAnnotation'],
|
||||
true,
|
||||
],
|
||||
'dontAddPureAnnotationWhenClassCanHoldMutableData' => [
|
||||
'<?php
|
||||
class B {
|
||||
public int $i = 5;
|
||||
}
|
||||
|
||||
class A {
|
||||
public B $b;
|
||||
|
||||
public function __construct(B $b) {
|
||||
$this->b = $b;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->b->i + 5;
|
||||
}
|
||||
}
|
||||
|
||||
$b = new B();
|
||||
|
||||
$a = new A($b);
|
||||
|
||||
echo $a->getPlus5();
|
||||
|
||||
$b->i = 6;
|
||||
|
||||
echo $a->getPlus5();',
|
||||
'<?php
|
||||
class B {
|
||||
public int $i = 5;
|
||||
}
|
||||
|
||||
class A {
|
||||
public B $b;
|
||||
|
||||
public function __construct(B $b) {
|
||||
$this->b = $b;
|
||||
}
|
||||
|
||||
public function getPlus5() {
|
||||
return $this->b->i + 5;
|
||||
}
|
||||
}
|
||||
|
||||
$b = new B();
|
||||
|
||||
$a = new A($b);
|
||||
|
||||
echo $a->getPlus5();
|
||||
|
||||
$b->i = 6;
|
||||
|
||||
echo $a->getPlus5();',
|
||||
'7.4',
|
||||
['MissingImmutableAnnotation'],
|
||||
true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -473,6 +473,27 @@ class ImmutableAnnotationTest extends TestCase
|
||||
) {}
|
||||
}'
|
||||
],
|
||||
'allowMutablePropertyFetch' => [
|
||||
'<?php
|
||||
class B {
|
||||
public int $j = 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class A {
|
||||
public int $i;
|
||||
|
||||
public function __construct(int $i) {
|
||||
$this->i = $i;
|
||||
}
|
||||
|
||||
public function getPlusOther(B $b) : int {
|
||||
return $this->i + $b->j;
|
||||
}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user