blob: d15a18b90b47a009f539b98bfe352d038665d4f0 [file] [log] [blame]
// Copyright (c) 2021, 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/naming_conventions.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/search/hierarchy.dart';
import 'package:analysis_server/src/utilities/change_builder.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/micro/resolve_file.dart';
import 'package:analyzer/src/dart/micro/utils.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
class CanRenameResponse {
final LineInfo lineInfo;
final RenameRefactoringElement refactoringElement;
final FileResolver _fileResolver;
final String filePath;
FlutterWidgetState? _flutterWidgetState;
CanRenameResponse(
this.lineInfo,
this.refactoringElement,
this._fileResolver,
this.filePath,
);
String get oldName => refactoringElement.element.displayName;
CheckNameResponse? checkNewName(String name) {
var element = refactoringElement.element;
RefactoringStatus? status;
if (element is FormalParameterElement) {
status = validateParameterName(name);
} else if (element is VariableElement) {
status = validateVariableName(name);
} else if (element is LocalFunctionElement ||
element is TopLevelFunctionElement) {
status = validateFunctionName(name);
} else if (element is FieldElement) {
status = validateFieldName(name);
} else if (element is MethodElement) {
status = validateMethodName(name);
} else if (element is TypeAliasElement) {
status = validateTypeAliasName(name);
} else if (element is InterfaceElement) {
status = validateClassName(name);
_flutterWidgetState = _findFlutterStateClass(element, name);
} else if (element is ConstructorElement) {
status = validateConstructorName(name);
_analyzePossibleConflicts(element, status, name);
} else if (element is MockLibraryImportElement) {
status = validateImportPrefixName(name);
}
if (status == null) {
return null;
}
return CheckNameResponse(status, this, name);
}
void _analyzePossibleConflicts(
ConstructorElement element,
RefactoringStatus result,
String newName,
) {
var parentClass = element.enclosingElement;
// Check if the "newName" is the name of the enclosing class.
if (parentClass.name == newName) {
result.addError(
'The constructor should not have the same name '
'as the name of the enclosing class.',
);
}
// check if there are members with "newName" in the same ClassElement
for (var newNameMember in getChildren(parentClass, newName)) {
var message = formatList(
"Class '{0}' already declares {1} with name '{2}'.",
[parentClass.displayName, getElementKindName(newNameMember), newName],
);
result.addError(message, newLocation_fromElement(newNameMember));
}
}
FlutterWidgetState? _findFlutterStateClass(Element element, String newName) {
if (element is ClassElement && element.isStatefulWidgetDeclaration) {
var oldStateName = '${element.displayName}State';
var library = element.library;
var state =
library.getClass(oldStateName) ?? library.getClass('_$oldStateName');
if (state != null) {
var flutterWidgetStateNewName = '${newName}State';
// If the State was private, ensure that it stays private.
if (state.name!.startsWith('_') &&
!flutterWidgetStateNewName.startsWith('_')) {
flutterWidgetStateNewName = '_$flutterWidgetStateNewName';
}
return FlutterWidgetState(state, flutterWidgetStateNewName);
}
}
return null;
}
}
class CheckNameResponse {
final RefactoringStatus status;
final CanRenameResponse canRename;
final String newName;
CheckNameResponse(this.status, this.canRename, this.newName);
LineInfo get lineInfo => canRename.lineInfo;
String get oldName => canRename.refactoringElement.element.displayName;
Future<RenameResponse?> computeRenameRanges2() async {
var elements = <Element>[];
var element = canRename.refactoringElement.element;
if (element is PropertyInducingElement && element.isSynthetic) {
var property = element;
var getter = property.getter;
var setter = property.setter;
elements.addIfNotNull(getter);
elements.addIfNotNull(setter);
} else {
elements.add(element);
}
var fileResolver = canRename._fileResolver;
var matches = <CiderSearchMatch>[];
for (var element in elements) {
matches.addAll(await fileResolver.findReferences(element));
}
FlutterWidgetRename? flutterRename;
if (canRename._flutterWidgetState != null) {
flutterRename = await _computeFlutterStateName();
}
var replaceMatches = <CiderReplaceMatch>[];
if (element is ConstructorElement) {
for (var match in matches) {
var replaceInfo = <ReplaceInfo>[];
for (var ref in match.references) {
String replacement = newName.isNotEmpty ? '.$newName' : '';
if (replacement.isEmpty &&
ref.kind == MatchKind.REFERENCE_BY_CONSTRUCTOR_TEAR_OFF) {
replacement = '.new';
}
if (ref.kind ==
MatchKind.INVOCATION_BY_ENUM_CONSTANT_WITHOUT_ARGUMENTS) {
replacement += '()';
}
replaceInfo.add(
ReplaceInfo(replacement, ref.startPosition, ref.length),
);
}
replaceMatches.addMatch(match.path, replaceInfo);
}
if (element.isSynthetic) {
var result = await _replaceSyntheticConstructor();
if (result != null) {
replaceMatches.addMatch(result.path, result.matches.toList());
}
}
} else if (element is MockLibraryImportElement) {
var replaceInfo = <ReplaceInfo>[];
for (var match in matches) {
for (var ref in match.references) {
if (newName.isEmpty) {
replaceInfo.add(ReplaceInfo('', ref.startPosition, ref.length));
} else {
var identifier = await _getInterpolationIdentifier(
match.path,
ref.startPosition,
);
if (identifier != null) {
var lineInfo = canRename.lineInfo;
replaceInfo.add(
ReplaceInfo(
'{$newName.${identifier.name}}',
lineInfo.getLocation(identifier.offset),
identifier.length,
),
);
} else {
replaceInfo.add(
ReplaceInfo('$newName.', ref.startPosition, ref.length),
);
}
}
}
replaceMatches.addMatch(match.path, replaceInfo);
var sourcePath = element.libraryFragment.source.fullName;
var infos = await _addElementDeclaration(element, sourcePath);
replaceMatches.addMatch(sourcePath, infos);
}
} else {
for (var match in matches) {
replaceMatches.addMatch(
match.path,
match.references
.map(
(info) => ReplaceInfo(newName, info.startPosition, info.length),
)
.toList(),
);
}
// add element declaration
var sourcePath = element.library!.firstFragment.source.fullName;
var infos = await _addElementDeclaration(element, sourcePath);
replaceMatches.addMatch(sourcePath, infos);
}
return RenameResponse(
matches,
this,
replaceMatches,
flutterWidgetRename: flutterRename,
);
}
Future<List<ReplaceInfo>> _addElementDeclaration(
Element element,
String sourcePath,
) async {
var infos = <ReplaceInfo>[];
if (element is PropertyInducingElement && element.isSynthetic) {
var getter = element.getter;
if (getter != null) {
infos.add(
ReplaceInfo(
newName,
lineInfo.getLocation(getter.firstFragment.nameOffset!),
getter.name!.length,
),
);
}
var setter = element.setter;
if (setter != null) {
infos.add(
ReplaceInfo(
newName,
lineInfo.getLocation(setter.firstFragment.nameOffset!),
setter.name!.length,
),
);
}
} else if (element is MockLibraryImportElement) {
var unit = (await canRename._fileResolver.resolve(path: sourcePath)).unit;
var index = element.libraryFragment.libraryImports.indexOf(
element.import,
);
var node = unit.directives.whereType<ImportDirective>().elementAt(index);
var prefixNode = node.prefix;
if (newName.isEmpty) {
// We should not get `prefix == null` because we check in
// `checkNewName` that the new name is different.
if (prefixNode != null) {
var prefixEnd = prefixNode.end;
infos.add(
ReplaceInfo(
newName,
lineInfo.getLocation(node.uri.end),
prefixEnd - node.uri.end,
),
);
}
} else {
if (prefixNode == null) {
var uriEnd = node.uri.end;
infos.add(
ReplaceInfo(' as $newName', lineInfo.getLocation(uriEnd), 0),
);
} else {
var offset = prefixNode.offset;
var length = prefixNode.length;
infos.add(ReplaceInfo(newName, lineInfo.getLocation(offset), length));
}
}
} else {
var location = (await canRename._fileResolver.resolve(
path: sourcePath,
)).lineInfo.getLocation(element.firstFragment.nameOffset!);
infos.add(ReplaceInfo(newName, location, element.name!.length));
}
return infos;
}
Future<FlutterWidgetRename?> _computeFlutterStateName() async {
var flutterState = canRename._flutterWidgetState;
var stateClass = flutterState!.state;
var stateName = flutterState.newName;
var match = await canRename._fileResolver.findReferences(stateClass);
var firstFragment = stateClass.firstFragment;
var libraryFragment = firstFragment.libraryFragment;
var sourcePath = libraryFragment.source.fullName;
var location = libraryFragment.lineInfo.getLocation(
firstFragment.nameOffset!,
);
CiderSearchMatch ciderMatch;
var searchInfo = CiderSearchInfo(
location,
stateClass.name!.length,
MatchKind.DECLARATION,
);
try {
ciderMatch = match.firstWhere((m) => m.path == sourcePath);
ciderMatch.references.add(searchInfo);
} catch (_) {
match.add(CiderSearchMatch(sourcePath, [searchInfo]));
}
var replacements =
match
.map(
(m) => CiderReplaceMatch(
m.path,
m.references
.map(
(p) => ReplaceInfo(
stateName,
p.startPosition,
stateClass.name!.length,
),
)
.toList(),
),
)
.toList();
return FlutterWidgetRename(stateName, match, replacements);
}
/// If the reference at [loc] is before an interpolated [SimpleIdentifier] in
/// an [InterpolationExpression] without surrounding curly brackets, return
/// it. Otherwise return `null`.
Future<SimpleIdentifier?> _getInterpolationIdentifier(
String path,
CharacterLocation loc,
) async {
var resolvedUnit = await canRename._fileResolver.resolve(path: path);
var lineInfo = resolvedUnit.lineInfo;
var node = resolvedUnit.unit.nodeCovering(
offset: lineInfo.getOffsetOfLine(loc.lineNumber - 1) + loc.columnNumber,
);
if (node is SimpleIdentifier) {
var parent = node.parent;
if (parent is InterpolationExpression && parent.rightBracket == null) {
return node;
}
}
return null;
}
Future<CiderReplaceMatch?> _replaceSyntheticConstructor() async {
var element = canRename.refactoringElement.element;
var interfaceElement = element.enclosingElement!;
var fileResolver = canRename._fileResolver;
var libraryPath = interfaceElement.library!.firstFragment.source.fullName;
var resolvedLibrary = await fileResolver.resolveLibrary2(path: libraryPath);
var result = resolvedLibrary.getFragmentDeclaration(
interfaceElement.firstFragment,
);
if (result == null) {
return null;
}
var resolvedUnit = result.resolvedUnit;
if (resolvedUnit == null) {
return null;
}
var node = result.node;
if (node is! NamedCompilationUnitMember) {
return null;
}
var edit = await buildEditForInsertedConstructor(
node,
resolvedUnit: resolvedUnit,
session: fileResolver.contextObjects!.analysisSession,
(builder) => builder.writeConstructorDeclaration(
interfaceElement.name!,
constructorName: newName,
isConst: node is EnumDeclaration,
),
);
if (edit == null) {
return null;
}
return CiderReplaceMatch(libraryPath, [
ReplaceInfo(
edit.replacement,
resolvedUnit.lineInfo.getLocation(edit.offset),
0,
),
]);
}
}
class CiderRenameComputer {
final FileResolver _fileResolver;
CiderRenameComputer(this._fileResolver);
/// Check if the identifier at the [line], [column] for the file at the
/// [filePath] can be renamed.
Future<CanRenameResponse?> canRename2(
String filePath,
int line,
int column,
) async {
var resolvedUnit = await _fileResolver.resolve(path: filePath);
var lineInfo = resolvedUnit.lineInfo;
var offset = lineInfo.getOffsetOfLine(line) + column;
var node = resolvedUnit.unit.nodeCovering(offset: offset);
var element = getElementOfNode2(node);
if (node == null || element == null) {
return null;
}
if (element.library?.isInSdk == true) {
return null;
}
if (element is MethodElement && element.isOperator) {
return null;
}
if (element is PropertyAccessorElement) {
element = element.variable;
}
if (!_canRenameElement(element)) {
return null;
}
var refactoring = RenameRefactoring.getElementToRename(node, element);
if (refactoring != null) {
return CanRenameResponse(lineInfo, refactoring, _fileResolver, filePath);
}
return null;
}
bool _canRenameElement(Element element) {
var enclosingElement = element.enclosingElement;
if (element is ConstructorElement) {
return true;
}
if (element is MockLibraryImportElement) {
return true;
}
if (element is LabelElement || element is LocalElement) {
return true;
}
if (enclosingElement is InterfaceElement ||
enclosingElement is ExtensionElement ||
enclosingElement is LibraryElement) {
return true;
}
return false;
}
}
class CiderReplaceMatch {
final String path;
List<ReplaceInfo> matches;
CiderReplaceMatch(this.path, this.matches);
}
class FlutterWidgetRename {
final String name;
// TODO(srawlins): Provide a deprecation message, or remove.
// ignore: provide_deprecation_message
@deprecated
final List<CiderSearchMatch> matches;
final List<CiderReplaceMatch> replacements;
FlutterWidgetRename(this.name, this.matches, this.replacements);
}
/// The corresponding `State` declaration of a Flutter `StatefulWidget`.
class FlutterWidgetState {
ClassElement state;
String newName;
FlutterWidgetState(this.state, this.newName);
}
class RenameResponse {
// TODO(srawlins): Provide a deprecation message, or remove.
// ignore: provide_deprecation_message
@deprecated
final List<CiderSearchMatch> matches;
final CheckNameResponse checkName;
final List<CiderReplaceMatch> replaceMatches;
FlutterWidgetRename? flutterWidgetRename;
RenameResponse(
this.matches,
this.checkName,
this.replaceMatches, {
this.flutterWidgetRename,
});
}
class ReplaceInfo {
final String replacementText;
final CharacterLocation startPosition;
final int length;
ReplaceInfo(this.replacementText, this.startPosition, this.length);
@override
int get hashCode => Object.hash(replacementText, startPosition, length);
@override
bool operator ==(Object other) =>
other is ReplaceInfo &&
replacementText == other.replacementText &&
startPosition == other.startPosition &&
length == other.length;
}
extension on List<CiderReplaceMatch> {
void addMatch(String path, List<ReplaceInfo> infos) {
for (var m in this) {
if (m.path == path) {
m.matches.addAll(infos);
return;
}
}
add(CiderReplaceMatch(path, infos));
}
}