diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index f51ad3b05..75a326382 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -669,6 +669,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer MethodComparator::compare( $codebase, + null, $implementer_classlike_storage ?: $storage, $interface_storage, $implementer_method_storage, @@ -938,6 +939,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer MethodComparator::compare( $codebase, + null, $storage, $parent_storage, $pseudo_method_storage, @@ -1839,6 +1841,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer MethodComparator::compare( $codebase, + $stmt, $class_storage, $declaring_storage, $implementer_method_storage, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index aeffc7e3f..e07682b20 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -298,6 +298,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer if (!isset($appearing_class_storage->class_implements[strtolower($overridden_fq_class_name)])) { MethodComparator::compare( $codebase, + \count($overridden_method_ids) === 1 ? $this->function : null, $declaring_class_storage, $parent_storage, $storage, diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 045aa4106..cae3ac0b8 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -1,9 +1,11 @@ methods->getDeclaringMethodId( new MethodIdentifier( @@ -140,6 +143,7 @@ class MethodComparator self::compareMethodParams( $codebase, + $stmt, $implementer_classlike_storage, $guide_classlike_storage, $implementer_called_class_name, @@ -289,6 +293,7 @@ class MethodComparator */ private static function compareMethodParams( Codebase $codebase, + ?ClassMethod $stmt, ClassLikeStorage $implementer_classlike_storage, ClassLikeStorage $guide_classlike_storage, string $implementer_called_class_name, @@ -387,6 +392,7 @@ class MethodComparator && count($implementer_method_storage->params) > 1 && $guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined + && $implementer_classlike_storage->location ) { $config = \Psalm\Config::getInstance(); @@ -395,21 +401,43 @@ class MethodComparator && !$config->isInProjectDirs($guide_classlike_storage->location->file_path) ) ) { - if (IssueBuffer::accepts( - new ParamNameMismatch( - 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong name $' - . $implementer_param->name . ', expecting $' - . $guide_param->name . ' as defined by ' - . $cased_guide_method_id, - $implementer_param->location - && $config->isInProjectDirs( - $implementer_param->location->file_path - ) - ? $implementer_param->location - : $code_location - ) - )) { - // fall through + if ($codebase->alter_code) { + $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance(); + + if ($stmt && isset($project_analyzer->getIssuesToFix()['ParamNameMismatch'])) { + $param_replacer = new ParamReplacementVisitor( + $implementer_param->name, + $guide_param->name + ); + + $traverser = new \PhpParser\NodeTraverser(); + $traverser->addVisitor($param_replacer); + $traverser->traverse([$stmt]); + + if ($replacements = $param_replacer->getReplacements()) { + \Psalm\Internal\FileManipulation\FileManipulationBuffer::add( + $implementer_classlike_storage->location->file_path, + $replacements + ); + } + } + } else { + if (IssueBuffer::accepts( + new ParamNameMismatch( + 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong name $' + . $implementer_param->name . ', expecting $' + . $guide_param->name . ' as defined by ' + . $cased_guide_method_id, + $implementer_param->location + && $config->isInProjectDirs( + $implementer_param->location->file_path + ) + ? $implementer_param->location + : $code_location + ) + )) { + // fall through + } } } } diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 5b16de1ec..c9b67f74c 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -23,6 +23,7 @@ use Psalm\Issue\MissingClosureReturnType; use Psalm\Issue\MissingParamType; use Psalm\Issue\MissingPropertyType; use Psalm\Issue\MissingReturnType; +use Psalm\Issue\ParamNameMismatch; use Psalm\Issue\PossiblyUndefinedGlobalVariable; use Psalm\Issue\PossiblyUndefinedVariable; use Psalm\Issue\PossiblyUnusedMethod; @@ -217,6 +218,7 @@ class ProjectAnalyzer MissingParamType::class, MissingPropertyType::class, MissingReturnType::class, + ParamNameMismatch::class, PossiblyUndefinedGlobalVariable::class, PossiblyUndefinedVariable::class, PossiblyUnusedMethod::class, diff --git a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php new file mode 100644 index 000000000..9425061a4 --- /dev/null +++ b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php @@ -0,0 +1,78 @@ + */ + private $replacements = []; + + /** @var bool */ + private $new_name_replaced = false; + + /** @var bool */ + private $new_new_name_used = false; + + public function __construct(string $old_name, string $new_name) + { + $this->old_name = $old_name; + $this->new_name = $new_name; + } + + /** + * @param PhpParser\Node $node + * + * @return null|int + */ + public function enterNode(PhpParser\Node $node) + { + if ($node instanceof PhpParser\Node\Expr\Variable) { + if ($node->name === $this->old_name) { + $this->replacements[] = new FileManipulation( + (int) $node->getAttribute('startFilePos') + 1, + (int) $node->getAttribute('endFilePos') + 1, + $this->new_name + ); + } elseif ($node->name === $this->new_name) { + if ($this->new_new_name_used) { + $this->replacements = []; + return PhpParser\NodeTraverser::STOP_TRAVERSAL; + } + + $this->replacements[] = new FileManipulation( + (int) $node->getAttribute('startFilePos') + 1, + (int) $node->getAttribute('endFilePos') + 1, + $this->new_name . '_new' + ); + + $this->new_name_replaced = true; + } elseif ($node->name === $this->new_name . '_new') { + if ($this->new_name_replaced) { + $this->replacements = []; + return PhpParser\NodeTraverser::STOP_TRAVERSAL; + } + + $this->new_new_name_used = true; + } + } + } + + /** + * @return list + */ + public function getReplacements() + { + return $this->replacements; + } +} diff --git a/tests/FileManipulation/ParamNameMismatchTest.php b/tests/FileManipulation/ParamNameMismatchTest.php new file mode 100644 index 000000000..4c13c70bd --- /dev/null +++ b/tests/FileManipulation/ParamNameMismatchTest.php @@ -0,0 +1,97 @@ + + */ + public function providerValidCodeParse() + { + return [ + 'fixMismatchingParamName74' => [ + ' [ + ' [ + '