1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add ability to move classes

This commit is contained in:
Brown 2019-06-04 16:36:32 -04:00 committed by Matthew Brown
parent 13779e760e
commit f309c755f8
22 changed files with 1133 additions and 168 deletions

View File

@ -207,6 +207,11 @@ class Codebase
*/
public $class_constants_to_rename = [];
/**
* @var array<string, string>
*/
public $classes_to_move = [];
/**
* @var array<string, string>
*/
@ -222,6 +227,11 @@ class Codebase
*/
public $class_constant_transforms = [];
/**
* @var array<string, string>
*/
public $class_transforms = [];
/**
* @var bool
*/

View File

@ -12,20 +12,43 @@ class FileManipulation
/** @var string */
public $insertion_text;
/** @var bool */
public $preserve_indentation;
/**
* @param int $start
* @param int $end
* @param string $insertion_text
*/
public function __construct(int $start, int $end, string $insertion_text)
public function __construct(int $start, int $end, string $insertion_text, bool $preserve_indentation = false)
{
$this->start = $start;
$this->end = $end;
$this->insertion_text = $insertion_text;
$this->preserve_indentation = $preserve_indentation;
}
public function getKey() : string
{
return sha1($this->start . ':' . $this->insertion_text);
}
public function transform(string $existing_contents) : string
{
if ($this->preserve_indentation) {
$newline_pos = strrpos($existing_contents, "\n", $this->start - strlen($existing_contents));
$newline_pos = $newline_pos !== false ? $newline_pos + 1 : 0;
$indentation = substr($existing_contents, $newline_pos, $this->start - $newline_pos);
if (trim($indentation) === '') {
$this->insertion_text = $this->insertion_text . $indentation;
}
}
return substr($existing_contents, 0, $this->start)
. $this->insertion_text
. substr($existing_contents, $this->end);
}
}

View File

@ -131,6 +131,64 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$project_analyzer = $this->file_analyzer->project_analyzer;
$codebase = $this->getCodebase();
if ($codebase->alter_code && $class->name && $codebase->classes_to_move) {
if (isset($codebase->classes_to_move[strtolower($this->fq_class_name)])) {
$destination_class = $codebase->classes_to_move[strtolower($this->fq_class_name)];
$source_class_parts = explode('\\', $this->fq_class_name);
$destination_class_parts = explode('\\', $destination_class);
array_pop($source_class_parts);
array_pop($destination_class_parts);
$source_ns = implode('\\', $source_class_parts);
$destination_ns = implode('\\', $destination_class_parts);
if (strtolower($source_ns) !== strtolower($destination_ns)) {
if ($storage->namespace_name_location) {
$bounds = $storage->namespace_name_location->getSelectionBounds();
$file_manipulations = [
new \Psalm\FileManipulation(
$bounds[0],
$bounds[1],
$destination_ns
)
];
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations
);
} elseif (!$source_ns) {
$class_start_pos = (int) $class->getAttribute('startFilePos');
$file_manipulations = [
new \Psalm\FileManipulation(
$class_start_pos,
$class_start_pos,
'namespace ' . $destination_ns . ';' . "\n\n",
true
)
];
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations
);
}
}
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$class->name,
$this->fq_class_name,
null
);
}
$classlike_storage_provider = $codebase->classlike_storage_provider;
$parent_fq_class_name = $this->parent_fq_class_name;
@ -152,6 +210,16 @@ class ClassAnalyzer extends ClassLikeAnalyzer
return false;
}
if ($codebase->alter_code && $codebase->classes_to_move) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$class->extends,
$parent_fq_class_name,
null
);
}
try {
$parent_class_storage = $classlike_storage_provider->get($parent_fq_class_name);
@ -279,6 +347,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer
);
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$interface_name,
$fq_interface_name,
null
);
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name);
} catch (\InvalidArgumentException $e) {
@ -681,22 +757,47 @@ class ClassAnalyzer extends ClassLikeAnalyzer
break;
}
$property_id = strtolower($this->fq_class_name) . '::$' . $prop->name;
if ($codebase->alter_code) {
$property_id = strtolower($this->fq_class_name) . '::$' . $prop->name;
foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
if ($property_id === $original_property_id) {
$file_manipulations = [
new \Psalm\FileManipulation(
(int) $prop->name->getAttribute('startFilePos'),
(int) $prop->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
)
];
$property_storage = $codebase->properties->getStorage($property_id);
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations
if ($property_storage->type
&& $property_storage->type_location
&& $property_storage->type_location !== $property_storage->signature_type_location
) {
$replace_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$property_storage->type,
$this->getFQCLN(),
$this->getFQCLN(),
$this->getParentFQCLN()
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$replace_type,
$property_storage->type_location,
null
);
}
foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
if ($property_id === $original_property_id) {
$file_manipulations = [
new \Psalm\FileManipulation(
(int) $prop->name->getAttribute('startFilePos'),
(int) $prop->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
)
];
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations
);
}
}
}
}

