blob: 89408ac97c88a512f7150f175df06407fd66a001 [file]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/scanner/token_impl.dart';
import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/correction/status.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring_internal.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analyzer/dart/analysis/code_style_options.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
/// Helper for renaming one or more elements.
class RenameProcessor {
final RefactoringWorkspace workspace;
final AnalysisSessionHelper sessionHelper;
final SourceChange change;
final String newName;
new(this.workspace, this.sessionHelper, this.change, this.newName);
/// Add the edit that updates the [element] declaration.
void addDeclarationEdit(Element? element) {
if (element == null) {
return;
} else if (element is LibraryElementImpl) {
// TODO(brianwilkerson): Consider adding public API to get the offset and
// length of the library's name.
var nameRange = range.startOffsetLength(
element.nameOffset,
element.nameLength,
);
var edit = newSourceEdit_range(nameRange, newName);
doSourceChange_addFragmentEdit(change, element.firstFragment, edit);
} else if (workspace.containsElement(element)) {
Fragment? fragment = element.firstFragment;
SourceRange? nameRange;
var replacement = newName;
var supportsPrivateNamedParameters =
element.library?.featureSet.isEnabled(
Feature.private_named_parameters,
) ??
false;
while (fragment != null) {
switch (fragment) {
case FieldFormalParameterFragment(:var privateName?)
when supportsPrivateNamedParameters:
// A private named parameter's element has the public name ("foo"),
// but the identifier we are renaming is the original private name
// ("_foo"). In that case, use the private name so that we have the
// correct length including the underscore.
nameRange = range.startOffsetLength(
fragment.nameOffset!,
privateName.length,
);
case SuperFormalParameterFragment()
when supportsPrivateNamedParameters &&
fragment.element.isNamed &&
newName.startsWith('_'):
// A super parameter works more like a named *argument* than a
// named parameter. If the corresponding parameter in the
// supertype is named and private, then refer to it by its public
// name in the super parameter.
nameRange = range.fragmentName(fragment);
replacement = correspondingPublicName(newName) ?? newName;
default:
nameRange = range.fragmentName(fragment);
}
if (nameRange != null) {
var edit = newSourceEdit_range(nameRange, replacement);
doSourceChange_addFragmentEdit(change, fragment, edit);
}
fragment = fragment.nextFragment;
}
}
}
/// Add edits that update [matches].
void addReferenceEdits(List<SearchMatch> matches) {
var references = getSourceReferences(matches);
for (var reference in references) {
if (!workspace.containsElement(reference.element)) {
continue;
}
reference.addEdit(change, newName);
}
}
/// Update the [element] declaration and references to it.
Future<void> renameElement(Element element) async {
addDeclarationEdit(element);
var matches = await workspace.searchEngine.searchReferences(element);
addReferenceEdits(matches);
}
/// Add an edit that replaces the specified region with [code].
/// Uses [referenceElement] to identify the file to update.
void replace({
required Element referenceElement,
required int offset,
required int length,
required String code,
}) {
var edit = SourceEdit(offset, length, code);
doSourceChange_addFragmentEdit(
change,
referenceElement.firstFragment,
edit,
);
}
}
/// Helper for renaming one or more elements.
class RenameProcessor2 {
final RefactoringWorkspace workspace;
final AnalysisSessionHelper sessionHelper;
final ChangeBuilder builder;
final String newName;
new(this.workspace, this.sessionHelper, this.builder, this.newName);
/// Add the edit that updates the [element] declaration.
Future<void> addDeclarationEdit(Element? element) async {
if (element == null) {
return;
} else if (element is LibraryElementImpl) {
// TODO(brianwilkerson): Consider adding public API to get the offset and
// length of the library's name.
var nameRange = range.startOffsetLength(
element.nameOffset,
element.nameLength,
);
await builder.addDartFileEdit(
element.firstFragment.libraryFragment.source.fullName,
(builder) {
builder.addSimpleReplacement(nameRange, newName);
},
);
} else if (workspace.containsElement(element)) {
Fragment? fragment = element.firstFragment;
SourceRange? nameRange;
var replacement = newName;
var supportsPrivateNamedParameters =
element.library?.featureSet.isEnabled(
Feature.private_named_parameters,
) ??
false;
while (fragment != null) {
switch (fragment) {
case FieldFormalParameterFragment(:var privateName?)
when supportsPrivateNamedParameters:
// A private named parameter's element has the public name ("foo"),
// but the identifier we are renaming is the original private name
// ("_foo"). In that case, use the private name so that we have the
// correct length including the underscore.
nameRange = range.startOffsetLength(
fragment.nameOffset!,
privateName.length,
);
case SuperFormalParameterFragment()
when supportsPrivateNamedParameters &&
fragment.element.isNamed &&
newName.startsWith('_'):
// A super parameter works more like a named *argument* than a
// named parameter. If the corresponding parameter in the
// supertype is named and private, then refer to it by its public
// name in the super parameter.
nameRange = range.fragmentName(fragment);
replacement = correspondingPublicName(newName) ?? newName;
default:
nameRange = range.fragmentName(fragment);
}
if (nameRange != null) {
await builder.addDartFileEdit(
fragment.libraryFragment!.source.fullName,
(builder) {
builder.addSimpleReplacement(nameRange!, replacement);
},
);
}
fragment = fragment.nextFragment;
}
}
}
/// Add edits that update [matches].
Future<void> addReferenceEdits(List<SearchMatch> matches) async {
var references = getSourceReferences(matches);
for (var reference in references) {
if (!workspace.containsElement(reference.element)) {
continue;
}
await builder.addDartFileEdit(reference.unitSource.fullName, (builder) {
builder.addSimpleReplacement(reference.range, newName);
});
}
}
/// Update the [element] declaration and references to it.
Future<void> renameElement(Element element) async {
await addDeclarationEdit(element);
var matches = await workspace.searchEngine.searchReferences(element);
await addReferenceEdits(matches);
}
/// Add an edit that replaces the specified region with [code].
/// Uses [referenceElement] to identify the file to update.
Future<void> replace({
required Element referenceElement,
required int offset,
required int length,
required String code,
}) async {
await builder.addDartFileEdit(
referenceElement.firstFragment.libraryFragment!.source.fullName,
(builder) {
builder.addSimpleReplacement(
range.startOffsetLength(offset, length),
code,
);
},
);
}
}
/// An abstract implementation of [RenameRefactoring].
abstract class RenameRefactoringImpl extends RefactoringImpl
implements RenameRefactoring {
final RefactoringWorkspace workspace;
final AnalysisSessionHelper sessionHelper;
final SearchEngine searchEngine;
final Element _element;
@override
final String elementKindName;
@override
final String oldName;
late SourceChange change;
late String newName;
new(this.workspace, this.sessionHelper, Element element)
: searchEngine = workspace.searchEngine,
_element = element,
elementKindName = element.kind.displayName,
oldName = _getOldName(element);
Element get element => _element;
@override
Future<RefactoringStatus> checkInitialConditions() {
var result = RefactoringStatus();
if (element.library?.isInSdk == true) {
var message = formatList(
"The {0} '{1}' is defined in the SDK, so cannot be renamed.",
[getElementKindName(element), getElementQualifiedName(element)],
);
result.addFatalError(message);
}
if (!workspace.containsElement(element)) {
var message = formatList(
"The {0} '{1}' is defined outside of the project, so cannot be renamed.",
[getElementKindName(element), getElementQualifiedName(element)],
);
result.addFatalError(message);
}
return Future.value(result);
}
@override
RefactoringStatus checkNewName() {
var result = RefactoringStatus();
if (newName == oldName) {
result.addFatalError(
'The new name must be different than the current name.',
);
}
return result;
}
@override
Future<SourceChange> createChange({ChangeBuilder? builder}) async {
var changeName = "$refactoringName '$oldName' to '$newName'";
change = SourceChange(changeName);
await fillChange();
return change;
}
/// Adds individual edits to [change].
Future<void> fillChange();
CodeStyleOptions getCodeStyleOptions(File file) => sessionHelper
.session
.analysisContext
.getAnalysisOptionsForFile(file)
.codeStyleOptions;
static String _getOldName(Element element) {
if (element is ConstructorElement) {
var name = element.name;
if (name == null || name == 'new') {
return '';
}
return name;
} else if (element is MockLibraryImportElement) {
var prefix = element.import.prefix?.element;
if (prefix != null) {
return prefix.displayName;
}
return '';
}
return element.displayName;
}
}