1
0
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:
Matthew Brown 2019-06-04 00:32:19 -04:00
parent 20422cf223
commit a9809ab28a
11 changed files with 781 additions and 198 deletions

View File

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

View File

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

View File

@ -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] . ' doesnt 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] . ' doesnt 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] . ' doesnt 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] . ' doesnt 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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