diff --git a/pkg/analysis_server/lib/src/services/correction/strings.dart b/pkg/analysis_server/lib/src/services/correction/strings.dart
index a72b6f6837b..ff5c49584d4 100644
--- a/pkg/analysis_server/lib/src/services/correction/strings.dart
+++ b/pkg/analysis_server/lib/src/services/correction/strings.dart
@@ -41,6 +41,22 @@ int compareStrings(String a, String b) {
return a.compareTo(b);
}
+/**
+ * Counts how many times [sub] appears in [str].
+ */
+int countMatches(String str, String sub) {
+ if (isEmpty(str) || isEmpty(sub)) {
+ return 0;
+ }
+ int count = 0;
+ int idx = 0;
+ while ((idx = str.indexOf(sub, idx)) != -1) {
+ count++;
+ idx += sub.length;
+ }
+ return count;
+}
+
/**
* Checks if [str] is `null`, empty or is whitespace.
*/
@@ -101,6 +117,7 @@ String removeStart(String str, String remove) {
return str;
}
+
String repeat(String s, int n) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < n; i++) {
diff --git a/pkg/analysis_server/lib/src/services/correction/util.dart b/pkg/analysis_server/lib/src/services/correction/util.dart
index 2023d2545d4..1c4b26c840f 100644
--- a/pkg/analysis_server/lib/src/services/correction/util.dart
+++ b/pkg/analysis_server/lib/src/services/correction/util.dart
@@ -4,6 +4,8 @@
library services.src.correction.util;
+import 'dart:math';
+
import 'package:analysis_server/src/protocol2.dart' show SourceEdit;
import 'package:analysis_server/src/services/correction/source_range.dart';
import 'package:analysis_server/src/services/correction/strings.dart';
@@ -15,6 +17,20 @@ import 'package:analyzer/src/generated/scanner.dart';
import 'package:analyzer/src/generated/source.dart';
+/**
+ * @return true if given [List]s are identical at given position.
+ */
+bool allListsIdentical(List lists, int position) {
+ Object element = lists[0][position];
+ for (List list in lists) {
+ if (list[position] != element) {
+ return false;
+ }
+ }
+ return true;
+}
+
+
/**
* TODO(scheglov) replace with nodes once there will be [CompilationUnit#getComments].
*
@@ -96,6 +112,27 @@ ExecutableElement getEnclosingExecutableElement(AstNode node) {
return null;
}
+
+/**
+ * @return the enclosing executable [AstNode].
+ */
+ AstNode getEnclosingExecutableNode(AstNode node) {
+ while (node != null) {
+ if (node is FunctionDeclaration) {
+ return node;
+ }
+ if (node is ConstructorDeclaration) {
+ return node;
+ }
+ if (node is MethodDeclaration) {
+ return node;
+ }
+ node = node.parent;
+ }
+ return null;
+}
+
+
/**
* Returns [getExpressionPrecedence] for the parent of [node],
* or `0` if the parent node is [ParenthesizedExpression].
@@ -110,6 +147,7 @@ int getExpressionParentPrecedence(AstNode node) {
return getExpressionPrecedence(parent);
}
+
/**
* Returns the precedence of [node] it is an [Expression], negative otherwise.
*/
@@ -120,6 +158,7 @@ int getExpressionPrecedence(AstNode node) {
return -1000;
}
+
/**
* Returns the namespace of the given [ImportElement].
*/
@@ -129,6 +168,58 @@ Map getImportNamespace(ImportElement imp) {
return namespace.definedNames;
}
+/**
+ * @return the nearest common ancestor [AstNode] of the given [AstNode]s.
+ */
+AstNode getNearestCommonAncestor(List nodes) {
+ // may be no nodes
+ if (nodes.isEmpty) {
+ return null;
+ }
+ // prepare parents
+ List> parents = [];
+ for (AstNode node in nodes) {
+ parents.add(getParents(node));
+ }
+ // find min length
+ int minLength = 1 << 20;
+ for (List parentList in parents) {
+ minLength = min(minLength, parentList.length);
+ }
+ // find deepest parent
+ int i = 0;
+ for (; i < minLength; i++) {
+ if (!allListsIdentical(parents, i)) {
+ break;
+ }
+ }
+ return parents[0][i - 1];
+}
+
+/**
+ * @return parent [AstNode]s from [CompilationUnit] (at index "0") to the given one.
+ */
+List getParents(AstNode node) {
+ // prepare number of parents
+ int numParents = 0;
+ {
+ AstNode current = node.parent;
+ while (current != null) {
+ numParents++;
+ current = current.parent;
+ }
+ }
+ // fill array of parents
+ List parents = new List(numParents);
+ AstNode current = node.parent;
+ int index = numParents;
+ while (current != null) {
+ parents[--index] = current;
+ current = current.parent;
+ }
+ return parents;
+}
+
/**
* If given [AstNode] is name of qualified property extraction, returns target from which
@@ -673,6 +764,19 @@ class CorrectionUtils {
String invertCondition(Expression expression) =>
_invertCondition0(expression)._source;
+ /**
+ * @return true if selection range contains only whitespace or comments
+ */
+ bool isJustWhitespaceOrComment(SourceRange range) {
+ String trimmedText = getRangeText(range).trim();
+ // may be whitespace
+ if (trimmedText.isEmpty) {
+ return true;
+ }
+ // may be comment
+ return TokenUtils.getTokens(trimmedText).isEmpty;
+ }
+
/**
* Returns the source with indentation changed from [oldIndent] to
* [newIndent], keeping indentation of lines relative to each other.
@@ -734,6 +838,40 @@ class CorrectionUtils {
return replaceSourceIndent(oldSource, oldIndent, newIndent);
}
+ /**
+ * @return true if "selection" covers "node" and there are any non-whitespace tokens
+ * between "selection" and "node" start/end.
+ */
+ bool selectionIncludesNonWhitespaceOutsideNode(SourceRange selection,
+ AstNode node) {
+ return _selectionIncludesNonWhitespaceOutsideRange(
+ selection,
+ rangeNode(node));
+ }
+
+ /**
+ * @return true if given range of [BinaryExpression] can be extracted.
+ */
+ bool validateBinaryExpressionRange(BinaryExpression binaryExpression, SourceRange range) {
+ // only parts of associative expression are safe to extract
+ if (!binaryExpression.operator.type.isAssociativeOperator) {
+ return false;
+ }
+ // prepare selected operands
+ List operands = _getOperandsInOrderFor(binaryExpression);
+ List subOperands = _getOperandsForSourceRange(operands, range);
+ // if empty, then something wrong with selection
+ if (subOperands.isEmpty) {
+ return false;
+ }
+ // may be some punctuation included into selection - operators, braces, etc
+ if (_selectionIncludesNonWhitespaceOutsideOperands(range, subOperands)) {
+ return false;
+ }
+ // OK
+ return true;
+ }
+
/**
* @return the [ImportElement] used to import given [Element] into [library].
* May be `null` if was not imported, i.e. declared in the same library.
@@ -838,6 +976,90 @@ class CorrectionUtils {
}
return _InvertedCondition._simple(getNodeText(expression));
}
+
+ bool _selectionIncludesNonWhitespaceOutsideOperands(SourceRange selection, List operands) {
+ return _selectionIncludesNonWhitespaceOutsideRange(selection, rangeNodes(operands));
+ }
+
+ /**
+ * @return true if "selection" covers "range" and there are any non-whitespace tokens
+ * between "selection" and "range" start/end.
+ */
+ bool _selectionIncludesNonWhitespaceOutsideRange(SourceRange selection,
+ SourceRange range) {
+ // selection should cover range
+ if (!selection.covers(range)) {
+ return false;
+ }
+ // non-whitespace between selection start and range start
+ if (!isJustWhitespaceOrComment(rangeStartStart(selection, range))) {
+ return true;
+ }
+ // non-whitespace after range
+ if (!isJustWhitespaceOrComment(rangeEndEnd(range, selection))) {
+ return true;
+ }
+ // only whitespace in selection around range
+ return false;
+ }
+
+ /**
+ * @return [Expression]s from operands which are completely covered by given
+ * [SourceRange]. Range should start and end between given [Expression]s.
+ */
+ static List _getOperandsForSourceRange(List operands, SourceRange range) {
+ assert(!operands.isEmpty);
+ List subOperands = [];
+ // track range enter/exit
+ bool entered = false;
+ bool exited = false;
+ // may be range starts before or on first operand
+ if (range.offset <= operands[0].offset) {
+ entered = true;
+ }
+ // iterate over gaps between operands
+ for (int i = 0; i < operands.length - 1; i++) {
+ Expression operand = operands[i];
+ Expression nextOperand = operands[i + 1];
+ SourceRange inclusiveGap = rangeEndStart(operand, nextOperand).getMoveEnd(1);
+ // add operand, if already entered range
+ if (entered) {
+ subOperands.add(operand);
+ // may be last operand in range
+ if (range.endsIn(inclusiveGap)) {
+ exited = true;
+ }
+ } else {
+ // may be first operand in range
+ if (range.startsIn(inclusiveGap)) {
+ entered = true;
+ }
+ }
+ }
+ // check if last operand is in range
+ Expression lastGroupMember = operands[operands.length - 1];
+ if (range.end == lastGroupMember.end) {
+ subOperands.add(lastGroupMember);
+ exited = true;
+ }
+ // we expect that range covers only given operands
+ if (!exited) {
+ return [];
+ }
+ // done
+ return subOperands;
+ }
+
+ /**
+ * @return all operands of the given [BinaryExpression] and its children with the same
+ * operator.
+ */
+ static List _getOperandsInOrderFor(BinaryExpression groupRoot) {
+ List operands = [];
+ TokenType groupOperatorType = groupRoot.operator.type;
+ groupRoot.accept(new _OrderedOperandsVisitor(groupOperatorType, operands));
+ return operands;
+ }
}
@@ -851,6 +1073,66 @@ class CorrectionUtils_InsertDesc {
}
+/**
+ * Utilities to work with [Token]s.
+ */
+class TokenUtils {
+ /**
+ * @return the first [KeywordToken] with given [Keyword], may be null if
+ * not found.
+ */
+ static KeywordToken findKeywordToken(List tokens, Keyword keyword) {
+ for (Token token in tokens) {
+ if (token is KeywordToken) {
+ KeywordToken keywordToken = token;
+ if (keywordToken.keyword == keyword) {
+ return keywordToken;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the first [Token] with given [TokenType], may be null if not
+ * found.
+ */
+ static Token findToken(List tokens, TokenType type) {
+ for (Token token in tokens) {
+ if (token.type == type) {
+ return token;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return [Token]s of the given Dart source, not null, may be empty if no
+ * tokens or some exception happens.
+ */
+ static List getTokens(String s) {
+ try {
+ List tokens = [];
+ Scanner scanner = new Scanner(null, new CharSequenceReader(s), null);
+ Token token = scanner.tokenize();
+ while (token.type != TokenType.EOF) {
+ tokens.add(token);
+ token = token.next;
+ }
+ return tokens;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ /**
+ * @return true if given [Token]s contain only single [Token] with given
+ * [TokenType].
+ */
+ static bool hasOnly(List tokens, TokenType type) =>
+ tokens.length == 1 && tokens[0].type == type;
+}
+
/**
* A container with a source and its precedence.
*/
@@ -893,3 +1175,20 @@ class _InvertedCondition {
static _InvertedCondition _simple(String source) =>
new _InvertedCondition(2147483647, source);
}
+
+
+class _OrderedOperandsVisitor extends GeneralizingAstVisitor {
+ final TokenType groupOperatorType;
+ final List operands;
+
+ _OrderedOperandsVisitor(this.groupOperatorType, this.operands);
+
+ @override
+ Object visitExpression(Expression node) {
+ if (node is BinaryExpression && node.operator.type == groupOperatorType) {
+ return super.visitNode(node);
+ }
+ operands.add(node);
+ return null;
+ }
+}
diff --git a/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart b/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart
new file mode 100644
index 00000000000..88b29de37d9
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/refactoring/extract_local.dart
@@ -0,0 +1,530 @@
+// 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.
+
+library services.src.refactoring.extract_local;
+
+import 'dart:async';
+
+import 'package:analysis_server/src/protocol2.dart' show SourceEdit;
+import 'package:analysis_server/src/services/correction/change.dart';
+import 'package:analysis_server/src/services/correction/selection_analyzer.dart';
+import 'package:analysis_server/src/services/correction/source_range.dart';
+import 'package:analysis_server/src/services/correction/status.dart';
+import 'package:analysis_server/src/services/correction/strings.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:analyzer/src/generated/ast.dart';
+import 'package:analyzer/src/generated/element.dart';
+import 'package:analyzer/src/generated/java_core.dart';
+import 'package:analyzer/src/generated/scanner.dart';
+import 'package:analyzer/src/generated/source.dart';
+
+
+const String _TOKEN_SEPARATOR = "\uFFFF";
+
+
+/**
+ * [ExtractLocalRefactoring] implementation.
+ */
+class ExtractLocalRefactoringImpl extends RefactoringImpl implements
+ ExtractLocalRefactoring {
+ final CompilationUnit unit;
+ final int selectionOffset;
+ final int selectionLength;
+ String file;
+ SourceRange selectionRange;
+ CorrectionUtils utils;
+
+ String name;
+ bool extractAll = true;
+ final List names = [];
+ final List offsets = [];
+ final List lengths = [];
+
+ Expression rootExpression;
+ Expression singleExpression;
+ bool wholeStatementExpression = false;
+ String stringLiteralPart;
+ final List occurrences = [];
+ final Set excludedVariableNames = new Set();
+
+ ExtractLocalRefactoringImpl(this.unit, this.selectionOffset,
+ this.selectionLength) {
+ file = unit.element.source.fullName;
+ selectionRange = new SourceRange(selectionOffset, selectionLength);
+ utils = new CorrectionUtils(unit);
+ }
+
+ String get declarationKeyword {
+ if (_isPartOfConstantExpression(rootExpression)) {
+ return "const";
+ } else {
+ return "var";
+ }
+ }
+
+ @override
+ String get refactoringName => 'Extract Local Variable';
+
+ @override
+ Future checkFinalConditions() {
+ RefactoringStatus result = new RefactoringStatus();
+ if (excludedVariableNames.contains(name)) {
+ result.addWarning(
+ format(
+ "A variable with name '{0}' is already defined in the visible scope.",
+ name));
+ }
+ return new Future.value(result);
+ }
+
+ @override
+ Future checkInitialConditions() {
+ RefactoringStatus result = new RefactoringStatus();
+ // selection
+ result.addStatus(_checkSelection());
+ // occurrences
+ if (!result.hasFatalError) {
+ _prepareOccurrences();
+ _prepareExcludedNames();
+ }
+ // suggested names
+ _prepareNames();
+ // done
+ return new Future.value(result);
+ }
+
+ @override
+ RefactoringStatus checkName() {
+ return validateVariableName(name);
+ }
+
+ @override
+ Future createChange() {
+ Change change = new Change(refactoringName);
+ // prepare occurrences
+ List occurrences;
+ if (extractAll) {
+ occurrences = this.occurrences;
+ } else {
+ occurrences = [selectionRange];
+ }
+ // If the whole expression of a statement is selected, like '1 + 2',
+ // then convert it into a variable declaration statement.
+ if (wholeStatementExpression && occurrences.length == 1) {
+ String keyword = declarationKeyword;
+ String declarationSource = '$keyword $name = ';
+ SourceEdit edit =
+ new SourceEdit(singleExpression.offset, 0, declarationSource);
+ change.addEdit(file, edit);
+ return new Future.value(change);
+ }
+ // add variable declaration
+ {
+ String declarationSource;
+ if (stringLiteralPart != null) {
+ declarationSource = "var ${name} = '${stringLiteralPart}';";
+ } else {
+ String keyword = declarationKeyword;
+ String initializerSource = utils.getRangeText(selectionRange);
+ declarationSource = "${keyword} ${name} = ${initializerSource};";
+ }
+ // prepare location for declaration
+ Statement targetStatement = _findTargetStatement(occurrences);
+ String prefix = utils.getNodePrefix(targetStatement);
+ // insert variable declaration
+ String eol = utils.endOfLine;
+ SourceEdit edit = new SourceEdit(
+ targetStatement.offset,
+ 0,
+ '${declarationSource}${eol}${prefix}');
+ change.addEdit(file, edit);
+ }
+ // prepare replacement
+ String occurrenceReplacement = name;
+ if (stringLiteralPart != null) {
+ occurrenceReplacement = "\${$name}";
+ }
+ // replace occurrences with variable reference
+ for (SourceRange range in occurrences) {
+ SourceEdit edit = editFromRange(range, occurrenceReplacement);
+ change.addEdit(file, edit);
+ }
+ // done
+ return new Future.value(change);
+ }
+
+ @override
+ bool requiresPreview() => false;
+
+ /**
+ * Checks if [selectionRange] selects [Expression] which can be extracted, and
+ * location of this [DartExpression] in AST allows extracting.
+ */
+ RefactoringStatus _checkSelection() {
+ _ExtractExpressionAnalyzer _selectionAnalyzer =
+ new _ExtractExpressionAnalyzer(selectionRange);
+ unit.accept(_selectionAnalyzer);
+ AstNode coveringNode = _selectionAnalyzer.coveringNode;
+ // may be fatal error
+ {
+ RefactoringStatus status = _selectionAnalyzer.status;
+ if (status.hasFatalError) {
+ return status;
+ }
+ }
+ // we need enclosing block to add variable declaration statement
+ if (coveringNode == null ||
+ coveringNode.getAncestor((node) => node is Block) == null) {
+ return new RefactoringStatus.fatal(
+ 'Expression inside of function must be selected '
+ 'to activate this refactoring.');
+ }
+ // part of string literal
+ if (coveringNode is StringLiteral) {
+ stringLiteralPart = utils.getRangeText(selectionRange);
+ if (stringLiteralPart.startsWith("'") ||
+ stringLiteralPart.startsWith('"') ||
+ stringLiteralPart.endsWith("'") ||
+ stringLiteralPart.endsWith('"')) {
+ return new RefactoringStatus.fatal(
+ 'Cannot extract only leading or trailing quote of string literal.');
+ }
+ return new RefactoringStatus();
+ }
+ // single node selected
+ if (_selectionAnalyzer.selectedNodes.length == 1 &&
+ !utils.selectionIncludesNonWhitespaceOutsideNode(
+ selectionRange,
+ _selectionAnalyzer.firstSelectedNode)) {
+ AstNode selectedNode = _selectionAnalyzer.firstSelectedNode;
+ if (selectedNode is Expression) {
+ rootExpression = selectedNode;
+ singleExpression = rootExpression;
+ wholeStatementExpression =
+ singleExpression.parent is ExpressionStatement;
+ return new RefactoringStatus();
+ }
+ }
+ // fragment of binary expression selected
+ if (coveringNode is BinaryExpression) {
+ BinaryExpression binaryExpression = coveringNode;
+ if (utils.validateBinaryExpressionRange(
+ binaryExpression,
+ selectionRange)) {
+ rootExpression = binaryExpression;
+ singleExpression = null;
+ return new RefactoringStatus();
+ }
+ }
+ // invalid selection
+ return new RefactoringStatus.fatal(
+ 'Expression must be selected to activate this refactoring.');
+ }
+
+ /**
+ * Returns [AstNode]s at the offsets of the given [SourceRange]s.
+ */
+ List _findNodes(List ranges) {
+ List nodes = [];
+ for (SourceRange range in ranges) {
+ AstNode node = new NodeLocator.con1(range.offset).searchWithin(unit);
+ nodes.add(node);
+ }
+ return nodes;
+ }
+
+ /**
+ * @return the [Statement] such that variable declaration added before it will be visible in
+ * all given occurrences.
+ */
+ Statement _findTargetStatement(List occurrences) {
+ List nodes = _findNodes(occurrences);
+ List firstParents = getParents(nodes[0]);
+ AstNode commonParent = getNearestCommonAncestor(nodes);
+ if (commonParent is Block) {
+ int commonIndex = firstParents.indexOf(commonParent);
+ return firstParents[commonIndex + 1] as Statement;
+ } else {
+ return commonParent.getAncestor((node) => node is Statement);
+ }
+ }
+
+ /**
+ * @return `true` if it is OK to extract the node with the given [SourceRange].
+ */
+ bool _isExtractable(SourceRange range) {
+ _ExtractExpressionAnalyzer analyzer = new _ExtractExpressionAnalyzer(range);
+ utils.unit.accept(analyzer);
+ return analyzer.status.isOK;
+ }
+
+ bool _isPartOfConstantExpression(AstNode node) {
+ if (node is TypedLiteral) {
+ return node.constKeyword != null;
+ }
+ if (node is InstanceCreationExpression) {
+ InstanceCreationExpression creation = node;
+ return creation.isConst;
+ }
+ if (node is ArgumentList ||
+ node is ConditionalExpression ||
+ node is BinaryExpression ||
+ node is ParenthesizedExpression ||
+ node is PrefixExpression ||
+ node is Literal ||
+ node is MapLiteralEntry) {
+ return _isPartOfConstantExpression(node.parent);
+ }
+ return false;
+ }
+
+ void _prepareExcludedNames() {
+ excludedVariableNames.clear();
+ // TODO(scheglov) clean up?
+ AstNode enclosingNode =
+ new NodeLocator.con1(selectionOffset).searchWithin(unit);
+ Block enclosingBlock = enclosingNode.getAncestor((node) => node is Block);
+ if (enclosingBlock != null) {
+ SourceRange newVariableVisibleRange =
+ rangeStartEnd(selectionRange, enclosingBlock.end);
+ ExecutableElement enclosingExecutable =
+ getEnclosingExecutableElement(enclosingNode);
+ if (enclosingExecutable != null) {
+ visitChildren(enclosingExecutable, (Element element) {
+ if (element is LocalElement) {
+ SourceRange elementRange = element.visibleRange;
+ if (elementRange != null &&
+ elementRange.intersects(newVariableVisibleRange)) {
+ excludedVariableNames.add(element.displayName);
+ }
+ }
+ return true;
+ });
+ }
+ }
+ }
+
+ void _prepareNames() {
+ names.clear();
+ // TODO(scheglov) implement
+// Set excluded = excludedVariableNames;
+// if (_stringLiteralPart != null) {
+// return getVariableNameSuggestions(_stringLiteralPart, excluded);
+// } else if (_singleExpression != null) {
+// _guessedNames = CorrectionUtils.getVariableNameSuggestions2(_singleExpression.staticType, _singleExpression, excluded);
+// } else {
+// _guessedNames = ArrayUtils.EMPTY_STRING_ARRAY;
+// }
+ }
+
+ /**
+ * @return all occurrences of the source which matches given selection, sorted by offset. First
+ * [SourceRange] is same as the given selection. May be empty, but not
+ * null.
+ */
+ List _prepareOccurrences() {
+ // prepare selection
+ String selectionSource;
+ {
+ String rawSelectionSource = utils.getRangeText(selectionRange);
+ List selectionTokens = TokenUtils.getTokens(rawSelectionSource);
+ selectionSource = selectionTokens.join(_TOKEN_SEPARATOR);
+ }
+ // prepare enclosing function
+ AstNode enclosingFunction;
+ {
+ AstNode selectionNode =
+ new NodeLocator.con1(selectionOffset).searchWithin(unit);
+ enclosingFunction = getEnclosingExecutableNode(selectionNode);
+ }
+ // visit function
+ enclosingFunction.accept(
+ new _OccurrencesVisitor(this, occurrences, selectionSource));
+ // done
+ return occurrences;
+ }
+}
+
+
+/**
+ * [SelectionAnalyzer] for [ExtractLocalRefactoringImpl].
+ */
+class _ExtractExpressionAnalyzer extends SelectionAnalyzer {
+ final RefactoringStatus status = new RefactoringStatus();
+
+ _ExtractExpressionAnalyzer(SourceRange selection) : super(selection);
+
+ /**
+ * Records fatal error with given message.
+ */
+ void invalidSelection(String message) {
+ _invalidSelection(message, null);
+ }
+
+ @override
+ Object visitAssignmentExpression(AssignmentExpression node) {
+ super.visitAssignmentExpression(node);
+ Expression lhs = node.leftHandSide;
+ if (_isFirstSelectedNode(lhs)) {
+ _invalidSelection(
+ 'Cannot extract the left-hand side of an assignment.',
+ new RefactoringStatusContext.forNode(lhs));
+ }
+ return null;
+ }
+
+ @override
+ Object visitSimpleIdentifier(SimpleIdentifier node) {
+ super.visitSimpleIdentifier(node);
+ if (_isFirstSelectedNode(node)) {
+ // name of declaration
+ if (node.inDeclarationContext()) {
+ invalidSelection('Cannot extract the name part of a declaration.');
+ }
+ // method name
+ Element element = node.bestElement;
+ if (element is FunctionElement || element is MethodElement) {
+ invalidSelection('Cannot extract a single method name.');
+ }
+ // name in property access
+ AstNode parent = node.parent;
+ if (parent is PrefixedIdentifier && identical(parent.identifier, node)) {
+ invalidSelection('Cannot extract name part of a property access.');
+ }
+ if (parent is PropertyAccess && identical(parent.propertyName, node)) {
+ invalidSelection('Cannot extract name part of a property access.');
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Records fatal error with given message and [RefactoringStatusContext].
+ */
+ void _invalidSelection(String message, RefactoringStatusContext context) {
+ status.addFatalError(message, context);
+ reset();
+ }
+
+ bool _isFirstSelectedNode(AstNode node) => identical(firstSelectedNode, node);
+}
+
+
+class _HasStatementVisitor extends GeneralizingAstVisitor {
+ final List result;
+
+ _HasStatementVisitor(this.result);
+
+ @override
+ visitStatement(Statement node) {
+ result[0] = true;
+ }
+}
+
+
+class _OccurrencesVisitor extends GeneralizingAstVisitor