1
0
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:
Brown 2020-08-24 19:24:27 -04:00 committed by Daniil Gentili
parent 1ac27d6d22
commit 91e1e5f0f6
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
21 changed files with 530 additions and 71 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 = [];
}
}

View File

@ -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 = [];
}
}

View File

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

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

View File

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