blob: 113f8d82675596a7c2d65688e73615b743a67c86 [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'
show newLocation_fromElement, newLocation_fromMatch;
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/naming_conventions.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/rename.dart';
import 'package:analysis_server/src/services/search/element_visitors.dart';
import 'package:analysis_server/src/services/search/search_engine.dart';
import 'package:analysis_server/src/utilities/flutter.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart' show Identifier;
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/generated/java_core.dart';
/// Checks if creating a top-level function with the given [name] in [library]
/// will cause any conflicts.
Future<RefactoringStatus> validateCreateFunction(
SearchEngine searchEngine, LibraryElement library, String name) {
return _CreateUnitMemberValidator(
searchEngine, library, ElementKind.FUNCTION, name)
.validate();
}
/// Checks if creating a top-level function with the given [name] in [element]
/// will cause any conflicts.
Future<RefactoringStatus> validateRenameTopLevel(
SearchEngine searchEngine, Element element, String name) {
return _RenameUnitMemberValidator(searchEngine, element, name).validate();
}
/// A [Refactoring] for renaming compilation unit member [Element]s.
class RenameUnitMemberRefactoringImpl extends RenameRefactoringImpl {
final ResolvedUnitResult resolvedUnit;
/// If the [element] is a Flutter `StatefulWidget` declaration, this is the
/// corresponding `State` declaration.
ClassElement? _flutterWidgetState;
/// If [_flutterWidgetState] is set, this is the new name of it.
String? _flutterWidgetStateNewName;
RenameUnitMemberRefactoringImpl(
RefactoringWorkspace workspace, this.resolvedUnit, Element element)
: super(workspace, element);
@override
String get refactoringName {
if (element is FunctionElement) {
return 'Rename Top-Level Function';
}
if (element is TopLevelVariableElement) {
return 'Rename Top-Level Variable';
}
if (element is TypeAliasElement) {
return 'Rename Type Alias';
}
return 'Rename Class';
}
@override
Future<RefactoringStatus> checkFinalConditions() async {
var status = await validateRenameTopLevel(searchEngine, element, newName);
final flutterWidgetState = _flutterWidgetState;
final flutterWidgetStateNewName = _flutterWidgetStateNewName;
if (flutterWidgetState != null && flutterWidgetStateNewName != null) {
_updateFlutterWidgetStateName();
status.addStatus(
await validateRenameTopLevel(
searchEngine,
flutterWidgetState,
flutterWidgetStateNewName,
),
);
}
return status;
}
@override
Future<RefactoringStatus> checkInitialConditions() {
_findFlutterStateClass();
return super.checkInitialConditions();
}
@override
RefactoringStatus checkNewName() {
var result = super.checkNewName();
if (element is TopLevelVariableElement) {
result.addStatus(validateVariableName(newName));
}
if (element is FunctionElement) {
result.addStatus(validateFunctionName(newName));
}
if (element is TypeAliasElement) {
result.addStatus(validateTypeAliasName(newName));
}
if (element is ClassElement) {
result.addStatus(validateClassName(newName));
}
return result;
}
@override
Future<void> fillChange() async {
// prepare elements
var elements = <Element>[];
if (element is PropertyInducingElement && element.isSynthetic) {
var property = element as PropertyInducingElement;
var getter = property.getter;
var setter = property.setter;
if (getter != null) {
elements.add(getter);
}
if (setter != null) {
elements.add(setter);
}
} else {
elements.add(element);
}
// Rename each element and references to it.
var processor = RenameProcessor(workspace, change, newName);
for (var element in elements) {
await processor.renameElement(element);
}
// If a StatefulWidget is being renamed, rename also its State.
final flutterWidgetState = _flutterWidgetState;
if (flutterWidgetState != null) {
_updateFlutterWidgetStateName();
await RenameProcessor(
workspace,
change,
_flutterWidgetStateNewName!,
).renameElement(flutterWidgetState);
}
}
void _findFlutterStateClass() {
if (Flutter.instance.isStatefulWidgetDeclaration(element)) {
var oldStateName = oldName + 'State';
var library = element.library!;
_flutterWidgetState =
library.getType(oldStateName) ?? library.getType('_' + oldStateName);
}
}
void _updateFlutterWidgetStateName() {
final flutterWidgetState = _flutterWidgetState;
if (flutterWidgetState != null) {
var flutterWidgetStateNewName = newName + 'State';
// If the State was private, ensure that it stays private.
if (flutterWidgetState.name.startsWith('_') &&
!flutterWidgetStateNewName.startsWith('_')) {
flutterWidgetStateNewName = '_' + flutterWidgetStateNewName;
}
_flutterWidgetStateNewName = flutterWidgetStateNewName;
}
}
}
/// The base class for the create and rename validators.
class _BaseUnitMemberValidator {
final SearchEngine searchEngine;
final LibraryElement library;
final ElementKind elementKind;
final String name;
final RefactoringStatus result = RefactoringStatus();
_BaseUnitMemberValidator(
this.searchEngine, this.library, this.elementKind, this.name);
/// Returns `true` if [element] is visible at the given [SearchMatch].
bool _isVisibleAt(Element element, SearchMatch at) {
var atLibrary = at.element.library!;
// may be the same library
if (library == atLibrary) {
return true;
}
// check imports
for (var importElement in atLibrary.imports) {
// ignore if imported with prefix
if (importElement.prefix != null) {
continue;
}
// check imported elements
if (getImportNamespace(importElement).containsValue(element)) {
return true;
}
}
// no, it is not visible
return false;
}
/// Validates if an element with the [name] will conflict with another
/// top-level [Element] in the same library.
void _validateWillConflict() {
visitLibraryTopLevelElements(library, (element) {
if (hasDisplayName(element, name)) {
var message = format("Library already declares {0} with name '{1}'.",
getElementKindName(element), name);
result.addError(message, newLocation_fromElement(element));
}
});
}
/// Validates if renamed [element] will shadow any [Element] named [name].
Future _validateWillShadow(Element? element) async {
var declarations = await searchEngine.searchMemberDeclarations(name);
for (var declaration in declarations) {
var member = declaration.element;
var declaringClass = member.enclosingElement as ClassElement;
var memberReferences = await searchEngine.searchReferences(member);
for (var memberReference in memberReferences) {
var refElement = memberReference.element;
// cannot be shadowed if qualified
if (memberReference.isQualified) {
continue;
}
// cannot be shadowed if declared in the same class as reference
var refClass = refElement.thisOrAncestorOfType<ClassElement>();
if (refClass == declaringClass) {
continue;
}
// ignore if not visible
if (element != null && !_isVisibleAt(element, memberReference)) {
continue;
}
// OK, reference will be shadowed be the element being renamed
var message = format(
element != null
? "Renamed {0} will shadow {1} '{2}'."
: "Created {0} will shadow {1} '{2}'.",
elementKind.displayName,
getElementKindName(member),
getElementQualifiedName(member));
result.addError(message, newLocation_fromMatch(memberReference));
}
}
}
}
/// Helper to check if the created element will cause any conflicts.
class _CreateUnitMemberValidator extends _BaseUnitMemberValidator {
_CreateUnitMemberValidator(
SearchEngine searchEngine,
LibraryElement library,
ElementKind elementKind,
String name,
) : super(searchEngine, library, elementKind, name);
Future<RefactoringStatus> validate() async {
_validateWillConflict();
await _validateWillShadow(null);
return result;
}
}
/// Helper to check if the renamed element will cause any conflicts.
class _RenameUnitMemberValidator extends _BaseUnitMemberValidator {
final Element element;
List<SearchMatch> references = <SearchMatch>[];
_RenameUnitMemberValidator(
SearchEngine searchEngine,
this.element,
String name,
) : super(searchEngine, element.library!, element.kind, name);
Future<RefactoringStatus> validate() async {
_validateWillConflict();
references = await searchEngine.searchReferences(element);
_validateWillBeInvisible();
_validateWillBeShadowed();
await _validateWillShadow(element);
return result;
}
/// Validates if any usage of [element] renamed to [name] will be invisible.
void _validateWillBeInvisible() {
if (!Identifier.isPrivateName(name)) {
return;
}
for (var reference in references) {
var refElement = reference.element;
var refLibrary = refElement.library!;
if (refLibrary != library) {
var message = format("Renamed {0} will be invisible in '{1}'.",
getElementKindName(element), getElementQualifiedName(refLibrary));
result.addError(message, newLocation_fromMatch(reference));
}
}
}
/// Validates if any usage of [element] renamed to [name] will be shadowed.
void _validateWillBeShadowed() {
for (var reference in references) {
var refElement = reference.element;
var refClass = refElement.thisOrAncestorOfType<ClassElement>();
if (refClass != null) {
visitChildren(refClass, (shadow) {
if (hasDisplayName(shadow, name)) {
var message = format(
"Reference to renamed {0} will be shadowed by {1} '{2}'.",
getElementKindName(element),
getElementKindName(shadow),
getElementQualifiedName(shadow));
result.addError(message, newLocation_fromElement(shadow));
}
return false;
});
}
}
}
}