/* * Copyright 2003-2010 the original author or authors. * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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 org.codehaus.groovy.eclipse.refactoring.core.extract; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassCodeVisitorSupport; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.DynamicVariable; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.InnerClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.VariableScope; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.DeclarationExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.eclipse.codebrowsing.fragments.IASTFragment; import org.codehaus.groovy.eclipse.codebrowsing.requestor.Region; import org.codehaus.groovy.eclipse.codebrowsing.selection.FindSurroundingNode; import org.codehaus.groovy.eclipse.codebrowsing.selection.FindSurroundingNode.VisitKind; import org.codehaus.groovy.eclipse.core.util.VisitCompleteException; import org.codehaus.groovy.eclipse.refactoring.core.utils.ASTTools; import org.codehaus.groovy.eclipse.refactoring.formatter.DefaultGroovyFormatter; import org.codehaus.groovy.eclipse.refactoring.formatter.FormatterPreferences; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.refactoring.CompilationUnitChange; import org.eclipse.jdt.groovy.core.util.ReflectionUtils; import org.eclipse.jdt.internal.corext.refactoring.Checks; import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages; import org.eclipse.jdt.internal.corext.refactoring.code.PromoteTempToFieldRefactoring; import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil; import org.eclipse.jdt.internal.corext.util.JdtFlags; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.TextUtilities; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.text.edits.TextEditGroup; /** * See GRECLIPSE-1436 for areas where this refactoring can be improved. * * @author Daniel Phan * @created 2012-01-26 */ public class ConvertGroovyLocalToFieldRefactoring extends PromoteTempToFieldRefactoring { private GroovyCompilationUnit unit; private String fieldName; private int fieldVisibility = -1; private int selectionStart; private int selectionLength; private IASTFragment selectionFragment; private VariableExpression variableExpressionInDeclaration; private DeclarationExpression declarationExpression; private ClassNode containingClassNode; private ModuleNode moduleNode; private MethodNode methodNode; private CompilationUnitChange change; public ConvertGroovyLocalToFieldRefactoring(GroovyCompilationUnit unit, int selectionStart, int selectionLength) { super(unit, selectionStart, selectionLength); this.unit = unit; this.selectionStart = selectionStart; this.selectionLength = selectionLength; } @Override public int getVisibility() { if (fieldVisibility == -1) { fieldVisibility = (Integer) ReflectionUtils.getPrivateField(PromoteTempToFieldRefactoring.class, "fVisibility", this); } return fieldVisibility; } @Override public boolean getDeclareFinal() { return false; } @Override public boolean getDeclareStatic() { return false; } public String getFieldName() { if (fieldName == null) { fieldName = (String) ReflectionUtils.getPrivateField(PromoteTempToFieldRefactoring.class, "fFieldName", this); } if ((fieldName == null || fieldName.length() == 0) && variableExpressionInDeclaration != null) { fieldName = variableExpressionInDeclaration.getName(); } return fieldName; } @Override public int getInitializeIn() { return INITIALIZE_IN_METHOD; } @Override public void setVisibility(int accessModifier) { super.setVisibility(accessModifier); fieldVisibility = accessModifier; } @Override public void setDeclareFinal(boolean declareFinal) { } @Override public void setDeclareStatic(boolean declareStatic) { } @Override public void setFieldName(String fieldName) { super.setFieldName(fieldName); this.fieldName = fieldName; } @Override public void setInitializeIn(int initializeIn) { } @Override public boolean canEnableSettingStatic() { return false; } @Override public boolean canEnableSettingFinal() { return false; } @Override public boolean canEnableSettingDeclareInConstructors() { return false; } @Override public boolean canEnableSettingDeclareInMethod() { return true; } @Override public boolean canEnableSettingDeclareInFieldDeclaration() { return false; } @Override public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException { try { pm.beginTask("", 5); RefactoringStatus result = Checks.validateEdit(unit, getValidationContext()); if (result.hasFatalError()) { return result; } pm.worked(1); IASTFragment selectionFragment = getSelectionFragment(); if (selectionFragment == null) { result.merge(RefactoringStatus .createFatalErrorStatus(RefactoringCoreMessages.PromoteTempToFieldRefactoring_select_declaration)); return result; } Expression selectedExpression = selectionFragment.getAssociatedExpression(); if (!(selectedExpression instanceof VariableExpression)) { result.merge(RefactoringStatus .createFatalErrorStatus(RefactoringCoreMessages.PromoteTempToFieldRefactoring_select_declaration)); return result; } pm.worked(1); VariableExpression selectedVariableExpression = (VariableExpression) selectedExpression; Variable declaredVariable = selectedVariableExpression.getAccessedVariable(); if (declaredVariable instanceof DynamicVariable) { result.merge(RefactoringStatus.createFatalErrorStatus("Cannot convert dynamic variable.")); return result; } if (!(declaredVariable instanceof VariableExpression)) { result.merge(RefactoringStatus .createFatalErrorStatus(RefactoringCoreMessages.PromoteTempToFieldRefactoring_select_declaration)); return result; } pm.worked(1); VariableExpression variableExpressionInDeclaration = (VariableExpression) declaredVariable; DeclarationExpression declarationExpression = getDeclarationExpression(variableExpressionInDeclaration); if (declarationExpression == null) { result.merge(RefactoringStatus.createFatalErrorStatus("Cannot find variable declaration.")); return result; } if (declarationExpression.isMultipleAssignmentDeclaration()) { result.merge(RefactoringStatus .createFatalErrorStatus("Cannot convert a variable declared using multiple assignment.")); return result; } pm.worked(1); // We should check declaration for local type usage here this.variableExpressionInDeclaration = variableExpressionInDeclaration; ClassNode containingClassNode = getContainingClassNode(); if (containingClassNode == null) { result.merge(RefactoringStatus.createFatalErrorStatus("Cannot find enclosing class declaration.")); return result; } if (containingClassNode.isScript()) { result.merge(RefactoringStatus.createFatalErrorStatus("Cannot add field to a script.")); return result; } if (containingClassNode.isInterface() || containingClassNode.isAnnotationDefinition()) { result.merge(RefactoringStatus.createFatalErrorStatus("Cannot add field to an interface or annotation definition.")); return result; } pm.worked(1); return result; } finally { pm.done(); } } @Override public String[] guessFieldNames() { return new String[] { variableExpressionInDeclaration.getName() }; } @Override public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException { try { pm.beginTask("", 4); FieldNode conflictingField = getContainingClassNode().getDeclaredField(getFieldName()); if (conflictingField != null) { return RefactoringStatus .createFatalErrorStatus(RefactoringCoreMessages.PromoteTempToFieldRefactoring_Name_conflict_with_field); } pm.worked(1); CompilationUnitChange change = new CompilationUnitChange("Convert Groovy Local Variable To Field", unit); change.setEdit(new MultiTextEdit()); // Create field TextEditGroup group = createFieldTextEditGroup(); change.addChangeGroup(new TextEditChangeGroup(change, group)); for (TextEdit edit : group.getTextEdits()) { change.addEdit(edit); } pm.worked(1); // Convert declaration to a reference group = declarationToReferenceTextEditGroup(); change.addChangeGroup(new TextEditChangeGroup(change, group)); for (TextEdit edit : group.getTextEdits()) { change.addEdit(edit); } pm.worked(1); // Rename variable references RefactoringStatus status = new RefactoringStatus(); group = renameVariableReferencesTextEditGroup(status); change.addChangeGroup(new TextEditChangeGroup(change, group)); for (TextEdit edit : group.getTextEdits()) { change.addEdit(edit); } pm.worked(1); this.change = change; return status; } finally { pm.done(); } } private TextEditGroup createFieldTextEditGroup() { ClassNode classNode = getContainingClassNode(); char[] contents = unit.getContents(); MethodNode method = getContainingMethodNode(); int methodLineOffset = method.getStart() - method.getColumnNumber() + 1; int methodOffset = method.getStart(); String methodIndentation = String.valueOf(CharOperation.subarray(contents, methodLineOffset, methodOffset)); int indentLevel = ASTTools.getCurrentIntentation(methodIndentation); String fieldText = null; try { fieldText = createFieldText(indentLevel); } catch (Exception e) {} TextEditGroup group = new TextEditGroup("Create field."); if (fieldText != null) { int insertOffset = CharOperation.indexOf('{', contents, classNode.getStart()) + 1; String newline = TextUtilities.determineLineDelimiter(String.valueOf(contents), "\n"); group.addTextEdit(new InsertEdit(insertOffset, newline + fieldText)); } return group; } /** * Adapted from * ExtractGroovyConstantRefactoring#createConstantText */ private String createFieldText(int indentLevel) throws MalformedTreeException, BadLocationException { StringBuilder sb = new StringBuilder(); String indentation = CodeFormatterUtil.createIndentString(indentLevel, unit.getJavaProject()); sb.append(indentation); String visibility = JdtFlags.getVisibilityString(getVisibility()); sb.append(visibility); if (!visibility.equals("")) { sb.append(" "); } char[] contents = unit.getContents(); // typeOrDef is the value of the token that comes before the variable // name in the declaration expression (e.g. "String", "int", "def"). String typeOrDef = new String(CharOperation.subarray(contents, declarationExpression.getStart(), variableExpressionInDeclaration.getStart())); sb.append(typeOrDef).append(getFieldName()); IDocument doc = new Document(sb.toString()); DefaultGroovyFormatter formatter = new DefaultGroovyFormatter(doc, new FormatterPreferences(unit), indentLevel); TextEdit edit = formatter.format(); edit.apply(doc); return doc.get(); } private TextEditGroup declarationToReferenceTextEditGroup() { TextEditGroup group = new TextEditGroup("Convert local variable declaration to reference."); int typeOrDefLength = variableExpressionInDeclaration.getStart() - declarationExpression.getStart(); group.addTextEdit(new ReplaceEdit(declarationExpression.getStart(), typeOrDefLength, "")); return group; } private TextEditGroup renameVariableReferencesTextEditGroup(RefactoringStatus status) { final Set<VariableExpression> references = new HashSet<VariableExpression>(); ClassCodeVisitorSupport referencesVisitor = new ClassCodeVisitorSupport() { @Override public void visitVariableExpression(VariableExpression variableExpression) { if (variableExpression.getAccessedVariable() == variableExpressionInDeclaration && variableExpression.getLineNumber() >= 0) { references.add(variableExpression); } } }; referencesVisitor.visitClass(getContainingClassNode()); Iterator<InnerClassNode> innerClasses = getContainingClassNode().getInnerClasses(); while (innerClasses != null && innerClasses.hasNext()) { ClassNode innerClass = innerClasses.next(); referencesVisitor.visitClass(innerClass); } TextEditGroup group = new TextEditGroup("Update local variables to reference field."); for (VariableExpression reference : references) { if (getUsedVariableAndFieldNames(reference).contains(getFieldName())) { status.merge(RefactoringStatus.createWarningStatus("New field conflicts with existing name.")); } group.addTextEdit(new ReplaceEdit(reference.getStart(), reference.getLength(), getFieldName())); } return group; } @Override public Change createChange(IProgressMonitor pm) throws CoreException { return change; } private IASTFragment getSelectionFragment() { if (selectionFragment == null) { selectionFragment = ASTTools.getSelectionFragment(getModuleNode(), selectionStart, selectionLength); } return selectionFragment; } private ClassNode getContainingClassNode() { if (containingClassNode == null) { ModuleNode moduleNode = getModuleNode(); if (moduleNode == null) { return null; } if (declarationExpression == null) { return null; } containingClassNode = ASTTools.getContainingClassNode(moduleNode, declarationExpression.getStart()); } return containingClassNode; } private ModuleNode getModuleNode() { if (moduleNode == null) { moduleNode = unit.getModuleNode(); } return moduleNode; } private DeclarationExpression getDeclarationExpression(final VariableExpression variableExpressionInDeclaration) { if (declarationExpression != null) { return declarationExpression; } ClassCodeVisitorSupport visitor = new ClassCodeVisitorSupport() { @Override public void visitDeclarationExpression(DeclarationExpression declarationExpression) { // Remember the most recent DeclarationExpression we visited. ConvertGroovyLocalToFieldRefactoring.this.declarationExpression = declarationExpression; super.visitDeclarationExpression(declarationExpression); } @Override public void visitVariableExpression(VariableExpression variableExpression) { // The moment we visit the variableExpressionInDeclaration node, // we know that the the most recent DeclarationExpression we've // visited is the one that contains // variableExpressionInDeclaration. // That's why we throw the VisitCompleteException to stop // visiting nodes. if (variableExpression == variableExpressionInDeclaration) { throw new VisitCompleteException(); } super.visitVariableExpression(variableExpression); } }; for (ClassNode classNode : getModuleNode().getClasses()) { try { visitor.visitClass(classNode); } catch (VisitCompleteException expected) { break; } // If a VisitCompleteException was not thrown, we did not find the // declarationExpression that contains // variableExpressionInDeclaration. // Therefore, we set declarationExpression to null, since it's just // some irrelevant DeclarationExpression. declarationExpression = null; } return declarationExpression; } /** * Same as getDeclarationExpression except with the containing method */ private MethodNode getContainingMethodNode() { if (methodNode != null) { return methodNode; } ClassCodeVisitorSupport visitor = new ClassCodeVisitorSupport() { @Override public void visitConstructorOrMethod(MethodNode methodNode, boolean isConstructor) { ConvertGroovyLocalToFieldRefactoring.this.methodNode = methodNode; super.visitConstructorOrMethod(methodNode, isConstructor); } @Override public void visitVariableExpression(VariableExpression variableExpression) { if (variableExpression == variableExpressionInDeclaration) { throw new VisitCompleteException(); } super.visitVariableExpression(variableExpression); } }; for (ClassNode classNode : getModuleNode().getClasses()) { try { visitor.visitClass(classNode); } catch (VisitCompleteException expected) { break; } methodNode = null; } return methodNode; } /** * Adapted from ExtractGroovyLocalRefactoring#getParentStack and * ExtractGroovyLocalRefactoring#getExcludedVariableNames */ private Set<String> getUsedVariableAndFieldNames(VariableExpression variableExpression) { FindSurroundingNode find = new FindSurroundingNode(new Region(variableExpression), VisitKind.PARENT_STACK); find.doVisitSurroundingNode(moduleNode); List<IASTFragment> parentStack = new ArrayList<IASTFragment>(find.getParentStack()); Collections.reverse(parentStack); Set<String> result = new HashSet<String>(); for (IASTFragment fragment : parentStack) { ASTNode astNode = fragment.getAssociatedNode(); VariableScope scope = null; if (astNode instanceof BlockStatement) { scope = ((BlockStatement) astNode).getVariableScope(); } else if (astNode instanceof MethodNode) { scope = ((MethodNode) astNode).getVariableScope(); } else if (astNode instanceof ClosureExpression) { scope = ((ClosureExpression) astNode).getVariableScope(); } else if (astNode instanceof ClassNode) { for (FieldNode field : ((ClassNode) astNode).getFields()) { if (field.getLineNumber() > 0) { result.add(field.getName()); } } } if (scope != null) { Iterator<Variable> declaredVariables = scope.getDeclaredVariablesIterator(); while (declaredVariables.hasNext()) { Variable variable = declaredVariables.next(); if (variable instanceof VariableExpression) { VariableExpression varExpression = (VariableExpression) variable; if (varExpression.getAccessedVariable() != variableExpression.getAccessedVariable()) { result.add(variable.getName()); } } else { result.add(variable.getName()); } } } } return result; } }