blob: 984c84208d860d225752de3d9de879aab251a5b9 [file] [log] [blame]
// 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: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/element/element2.dart';
import 'package:analyzer/file_system/file_system.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/range_factory.dart';
/// Helper for renaming one or more elements.
class RenameProcessor {
final RefactoringWorkspace workspace;
final AnalysisSessionHelper sessionHelper;
final SourceChange change;
final String newName;
RenameProcessor(
this.workspace,
this.sessionHelper,
this.change,
this.newName,
);
/// Add the edit that updates the [element] declaration.
void addDeclarationEdit(Element2? 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;
while (fragment != null) {
var edit = newSourceEdit_range(range.fragmentName(fragment)!, newName);
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(Element2 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 Element2 referenceElement,
required int offset,
required int length,
required String code,
}) {
var edit = SourceEdit(offset, length, code);
doSourceChange_addFragmentEdit(
change,
referenceElement.firstFragment,
edit,
);
}
}
/// An abstract implementation of [RenameRefactoring].
abstract class RenameRefactoringImpl extends RefactoringImpl
implements RenameRefactoring {
final RefactoringWorkspace workspace;
final AnalysisSessionHelper sessionHelper;
final SearchEngine searchEngine;
final Element2 _element;
@override
final String elementKindName;
@override
final String oldName;
late SourceChange change;
late String newName;
RenameRefactoringImpl(this.workspace, this.sessionHelper, Element2 element)
: searchEngine = workspace.searchEngine,
_element = element,
elementKindName = element.kind.displayName,
oldName = _getOldName(element);
Element2 get element => _element;
@override
Future<RefactoringStatus> checkInitialConditions() {
var result = RefactoringStatus();
if (element.library2?.isInSdk == true) {
var message = format(
"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 = format(
"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() 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(Element2 element) {
if (element is ConstructorElement2) {
var name = element.name3;
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;
}
}