| // 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 'dart:async'; |
| |
| 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/analyzer.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/src/dart/analysis/session_helper.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_dart.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| /// [ExtractWidgetRefactoring] implementation. |
| class ExtractWidgetRefactoringImpl extends RefactoringImpl |
| implements ExtractWidgetRefactoring { |
| final SearchEngine searchEngine; |
| final ResolveResult resolveResult; |
| final AnalysisSessionHelper sessionHelper; |
| final int offset; |
| final int length; |
| |
| CorrectionUtils utils; |
| |
| ClassElement classBuildContext; |
| ClassElement classKey; |
| ClassElement classStatelessWidget; |
| ClassElement classWidget; |
| PropertyAccessorElement accessorRequired; |
| |
| @override |
| 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. |
| List<_Parameter> _parameters = []; |
| |
| ExtractWidgetRefactoringImpl( |
| this.searchEngine, this.resolveResult, this.offset, this.length) |
| : sessionHelper = new AnalysisSessionHelper(resolveResult.session) { |
| utils = |
| new CorrectionUtils(resolveResult.unit, buffer: resolveResult.content); |
| } |
| |
| @override |
| String get refactoringName { |
| return 'Extract Widget'; |
| } |
| |
| @override |
| Future<RefactoringStatus> checkFinalConditions() async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| RefactoringStatus result = new RefactoringStatus(); |
| result.addStatus(validateClassName(name)); |
| return result; |
| } |
| |
| @override |
| Future<RefactoringStatus> checkInitialConditions() async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| RefactoringStatus result = new RefactoringStatus(); |
| |
| result.addStatus(_checkSelection()); |
| if (result.hasFatalError) { |
| return result; |
| } |
| |
| AstNode astNode = _expression ?? _method ?? _statements.first; |
| _enclosingUnitMember = astNode.getAncestor((n) { |
| return n is CompilationUnitMember && n.parent is CompilationUnit; |
| }); |
| |
| result.addStatus(await _initializeClasses()); |
| result.addStatus(await _initializeParameters()); |
| |
| return result; |
| } |
| |
| @override |
| RefactoringStatus checkName() { |
| RefactoringStatus result = new RefactoringStatus(); |
| |
| // Validate the name. |
| result.addStatus(validateClassName(name)); |
| |
| // Check for duplicate declarations. |
| if (!result.hasFatalError) { |
| visitLibraryTopLevelElements(resolveResult.libraryElement, (element) { |
| if (hasDisplayName(element, name)) { |
| String 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 { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| var changeBuilder = new DartChangeBuilder(sessionHelper.session); |
| await changeBuilder.addFileEdit(resolveResult.path, (builder) { |
| 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 changeBuilder.sourceChange; |
| } |
| |
| @override |
| bool isAvailable() { |
| return !_checkSelection().hasFatalError; |
| } |
| |
| /// Checks if [offset] is a widget creation expression that can be extracted. |
| RefactoringStatus _checkSelection() { |
| AstNode node = new NodeLocator(offset, offset + length) |
| .searchWithin(resolveResult.unit); |
| |
| // Treat single ReturnStatement as its expression. |
| if (node is ReturnStatement) { |
| node = (node as ReturnStatement).expression; |
| } |
| |
| // Find the enclosing class. |
| _enclosingClassNode = node?.getAncestor((n) => n is ClassDeclaration); |
| _enclosingClassElement = _enclosingClassNode?.declaredElement; |
| |
| // new MyWidget(...) |
| InstanceCreationExpression newExpression = identifyNewExpression(node); |
| if (isWidgetCreation(newExpression)) { |
| _expression = newExpression; |
| return new RefactoringStatus(); |
| } |
| |
| // Block with selected statements. |
| if (node is Block) { |
| var selectionRange = new 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 && |
| isWidgetExpression(lastStatement.expression)) { |
| _statements = statements; |
| _statementsRange = range.startEnd(statements.first, statements.last); |
| return new RefactoringStatus(); |
| } else { |
| return new 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) { |
| DartType returnType = node.returnType?.type; |
| if (isWidgetType(returnType) && node.body != null) { |
| _method = node; |
| return new RefactoringStatus(); |
| } |
| break; |
| } |
| } |
| |
| // Invalid selection. |
| return new RefactoringStatus.fatal( |
| 'Can only extract a widget expression or a method returning widget.'); |
| } |
| |
| Future<RefactoringStatus> _initializeClasses() async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| var result = new RefactoringStatus(); |
| |
| Future<ClassElement> getClass(String name) async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| const uri = 'package:flutter/widgets.dart'; |
| var element = await sessionHelper.getClass(uri, name); |
| if (element == null) { |
| result.addFatalError("Unable to find '$name' in $uri"); |
| } |
| return element; |
| } |
| |
| Future<PropertyAccessorElement> getAccessor(String uri, String name) async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| 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 { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| _ParametersCollector collector; |
| if (_expression != null) { |
| SourceRange localRange = range.node(_expression); |
| collector = new _ParametersCollector(_enclosingClassElement, localRange); |
| _expression.accept(collector); |
| } |
| if (_statements != null) { |
| collector = |
| new _ParametersCollector(_enclosingClassElement, _statementsRange); |
| for (var statement in _statements) { |
| statement.accept(collector); |
| } |
| } |
| if (_method != null) { |
| SourceRange localRange = range.node(_method); |
| collector = new _ParametersCollector(_enclosingClassElement, localRange); |
| _method.body.accept(collector); |
| } |
| |
| _parameters |
| ..clear() |
| ..addAll(collector.parameters); |
| |
| // We added fields, now add the method parameters. |
| if (_method != null) { |
| for (var parameter in _method.parameters.parameters) { |
| if (parameter is DefaultFormalParameter) { |
| DefaultFormalParameter defaultFormalParameter = parameter; |
| parameter = defaultFormalParameter.parameter; |
| } |
| if (parameter is NormalFormalParameter) { |
| _parameters.add(new _Parameter( |
| parameter.identifier.name, parameter.declaredElement.type, |
| isMethodParameter: true)); |
| } |
| } |
| } |
| |
| RefactoringStatus 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 = new Set<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) { |
| SourceRange methodRange = range.node(_method); |
| SourceRange linesRange = |
| utils.getLinesRange(methodRange, skipLeadingEmptyLines: true); |
| builder.addDeletion(linesRange); |
| } |
| |
| String _replaceIndent(String code, String indentOld, String indentNew) { |
| var regExp = new 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 = new _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('new $name('); |
| |
| // Insert field references (as named arguments). |
| // Ensure that invocation arguments are named. |
| int argumentIndex = 0; |
| for (var parameter in _parameters) { |
| if (parameter != _parameters.first) { |
| builder.write(', '); |
| } |
| builder.write(parameter.name); |
| builder.write(': '); |
| if (parameter.isMethodParameter) { |
| Expression argument = arguments[argumentIndex++]; |
| if (argument is NamedExpression) { |
| argument = (argument as NamedExpression).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.type, |
| 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.type); |
| builder.writeln(','); |
| |
| // Add parameters for fields, local, and method parameters. |
| for (var parameter in _parameters) { |
| builder.write(' '); |
| 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.type, |
| parameterWriter: () { |
| builder.writeParameter('context', type: classBuildContext.type); |
| }, |
| bodyWriter: () { |
| if (_expression != null) { |
| String indentOld = utils.getLinePrefix(_expression.offset); |
| String indentNew = ' '; |
| |
| String 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) { |
| String indentOld = utils.getLinePrefix(_statementsRange.offset); |
| String indentNew = ' '; |
| |
| String code = utils.getRangeText(_statementsRange); |
| code = _replaceIndent(code, indentOld, indentNew); |
| |
| builder.writeln('{'); |
| |
| builder.write(indentNew); |
| builder.write(code); |
| builder.writeln(); |
| |
| builder.writeln(' }'); |
| } else { |
| String code = utils.getNodeText(_method.body); |
| builder.writeln(code); |
| } |
| }, |
| ); |
| }, |
| ); |
| }); |
| } |
| |
| /// Write instantiation of the new widget class. |
| void _writeWidgetInstantiation(DartEditBuilder builder) { |
| builder.write('new $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 MethodElement 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]. |
| String constructorName; |
| |
| _Parameter(this.name, this.type, {this.isMethodParameter: false}); |
| } |
| |
| class _ParametersCollector extends RecursiveAstVisitor<void> { |
| final ClassElement enclosingClass; |
| final SourceRange expressionRange; |
| |
| final RefactoringStatus status = new RefactoringStatus(); |
| final Set<Element> uniqueElements = new Set<Element>(); |
| final List<_Parameter> parameters = []; |
| |
| List<ClassElement> enclosingClasses; |
| |
| _ParametersCollector(this.enclosingClass, this.expressionRange); |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| Element element = node.staticElement; |
| if (element == null) { |
| return; |
| } |
| String 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) { |
| PropertyInducingElement 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(new _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) { |
| if (enclosingClass != null) { |
| if (enclosingClasses == null) { |
| enclosingClasses = <ClassElement>[] |
| ..add(enclosingClass) |
| ..addAll(enclosingClass.allSupertypes.map((t) => t.element)); |
| } |
| return enclosingClasses.contains(element.enclosingElement); |
| } |
| return false; |
| } |
| } |