blob: 6fe47788983a77ca9bcb7be0212c471373723daf [file] [log] [blame]
// Copyright (c) 2018, 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/naming_conventions.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/refactoring_internal.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/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/generated/source.dart' show SourceRange;
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
/// [ExtractWidgetRefactoring] implementation.
class ExtractWidgetRefactoringImpl extends RefactoringImpl
implements ExtractWidgetRefactoring {
final SearchEngine searchEngine;
final ResolvedUnitResult resolveResult;
final AnalysisSessionHelper sessionHelper;
final int offset;
final int length;
late CorrectionUtils utils;
ClassElement? classBuildContext;
ClassElement? classKey;
ClassElement? classStatelessWidget;
ClassElement? classWidget;
PropertyAccessorElement? accessorRequired;
@override
late String name;
/// If [offset] is in a class, the node of this class, `null` otherwise.
ClassDeclaration? _enclosingClassNode;
/// If [offset] is in a class, the element of this class, `null` otherwise.
ClassElement? _enclosingClassElement;
/// The [CompilationUnitMember] that encloses the [offset].
CompilationUnitMember? _enclosingUnitMember;
/// The widget creation expression to extract.
InstanceCreationExpression? _expression;
/// The statements covered by [offset] and [length] to extract.
List<Statement>? _statements;
/// The [SourceRange] that covers [_statements].
SourceRange? _statementsRange;
/// The method returning widget to extract.
MethodDeclaration? _method;
/// The parameters for the new widget class - referenced fields of the
/// [_enclosingClassElement], local variables referenced by [_expression],
/// and [_method] parameters.
final List<_Parameter> _parameters = [];
ExtractWidgetRefactoringImpl(
this.searchEngine, this.resolveResult, this.offset, this.length)
: sessionHelper = AnalysisSessionHelper(resolveResult.session) {
utils = CorrectionUtils(resolveResult);
}
@override
String get refactoringName {
return 'Extract Widget';
}
FeatureSet get _featureSet {
return resolveResult.unit.featureSet;
}
Flutter get _flutter => Flutter.instance;
bool get _isNonNullable => _featureSet.isEnabled(Feature.non_nullable);
@override
Future<RefactoringStatus> checkFinalConditions() async {
var result = RefactoringStatus();
result.addStatus(validateClassName(name));
return result;
}
@override
Future<RefactoringStatus> checkInitialConditions() async {
var result = RefactoringStatus();
result.addStatus(_checkSelection());
if (result.hasFatalError) {
return result;
}
var astNode = _expression ?? _method ?? _statements!.first;
_enclosingUnitMember = astNode.thisOrAncestorMatching((n) {
return n is CompilationUnitMember && n.parent is CompilationUnit;
});
result.addStatus(await _initializeClasses());
result.addStatus(await _initializeParameters());
return result;
}
@override
RefactoringStatus checkName() {
var result = RefactoringStatus();
// Validate the name.
result.addStatus(validateClassName(name));
// Check for duplicate declarations.
if (!result.hasFatalError) {
visitLibraryTopLevelElements(resolveResult.libraryElement, (element) {
if (hasDisplayName(element, name)) {
var message = format("Library already declares {0} with name '{1}'.",
getElementKindName(element), name);
result.addError(message, newLocation_fromElement(element));
}
});
}
return result;
}
@override
Future<SourceChange> createChange() async {
var builder =
ChangeBuilder(session: sessionHelper.session, eol: utils.endOfLine);
await builder.addDartFileEdit(resolveResult.path, (builder) {
final expression = _expression;
final statements = _statements;
if (expression != null) {
builder.addReplacement(range.node(expression), (builder) {
_writeWidgetInstantiation(builder);
});
} else if (statements != null) {
builder.addReplacement(_statementsRange!, (builder) {
builder.write('return ');
_writeWidgetInstantiation(builder);
builder.write(';');
});
} else {
_removeMethodDeclaration(builder);
_replaceInvocationsWithInstantiations(builder);
}
_writeWidgetDeclaration(builder);
});
return builder.sourceChange;
}
@override
bool isAvailable() {
return !_checkSelection().hasFatalError;
}
/// Checks if [offset] is a widget creation expression that can be extracted.
RefactoringStatus _checkSelection() {
var node =
NodeLocator(offset, offset + length).searchWithin(resolveResult.unit);
// Treat single ReturnStatement as its expression.
if (node is ReturnStatement) {
node = node.expression;
}
// Find the enclosing class.
_enclosingClassNode = node?.thisOrAncestorOfType<ClassDeclaration>();
_enclosingClassElement = _enclosingClassNode?.declaredElement;
// new MyWidget(...)
var newExpression = _flutter.identifyNewExpression(node);
if (_flutter.isWidgetCreation(newExpression)) {
_expression = newExpression;
return RefactoringStatus();
}
// Block with selected statements.
if (node is Block) {
var selectionRange = SourceRange(offset, length);
var statements = <Statement>[];
for (var statement in node.statements) {
var statementRange = range.node(statement);
if (statementRange.intersects(selectionRange)) {
statements.add(statement);
}
}
if (statements.isNotEmpty) {
var lastStatement = statements.last;
if (lastStatement is ReturnStatement &&
_flutter.isWidgetExpression(lastStatement.expression)) {
_statements = statements;
_statementsRange = range.startEnd(statements.first, statements.last);
return RefactoringStatus();
} else {
return RefactoringStatus.fatal(
'The last selected statement must return a widget.');
}
}
}
// Widget myMethod(...) { ... }
for (; node != null; node = node.parent) {
if (node is FunctionBody) {
break;
}
if (node is MethodDeclaration) {
var returnType = node.returnType?.type;
if (_flutter.isWidgetType(returnType)) {
_method = node;
return RefactoringStatus();
}
break;
}
}
// Invalid selection.
return RefactoringStatus.fatal(
'Can only extract a widget expression or a method returning widget.');
}
Future<RefactoringStatus> _initializeClasses() async {
var result = RefactoringStatus();
Future<ClassElement?> getClass(String name) async {
var element = await sessionHelper.getClass(_flutter.widgetsUri, name);
if (element == null) {
result.addFatalError(
"Unable to find '$name' in ${_flutter.widgetsUri}",
);
}
return element;
}
Future<PropertyAccessorElement?> getAccessor(
String uri, String name) async {
var element = await sessionHelper.getTopLevelPropertyAccessor(uri, name);
if (element == null) {
result.addFatalError("Unable to find 'required' in $uri");
}
return element;
}
classBuildContext = await getClass('BuildContext');
classKey = await getClass('Key');
classStatelessWidget = await getClass('StatelessWidget');
classWidget = await getClass('Widget');
accessorRequired = await getAccessor('package:meta/meta.dart', 'required');
return result;
}
/// Prepare referenced local variables and fields, that should be turned
/// into the widget class fields and constructor parameters.
Future<RefactoringStatus> _initializeParameters() async {
_ParametersCollector? collector;
final expression = _expression;
if (expression != null) {
var localRange = range.node(expression);
collector = _ParametersCollector(_enclosingClassElement, localRange);
expression.accept(collector);
}
final statements = _statements;
if (statements != null) {
collector =
_ParametersCollector(_enclosingClassElement, _statementsRange!);
for (var statement in statements) {
statement.accept(collector);
}
}
final method = _method;
if (method != null) {
var localRange = range.node(method);
collector = _ParametersCollector(_enclosingClassElement, localRange);
method.body.accept(collector);
}
_parameters
..clear()
..addAll(collector!.parameters);
// We added fields, now add the method parameters.
if (method != null) {
var parameterList = method.parameters;
if (parameterList != null) {
for (var parameter in parameterList.parameters) {
if (parameter is DefaultFormalParameter) {
parameter = parameter.parameter;
}
if (parameter is NormalFormalParameter) {
_parameters.add(_Parameter(
parameter.identifier!.name, parameter.declaredElement!.type,
isMethodParameter: true));
}
}
}
}
var status = collector.status;
// If there is an existing parameter "key" warn the user.
// We could rename it, but that would require renaming references to it.
// It is probably pretty rare, and the user can always rename before.
for (var parameter in _parameters) {
if (parameter.name == 'key') {
status.addError(
"The parameter 'key' will conflict with the widget 'key'.");
}
}
// Collect used public names.
var usedNames = <String>{};
for (var parameter in _parameters) {
if (!parameter.name.startsWith('_')) {
usedNames.add(parameter.name);
}
}
// Give each private parameter a public name for the constructor.
for (var parameter in _parameters) {
var name = parameter.name;
if (name.startsWith('_')) {
var baseName = name.substring(1);
for (var i = 1;; i++) {
name = i == 1 ? baseName : '$baseName$i';
if (usedNames.add(name)) {
break;
}
}
}
parameter.constructorName = name;
}
return collector.status;
}
/// Remove the [_method] declaration.
void _removeMethodDeclaration(DartFileEditBuilder builder) {
var methodRange = range.node(_method!);
var linesRange =
utils.getLinesRange(methodRange, skipLeadingEmptyLines: true);
builder.addDeletion(linesRange);
}
String _replaceIndent(String code, String indentOld, String indentNew) {
var regExp = RegExp('^$indentOld', multiLine: true);
return code.replaceAll(regExp, indentNew);
}
/// Replace invocations of the [_method] with instantiations of the new
/// widget class.
void _replaceInvocationsWithInstantiations(DartFileEditBuilder builder) {
var collector = _MethodInvocationsCollector(_method!.declaredElement!);
_enclosingClassNode!.accept(collector);
for (var invocation in collector.invocations) {
List<Expression> arguments = invocation.argumentList.arguments;
builder.addReplacement(range.node(invocation), (builder) {
builder.write('$name(');
// Insert field references (as named arguments).
// Ensure that invocation arguments are named.
var argumentIndex = 0;
for (var parameter in _parameters) {
if (parameter != _parameters.first) {
builder.write(', ');
}
builder.write(parameter.name);
builder.write(': ');
if (parameter.isMethodParameter) {
var argument = arguments[argumentIndex++];
if (argument is NamedExpression) {
argument = argument.expression;
}
builder.write(utils.getNodeText(argument));
} else {
builder.write(parameter.name);
}
}
builder.write(')');
});
}
}
/// Write declaration of the new widget class.
void _writeWidgetDeclaration(DartFileEditBuilder builder) {
builder.addInsertion(_enclosingUnitMember!.end, (builder) {
builder.writeln();
builder.writeln();
builder.writeClassDeclaration(
name,
superclass: classStatelessWidget!.instantiate(
typeArguments: const [],
nullabilitySuffix: NullabilitySuffix.none,
),
membersWriter: () {
// Add the constructor.
builder.write(' ');
builder.writeConstructorDeclaration(
name,
isConst: true,
parameterWriter: () {
builder.writeln('{');
// Add the required `key` parameter.
builder.write(' ');
builder.writeParameter(
'key',
type: classKey!.instantiate(
typeArguments: const [],
nullabilitySuffix: _isNonNullable
? NullabilitySuffix.question
: NullabilitySuffix.star,
),
);
builder.writeln(',');
// Add parameters for fields, local, and method parameters.
for (var parameter in _parameters) {
builder.write(' ');
if (_isNonNullable) {
builder.write('required');
} else {
builder.write('@');
builder.writeReference(accessorRequired!);
}
builder.write(' ');
if (parameter.constructorName != parameter.name) {
builder.writeType(parameter.type);
builder.write(' ');
builder.write(parameter.constructorName);
} else {
builder.write('this.');
builder.write(parameter.name);
}
builder.writeln(',');
}
builder.write(' }');
},
initializerWriter: () {
for (var parameter in _parameters) {
if (parameter.constructorName != parameter.name) {
builder.write(parameter.name);
builder.write(' = ');
builder.write(parameter.constructorName);
builder.write(', ');
}
}
builder.write('super(key: key)');
},
);
builder.writeln();
builder.writeln();
// Add the fields for the parameters.
if (_parameters.isNotEmpty) {
for (var parameter in _parameters) {
builder.write(' ');
builder.writeFieldDeclaration(parameter.name,
isFinal: true, type: parameter.type);
builder.writeln();
}
builder.writeln();
}
// Widget build(BuildContext context) { ... }
builder.writeln(' @override');
builder.write(' ');
builder.writeFunctionDeclaration(
'build',
returnType: classWidget!.instantiate(
typeArguments: const [],
nullabilitySuffix: NullabilitySuffix.none,
),
parameterWriter: () {
builder.writeParameter(
'context',
type: classBuildContext!.instantiate(
typeArguments: const [],
nullabilitySuffix: NullabilitySuffix.none,
),
);
},
bodyWriter: () {
final expression = _expression;
if (expression != null) {
var indentOld = utils.getLinePrefix(expression.offset);
var indentNew = ' ';
var code = utils.getNodeText(expression);
code = _replaceIndent(code, indentOld, indentNew);
builder.writeln('{');
builder.write(' return ');
builder.write(code);
builder.writeln(';');
builder.writeln(' }');
} else if (_statements != null) {
var indentOld = utils.getLinePrefix(_statementsRange!.offset);
var indentNew = ' ';
var code = utils.getRangeText(_statementsRange!);
code = _replaceIndent(code, indentOld, indentNew);
builder.writeln('{');
builder.write(indentNew);
builder.write(code);
builder.writeln();
builder.writeln(' }');
} else {
var code = utils.getNodeText(_method!.body);
builder.writeln(code);
}
},
);
},
);
});
}
/// Write instantiation of the new widget class.
void _writeWidgetInstantiation(DartEditBuilder builder) {
builder.write('$name(');
for (var parameter in _parameters) {
if (parameter != _parameters.first) {
builder.write(', ');
}
builder.write(parameter.constructorName);
builder.write(': ');
builder.write(parameter.name);
}
builder.write(')');
}
}
class _MethodInvocationsCollector extends RecursiveAstVisitor<void> {
final ExecutableElement methodElement;
final List<MethodInvocation> invocations = [];
_MethodInvocationsCollector(this.methodElement);
@override
void visitMethodInvocation(MethodInvocation node) {
if (node.methodName.staticElement == methodElement) {
invocations.add(node);
} else {
super.visitMethodInvocation(node);
}
}
}
class _Parameter {
/// The name which is used to reference this parameter in the expression
/// being extracted, and also the name of the field in the new widget.
final String name;
final DartType type;
/// Whether the parameter is a parameter of the method being extracted.
final bool isMethodParameter;
/// If the [name] is private, the public name to use in the new widget
/// constructor. If the [name] is already public, then the [name].
late String constructorName;
_Parameter(this.name, this.type, {this.isMethodParameter = false});
}
class _ParametersCollector extends RecursiveAstVisitor<void> {
final ClassElement? enclosingClass;
final SourceRange expressionRange;
final RefactoringStatus status = RefactoringStatus();
final Set<Element> uniqueElements = <Element>{};
final List<_Parameter> parameters = [];
List<ClassElement>? enclosingClasses;
_ParametersCollector(this.enclosingClass, this.expressionRange);
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var element = node.writeOrReadElement;
if (element == null) {
return;
}
var elementName = element.displayName;
DartType? type;
if (element is MethodElement) {
if (_isMemberOfEnclosingClass(element)) {
status.addError(
'Reference to an enclosing class method cannot be extracted.');
}
} else if (element is LocalVariableElement) {
if (!expressionRange.contains(element.nameOffset)) {
if (node.inSetterContext()) {
status.addError("Write to '$elementName' cannot be extracted.");
} else {
type = element.type;
}
}
} else if (element is PropertyAccessorElement) {
var field = element.variable;
if (_isMemberOfEnclosingClass(field)) {
if (node.inSetterContext()) {
status.addError("Write to '$elementName' cannot be extracted.");
} else {
type = field.type;
}
}
}
// TODO(scheglov) support for ParameterElement
if (type != null && uniqueElements.add(element)) {
parameters.add(_Parameter(elementName, type));
}
}
/// Return `true` if the given [element] is a member of the [enclosingClass]
/// or one of its supertypes, interfaces, or mixins.
bool _isMemberOfEnclosingClass(Element element) {
final enclosingClass = this.enclosingClass;
if (enclosingClass != null) {
final enclosingClasses = this.enclosingClasses ??= <ClassElement>[]
..add(enclosingClass)
..addAll(enclosingClass.allSupertypes.map((t) => t.element));
return enclosingClasses.contains(element.enclosingElement);
}
return false;
}
}