View File

@ -361,12 +361,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$check_stmts = true;
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
if ($codebase->alter_code) {
foreach ($this->function->params as $param) {
$param_name_node = null;
@ -379,7 +374,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
if ($param_name_node) {
$resolved_name = (string) $param_name_node->getAttribute('resolvedName');
$resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($param_name_node, $this->getAliases());
$parent_fqcln = $this->getParentFQCLN();
@ -389,12 +384,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$resolved_name = $parent_fqcln;
}
$codebase->classlikes->airliftClassLikeReference(
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$param_name_node,
$resolved_name,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
(int) $param_name_node->getAttribute('startFilePos'),
(int) $param_name_node->getAttribute('endFilePos') + 1
$context->calling_method_id
);
}
}
@ -411,7 +406,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
if ($return_name_node) {
$resolved_name = (string) $return_name_node->getAttribute('resolvedName');
$resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($return_name_node, $this->getAliases());
$parent_fqcln = $this->getParentFQCLN();
@ -421,12 +416,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$resolved_name = $parent_fqcln;
}
$codebase->classlikes->airliftClassLikeReference(
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$return_name_node,
$resolved_name,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
(int) $return_name_node->getAttribute('startFilePos'),
(int) $return_name_node->getAttribute('endFilePos') + 1
$context->calling_method_id
);
}
}
@ -435,8 +430,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
&& $storage->return_type_location
&& $storage->return_type_location !== $storage->signature_return_type_location
) {
$bounds = $storage->return_type_location->getSelectionBounds();
$replace_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$storage->return_type,
@ -445,12 +438,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$this->getParentFQCLN()
);
$codebase->classlikes->airliftDocblockType(
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$replace_type,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
$bounds[0],
$bounds[1]
$storage->return_type_location,
$context->calling_method_id
);
}
@ -459,8 +452,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
&& $function_param->type_location
&& $function_param->type_location !== $function_param->signature_type_location
) {
$bounds = $function_param->type_location->getSelectionBounds();
$replace_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$function_param->type,
@ -469,12 +460,12 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$this->getParentFQCLN()
);
$codebase->classlikes->airliftDocblockType(
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$replace_type,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
$bounds[0],
$bounds[1]
$function_param->type_location,
$context->calling_method_id
);
}
}

View File

