mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Add property moving & renaming
This commit is contained in:
parent
20422cf223
commit
a9809ab28a
@ -187,11 +187,41 @@ class Codebase
|
||||
*/
|
||||
public $methods_to_rename = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $properties_to_move = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $properties_to_rename = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $class_constants_to_move = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $class_constants_to_rename = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $call_transforms = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $property_transforms = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $class_constant_transforms = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
|
@ -680,6 +680,25 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$member_stmts[] = $stmt;
|
||||
break;
|
||||
}
|
||||
|
||||
$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
|
||||
)
|
||||
];
|
||||
|
||||
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
|
||||
$this->getFilePath(),
|
||||
$file_manipulations
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
$member_stmts[] = $stmt;
|
||||
|
@ -543,12 +543,20 @@ class ProjectAnalyzer
|
||||
throw new \Psalm\Exception\RefactorException('Cannot yet refactor classes');
|
||||
}
|
||||
|
||||
if (!$this->codebase->classlikes->classExists($source_parts[0])) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Source class ' . $source_parts[0] . ' doesn’t exist'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->codebase->methods->methodExists($source)) {
|
||||
if ($this->codebase->methods->methodExists($destination)) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination ' . $destination . ' already exists'
|
||||
'Destination method ' . $destination . ' already exists'
|
||||
);
|
||||
} elseif (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
||||
}
|
||||
|
||||
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
||||
);
|
||||
@ -572,8 +580,87 @@ class ProjectAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($source_parts[1][0] === '$') {
|
||||
if ($destination_parts[1][0] !== '$') {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination property must be of the form Foo::$bar'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->codebase->properties->propertyExists($source, true)) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Property ' . $source . ' does not exist'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->codebase->properties->propertyExists($destination, true)) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination property ' . $destination . ' already exists'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
||||
);
|
||||
}
|
||||
|
||||
$source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
|
||||
|
||||
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
|
||||
$source_storage = $this->codebase->properties->getStorage($source);
|
||||
|
||||
if (!$source_storage->is_static) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Cannot move non-static property ' . $source
|
||||
);
|
||||
}
|
||||
|
||||
$this->codebase->properties_to_move[$source_id] = $destination;
|
||||
} else {
|
||||
$this->codebase->properties_to_rename[$source_id] = substr($destination_parts[1], 1);
|
||||
}
|
||||
|
||||
$this->codebase->property_transforms[$source_id] = $destination;
|
||||
continue;
|
||||
}
|
||||
|
||||
$source_class_constants = $this->codebase->classlikes->getConstantsForClass(
|
||||
$source_parts[0],
|
||||
\ReflectionProperty::IS_PRIVATE
|
||||
);
|
||||
|
||||
if (isset($source_class_constants[$source_parts[1]])) {
|
||||
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
||||
);
|
||||
}
|
||||
|
||||
$destination_class_constants = $this->codebase->classlikes->getConstantsForClass(
|
||||
$destination_parts[0],
|
||||
\ReflectionProperty::IS_PRIVATE
|
||||
);
|
||||
|
||||
if (isset($destination_class_constants[$destination_parts[1]])) {
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'Destination constant ' . $destination . ' already exists'
|
||||
);
|
||||
}
|
||||
|
||||
$source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
|
||||
|
||||
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
|
||||
$this->codebase->class_constants_to_move[$source_id] = $destination;
|
||||
} else {
|
||||
$this->codebase->class_constants_to_rename[$source_id] = $destination_parts[1];
|
||||
}
|
||||
|
||||
$this->codebase->class_constant_transforms[$source_id] = $destination;
|
||||
}
|
||||
|
||||
throw new \Psalm\Exception\RefactorException(
|
||||
'At present Psalm can only move static methods (attempted to move ' . $source . ')'
|
||||
'Psalm cannot locate ' . $source
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -588,6 +675,11 @@ class ProjectAnalyzer
|
||||
$this->codebase->methods,
|
||||
$this->progress
|
||||
);
|
||||
|
||||
$this->codebase->classlikes->moveProperties(
|
||||
$this->codebase->properties,
|
||||
$this->progress
|
||||
);
|
||||
}
|
||||
|
||||
public function migrateCode() : void
|
||||
@ -625,19 +717,11 @@ class ProjectAnalyzer
|
||||
|
||||
$existing_contents = $this->codebase->file_provider->getContents($file_path);
|
||||
|
||||
$pre_applied_manipulations = [];
|
||||
|
||||
foreach ($file_manipulations as $manipulation) {
|
||||
if (isset($pre_applied_manipulations[$manipulation->getKey()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing_contents
|
||||
= substr($existing_contents, 0, $manipulation->start)
|
||||
. $manipulation->insertion_text
|
||||
. substr($existing_contents, $manipulation->end);
|
||||
|
||||
$pre_applied_manipulations[$manipulation->getKey()] = true;
|
||||
}
|
||||
|
||||
$this->codebase->file_provider->setContents($file_path, $existing_contents);
|
||||
|
@ -9,6 +9,7 @@ use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TypeAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Issue\DeprecatedProperty;
|
||||
@ -510,6 +511,27 @@ class PropertyAssignmentAnalyzer
|
||||
false
|
||||
);
|
||||
|
||||
if ($codebase->properties_to_rename) {
|
||||
$declaring_property_id = strtolower($declaring_property_class) . '::$' . $prop_name;
|
||||
|
||||
foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
|
||||
if ($declaring_property_id === $original_property_id) {
|
||||
$file_manipulations = [
|
||||
new \Psalm\FileManipulation(
|
||||
(int) $stmt->name->getAttribute('startFilePos'),
|
||||
(int) $stmt->name->getAttribute('endFilePos') + 1,
|
||||
$new_property_name
|
||||
)
|
||||
];
|
||||
|
||||
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$file_manipulations
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
|
||||
|
||||
$property_storage = null;
|
||||
@ -879,6 +901,22 @@ 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) {
|
||||
@ -924,6 +962,40 @@ class PropertyAssignmentAnalyzer
|
||||
false
|
||||
);
|
||||
|
||||
$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);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get((string)$declaring_property_class);
|
||||
|
||||
$property_storage = $class_storage->properties[$prop_name->name];
|
||||
|
@ -7,6 +7,7 @@ use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Issue\DeprecatedProperty;
|
||||
@ -629,6 +630,27 @@ class PropertyFetchAnalyzer
|
||||
true
|
||||
);
|
||||
|
||||
if ($codebase->properties_to_rename) {
|
||||
$declaring_property_id = strtolower($declaring_property_class) . '::$' . $prop_name;
|
||||
|
||||
foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
|
||||
if ($declaring_property_id === $original_property_id) {
|
||||
$file_manipulations = [
|
||||
new \Psalm\FileManipulation(
|
||||
(int) $stmt->name->getAttribute('startFilePos'),
|
||||
(int) $stmt->name->getAttribute('endFilePos') + 1,
|
||||
$new_property_name
|
||||
)
|
||||
];
|
||||
|
||||
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$file_manipulations
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$declaring_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$declaring_property_class
|
||||
);
|
||||
@ -875,6 +897,22 @@ class PropertyFetchAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($fq_class_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
|
||||
);
|
||||
}
|
||||
|
||||
$stmt->class->inferredType = $fq_class_name ? new Type\Union([new TNamedObject($fq_class_name)]) : null;
|
||||
}
|
||||
|
||||
@ -990,6 +1028,40 @@ class PropertyFetchAnalyzer
|
||||
true
|
||||
);
|
||||
|
||||
$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);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get((string)$declaring_property_class);
|
||||
$property = $class_storage->properties[$prop_name];
|
||||
|
||||
|
@ -1155,19 +1155,11 @@ class Analyzer
|
||||
|
||||
$existing_contents = $this->file_provider->getContents($file_path);
|
||||
|
||||
$pre_applied_manipulations = [];
|
||||
|
||||
foreach ($file_manipulations as $manipulation) {
|
||||
if (isset($pre_applied_manipulations[$manipulation->getKey()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing_contents
|
||||
= substr($existing_contents, 0, $manipulation->start)
|
||||
. $manipulation->insertion_text
|
||||
. substr($existing_contents, $manipulation->end);
|
||||
|
||||
$pre_applied_manipulations[$manipulation->getKey()] = true;
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
|
@ -721,43 +721,43 @@ class ClassLikes
|
||||
return;
|
||||
}
|
||||
|
||||
$progress->debug('Refacting methods ' . PHP_EOL);
|
||||
$progress->debug('Refactoring methods ' . PHP_EOL);
|
||||
|
||||
$code_migrations = [];
|
||||
|
||||
foreach ($codebase->methods_to_move as $original => $eventual) {
|
||||
foreach ($codebase->methods_to_move as $source => $destination) {
|
||||
try {
|
||||
$original_method_storage = $methods->getStorage($original);
|
||||
$source_method_storage = $methods->getStorage($source);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list($eventual_fq_class_name, $eventual_name) = explode('::', $eventual);
|
||||
list($destination_fq_class_name, $destination_name) = explode('::', $destination);
|
||||
|
||||
try {
|
||||
$classlike_storage = $this->classlike_storage_provider->get($eventual_fq_class_name);
|
||||
$classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($classlike_storage->stmt_location
|
||||
&& $this->config->isInProjectDirs($classlike_storage->stmt_location->file_path)
|
||||
&& $original_method_storage->stmt_location
|
||||
&& $original_method_storage->stmt_location->file_path
|
||||
&& $original_method_storage->location
|
||||
&& $source_method_storage->stmt_location
|
||||
&& $source_method_storage->stmt_location->file_path
|
||||
&& $source_method_storage->location
|
||||
) {
|
||||
$new_class_bounds = $classlike_storage->stmt_location->getSnippetBounds();
|
||||
$old_method_bounds = $original_method_storage->stmt_location->getSnippetBounds();
|
||||
$old_method_bounds = $source_method_storage->stmt_location->getSnippetBounds();
|
||||
|
||||
$old_method_name_bounds = $original_method_storage->location->getSelectionBounds();
|
||||
$old_method_name_bounds = $source_method_storage->location->getSelectionBounds();
|
||||
|
||||
FileManipulationBuffer::add(
|
||||
$original_method_storage->stmt_location->file_path,
|
||||
$source_method_storage->stmt_location->file_path,
|
||||
[
|
||||
new \Psalm\FileManipulation(
|
||||
$old_method_name_bounds[0],
|
||||
$old_method_name_bounds[1],
|
||||
$eventual_name
|
||||
$destination_name
|
||||
)
|
||||
]
|
||||
);
|
||||
@ -773,7 +773,7 @@ class ClassLikes
|
||||
}
|
||||
|
||||
$code_migrations[] = new \Psalm\Internal\FileManipulation\CodeMigration(
|
||||
$original_method_storage->stmt_location->file_path,
|
||||
$source_method_storage->stmt_location->file_path,
|
||||
$old_method_bounds[0],
|
||||
$old_method_bounds[1],
|
||||
$classlike_storage->stmt_location->file_path,
|
||||
@ -785,6 +785,107 @@ class ClassLikes
|
||||
FileManipulationBuffer::addCodeMigrations($code_migrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function moveProperties(Properties $properties, Progress $progress = null)
|
||||
{
|
||||
if ($progress === null) {
|
||||
$progress = new VoidProgress();
|
||||
}
|
||||
|
||||
$project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
if (!$codebase->properties_to_move) {
|
||||
return;
|
||||
}
|
||||
|
||||
$progress->debug('Refacting properties ' . PHP_EOL);
|
||||
|
||||
$code_migrations = [];
|
||||
|
||||
foreach ($codebase->properties_to_move as $source => $destination) {
|
||||
try {
|
||||
$source_property_storage = $properties->getStorage($source);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list($source_fq_class_name) = explode('::', $source);
|
||||
list($destination_fq_class_name, $destination_name) = explode('::$', $destination);
|
||||
|
||||
$source_classlike_storage = $this->classlike_storage_provider->get($source_fq_class_name);
|
||||
$destination_classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
|
||||
|
||||
if ($destination_classlike_storage->stmt_location
|
||||
&& $this->config->isInProjectDirs($destination_classlike_storage->stmt_location->file_path)
|
||||
&& $source_property_storage->stmt_location
|
||||
&& $source_property_storage->stmt_location->file_path
|
||||
&& $source_property_storage->location
|
||||
) {
|
||||
if ($source_property_storage->type
|
||||
&& $source_property_storage->type_location
|
||||
&& $source_property_storage->type_location !== $source_property_storage->signature_type_location
|
||||
) {
|
||||
$bounds = $source_property_storage->type_location->getSelectionBounds();
|
||||
|
||||
$replace_type = \Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::fleshOutType(
|
||||
$codebase,
|
||||
$source_property_storage->type,
|
||||
$source_classlike_storage->name,
|
||||
$source_classlike_storage->name,
|
||||
$source_classlike_storage->parent_class
|
||||
);
|
||||
|
||||
$this->airliftDocblockType(
|
||||
$replace_type,
|
||||
$destination_fq_class_name,
|
||||
$source_property_storage->stmt_location->file_path,
|
||||
$bounds[0],
|
||||
$bounds[1]
|
||||
);
|
||||
}
|
||||
|
||||
$new_class_bounds = $destination_classlike_storage->stmt_location->getSnippetBounds();
|
||||
$old_property_bounds = $source_property_storage->stmt_location->getSnippetBounds();
|
||||
|
||||
$old_property_name_bounds = $source_property_storage->location->getSelectionBounds();
|
||||
|
||||
FileManipulationBuffer::add(
|
||||
$source_property_storage->stmt_location->file_path,
|
||||
[
|
||||
new \Psalm\FileManipulation(
|
||||
$old_property_name_bounds[0],
|
||||
$old_property_name_bounds[1],
|
||||
'$' . $destination_name
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
$selection = $destination_classlike_storage->stmt_location->getSnippet();
|
||||
|
||||
$insert_pos = strrpos($selection, "\n", -1);
|
||||
|
||||
if (!$insert_pos) {
|
||||
$insert_pos = strlen($selection) - 1;
|
||||
} else {
|
||||
$insert_pos++;
|
||||
}
|
||||
|
||||
$code_migrations[] = new \Psalm\Internal\FileManipulation\CodeMigration(
|
||||
$source_property_storage->stmt_location->file_path,
|
||||
$old_property_bounds[0],
|
||||
$old_property_bounds[1],
|
||||
$destination_classlike_storage->stmt_location->file_path,
|
||||
$new_class_bounds[0] + $insert_pos
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileManipulationBuffer::addCodeMigrations($code_migrations);
|
||||
}
|
||||
|
||||
public function airliftClassLikeReference(
|
||||
string $fq_class_name,
|
||||
string $destination_fq_class_name,
|
||||
|
@ -23,9 +23,13 @@ class FileManipulationBuffer
|
||||
*/
|
||||
public static function add($file_path, array $file_manipulations)
|
||||
{
|
||||
self::$file_manipulations[$file_path] = isset(self::$file_manipulations[$file_path])
|
||||
? array_merge(self::$file_manipulations[$file_path], $file_manipulations)
|
||||
: $file_manipulations;
|
||||
if (!isset(self::$file_manipulations[$file_path])) {
|
||||
self::$file_manipulations[$file_path] = [];
|
||||
}
|
||||
|
||||
foreach ($file_manipulations as $file_manipulation) {
|
||||
self::$file_manipulations[$file_path][$file_manipulation->getKey()] = $file_manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param CodeMigration[] $code_migrations */
|
||||
@ -149,16 +153,18 @@ class FileManipulationBuffer
|
||||
$code_migration->destination_start
|
||||
);
|
||||
|
||||
$code_migration_manipulations[$code_migration->destination_file_path][]
|
||||
= new FileManipulation(
|
||||
$code_migration->destination_start + $destination_start_offset,
|
||||
$code_migration->destination_start + $destination_start_offset,
|
||||
PHP_EOL . substr(
|
||||
$file_provider->getContents($code_migration->source_file_path),
|
||||
$code_migration->source_start + $start_offset,
|
||||
$code_migration->source_end - $code_migration->source_start + $middle_offset
|
||||
) . PHP_EOL
|
||||
);
|
||||
$manipulation = new FileManipulation(
|
||||
$code_migration->destination_start + $destination_start_offset,
|
||||
$code_migration->destination_start + $destination_start_offset,
|
||||
PHP_EOL . substr(
|
||||
$file_provider->getContents($code_migration->source_file_path),
|
||||
$code_migration->source_start + $start_offset,
|
||||
$code_migration->source_end - $code_migration->source_start + $middle_offset
|
||||
) . PHP_EOL
|
||||
);
|
||||
|
||||
$code_migration_manipulations[$code_migration->destination_file_path][$manipulation->getKey()]
|
||||
= $manipulation;
|
||||
}
|
||||
|
||||
return $code_migration_manipulations;
|
||||
|
@ -79,27 +79,24 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
|
||||
public function providerValidCodeParse()
|
||||
{
|
||||
return [
|
||||
'moveSimpleStaticMethod' => [
|
||||
'moveSimpleStaticMethodWithForeachIterator' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
const C = 5;
|
||||
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public static function Foo() {
|
||||
return new ArrayObject([self::C]);
|
||||
return new ArrayObject([1, 2, 3]);
|
||||
}
|
||||
}
|
||||
|
||||
class B {
|
||||
public static function bar() : void {
|
||||
A::Foo();
|
||||
|
||||
foreach (A::Foo() as $f) {}
|
||||
}
|
||||
}',
|
||||
@ -109,24 +106,99 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
const C = 5;
|
||||
|
||||
|
||||
}
|
||||
|
||||
class B {
|
||||
public static function bar() : void {
|
||||
B::Fe();
|
||||
|
||||
foreach (B::Fe() as $f) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public static function Fe() {
|
||||
return new ArrayObject([A::C]);
|
||||
return new ArrayObject([1, 2, 3]);
|
||||
}
|
||||
}',
|
||||
[
|
||||
'Ns\A::Foo' => 'Ns\B::Fe',
|
||||
]
|
||||
],
|
||||
'moveSimpleStaticMethodWithConstant' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
const C = 5;
|
||||
|
||||
public static function Foo() : void {
|
||||
echo self::C;
|
||||
}
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
const C = 5;
|
||||
|
||||
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
|
||||
public static function Fe() : void {
|
||||
echo A::C;
|
||||
}
|
||||
}',
|
||||
[
|
||||
'Ns\A::Foo' => 'Ns\B::Fe',
|
||||
]
|
||||
],
|
||||
'moveSimpleStaticMethodWithProperty' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var int */
|
||||
public static $baz;
|
||||
|
||||
public static function Foo() : void {
|
||||
echo self::$baz;
|
||||
echo A::$baz . " ";
|
||||
self::$baz = 12;
|
||||
A::$baz = 14;
|
||||
}
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var int */
|
||||
public static $baz;
|
||||
|
||||
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
|
||||
public static function Fe() : void {
|
||||
echo A::$baz;
|
||||
echo A::$baz . " ";
|
||||
A::$baz = 12;
|
||||
A::$baz = 14;
|
||||
}
|
||||
}',
|
||||
[
|
||||
'Ns\A::Foo' => 'Ns\B::Fe',
|
||||
@ -483,6 +555,67 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
|
||||
'Ns1\A::Foo' => 'Ns2\Ns3\B::Fedcba',
|
||||
]
|
||||
],
|
||||
'renameInstanceMethod' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public function Foo() {
|
||||
return new ArrayObject([self::C]);
|
||||
}
|
||||
|
||||
public function bat() {
|
||||
$this->foo();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
public static function bar(A $a) : void {
|
||||
$a->Foo();
|
||||
|
||||
$this->foo();
|
||||
parent::foo();
|
||||
|
||||
foreach ($a->Foo() as $f) {}
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public function Fedcba() {
|
||||
return new ArrayObject([self::C]);
|
||||
}
|
||||
|
||||
public function bat() {
|
||||
$this->Fedcba();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
public static function bar(A $a) : void {
|
||||
$a->Fedcba();
|
||||
|
||||
$this->Fedcba();
|
||||
parent::Fedcba();
|
||||
|
||||
foreach ($a->Fedcba() as $f) {}
|
||||
}
|
||||
}',
|
||||
[
|
||||
'Ns\A::foo' => 'Ns\A::Fedcba',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,144 +0,0 @@
|
||||
<?php
|
||||
namespace Psalm\Tests\FileManipulation;
|
||||
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use Psalm\Tests\TestConfig;
|
||||
|
||||
class MethodRenameTest 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> $to_refactor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testValidCode(
|
||||
string $input_code,
|
||||
string $output_code,
|
||||
array $to_refactor
|
||||
) {
|
||||
$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($to_refactor);
|
||||
|
||||
$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 [
|
||||
'renameMethod' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public function Foo() {
|
||||
return new ArrayObject([self::C]);
|
||||
}
|
||||
|
||||
public function bat() {
|
||||
$this->foo();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
public static function bar(A $a) : void {
|
||||
$a->Foo();
|
||||
|
||||
$this->foo();
|
||||
parent::foo();
|
||||
|
||||
foreach ($a->Foo() as $f) {}
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
/**
|
||||
* @return ArrayObject<int, int>
|
||||
*/
|
||||
public function Fedcba() {
|
||||
return new ArrayObject([self::C]);
|
||||
}
|
||||
|
||||
public function bat() {
|
||||
$this->Fedcba();
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
public static function bar(A $a) : void {
|
||||
$a->Fedcba();
|
||||
|
||||
$this->Fedcba();
|
||||
parent::Fedcba();
|
||||
|
||||
foreach ($a->Fedcba() as $f) {}
|
||||
}
|
||||
}',
|
||||
[
|
||||
'Ns\A::foo' => 'Ns\A::Fedcba',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
218
tests/FileManipulation/PropertyMoveTest.php
Normal file
218
tests/FileManipulation/PropertyMoveTest.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
namespace Psalm\Tests\FileManipulation;
|
||||
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use Psalm\Tests\TestConfig;
|
||||
|
||||
class PropertyMoveTest 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> $properties_to_move
|
||||
* @param array<string, string> $call_transforms
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testValidCode(
|
||||
string $input_code,
|
||||
string $output_code,
|
||||
array $properties_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($properties_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 [
|
||||
'moveSimpleStaticProperty' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
/** @var ArrayObject<int, int> */
|
||||
public static $foo;
|
||||
}
|
||||
|
||||
class B {
|
||||
public static function bar() : void {
|
||||
foreach (A::$foo as $f) {}
|
||||
}
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
class A {
|
||||
|
||||
}
|
||||
|
||||
class B {
|
||||
public static function bar() : void {
|
||||
foreach (B::$fooBar as $f) {}
|
||||
}
|
||||
|
||||
/** @var ArrayObject<int, int> */
|
||||
public static $fooBar;
|
||||
}',
|
||||
[
|
||||
'Ns\A::$foo' => 'Ns\B::$fooBar',
|
||||
]
|
||||
],
|
||||
'renameInstanceProperty' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var ?int */
|
||||
public $foo;
|
||||
}
|
||||
|
||||
function foo(A $a) {
|
||||
echo $a->foo;
|
||||
$a->foo = 10;
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var ?int */
|
||||
public $fooBar;
|
||||
}
|
||||
|
||||
function foo(A $a) {
|
||||
echo $a->fooBar;
|
||||
$a->fooBar = 10;
|
||||
}',
|
||||
[
|
||||
'Ns\A::$foo' => 'Ns\A::$fooBar',
|
||||
]
|
||||
],
|
||||
'renameStaticProperty' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var ?int */
|
||||
public static $foo;
|
||||
}
|
||||
|
||||
function foo() {
|
||||
echo A::$foo;
|
||||
A::$foo = 10;
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var ?int */
|
||||
public static $fooBar;
|
||||
}
|
||||
|
||||
function foo() {
|
||||
echo A::$fooBar;
|
||||
A::$fooBar = 10;
|
||||
}',
|
||||
[
|
||||
'Ns\A::$foo' => 'Ns\A::$fooBar',
|
||||
]
|
||||
],
|
||||
'moveStaticProperty' => [
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
/** @var ?int */
|
||||
public static $foo;
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
}
|
||||
|
||||
function foo() {
|
||||
echo A::$foo;
|
||||
A::$foo = 10;
|
||||
}',
|
||||
'<?php
|
||||
namespace Ns;
|
||||
|
||||
class A {
|
||||
|
||||
}
|
||||
|
||||
class B {
|
||||
|
||||
|
||||
/** @var null|int */
|
||||
public static $fooBar;
|
||||
}
|
||||
|
||||
function foo() {
|
||||
echo B::$fooBar;
|
||||
B::$fooBar = 10;
|
||||
}',
|
||||
[
|
||||
'Ns\A::$foo' => 'Ns\B::$fooBar',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user