/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.engine.services.internal.refactoring; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.dart.engine.ast.ArgumentList; import com.google.dart.engine.ast.AstNode; import com.google.dart.engine.ast.BinaryExpression; import com.google.dart.engine.ast.Block; import com.google.dart.engine.ast.CompilationUnit; import com.google.dart.engine.ast.ConditionalExpression; import com.google.dart.engine.ast.Expression; import com.google.dart.engine.ast.ExpressionStatement; import com.google.dart.engine.ast.InstanceCreationExpression; import com.google.dart.engine.ast.Literal; import com.google.dart.engine.ast.MapLiteralEntry; import com.google.dart.engine.ast.ParenthesizedExpression; import com.google.dart.engine.ast.PrefixExpression; import com.google.dart.engine.ast.SimpleStringLiteral; import com.google.dart.engine.ast.Statement; import com.google.dart.engine.ast.StringLiteral; import com.google.dart.engine.ast.TypedLiteral; import com.google.dart.engine.ast.visitor.GeneralizingAstVisitor; import com.google.dart.engine.ast.visitor.NodeLocator; import com.google.dart.engine.element.ExecutableElement; import com.google.dart.engine.element.LocalElement; import com.google.dart.engine.element.visitor.GeneralizingElementVisitor; import com.google.dart.engine.scanner.Token; import com.google.dart.engine.services.assist.AssistContext; import com.google.dart.engine.services.change.Change; import com.google.dart.engine.services.change.Edit; import com.google.dart.engine.services.change.SourceChange; import com.google.dart.engine.services.internal.correction.CorrectionUtils; import com.google.dart.engine.services.internal.util.ExecutionUtils; import com.google.dart.engine.services.internal.util.RunnableEx; import com.google.dart.engine.services.internal.util.TokenUtils; import com.google.dart.engine.services.refactoring.ExtractLocalRefactoring; import com.google.dart.engine.services.refactoring.NamingConventions; import com.google.dart.engine.services.refactoring.ProgressMonitor; import com.google.dart.engine.services.status.RefactoringStatus; import com.google.dart.engine.utilities.source.SourceRange; import static com.google.dart.engine.utilities.source.SourceRangeFactory.rangeNode; import static com.google.dart.engine.utilities.source.SourceRangeFactory.rangeStartEnd; import static com.google.dart.engine.utilities.source.SourceRangeFactory.rangeStartLength; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import java.text.MessageFormat; import java.util.List; import java.util.Set; /** * Implementation of {@link ExtractLocalRefactoring}. */ public class ExtractLocalRefactoringImpl extends RefactoringImpl implements ExtractLocalRefactoring { private static final String TOKEN_SEPARATOR = "\uFFFF"; private final AssistContext context; private final SourceRange selectionRange; private final int selectionStart; private final CompilationUnit unitNode; private final CorrectionUtils utils; private ExtractExpressionAnalyzer selectionAnalyzer; private Expression rootExpression; private Expression singleExpression; private boolean wholeStatementExpression; private String stringLiteralPart; private List<SourceRange> occurrences = Lists.newArrayList(); private String localName; private boolean replaceAllOccurrences = true; private Set<String> excludedVariableNames; private String[] guessedNames; public ExtractLocalRefactoringImpl(AssistContext context) throws Exception { this.context = context; this.selectionRange = context.getSelectionRange(); this.selectionStart = selectionRange.getOffset(); this.unitNode = context.getCompilationUnit(); this.utils = new CorrectionUtils(unitNode); } @Override public RefactoringStatus checkFinalConditions(ProgressMonitor pm) throws Exception { pm = checkProgressMonitor(pm); pm.beginTask("Checking final conditions", 1); try { RefactoringStatus result = new RefactoringStatus(); // name result.merge(NamingConventions.validateVariableName(localName)); if (getExcludedVariableNames().contains(localName)) { result.addWarning(MessageFormat.format( "A variable with name ''{0}'' is already defined in the visible scope.", localName)); } // done return result; } finally { pm.done(); } } @Override public RefactoringStatus checkInitialConditions(ProgressMonitor pm) throws Exception { pm = checkProgressMonitor(pm); pm.beginTask("Checking initial conditions", 2); try { RefactoringStatus result = new RefactoringStatus(); // selection result.merge(checkSelection()); pm.worked(1); // occurrences if (!result.hasFatalError()) { occurrences = getOccurrences(); } pm.worked(1); // done return result; } finally { pm.done(); } } @Override public RefactoringStatus checkLocalName(String newName) { return NamingConventions.validateVariableName(newName); } @Override public Change createChange(ProgressMonitor pm) throws Exception { pm = checkProgressMonitor(pm); SourceChange change = new SourceChange(getRefactoringName(), context.getSource()); // prepare occurrences List<SourceRange> occurrences; if (replaceAllOccurrences) { occurrences = this.occurrences; } else { occurrences = ImmutableList.of(selectionRange); } // If the whole expression of a statement is selected, like '1 + 2', // then convert it into a variable declaration statement. if (wholeStatementExpression && occurrences.size() == 1) { String keyword = getDeclarationKeyword(); String declarationSource = keyword + " " + localName + " = "; Edit edit = new Edit(singleExpression.getOffset(), 0, declarationSource); change.addEdit(edit, "Add variable declaration"); return change; } // add variable declaration { String declarationSource; if (stringLiteralPart != null) { declarationSource = "var " + localName + " = " + "'" + stringLiteralPart + "';"; } else { String keyword = getDeclarationKeyword(); String initializerSource = utils.getText(selectionRange); declarationSource = keyword + " " + localName + " = " + initializerSource + ";"; } // prepare location for declaration Statement targetStatement = findTargetStatement(occurrences); String prefix = utils.getNodePrefix(targetStatement); // insert variable declaration String eol = utils.getEndOfLine(); Edit edit = new Edit(targetStatement.getOffset(), 0, declarationSource + eol + prefix); change.addEdit(edit, "Add variable declaration"); } // prepare replacement String occurrenceReplacement = localName; if (stringLiteralPart != null) { occurrenceReplacement = "${" + localName + "}"; } // replace occurrences with variable reference for (SourceRange range : occurrences) { Edit edit = new Edit(range, occurrenceReplacement); change.addEdit(edit, "Replace expression with variable reference"); } return change; } @Override public String getRefactoringName() { return "Extract Local Variable"; } @Override public String[] guessNames() { if (guessedNames == null) { Set<String> excluded = getExcludedVariableNames(); if (stringLiteralPart != null) { return CorrectionUtils.getVariableNameSuggestions(stringLiteralPart, excluded); } else if (singleExpression != null) { guessedNames = CorrectionUtils.getVariableNameSuggestions( singleExpression.getStaticType(), singleExpression, excluded); } else { guessedNames = ArrayUtils.EMPTY_STRING_ARRAY; } } return guessedNames; } @Override public boolean hasSeveralOccurrences() { return occurrences.size() > 1; } @Override public void setLocalName(String localName) { this.localName = localName; } @Override public void setReplaceAllOccurrences(boolean replaceAllOccurrences) { this.replaceAllOccurrences = replaceAllOccurrences; } /** * Checks if {@link #selectionRange} selects {@link Expression} which can be extracted, and * location of this {@link DartExpression} in AST allows extracting. */ private RefactoringStatus checkSelection() { selectionAnalyzer = new ExtractExpressionAnalyzer(selectionRange); unitNode.accept(selectionAnalyzer); AstNode coveringNode = selectionAnalyzer.getCoveringNode(); // may be fatal error { RefactoringStatus status = selectionAnalyzer.getStatus(); if (status.hasFatalError()) { return status; } } // we need enclosing block to add variable declaration statement if (coveringNode == null || coveringNode.getAncestor(Block.class) == null) { return RefactoringStatus.createFatalErrorStatus("Expression inside of function must be selected to activate this refactoring."); } // part of string literal if (coveringNode instanceof StringLiteral) { stringLiteralPart = utils.getText(selectionRange); if (stringLiteralPart.startsWith("'") || stringLiteralPart.startsWith("\"") || stringLiteralPart.endsWith("'") || stringLiteralPart.endsWith("\"")) { return RefactoringStatus.createFatalErrorStatus("Cannot extract only leading or trailing quote of string literal."); } return new RefactoringStatus(); } // single node selected if (selectionAnalyzer.getSelectedNodes().size() == 1 && !utils.selectionIncludesNonWhitespaceOutsideNode( selectionRange, selectionAnalyzer.getFirstSelectedNode())) { AstNode selectedNode = selectionAnalyzer.getFirstSelectedNode(); if (selectedNode instanceof Expression) { rootExpression = (Expression) selectedNode; singleExpression = rootExpression; wholeStatementExpression = singleExpression.getParent() instanceof ExpressionStatement; return new RefactoringStatus(); } } // fragment of binary expression selected if (coveringNode instanceof BinaryExpression) { BinaryExpression binaryExpression = (BinaryExpression) coveringNode; if (utils.validateBinaryExpressionRange(binaryExpression, selectionRange)) { rootExpression = binaryExpression; singleExpression = null; return new RefactoringStatus(); } } // invalid selection return RefactoringStatus.createFatalErrorStatus("Expression must be selected to activate this refactoring."); } /** * @return the {@link AstNode}s at given {@link SourceRange}s. */ private List<AstNode> findNodes(List<SourceRange> ranges) { List<AstNode> nodes = Lists.newArrayList(); for (SourceRange range : ranges) { AstNode node = new NodeLocator(range.getOffset()).searchWithin(unitNode); nodes.add(node); } return nodes; } /** * @return the {@link Statement} such that variable declaration added before it will be visible in * all given occurrences. */ private Statement findTargetStatement(List<SourceRange> occurrences) { List<AstNode> nodes = findNodes(occurrences); List<AstNode> firstParents = CorrectionUtils.getParents(nodes.get(0)); AstNode commonParent = CorrectionUtils.getNearestCommonAncestor(nodes); if (commonParent instanceof Block) { int commonIndex = firstParents.indexOf(commonParent); return (Statement) firstParents.get(commonIndex + 1); } else { return commonParent.getAncestor(Statement.class); } } private String getDeclarationKeyword() { if (isPartOfConstantExpression(rootExpression)) { return "const"; } else { return "var"; } } /** * @return the {@link Set} of local names that are visible at the place where "localName" will be * used, "localName" should not be one of them. */ private Set<String> getExcludedVariableNames() { if (excludedVariableNames == null) { excludedVariableNames = Sets.newHashSet(); ExecutionUtils.runIgnore(new RunnableEx() { @Override public void run() throws Exception { AstNode enclosingNode = new NodeLocator(selectionStart).searchWithin(unitNode); Block enclosingBlock = enclosingNode.getAncestor(Block.class); if (enclosingBlock != null) { final SourceRange newVariableVisibleRange = rangeStartEnd( selectionRange, enclosingBlock.getEnd()); ExecutableElement enclosingExecutable = CorrectionUtils.getEnclosingExecutableElement(enclosingNode); if (enclosingExecutable != null) { enclosingExecutable.accept(new GeneralizingElementVisitor<Void>() { @Override public Void visitLocalElement(LocalElement element) { SourceRange elementRange = element.getVisibleRange(); if (elementRange != null && elementRange.intersects(newVariableVisibleRange)) { excludedVariableNames.add(element.getDisplayName()); } return super.visitLocalElement(element); } }); } } } }); } return excludedVariableNames; } /** * @return all occurrences of the source which matches given selection, sorted by offset. First * {@link SourceRange} is same as the given selection. May be empty, but not * <code>null</code>. */ private List<SourceRange> getOccurrences() { final List<SourceRange> occurrences = Lists.newArrayList(); // prepare selection final String selectionSource; { String rawSelectionSoruce = utils.getText(selectionRange); List<Token> selectionTokens = TokenUtils.getTokens(rawSelectionSoruce); selectionSource = StringUtils.join(selectionTokens, TOKEN_SEPARATOR); } // prepare enclosing function AstNode enclosingFunction; { AstNode selectionNode = new NodeLocator(selectionStart).searchWithin(unitNode); enclosingFunction = CorrectionUtils.getEnclosingExecutableNode(selectionNode); } // visit function enclosingFunction.accept(new GeneralizingAstVisitor<Void>() { @Override public Void visitBinaryExpression(BinaryExpression node) { if (!hasStatements(node)) { tryToFindOccurrenceFragment(node); return null; } return super.visitBinaryExpression(node); } @Override public Void visitExpression(Expression node) { if (isExtractable(rangeNode(node))) { tryToFindOccurrence(node); } return super.visitExpression(node); } @Override public Void visitSimpleStringLiteral(SimpleStringLiteral node) { if (stringLiteralPart != null) { int occuLength = stringLiteralPart.length(); String value = node.getValue(); int valueOffset = node.getOffset() + (node.isMultiline() ? 3 : 1); int lastIndex = 0; while (true) { int index = value.indexOf(stringLiteralPart, lastIndex); if (index == -1) { break; } lastIndex = index + occuLength; int occuStart = valueOffset + index; SourceRange occuRange = rangeStartLength(occuStart, occuLength); occurrences.add(occuRange); } return null; } return visitExpression(node); } private void addOccurrence(SourceRange range) { if (range.intersects(selectionRange)) { occurrences.add(selectionRange); } else { occurrences.add(range); } } private boolean hasStatements(AstNode root) { final boolean result[] = {false}; root.accept(new GeneralizingAstVisitor<Void>() { @Override public Void visitStatement(Statement node) { result[0] = true; return null; } }); return result[0]; } private void tryToFindOccurrence(Expression node) { String nodeSource = utils.getText(node); List<Token> nodeToken = TokenUtils.getTokens(nodeSource); nodeSource = StringUtils.join(nodeToken, TOKEN_SEPARATOR); if (nodeSource.equals(selectionSource)) { SourceRange occuRange = rangeNode(node); addOccurrence(occuRange); } } private void tryToFindOccurrenceFragment(Expression node) { int nodeOffset = node.getOffset(); String nodeSource = utils.getText(node); List<Token> nodeTokens = TokenUtils.getTokens(nodeSource); nodeSource = StringUtils.join(nodeTokens, TOKEN_SEPARATOR); // find "selection" in "node" tokens int lastIndex = 0; while (true) { // find next occurrence int index = nodeSource.indexOf(selectionSource, lastIndex); if (index == -1) { break; } lastIndex = index + selectionSource.length(); // find start/end tokens int startTokenIndex = StringUtils.countMatches( nodeSource.substring(0, index), TOKEN_SEPARATOR); int endTokenIndex = StringUtils.countMatches( nodeSource.substring(0, lastIndex), TOKEN_SEPARATOR); Token startToken = nodeTokens.get(startTokenIndex); Token endToken = nodeTokens.get(endTokenIndex); // add occurrence range int occuStart = nodeOffset + startToken.getOffset(); int occuEnd = nodeOffset + endToken.getEnd(); SourceRange occuRange = rangeStartEnd(occuStart, occuEnd); addOccurrence(occuRange); } } }); // done return occurrences; } /** * @return {@code true} if it is OK to extract the node with the given {@link SourceRange}. */ private boolean isExtractable(SourceRange range) { ExtractExpressionAnalyzer analyzer = new ExtractExpressionAnalyzer(range); utils.getUnit().accept(analyzer); return analyzer.getStatus().isOK(); } private boolean isPartOfConstantExpression(AstNode node) { if (node instanceof TypedLiteral) { return ((TypedLiteral) node).getConstKeyword() != null; } if (node instanceof InstanceCreationExpression) { InstanceCreationExpression creation = (InstanceCreationExpression) node; return creation.isConst(); } if (node instanceof ArgumentList || node instanceof ConditionalExpression || node instanceof BinaryExpression || node instanceof ParenthesizedExpression || node instanceof PrefixExpression || node instanceof Literal || node instanceof MapLiteralEntry) { return isPartOfConstantExpression(node.getParent()); } return false; } }