@ -539,16 +539,77 @@ class ProjectAnalyzer
$source_parts = explode('::', $source);
$destination_parts = explode('::', $destination);
if (count($source_parts) === 1 || count($destination_parts) === 1) {
throw new \Psalm\Exception\RefactorException('Cannot yet refactor classes');
}
if (!$this->codebase->classlikes->classExists($source_parts[0])) {
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($source_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Source class ' . $source_parts[0] . ' doesnt exist'
);
}
if (count($source_parts) === 1 && count($destination_parts) === 1) {
if ($this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Destination class ' . $destination_parts[0] . ' already exists'
);
}
$source_class_storage = $this->codebase->classlike_storage_provider->get($source_parts[0]);
foreach ($source_class_storage->methods as $method_name => $method_storage) {
if ($method_storage->is_static) {
$old_method_id = strtolower($source_parts[0] . '::' . $method_name);
$new_method_id = $destination_parts[0] . '::' . $method_name;
$this->codebase->call_transforms[$old_method_id . '\((.*\))'] = $new_method_id . '($1)';
}
}
foreach ($source_class_storage->properties as $property_name => $property_storage) {
if ($property_storage->is_static) {
$old_property_id = strtolower($source_parts[0]) . '::' . $property_name;
$new_property_id = $destination_parts[0] . '::' . $property_name;
$this->codebase->property_transforms[$old_property_id] = $new_property_id;
}
}
$all_class_consts = array_merge(
$source_class_storage->public_class_constants,
$source_class_storage->protected_class_constants,
$source_class_storage->private_class_constants
);
foreach ($all_class_consts as $const_name => $_) {
$old_const_id = strtolower($source_parts[0]) . '::' . $const_name;
$new_const_id = $destination_parts[0] . '::' . $const_name;
$this->codebase->class_constant_transforms[$old_const_id] = $new_const_id;
}
$destination_parts = explode('\\', $destination);
array_pop($destination_parts);
$destination_ns = implode('\\', $destination_parts);
$this->codebase->classes_to_move[strtolower($source)] = $destination;
$destination_class_storage = $this->codebase->classlike_storage_provider->create($destination);
$destination_class_storage->name = $destination;
if ($source_class_storage->aliases) {
$destination_class_storage->aliases = clone $source_class_storage->aliases;
$destination_class_storage->aliases->namespace = $destination_ns;
}
$destination_class_storage->location = $source_class_storage->location;
$destination_class_storage->stmt_location = $source_class_storage->stmt_location;
$destination_class_storage->populated = true;
$this->codebase->class_transforms[strtolower($source)] = $destination;
continue;
}
if ($this->codebase->methods->methodExists($source)) {
if ($this->codebase->methods->methodExists($destination)) {
throw new \Psalm\Exception\RefactorException(
@ -631,7 +692,7 @@ class ProjectAnalyzer
);
if (isset($source_class_constants[$source_parts[1]])) {
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Destination class ' . $destination_parts[0] . ' doesnt exist'
);
@ -682,9 +743,11 @@ class ProjectAnalyzer
$this->progress
);
$this->codebase->classlikes->moveConstants(
$this->codebase->classlikes->moveClassConstants(
$this->progress
);
$this->codebase->classlikes->moveClasses();
}
public function migrateCode() : void
@ -723,10 +786,7 @@ class ProjectAnalyzer
$existing_contents = $this->codebase->file_provider->getContents($file_path);
foreach ($file_manipulations as $manipulation) {
$existing_contents
= substr($existing_contents, 0, $manipulation->start)
. $manipulation->insertion_text
. substr($existing_contents, $manipulation->end);
$existing_contents = $manipulation->transform($existing_contents);
}
$this->codebase->file_provider->setContents($file_path, $existing_contents);

View File

@ -110,6 +110,26 @@ class ForeachAnalyzer
$statements_analyzer->getParentFQCLN()
);
if ($var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$type_location = new CodeLocation\DocblockTypeLocation(
$statements_analyzer,
$var_comment->type_start,
$var_comment->type_end,
$var_comment->line_number,
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$statements_analyzer,
$comment_type,
$type_location,
$context->calling_method_id
);
}
if (isset($context->vars_in_scope[$var_comment->var_id])
|| $statements_analyzer->isSuperGlobal($var_comment->var_id)
) {

View File

@ -901,22 +901,6 @@ class PropertyAssignmentAnalyzer
$codebase = $statements_analyzer->getCodebase();
if ($stmt->class instanceof PhpParser\Node\Name
&& $codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftClassLikeReference(
$fq_class_name,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1
);
}
$prop_name = $stmt->name;
if (!$prop_name instanceof PhpParser\Node\Identifier) {
@ -964,35 +948,45 @@ class PropertyAssignmentAnalyzer
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern
&& $stmt->class instanceof PhpParser\Node\Name
) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
);
$file_manipulations = [];
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
$file_manipulations = [];
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}

