1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Allow instance method renaming, too

This commit is contained in:
Matthew Brown 2019-06-02 12:02:32 -04:00
parent 7f86e3cdc5
commit 2439a9f6a0
15 changed files with 351 additions and 54 deletions

View File

@ -180,7 +180,12 @@ class Codebase
/**
* @var array<string, string>
*/
public $method_migrations = [];
public $methods_to_move = [];
/**
* @var array<string, string>
*/
public $methods_to_rename = [];
/**
* @var array<string, string>

View File

@ -361,11 +361,11 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$check_stmts = true;
if ($codebase->method_migrations
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
foreach ($this->function->params as $param) {
$param_name_node = null;
@ -480,6 +480,25 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
}
foreach ($codebase->methods_to_rename as $original_method_id => $new_method_name) {
if ($this->function instanceof ClassMethod
&& strtolower($this->getMethodId()) === $original_method_id
) {
$file_manipulations = [
new \Psalm\FileManipulation(
(int) $this->function->name->getAttribute('startFilePos'),
(int) $this->function->name->getAttribute('endFilePos') + 1,
$new_method_name
)
];
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations
);
}
}
foreach ($params as $offset => $function_param) {
$signature_type = $function_param->signature_type;
$signature_type_location = $function_param->signature_type_location;

View File

@ -530,7 +530,7 @@ class ProjectAnalyzer
throw new \UnexpectedValueException('Should not be checking references');
}
$this->codebase->classlikes->refactorMethods(
$this->codebase->classlikes->moveMethods(
$this->codebase->methods,
$this->progress
);

View File

@ -120,14 +120,14 @@ class AssignmentAnalyzer
$statements_analyzer->getSuppressedIssues()
);
if ($codebase->method_migrations
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
&& $var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftDocblockType(
$var_comment_type,

View File

@ -1175,6 +1175,27 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
}
if ($codebase->methods_to_rename) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
foreach ($codebase->methods_to_rename as $original_method_id => $new_method_name) {
if ($declaring_method_id && strtolower($declaring_method_id) === $original_method_id) {
$file_manipulations = [
new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
$new_method_name
)
];
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
$statements_analyzer->getFilePath(),
$file_manipulations
);
}
}
}
if ($config->after_method_checks) {
$file_manipulations = [];

View File

@ -247,11 +247,11 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
if ($fq_class_name) {
if ($stmt->class instanceof PhpParser\Node\Name
&& $codebase->method_migrations
&& $codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftClassLikeReference(
$fq_class_name,

View File

@ -831,20 +831,23 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
&& $stmt->class instanceof PhpParser\Node\Name
) {
$new_method_id = substr($transformation, 0, -4);
list($old_declaring_fq_class_name) = explode('::', $declaring_method_id);
list($new_fq_class_name, $new_method_name) = explode('::', $new_method_id);
$file_manipulations = [];
$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
)
);
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'),
@ -860,11 +863,11 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
if (!$moved_call
&& $codebase->method_migrations
&& $codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftClassLikeReference(
$fq_class_name,

View File

@ -140,11 +140,11 @@ class ConstFetchAnalyzer
}
}
if ($codebase->method_migrations
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftClassLikeReference(
$fq_class_name,

View File

@ -74,14 +74,14 @@ class ReturnAnalyzer
$statements_analyzer->getParentFQCLN()
);
if ($codebase->method_migrations
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
&& $var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$destination_method_id = $codebase->method_migrations[strtolower($context->calling_method_id)];
$destination_method_id = $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftDocblockType(
$comment_type,

View File

@ -950,15 +950,15 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
$this->getSuppressedIssues()
);
if ($codebase->method_migrations
if ($codebase->methods_to_move
&& $context->calling_method_id
&& isset($codebase->method_migrations[strtolower($context->calling_method_id)])
&& isset($codebase->methods_to_move[strtolower($context->calling_method_id)])
&& $var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$destination_method_id
= $codebase->method_migrations[strtolower($context->calling_method_id)];
= $codebase->methods_to_move[strtolower($context->calling_method_id)];
$codebase->classlikes->airliftDocblockType(
$var_comment_type,

View File

@ -708,7 +708,7 @@ class ClassLikes
/**
* @return void
*/
public function refactorMethods(Methods $methods, Progress $progress = null)
public function moveMethods(Methods $methods, Progress $progress = null)
{
if ($progress === null) {
$progress = new VoidProgress();
@ -717,7 +717,7 @@ class ClassLikes
$project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
$codebase = $project_analyzer->getCodebase();
if (!$codebase->method_migrations) {
if (!$codebase->methods_to_move) {
return;
}
@ -725,7 +725,7 @@ class ClassLikes
$code_migrations = [];
foreach ($codebase->method_migrations as $original => $eventual) {
foreach ($codebase->methods_to_move as $original => $eventual) {
try {
$original_method_storage = $methods->getStorage($original);
} catch (\InvalidArgumentException $e) {

View File

@ -166,10 +166,6 @@ function getArguments() : array
continue;
}
if (substr($input_path, 0, 2) === '--' && strlen($input_path) > 2) {
continue;
}
$filtered_input_paths[] = $input_path;
}

View File

@ -21,7 +21,7 @@ $args = array_slice($argv, 1);
$valid_short_options = ['f:', 'm', 'h', 'r:'];
$valid_long_options = [
'help', 'debug', 'config:', 'root:',
'threads:',
'threads:', 'move:', 'into:', 'rename:', 'to:',
];
// get options from command line
@ -99,8 +99,22 @@ Options:
-r, --root
If running Psalm globally you'll need to specify a project root. Defaults to cwd
--threads=INT
--threads=auto
If greater than one, Psalm will run analysis on multiple threads, speeding things up.
By default
--move "[Identifier]" --into "[Class]"
Moves the specified item into the class. More than one item can be moved into a class
by passing a comma-separated list of values e.g.
--move "Ns\Foo::bar,Ns\Foo::baz" --into "Biz\Bang\DestinationClass"
--rename "[Identifier]" --to "[newName]"
Renames a specfied item (e.g. method) and updates all references to it that Psalm can
identify.
--move-and-rename "[Identifier]" --to "[NewIdentifier]"
Moves the specified item to the destination with a new name
HELP;
@ -137,7 +151,98 @@ if ($path_to_config === false) {
die('Could not resolve path to config ' . (string)$options['c'] . PHP_EOL);
}
$args = getArguments();
$operation = null;
$last_arg = null;
$to_move = [];
$to_rename = [];
foreach ($args as $arg) {
if ($arg === '--move') {
$operation = 'move';
continue;
}
if ($arg === '--into') {
if ($operation !== 'move' || !$last_arg) {
die('--into is not expected here' . PHP_EOL);
}
$operation = 'move_into';
continue;
}
if ($arg === '--rename') {
$operation = 'rename';
continue;
}
if ($arg === '--move-and-rename') {
$operation = 'move_and_rename';
continue;
}
if ($arg === '--to') {
if (($operation !== 'rename' && $operation !== 'move_and_rename') || !$last_arg) {
die('--to is not expected here' . PHP_EOL);
}
if ($operation === 'rename') {
$operation = 'rename_to';
} else {
$operation = 'move_and_rename_to';
}
continue;
}
if ($arg[0] === '-') {
$operation = null;
continue;
}
if ($operation === 'move_into' || $operation === 'rename_to' || $operation === 'move_and_rename_to') {
if (!$last_arg) {
die('Expecting a previous argument' . PHP_EOL);
}
if ($operation === 'move_into') {
$last_arg_parts = preg_split('/, ?/', $last_arg);
foreach ($last_arg_parts as $last_arg_part) {
list(, $identifier_name) = explode('::', $last_arg_part);
$to_move[strtolower($last_arg_part)] = $arg . '::' . $identifier_name;
$to_rename[strtolower($last_arg_part) . '\((.*\))'] = $arg . '::' . $identifier_name . '($1)';
}
} elseif ($operation === 'move_and_rename_to') {
$to_move[strtolower($last_arg)] = $arg;
$to_rename[strtolower($last_arg) . '\((.*\))'] = $arg . '($1)';
} else {
$to_rename[strtolower($last_arg) . '\((.*\))'] = $arg . '($1)';
}
$last_arg = null;
$operation = null;
continue;
}
if ($operation === 'move' || $operation === 'rename') {
$last_arg = $arg;
continue;
}
die('Unexpected argument "' . $arg . '"' . PHP_EOL);
}
if (!$to_move && !$to_rename) {
die('No --move or --rename arguments supplied' . PHP_EOL);
}
// initialise custom config, if passed
// Initializing the config can be slow, so any UI logic should precede it, if possible
if ($path_to_config) {
$config = Config::loadFromXMLFile($path_to_config, $current_dir);
} else {
@ -146,7 +251,9 @@ if ($path_to_config) {
$config->setComposerClassLoader($first_autoloader);
$threads = isset($options['threads']) ? (int)$options['threads'] : 1;
$threads = isset($options['threads'])
? (int)$options['threads']
: max(1, ProjectAnalyzer::getCpuCount() - 2);
$providers = new Psalm\Internal\Provider\Providers(
new Psalm\Internal\Provider\FileProvider(),
@ -172,16 +279,10 @@ $project_analyzer = new ProjectAnalyzer(
$config->visitComposerAutoloadFiles($project_analyzer);
$args = getArguments();
if (count($args) !== 3 || $args[1] !== 'into') {
die('Expecting XXX into YYY' . PHP_EOL);
}
$codebase = $project_analyzer->getCodebase();
$codebase->method_migrations = [strtolower($args[0]) => $args[2]];
$codebase->call_transforms = [strtolower($args[0]) . '\((.*\))' => $args[2] . '($1)'];
$codebase->methods_to_move = $to_move;
$codebase->call_transforms = $to_rename;
$project_analyzer->refactorCodeAfterCompletion();

View File

@ -24,7 +24,7 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
*
* @param string $input_code
* @param string $output_code
* @param array<string, string> $method_migrations
* @param array<string, string> $methods_to_move
* @param array<string, string> $call_transforms
*
* @return void
@ -32,7 +32,7 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
public function testValidCode(
string $input_code,
string $output_code,
array $method_migrations,
array $methods_to_move,
array $call_transforms
) {
$test_name = $this->getTestName();
@ -61,7 +61,7 @@ class MoveMethodTest extends \Psalm\Tests\TestCase
$codebase = $this->project_analyzer->getCodebase();
$codebase->method_migrations = $method_migrations;
$codebase->methods_to_move = $methods_to_move;
$codebase->call_transforms = $call_transforms;
$this->project_analyzer->refactorCodeAfterCompletion();

View File

@ -0,0 +1,152 @@
<?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> $methods_to_rename
* @param array<string, string> $call_transforms
*
* @return void
*/
public function testValidCode(
string $input_code,
string $output_code,
array $methods_to_rename,
array $call_transforms
) {
$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();
$codebase->call_transforms = $call_transforms;
$codebase->methods_to_rename = $methods_to_rename;
$this->project_analyzer->refactorCodeAfterCompletion();
$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>,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' => 'Fedcba',
],
[
'ns\a::foo\((.*\))' => 'Ns\A::Fedcba($1)',
]
],
];
}
}