| // 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)); |
| } |
| } |