View File

@ -120,21 +120,23 @@ class AssignmentAnalyzer
$statements_analyzer->getSuppressedIssues()
);
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
&& $var_comment->type_start
if ($var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftDocblockType(
$var_comment_type,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
$type_location = new CodeLocation\DocblockTypeLocation(
$statements_analyzer,
$var_comment->type_start,
$var_comment->type_end
$var_comment->type_end,
$var_comment->line_number,
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$statements_analyzer,
$var_comment_type,
$type_location,
$context->calling_method_id
);
}

View File

@ -246,19 +246,13 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
}
if ($fq_class_name) {
if ($stmt->class instanceof PhpParser\Node\Name
&& $codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftClassLikeReference(
if ($codebase->alter_code) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
explode('::', $destination_method_id)[0],
$statements_analyzer->getFilePath(),
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1
$context->calling_method_id
);
}

View File

@ -1030,35 +1030,45 @@ class PropertyFetchAnalyzer
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern
&& $stmt->class instanceof PhpParser\Node\Name
) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
);
$file_manipulations = [];
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
list($old_declaring_fq_class_name) = explode('::$', $declaring_property_id);
list($new_fq_class_name, $new_property_name) = explode('::$', $transformation);
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
$file_manipulations = [];
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}

View File

@ -579,6 +579,16 @@ class ExpressionAnalyzer
$fq_class_name
);
}
if ($codebase->alter_code) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
);
}
}
}

View File

