| // 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/flutter.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/ast/utilities.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'; |
| |
| 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; |
| _flutterWidgetState = _findFlutterStateClass(element, name); |
| |
| RefactoringStatus? status; |
| if (element is ParameterElement) { |
| status = validateParameterName(name); |
| } else if (element is VariableElement) { |
| status = validateVariableName(name); |
| } else if (element is FunctionElement) { |
| 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 ClassElement) { |
| status = validateClassName(name); |
| } else if (element is ConstructorElement) { |
| status = validateConstructorName(name); |
| _analyzePossibleConflicts(element, status, name); |
| } else if (element is LibraryImportElement) { |
| status = validateImportPrefixName(name); |
| } |
| |
| if (status == null) { |
| return null; |
| } |
| return CheckNameResponse(status, this, name); |
| } |
| |
| void _analyzePossibleConflicts( |
| ConstructorElement element, RefactoringStatus result, String newName) { |
| var parentClass = element.enclosingElement3; |
| // 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 = format("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 (Flutter.instance.isStatefulWidgetDeclaration(element)) { |
| 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.findReferences2(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 LibraryImportElement) { |
| 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.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.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) { |
| if (element.getter != null) { |
| infos.add(ReplaceInfo( |
| newName, |
| lineInfo.getLocation(element.getter!.nameOffset), |
| element.getter!.nameLength)); |
| } |
| if (element.setter != null) { |
| infos.add(ReplaceInfo( |
| newName, |
| lineInfo.getLocation(element.setter!.nameOffset), |
| element.setter!.nameLength)); |
| } |
| } else if (element is LibraryImportElement) { |
| var unit = |
| (await canRename._fileResolver.resolve2(path: sourcePath)).unit; |
| var index = element.library.libraryImports.indexOf(element); |
| var node = unit.directives.whereType<ImportDirective>().elementAt(index); |
| final 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.resolve2(path: sourcePath)) |
| .lineInfo |
| .getLocation(element.nameOffset); |
| infos.add(ReplaceInfo(newName, location, element.nameLength)); |
| } |
| return infos; |
| } |
| |
| Future<FlutterWidgetRename?> _computeFlutterStateName() async { |
| var flutterState = canRename._flutterWidgetState; |
| var stateClass = flutterState!.state; |
| var stateName = flutterState.newName; |
| var match = await canRename._fileResolver.findReferences2(stateClass); |
| var sourcePath = stateClass.source.fullName; |
| var location = stateClass.enclosingElement3.lineInfo |
| .getLocation(stateClass.nameOffset); |
| CiderSearchMatch ciderMatch; |
| var searchInfo = |
| CiderSearchInfo(location, stateClass.nameLength, 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.nameLength)) |
| .toList())) |
| .toList(); |
| return FlutterWidgetRename(stateName, match, replacements); |
| } |
| |
| /// If the given [reference] 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.resolve2(path: path); |
| var lineInfo = resolvedUnit.lineInfo; |
| var node = NodeLocator( |
| lineInfo.getOffsetOfLine(loc.lineNumber - 1) + loc.columnNumber) |
| .searchWithin(resolvedUnit.unit); |
| 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 classElement = element.enclosingElement3; |
| |
| var fileResolver = canRename._fileResolver; |
| var libraryPath = classElement!.library!.source.fullName; |
| var resolvedLibrary = await fileResolver.resolveLibrary2(path: libraryPath); |
| var result = resolvedLibrary.getElementDeclaration(classElement); |
| if (result == null) { |
| return null; |
| } |
| |
| var resolvedUnit = result.resolvedUnit; |
| if (resolvedUnit == null) { |
| return null; |
| } |
| |
| var node = result.node; |
| if (node is ClassDeclaration) { |
| var utils = CorrectionUtils(resolvedUnit); |
| var location = utils.prepareNewConstructorLocation( |
| fileResolver.contextObjects!.analysisSession, node); |
| if (location == null) { |
| return null; |
| } |
| |
| var header = '${classElement.name}.$newName();'; |
| return CiderReplaceMatch(libraryPath, [ |
| ReplaceInfo(location.prefix + header + location.suffix, |
| resolvedUnit.lineInfo.getLocation(location.offset), 0) |
| ]); |
| } else if (node is EnumDeclaration) { |
| var utils = CorrectionUtils(resolvedUnit); |
| var location = utils.prepareEnumNewConstructorLocation(node); |
| if (location == null) { |
| return null; |
| } |
| |
| var header = 'const ${classElement.name}.$newName();'; |
| return CiderReplaceMatch(libraryPath, [ |
| ReplaceInfo(location.prefix + header + location.suffix, |
| resolvedUnit.lineInfo.getLocation(location.offset), 0) |
| ]); |
| } |
| return null; |
| } |
| } |
| |
| 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.resolve2(path: filePath); |
| var lineInfo = resolvedUnit.lineInfo; |
| var offset = lineInfo.getOffsetOfLine(line) + column; |
| |
| var node = NodeLocator(offset).searchWithin(resolvedUnit.unit); |
| var element = getElementOfNode(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.enclosingElement3; |
| if (element is ConstructorElement) { |
| return true; |
| } |
| if (element is LibraryImportElement) { |
| return true; |
| } |
| if (element is LabelElement || element is LocalElement) { |
| return true; |
| } |
| if (enclosingElement is ClassElement || |
| enclosingElement is ExtensionElement || |
| enclosingElement is CompilationUnitElement) { |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| class CiderReplaceMatch { |
| final String path; |
| List<ReplaceInfo> matches; |
| |
| CiderReplaceMatch(this.path, this.matches); |
| } |
| |
| class FlutterWidgetRename { |
| final String name; |
| @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 { |
| @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)); |
| } |
| } |