@ -950,22 +950,23 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
$this->getSuppressedIssues()
);
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
&& $var_comment->type_start
if ($var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$destination_method_id
= $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftDocblockType(
$var_comment_type,
explode('::', $destination_method_id)[0],
$this->getFilePath(),
$type_location = new CodeLocation\DocblockTypeLocation(
$this,
$var_comment->type_start,
$var_comment->type_end
$var_comment->type_end,
$var_comment->line_number
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$var_comment_type,
$type_location,
$context->calling_method_id
);
}

View File

@ -1156,10 +1156,7 @@ class Analyzer
$existing_contents = $this->file_provider->getContents($file_path);
foreach ($file_manipulations as $manipulation) {
$existing_contents
= substr($existing_contents, 0, $manipulation->start)
. $manipulation->insertion_text
. substr($existing_contents, $manipulation->end);
$existing_contents = $manipulation->transform($existing_contents);
}
if ($dry_run) {

View File

@ -889,7 +889,7 @@ class ClassLikes
/**
* @return void
*/
public function moveConstants(Progress $progress = null)
public function moveClassConstants(Progress $progress = null)
{
if ($progress === null) {
$progress = new VoidProgress();
@ -959,6 +959,199 @@ class ClassLikes
FileManipulationBuffer::addCodeMigrations($code_migrations);
}
/**
* @return void
*/
public function moveClasses()
{
}
public function handleClassLikeReferenceInMigration(
\Psalm\Codebase $codebase,
\Psalm\StatementsSource $source,
PhpParser\Node $class_name_node,
string $fq_class_name,
?string $calling_method_id
) : bool {
$calling_fq_class_name = $source->getFQCLN();
// if we're inside a moved class static method
if ($codebase->methods_to_move
&& $calling_fq_class_name
&& $calling_method_id
&& isset($codebase->methods_to_move[strtolower($calling_method_id)])
) {
$destination_class = explode('::', $codebase->methods_to_move[strtolower($calling_method_id)])[0];
$intended_fq_class_name = strtolower($calling_fq_class_name) === strtolower($fq_class_name)
&& isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
? $destination_class
: $fq_class_name;
$this->airliftClassLikeReference(
$intended_fq_class_name,
$destination_class,
$source->getFilePath(),
(int) $class_name_node->getAttribute('startFilePos'),
(int) $class_name_node->getAttribute('endFilePos') + 1
);
return true;
}
// if we're inside a moved class (could be a method, could be a property/class const default)
if ($codebase->classes_to_move
&& $calling_fq_class_name
&& isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
) {
$destination_class = $codebase->classes_to_move[strtolower($calling_fq_class_name)];
if ($class_name_node instanceof PhpParser\Node\Identifier) {
$destination_parts = explode('\\', $destination_class);
$destination_class_name = array_pop($destination_parts);
$file_manipulations = [];
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $class_name_node->getAttribute('startFilePos'),
(int) $class_name_node->getAttribute('endFilePos') + 1,
$destination_class_name
);
FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
} else {
$this->airliftClassLikeReference(
strtolower($calling_fq_class_name) === strtolower($fq_class_name)
? $destination_class
: $fq_class_name,
$destination_class,
$source->getFilePath(),
(int) $class_name_node->getAttribute('startFilePos'),
(int) $class_name_node->getAttribute('endFilePos') + 1
);
}
return true;
}
// if we're outside a moved class, but we're changing all references to a class
foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) {
if (strtolower($fq_class_name) === $old_fq_class_name) {
$file_manipulations = [];
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $class_name_node->getAttribute('startFilePos'),
(int) $class_name_node->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$source->getNamespace(),
$source->getAliasedClassesFlipped(),
$calling_fq_class_name
)
);
FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
return true;
}
}
return false;
}
public function handleDocblockTypeInMigration(
\Psalm\Codebase $codebase,
\Psalm\StatementsSource $source,
Type\Union $type,
CodeLocation $type_location,
?string $calling_method_id
) : void {
$calling_fq_class_name = $source->getFQCLN();
$moved_type = false;
// if we're inside a moved class static method
if ($codebase->methods_to_move
&& $calling_fq_class_name
&& $calling_method_id
&& isset($codebase->methods_to_move[strtolower($calling_method_id)])
) {
$bounds = $type_location->getSelectionBounds();
$destination_class = explode('::', $codebase->methods_to_move[strtolower($calling_method_id)])[0];
$this->airliftDocblockType(
$type,
$destination_class,
$source->getFilePath(),
$bounds[0],
$bounds[1]
);
$moved_type = true;
}
// if we're inside a moved class (could be a method, could be a property/class const default)
if (!$moved_type
&& $codebase->classes_to_move
&& $calling_fq_class_name
&& isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
) {
$bounds = $type_location->getSelectionBounds();
$destination_class = $codebase->classes_to_move[strtolower($calling_fq_class_name)];
if ($type->containsClassLike(strtolower($calling_fq_class_name))) {
$type = clone $type;
$type->replaceClassLike(strtolower($calling_fq_class_name), $destination_class);
}
$this->airliftDocblockType(
$type,
$destination_class,
$source->getFilePath(),
$bounds[0],
$bounds[1]
);
$moved_type = true;
}
// if we're outside a moved class, but we're changing all references to a class
if (!$moved_type) {
foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) {
if ($type->containsClassLike($old_fq_class_name)) {
$type = clone $type;
$type->replaceClassLike($old_fq_class_name, $new_fq_class_name);
$bounds = $type_location->getSelectionBounds();
$file_manipulations = [];
$file_manipulations[] = new \Psalm\FileManipulation(
$bounds[0],
$bounds[1],
$type->toNamespacedString(
$source->getNamespace(),
$source->getAliasedClassesFlipped(),
null,
false
)
);
FileManipulationBuffer::add(
$source->getFilePath(),
$file_manipulations
);
}
}
}
}
public function airliftClassLikeReference(
string $fq_class_name,
string $destination_fq_class_name,

View File

@ -93,6 +93,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
/** @var int */
private $php_minor_version;
/** @var PhpParser\Node\Name|null */
private $namespace_name;
/**
* @var array<string, array<int, string>>
*/
@ -161,6 +164,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
$this->file_aliases = $this->aliases;
$this->namespace_name = $node->name;
$this->aliases = new Aliases(
$node->name ? implode('\\', $node->name->parts) : '',
$this->aliases->uses,
@ -818,6 +824,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->stmt_location = $class_location;
$storage->location = $name_location;
if ($this->namespace_name) {
$storage->namespace_name_location = new CodeLocation($this->file_scanner, $this->namespace_name);
}
$storage->user_defined = !$this->codebase->register_stub_files;
$storage->stubbed = $this->codebase->register_stub_files;
$storage->aliases = $this->aliases;

View File

@ -176,6 +176,11 @@ class ClassLikeStorage
*/
public $stmt_location;
/**
* @var CodeLocation|null
*/
public $namespace_name_location;
/**
* @var bool
*/

View File

@ -1039,6 +1039,22 @@ abstract class Type
return $aliased_classes[strtolower($value)];
}
if (strpos($value, '\\')) {
$parts = explode('\\', $value);
$suffix = array_pop($parts);
while ($parts) {
$left = implode('\\', $parts);
if (isset($aliased_classes[strtolower($left)])) {
return $aliased_classes[strtolower($left)] . '\\' . $suffix;
}
$suffix = array_pop($parts) . '\\' . $suffix;
}
}
return '\\' . $value;
}

View File

@ -709,6 +709,148 @@ abstract class Atomic
}
}
public function containsClassLike(string $fq_classlike_name) : bool
{
if ($this instanceof TNamedObject) {
if (strtolower($this->value) === $fq_classlike_name) {
return true;
}
}
if ($this instanceof TNamedObject
|| $this instanceof TIterable
|| $this instanceof TTemplateParam
) {
if ($this->extra_types) {
foreach ($this->extra_types as $extra_type) {
if ($extra_type->containsClassLike($fq_classlike_name)) {
return true;
}
}
}
}
if ($this instanceof TScalarClassConstant) {
if (strtolower($this->fq_classlike_name) === $fq_classlike_name) {
return true;
}
}
if ($this instanceof TClassString && $this->as !== 'object') {
if (strtolower($this->as) === $fq_classlike_name) {
return true;
}
}
if ($this instanceof TTemplateParam) {
if ($this->as->containsClassLike($fq_classlike_name)) {
return true;
}
}
if ($this instanceof TLiteralClassString) {
if (strtolower($this->value) === $fq_classlike_name) {
return true;
}
}
if ($this instanceof Type\Atomic\TArray
|| $this instanceof Type\Atomic\TGenericObject
|| $this instanceof Type\Atomic\TIterable
) {
foreach ($this->type_params as $type_param) {
if ($type_param->containsClassLike($fq_classlike_name)) {
return true;
}
}
}
if ($this instanceof Type\Atomic\TFn
|| $this instanceof Type\Atomic\TCallable
) {
if ($this->params) {
foreach ($this->params as $param) {
if ($param->type && $param->type->containsClassLike($fq_classlike_name)) {
return true;
}
}
}
if ($this->return_type && $this->return_type->containsClassLike($fq_classlike_name)) {
return true;
}
}
return false;
}
public function replaceClassLike(string $old, string $new) : void
{
if ($this instanceof TNamedObject) {
if (strtolower($this->value) === $old) {
$this->value = $new;
}
}
if ($this instanceof TNamedObject
|| $this instanceof TIterable
|| $this instanceof TTemplateParam
) {
if ($this->extra_types) {
foreach ($this->extra_types as $extra_type) {
$extra_type->replaceClassLike($old, $new);
}
}
}
if ($this instanceof TScalarClassConstant) {
if (strtolower($old) === $new) {
$this->fq_classlike_name = $new;
}
}
if ($this instanceof TClassString && $this->as !== 'object') {
if (strtolower($this->as) === $old) {
$this->as = $new;
}
}
if ($this instanceof TTemplateParam) {
$this->as->replaceClassLike($old, $new);
}
if ($this instanceof TLiteralClassString) {
if (strtolower($this->value) === $old) {
$this->value = $new;
}
}
if ($this instanceof Type\Atomic\TArray
|| $this instanceof Type\Atomic\TGenericObject
|| $this instanceof Type\Atomic\TIterable
) {
foreach ($this->type_params as $type_param) {
$type_param->replaceClassLike($old, $new);
}
}
if ($this instanceof Type\Atomic\TFn
|| $this instanceof Type\Atomic\TCallable
) {
if ($this->params) {
foreach ($this->params as $param) {
if ($param->type) {
$param->type->replaceClassLike($old, $new);
}
}
}
if ($this->return_type) {
$this->return_type->replaceClassLike($old, $new);
}
}
}
/**
* @param Atomic $other
*

View File

@ -1674,6 +1674,30 @@ class Union
}
}
public function containsClassLike(string $fq_class_like_name) : bool
{
foreach ($this->types as $atomic_type) {
if ($atomic_type->containsClassLike($fq_class_like_name)) {
return true;
}
}
return false;
}
public function replaceClassLike(string $old, string $new) : void
{
foreach ($this->types as $key => $atomic_type) {
$atomic_type->replaceClassLike($old, $new);
unset($this->types[$key]);
$this->types[$atomic_type->getKey()] = $atomic_type;
}
$this->id = null;
}
/**
* @return bool
*/

View File

@ -0,0 +1,363 @@
<?php
namespace Psalm\Tests\FileManipulation;
use Psalm\Context;
use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Tests\Internal\Provider;
use Psalm\Tests\TestConfig;
class ClassMoveTest extends \Psalm\Tests\TestCase
{
/** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */
protected $project_analyzer;
public function setUp() : void
{
FileAnalyzer::clearCache();
\Psalm\Internal\FileManipulation\FunctionDocblockManipulator::clearCache();
$this->file_provider = new Provider\FakeFileProvider();
}
/**
* @dataProvider providerValidCodeParse
*
* @param string $input_code
* @param string $output_code
* @param array<string, string> $constants_to_move
* @param array<string, string> $call_transforms
*
* @return void
*/
public function testValidCode(
string $input_code,
string $output_code,
array $constants_to_move
) {
$test_name = $this->getTestName();
if (strpos($test_name, 'SKIPPED-') !== false) {
$this->markTestSkipped('Skipped due to a bug.');
}
$config = new TestConfig();
$this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer(
$config,
new \Psalm\Internal\Provider\Providers(
$this->file_provider,
new Provider\FakeParserCacheProvider()
)
);
$context = new Context();
$file_path = self::$src_dir_path . 'somefile.php';
$this->addFile(
$file_path,
$input_code
);
$codebase = $this->project_analyzer->getCodebase();
$this->project_analyzer->refactorCodeAfterCompletion($constants_to_move);
$this->analyzeFile($file_path, $context);
$this->project_analyzer->prepareMigration();
$codebase->analyzer->updateFile($file_path, false);
$this->project_analyzer->migrateCode();
$this->assertSame($output_code, $codebase->getFileContents($file_path));
}
/**
* @return array<string,array{string,string,array<string, string>}>
*/
public function providerValidCodeParse()
{
return [
'renameEmptyClass' => [
'<?php
namespace Ns;
class A {}
class C extends A {
/**
* @var A
*/
public $one;
}
/**
* @param A $a
* @return A
*/
function foo(A $a) : A {
return $a;
}
/** @var A */
$i = new A();
if ($i instanceof A) {}',
'<?php
namespace Ns;
class B {}
class C extends B {
/**
* @var B
*/
public $one;
}
/**
* @param B $a
* @return B
*/
function foo(B $a) : B {
return $a;
}
/** @var B */
$i = new B();
if ($i instanceof B) {}',
[
'Ns\A' => 'Ns\B',
]
],
'renameClassWithInstanceMethod' => [
'<?php
namespace Ns;
class A {
/**
* @param self $one
* @param A $two
*/
public function foo(self $one, A $two) : void {}
}
function foo(A $a) : A {
return $a->foo($a, $a);
}',
'<?php
namespace Ns;
class B {
/**
* @param self $one
* @param self $two
*/
public function foo(self $one, self $two) : void {}
}
function foo(B $a) : B {
return $a->foo($a, $a);
}',
[
'Ns\A' => 'Ns\B',
]
],
'renameClassWithStaticMethod' => [
'<?php
namespace Ns;
class A {
/**
* @param self $one
* @param A $two
*/
public static function foo(self $one, A $two) : void {
A::foo($one, $two);
}
}
function foo() {
A::foo(new A(), A::foo());
}',
'<?php
namespace Ns;
class B {
/**
* @param self $one
* @param self $two
*/
public static function foo(self $one, self $two) : void {
B::foo($one, $two);
}
}
function foo() {
B::foo(new B(), B::foo());
}',
[
'Ns\A' => 'Ns\B',
]
],
'renameClassWithInstanceProperty' => [
'<?php
namespace Ns;
class A {
/**
* @var A
*/
public $one;
/**
* @var self
*/
public $two;
}',
'<?php
namespace Ns;
class B {
/**
* @var self
*/
public $one;
/**
* @var self
*/
public $two;
}',
[
'Ns\A' => 'Ns\B',
]
],
'renameClassWithStaticProperty' => [
'<?php
namespace Ns;
class A {
/**
* @var string
*/
public static $one = "one";
}
echo A::$one;
A::$one = "two";',
'<?php
namespace Ns;
class B {
/**
* @var string
*/
public static $one = "one";
}
echo B::$one;
B::$one = "two";',
[
'Ns\A' => 'Ns\B',
]
],
'moveClassIntoNamespace' => [
'<?php
class A {
/** @var ?Exception */
public $x;
/**
* @param ArrayObject<int, A> $a
*/
public function foo(ArrayObject $a) : Exception {
foreach ($a as $b) {
$b->bar();
}
return new Exception("bad");
}
public function bar() : void {}
}',
'<?php
namespace Foo\Bar\Baz;
class B {
/** @var null|\Exception */
public $x;
/**
* @param \ArrayObject<int, self> $a
*/
public function foo(\ArrayObject $a) : \Exception {
foreach ($a as $b) {
$b->bar();
}
return new \Exception("bad");
}
public function bar() : void {}
}',
[
'A' => 'Foo\Bar\Baz\B',
]
],
'moveClassDeeperIntoNamespace' => [
'<?php
namespace Foo;
use Exception;
use ArrayObject;
class A {
/** @var ?Exception */
public $x;
/**
* @param ArrayObject<int, A> $a
*/
public function foo(ArrayObject $a) : Exception {
foreach ($a as $b) {
$b->bar();
}
return new Exception("bad");
}
public function bar() : void {}
}',
'<?php
namespace Foo\Bar\Baz;
use Exception;
use ArrayObject;
class B {
/** @var null|Exception */
public $x;
/**
* @param ArrayObject<int, self> $a
*/
public function foo(ArrayObject $a) : Exception {
foreach ($a as $b) {
$b->bar();
}
return new Exception("bad");
}
public function bar() : void {}
}',
[
'Foo\A' => 'Foo\Bar\Baz\B',
]
],
];
}
}

View File

@ -6,7 +6,7 @@ use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Tests\Internal\Provider;
use Psalm\Tests\TestConfig;
class MoveMethodTest extends \Psalm\Tests\TestCase
class MethodMoveTest extends \Psalm\Tests\TestCase
{
/** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */
protected $project_analyzer;

View File

@ -888,9 +888,9 @@ class ReturnTypeManipulationTest extends FileManipulationTest
class D {
/**
* @return \A\B\C[]
* @return B\C[]
*
* @psalm-return array{0:\A\B\C}
* @psalm-return array{0:B\C}
*/
public function getArrayOfC(): array {
return [new \A\B\C];