From 69b5996f1c66ac5e647039098cf25f055c2f67e3 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Tue, 21 Feb 2017 20:05:14 +0100 Subject: [PATCH 1/6] feature: change LocalVariable name --- .../refactoring/AbstractRenameRefactor.java | 111 ++++++++ .../refactoring/ChangeLocalVariableName.java | 238 +++++++++++++++++ src/main/java/spoon/refactoring/Issue.java | 21 ++ .../java/spoon/refactoring/IssueImpl.java | 36 +++ src/main/java/spoon/refactoring/Refactor.java | 27 ++ .../refactoring/ChangeVariableNameTest.java | 244 ++++++++++++++++++ .../refactoring/testclasses/TryRename.java | 16 ++ .../testclasses/VariableRename.java | 244 ++++++++++++++++++ 8 files changed, 937 insertions(+) create mode 100644 src/main/java/spoon/refactoring/AbstractRenameRefactor.java create mode 100644 src/main/java/spoon/refactoring/ChangeLocalVariableName.java create mode 100644 src/main/java/spoon/refactoring/Issue.java create mode 100644 src/main/java/spoon/refactoring/IssueImpl.java create mode 100644 src/main/java/spoon/refactoring/Refactor.java create mode 100644 src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java create mode 100644 src/test/java/spoon/test/refactoring/testclasses/TryRename.java create mode 100644 src/test/java/spoon/test/refactoring/testclasses/VariableRename.java diff --git a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java new file mode 100644 index 00000000000..a43f238297d --- /dev/null +++ b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.reference.CtReference; +import spoon.reflect.visitor.chain.CtConsumer; + +public abstract class AbstractRenameRefactor implements Refactor { + public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); + + protected T target; + protected String newName; + protected Pattern newNameValidationRE; + + protected AbstractRenameRefactor(Pattern newNameValidationRE) { + this.newNameValidationRE = newNameValidationRE; + } + + @Override + public void refactor() { + if (getTarget() == null) { + throw new SpoonException("The target of refactoring is not defined"); + } + if (getNewName() == null) { + throw new SpoonException("The new name of refactoring is not defined"); + } + List issues = getIssues(); + if (issues.isEmpty() == false) { + throw new SpoonException("Refactoring cannot be processed. There are issues: " + issues.toString()); + } + refactorNoCheck(); + } + + protected void refactorNoCheck() { + forEachReference(new CtConsumer() { + @Override + public void accept(CtReference t) { + t.setSimpleName(AbstractRenameRefactor.this.newName); + } + }); + target.setSimpleName(newName); + } + + protected abstract void forEachReference(CtConsumer consumer); + + @Override + public List getIssues() { + List issues = new ArrayList<>(); + detectIssues(issues); + return issues; + } + + protected void detectIssues(List issues) { + checkNewNameIsValid(issues); + detectNameConflicts(issues); + } + + /** + * checks whether {@link #newName} is valid java identifier + * @param issues + */ + protected void checkNewNameIsValid(List issues) { + } + + protected void detectNameConflicts(List issues) { + } + + + protected boolean isJavaIdentifier(String name) { + return javaIdentifierRE.matcher(name).matches(); + } + + public T getTarget() { + return target; + } + + public void setTarget(T target) { + this.target = target; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + if (newNameValidationRE != null && newNameValidationRE.matcher(newName).matches() == false) { + throw new SpoonException("New name \"" + newName + "\" is not valid name"); + } + this.newName = newName; + } +} diff --git a/src/main/java/spoon/refactoring/ChangeLocalVariableName.java b/src/main/java/spoon/refactoring/ChangeLocalVariableName.java new file mode 100644 index 00000000000..47bb5045123 --- /dev/null +++ b/src/main/java/spoon/refactoring/ChangeLocalVariableName.java @@ -0,0 +1,238 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import spoon.SpoonException; +import spoon.reflect.code.CtCatchVariable; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtParameter; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.reference.CtFieldReference; +import spoon.reflect.reference.CtLocalVariableReference; +import spoon.reflect.reference.CtReference; +import spoon.reflect.reference.CtVariableReference; +import spoon.reflect.visitor.Filter; +import spoon.reflect.visitor.chain.CtConsumer; +import spoon.reflect.visitor.chain.CtQueryable; +import spoon.reflect.visitor.chain.CtScannerListener; +import spoon.reflect.visitor.chain.ScanningMode; +import spoon.reflect.visitor.filter.LocalVariableReferenceFunction; +import spoon.reflect.visitor.filter.LocalVariableScopeFunction; +import spoon.reflect.visitor.filter.PotentialVariableDeclarationFunction; +import spoon.reflect.visitor.filter.SiblingsFunction; +import spoon.reflect.visitor.filter.SiblingsFunction.Mode; +import spoon.reflect.visitor.filter.VariableReferenceFunction; + +public class ChangeLocalVariableName extends AbstractRenameRefactor> { + + public static Pattern validVariableNameRE = javaIdentifierRE; + + public ChangeLocalVariableName() { + super(validVariableNameRE); + } + + @Override + protected void forEachReference(CtConsumer consumer) { + getTarget().map(new VariableReferenceFunction()).forEach(consumer); + } + + private static class QueryDriver implements CtScannerListener { + int nrOfNestedLocalClasses = 0; + CtElement ignoredParent; + + @Override + public ScanningMode enter(CtElement element) { + if (ignoredParent != null && element instanceof CtElement) { + CtElement ele = (CtElement) element; + if (ele.hasParent(ignoredParent)) { + return ScanningMode.SKIP_ALL; + } + } + if (element instanceof CtType) { + nrOfNestedLocalClasses++; + } + return ScanningMode.NORMAL; + } + + @Override + public void exit(CtElement element) { + if (ignoredParent == element) { + //we are living scope of ignored parent. We can stop checking it + ignoredParent = null; + } + if (element instanceof CtType) { + nrOfNestedLocalClasses--; + } + } + + public void ignoreChildrenOf(CtElement element) { + if (ignoredParent != null) { + throw new SpoonException("Unexpected state. The ignoredParent is already set"); + } + ignoredParent = element; + } + + public boolean isInContextOfLocalClass() { + return nrOfNestedLocalClasses > 0; + } + } + + @Override + protected void detectNameConflicts(final List issues) { + /* + * There can be these conflicts + * 1) target variable would shadow before declared variable (parameter, localVariable, catchVariable) + * -------------------------------------------------------------------------------------------------- + */ + PotentialVariableDeclarationFunction potentialDeclarationFnc = new PotentialVariableDeclarationFunction(newName); + CtVariable var = getTarget().map(potentialDeclarationFnc).first(); + if (var != null) { + if (var instanceof CtField) { + /* + * we have found a field of same name. + * It is not problem, because variables can hide field declaration. + * Do nothing - OK + */ + } else if (potentialDeclarationFnc.isTypeOnTheWay()) { + /* + * There is a local class declaration between future variable reference and variable declaration `var`. + * The found variable declaration `var` can be hidden by target variable with newName + * as long as there is no reference to `var` in visibility scope of the target variable. + * So search for such `var` reference now + */ + CtVariableReference shadowedVar = target + .map(new SiblingsFunction().includingSelf(true).mode(Mode.NEXT)) + .map(new VariableReferenceFunction(var)).first(); + if (shadowedVar != null) { + //found variable reference, which would be shadowed by variable after rename. + createNameConflictIssue(issues, var, shadowedVar); + } else { + /* + * there is no local variable reference, which would be shadowed by variable after rename. + * OK + */ + } + } else { + /* + * the found variable is in conflict with target variable with newName + */ + createNameConflictIssue(issues, var); + } + } + /* + * 2) target variable is shadowed by later declared variable + * --------------------------------------------------------- + */ + final QueryDriver queryDriver = new QueryDriver(); + getTarget().map(new LocalVariableScopeFunction(queryDriver)).select(new Filter() { + /** + * return true for all CtVariables, which are in conflict + */ + @Override + public boolean matches(CtElement element) { + if (element instanceof CtType) { + CtType localClass = (CtType) element; + //TODO use faster hasField, implemented using map(new AllFieldsFunction()).select(new NameFilter(newName)).first()!=null + Collection> fields = localClass.getAllFields(); + for (CtFieldReference fieldRef : fields) { + if (newName.equals(fieldRef.getSimpleName())) { + /* + * we have found a local class field, which will shadow input local variable if it's reference is in visibility scope of that field. + * Search for target variable reference in visibility scope of this field. + * If found than we cannot rename target variable to newName, because that reference would be shadowed + */ + queryDriver.ignoreChildrenOf(element); + CtLocalVariableReference shadowedVar = element.map(new LocalVariableReferenceFunction(target)).first(); + if (shadowedVar != null) { + createNameConflictIssue(issues, fieldRef.getFieldDeclaration(), shadowedVar); + return true; + } + return false; + } + } + return false; + } + if (element instanceof CtVariable) { + CtVariable variable = (CtVariable) element; + if (newName.equals(variable.getSimpleName()) == false) { + //the variable with different name. Ignore it + return false; + } + //we have found a variable with new name + if (variable instanceof CtField) { + throw new SpoonException("This should not happen. The children of local class which contains a field with new name should be skipped!"); + } + if (variable instanceof CtCatchVariable || variable instanceof CtLocalVariable || variable instanceof CtParameter) { + /* + * we have found a catch variable or local variable or parameter with new name. + */ + if (queryDriver.isInContextOfLocalClass()) { + /* + * We are in context of local class. + * This variable would shadow input local variable after rename + * so we cannot rename if there exist a local variable reference in variable visibility scope. + */ + queryDriver.ignoreChildrenOf(variable.getParent()); + CtQueryable searchScope; + if (variable instanceof CtLocalVariable) { + searchScope = variable.map(new SiblingsFunction().includingSelf(true).mode(Mode.NEXT)); + } else { + searchScope = variable.getParent(); + } + + CtLocalVariableReference shadowedVar = searchScope.map(new LocalVariableReferenceFunction(target)).first(); + if (shadowedVar != null) { + //found local variable reference, which would be shadowed by variable after rename. + createNameConflictIssue(issues, variable, shadowedVar); + return true; + } + //there is no local variable reference, which would be shadowed by variable after rename. + return false; + } else { + /* + * We are not in context of local class. + * So this variable is in conflict. Return it + */ + createNameConflictIssue(issues, variable); + return true; + } + } else { + //CtField should not be there, because the children of local class which contains a field with new name should be skipped! + //Any new variable type??? + throw new SpoonException("Unexpected variable " + variable.getClass().getName()); + } + } + return false; + } + }).first(); + } + + protected void createNameConflictIssue(List issues, CtVariable conflictVar) { + issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " is in conflict.")); + } + protected void createNameConflictIssue(List issues, CtVariable conflictVar, CtVariableReference shadowedVarRef) { + issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " would shadow local variable reference.")); + } + +} diff --git a/src/main/java/spoon/refactoring/Issue.java b/src/main/java/spoon/refactoring/Issue.java new file mode 100644 index 00000000000..011278c9805 --- /dev/null +++ b/src/main/java/spoon/refactoring/Issue.java @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +public interface Issue { + String getMessage(); +} diff --git a/src/main/java/spoon/refactoring/IssueImpl.java b/src/main/java/spoon/refactoring/IssueImpl.java new file mode 100644 index 00000000000..9161fc0575a --- /dev/null +++ b/src/main/java/spoon/refactoring/IssueImpl.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +public class IssueImpl implements Issue { + + private String message; + + public IssueImpl(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String toString() { + return getMessage(); + } +} diff --git a/src/main/java/spoon/refactoring/Refactor.java b/src/main/java/spoon/refactoring/Refactor.java new file mode 100644 index 00000000000..b6ab969d09c --- /dev/null +++ b/src/main/java/spoon/refactoring/Refactor.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +import java.util.List; + +/** + * + */ +public interface Refactor { + List getIssues(); + void refactor(); +} diff --git a/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java b/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java new file mode 100644 index 00000000000..5730c937b94 --- /dev/null +++ b/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java @@ -0,0 +1,244 @@ +package spoon.test.refactoring; + +import static org.junit.Assert.*; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.Before; +import org.junit.Test; + +import spoon.Launcher; +import spoon.OutputType; +import spoon.SpoonException; +import spoon.SpoonModelBuilder; +import spoon.refactoring.ChangeLocalVariableName; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.declaration.CtAnnotation; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtTypeReference; +import spoon.test.refactoring.testclasses.TryRename; +import spoon.test.refactoring.testclasses.VariableRename; +import spoon.testing.utils.ModelUtils; + +public class ChangeVariableNameTest +{ + Launcher launcher; + Factory factory; + CtClass varRenameClass; + CtTypeReference tryRename; + CtLocalVariable local1Var; + + @Before + public void setup() throws Exception { + varRenameClass = (CtClass)ModelUtils.buildClass(VariableRename.class); + launcher = new Launcher(); + launcher.addInputResource("./src/test/java/spoon/test/refactoring/testclasses/VariableRename.java"); + File outputBinDirectory = new File("./target/spooned-refactoring-test"); + if (!outputBinDirectory.exists()) { + outputBinDirectory.mkdirs(); + } + launcher.setBinaryOutputDirectory(outputBinDirectory); + launcher.setSourceOutputDirectory(outputBinDirectory); + launcher.buildModel(); + factory = launcher.getFactory(); + varRenameClass = factory.Class().get(VariableRename.class); + tryRename = factory.createCtTypeReference(TryRename.class); + local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); + } + + @Test + public void testModelConsistency() throws Throwable { + new VariableRename(); + } + + String[] DEBUG = new String[]{/*"nestedClassMethodWithoutRefs", "var3", "var1"*/}; + + @Test + public void testRenameLocalVariableToUsedName() throws Exception { + + varRenameClass.getMethods().forEach(method->{ + //debugging support + if(DEBUG.length==3 && DEBUG[0].equals(method.getSimpleName())==false) return; + method.filterChildren((CtVariable var)->true) + .map((CtVariable var)->var.getAnnotation(tryRename)) + .forEach((CtAnnotation annotation)->{ + String[] newNames = annotation.getActualAnnotation().value(); + CtVariable targetVariable = (CtVariable)annotation.getAnnotatedElement(); + for (String newName : newNames) { + boolean renameShouldPass = newName.startsWith("-")==false; + if (!renameShouldPass) { + newName = newName.substring(1); + } + if (targetVariable instanceof CtLocalVariable) { + //debugging support + if(DEBUG.length==3 && DEBUG[1].equals(targetVariable.getSimpleName()) && DEBUG[2].equals(newName)) { + //put breakpoint here and continue debugging of the buggy case + this.getClass(); + } + checkLocalVariableRename((CtLocalVariable) targetVariable, newName, renameShouldPass); + } else { + //TODO rename of other variables + } + } + }); + }); + } + + protected void checkLocalVariableRename(CtLocalVariable targetVariable, String newName, boolean renameShouldPass) { + + String originName = targetVariable.getSimpleName(); + ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + refactor.setTarget(targetVariable); + refactor.setNewName(newName); + if(renameShouldPass) { + try { + refactor.refactor(); + } catch(SpoonException e) { + throw new AssertionError(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" should NOT fail when trying rename to \""+newName+"\"\n"+targetVariable.toString(), e); + } + assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but the name of variable was not changed", newName, targetVariable.getSimpleName()); + printModelAndTestConsistency(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\""); + } else { + try { + refactor.refactor(); + fail(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" should fail when trying rename to \""+newName+"\""); + } catch(SpoonException e) { + } + assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" failed when trying rename to \""+newName+"\" but the name of variable should not be changed", originName, targetVariable.getSimpleName()); + } + if(renameShouldPass) { + rollback(targetVariable, originName); + } + assertEquals(originName, targetVariable.getSimpleName()); + } + + private void rollback(CtLocalVariable targetVariable, String originName) { + String newName = targetVariable.getSimpleName(); + ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + refactor.setTarget(targetVariable); + //rollback changes + refactor.setNewName(originName); + try { + refactor.refactor(); + } catch(SpoonException e) { + throw new AssertionError(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but rename back to \""+originName+"\" failed", e); + } + } + + private void printModelAndTestConsistency(String refactoringDescription) { +// 1) print modified model, + try { + launcher.getModelBuilder().generateProcessedSourceFiles(OutputType.CLASSES); + } catch (Throwable e) { + new AssertionError("The printing of java sources failed after: "+refactoringDescription, e); + } + +// 2) build it + try { +// launcher.getModelBuilder().compile(SpoonModelBuilder.InputType.FILES); + launcher.getModelBuilder().compile(SpoonModelBuilder.InputType.CTTYPES); + } catch (Throwable e) { + new AssertionError("The compilation of java sources in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e); + } +// 3) create instance using that new model and test consistency + try { +// varRenameClass.newInstance(); + TestClassloader classLoader = new TestClassloader(); + Class testModelClass = classLoader.loadClass(VariableRename.class.getName()); + testModelClass.newInstance(); + } catch (Throwable e) { + throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e); + } + } + + private class TestClassloader extends URLClassLoader { + TestClassloader() throws MalformedURLException { + super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, ChangeVariableNameTest.class.getClassLoader()); + } + + @Override + public Class loadClass(String s) throws ClassNotFoundException { + try { + return findClass(s); + } catch (Exception e) { + return super.loadClass(s); + } + } + } + + + private String getParentMethodName(CtElement ele) { + CtMethod parentMethod = ele.getParent(CtMethod.class); + CtMethod m; + while(parentMethod!=null && (m=parentMethod.getParent(CtMethod.class))!=null) { + parentMethod = m; + } + if(parentMethod!=null) { + return parentMethod.getParent(CtType.class).getSimpleName()+"#"+parentMethod.getSimpleName(); + } else { + return ele.getParent(CtType.class).getSimpleName()+"#annonymous block"; + } + } + + + @Test + public void testRefactorWithoutTarget() throws Exception { + + ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + refactor.setNewName("local1"); + try { + refactor.refactor(); + fail(); + } catch(SpoonException e) { + + } + } + + @Test + public void testRenameLocalVariableToSameName() throws Exception { + + ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + refactor.setTarget(local1Var); + refactor.setNewName("local1"); + refactor.refactor(); + assertEquals("local1", local1Var.getSimpleName()); + } + + @Test + public void testRenameLocalVariableToInvalidName() throws Exception { + + ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + refactor.setTarget(local1Var); + try { + refactor.setNewName(""); + fail(); + } catch(SpoonException e) { + } + + try { + refactor.setNewName("x "); + fail(); + } catch(SpoonException e) { + } + + try { + refactor.setNewName("x y"); + fail(); + } catch(SpoonException e) { + } + + try { + refactor.setNewName("x("); + fail(); + } catch(SpoonException e) { + } + } +} diff --git a/src/test/java/spoon/test/refactoring/testclasses/TryRename.java b/src/test/java/spoon/test/refactoring/testclasses/TryRename.java new file mode 100644 index 00000000000..60f550614ac --- /dev/null +++ b/src/test/java/spoon/test/refactoring/testclasses/TryRename.java @@ -0,0 +1,16 @@ +package spoon.test.refactoring.testclasses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.LOCAL_VARIABLE, ElementType.PARAMETER, ElementType.FIELD}) +public @interface TryRename { + /** + * @return the list of names which should be tried by refactoring. + * If the name starts with prefix "-", then this refactoring should fail on some validation issue + */ + String[] value(); +} diff --git a/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java b/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java new file mode 100644 index 00000000000..25278e13f7e --- /dev/null +++ b/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java @@ -0,0 +1,244 @@ +package spoon.test.refactoring.testclasses; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.junit.Assert.*; + +public class VariableRename +{ + public VariableRename() throws Throwable + { + int local1 = 0; + //call all not private methods of this class automatically, to check assertions + Method[] methods = getClass().getDeclaredMethods(); + for (Method method : methods) { + try { + if(Modifier.isPrivate(method.getModifiers())) { + continue; + } + method.invoke(this); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new RuntimeException("Invocation of method "+method.getName()+" failed", e); + } + } + } + + void callConflictWithParam() { + conflictWithParam(2); + } + + /** + * tests conflict of local variable with parameter + */ + private void conflictWithParam(@TryRename("-var1") int var2) { + @TryRename("-var2") + int var1 = 1; + assertTrue(var1 == 1); + assertTrue(var2 == 2); + } + + /** + * tests conflict of local variable with CtCatchVariable + */ + private void conflictWithCatchVariable() { + @TryRename({"-var2", "-var3"}) + int var1 = 1; + try { + assertTrue(var1 == 1); + @TryRename({"-var1", "var3"}) + int var2 = 2; + assertTrue(var2 == 2); + throw new NumberFormatException(); + } catch (@TryRename({"-var1", "var2"}) NumberFormatException var3) { + assertTrue(var1 == 1); + } + assertTrue(var1 == 1); + } + + /** + * Tests nested class and conflict with field, + * and nested local variable references, which must would be shadowed + */ + void nestedClassMethodWithRefs() { + @TryRename({"-var2", "-var3", "-var4", "-var5", "-var6"}) + int var1 = 1; + new Consumer() { + //must not rename to var1, because it would shadow var1 reference below + @TryRename({"-var1", "-var3", "-var4", "-var5", "-var6"}) + int var2 = 2; + @Override + public void accept( + //must not rename to var1, because reference to var1 below would be shadowed + @TryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 + ) { + //cannot rename to var1, because reference to var1 below would be shadowed + @TryRename({"-var1", "var2", "-var4", "-var5", "-var6"}) + int var3 = 3; + try { + //cannot rename to var1, because reference to var1 below would be shadowed + @TryRename({"-var1", "var2", "-var3", "-var4", "var6"}) + int var5 = 5; + assertTrue(var1 == 1); + assertTrue(var2 == 2); + assertTrue(var3 == 3); + assertTrue(var4 == 4); + assertTrue(var5 == 5); + throw new NumberFormatException(); + } catch ( + //cannot rename to var1, because reference to var1 below would be shadowed + @TryRename({"-var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { + assertTrue(var1 == 1); + assertTrue(var2 == 2); + assertTrue(var3 == 3); + assertTrue(var4 == 4); + } + } + }.accept(4); + assertTrue(var1 == 1); + } + /** + * Tests nested class and conflict with field, + * and no nested local variable references so rename is possible + */ + void nestedClassMethodWithoutRefs() { + @TryRename({"var2", "var3", "var4", "var5", "var6"}) + int var1 = 1; + new Consumer() { + //must not rename to var1, because it would shadow var1 reference below + @TryRename({"-var1", "var3", "var4", "var5", "var6"}) + int var2 = 2; + @Override + public void accept( + //must not rename to var1, because reference to var1 below would be shadowed + @TryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 + ) { + //can rename to var1, because reference to var1 below is not shadowed + @TryRename({"var1", "var2", "-var4", "-var5", "-var6"}) + int var3 = 3; + try { + //can rename to var1, because reference to var1 below is not shadowed + @TryRename({"var1", "var2", "-var3", "-var4", "var6"}) + int var5 = 5; +// assertTrue(var1 == 1);//do not reference it in scope of other vars, so it can be renamed + assertTrue(var2 == 2); + assertTrue(var3 == 3); + assertTrue(var4 == 4); + assertTrue(var5 == 5); + throw new NumberFormatException(); + } catch ( + //can rename to var1, because reference to var1 below is not shadowed + @TryRename({"var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { +// assertTrue(var1 == 1);//do not reference it in scope of other vars, so it can be renamed + assertTrue(var2 == 2); + assertTrue(var3 == 3); + assertTrue(var4 == 4); + } + } + }.accept(4); + assertTrue(var1 == 1); + } + + void nestedClassMethodWithShadowVarWithRefs() { + @TryRename({"-var2", "var3"}) + int var1 = 2; + new Runnable() { + @TryRename({"var1", "var3"}) + int var2 = 3; + @Override + public void run() { + assertTrue(var1 == 2); + @TryRename({"-var1", "var2"}) + int var3 = 1; + //this var1 shadows above defined var1. It can be renamed + @TryRename({"var2", "-var3"}) + int var1 = 4; + assertTrue(var1 == 4); + assertTrue(var2 == 3); + assertTrue(var3 == 1); + } + }.run(); + assertTrue(var1 == 2); + } + void nestedClassMethodWithShadowVarWithoutRefs() { + @TryRename({"var2", "var3"}) + int var1 = 2; + new Runnable() { + @TryRename({"var1", "var3"}) + int var2 = 3; + @Override + public void run() { + @TryRename({"-var1", "var2"}) +// assertTrue(var1 == 2); //the var1 is not referenced so it can be renamed + int var3 = 1; + //this var1 shadows above defined var1. It can be renamed + @TryRename({"var2", "-var3"}) + int var1 = 4; + assertTrue(var1 == 4); + assertTrue(var2 == 3); + assertTrue(var3 == 1); + } + }.run(); + assertTrue(var1 == 2); + } + + void nestedClassMethodWithShadowVarAndField() { + @TryRename({"var2", "var3"}) + int var1 = 2; + new Runnable() { + @TryRename({"var2", "var3"}) + //this var1 shadows above defined var1. + int var1 = 3; + @Override + public void run() { + @TryRename({"-var1", "var2"}) + int var2 = 1; + assertTrue(var1 == 3); + @TryRename({"-var2", "var3"}) + int var1 = 4; + assertTrue(var1 == 4); + assertTrue(this.var1 == 3); + assertTrue(var2 == 1); + } + }.run(); + assertTrue(var1 == 2); + } + + void lambda() { + @TryRename({"-var2", "-var3"}) + int var1 = 1; + assertTrue(var1 == 1); + Function fnc = (@TryRename({"-var1", "-var3"}) Integer var2)->{ + @TryRename({"-var1", "-var2"}) + int var3 = 3; + assertTrue(var1 == 1); + assertTrue(var2 == 2); + assertTrue(var3 == 3); + return var2; + }; + assertTrue(fnc.apply(2) == 2); + } + + void tryCatch() { + @TryRename({"-var2", "-var3", "-var4"}) + int var1 = 1; + assertTrue(var1 == 1); + try { + @TryRename({"-var1","var3","var4"}) + int var2 = 2; + assertTrue(var1 == 1); + assertTrue(var2 == 2); + throw new Exception("ex2"); + } catch (@TryRename({"-var1", "var2", "-var4"}) Exception var3) { + @TryRename({"-var1", "var2", "-var3"}) + int var4 = 4; + assertTrue(var1 == 1); + assertTrue(var4 == 4); + } + } +} From 85135df1aeacc4af33357bba1a70e25b387ca11b Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Sun, 19 Mar 2017 09:45:04 +0100 Subject: [PATCH 2/6] feature: RenameLocalVariableRefactor --- .../refactoring/AbstractRenameRefactor.java | 38 +---- src/main/java/spoon/refactoring/Issue.java | 21 --- .../java/spoon/refactoring/IssueImpl.java | 36 ----- src/main/java/spoon/refactoring/Refactor.java | 14 +- .../refactoring/RefactoringException.java | 23 +++ ....java => RenameLocalVariableRefactor.java} | 62 +++++--- ...a => RenameLocalVariableRefactorTest.java} | 135 ++++++++++-------- ...nameLocalVariableRefactorTestSubject.java} | 84 ++++++----- .../{TryRename.java => TestTryRename.java} | 2 +- 9 files changed, 204 insertions(+), 211 deletions(-) delete mode 100644 src/main/java/spoon/refactoring/Issue.java delete mode 100644 src/main/java/spoon/refactoring/IssueImpl.java create mode 100644 src/main/java/spoon/refactoring/RefactoringException.java rename src/main/java/spoon/refactoring/{ChangeLocalVariableName.java => RenameLocalVariableRefactor.java} (76%) rename src/test/java/spoon/test/refactoring/{ChangeVariableNameTest.java => RenameLocalVariableRefactorTest.java} (60%) rename src/test/java/spoon/test/refactoring/testclasses/{VariableRename.java => RenameLocalVariableRefactorTestSubject.java} (68%) rename src/test/java/spoon/test/refactoring/testclasses/{TryRename.java => TestTryRename.java} (91%) diff --git a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java index a43f238297d..2510906ca6c 100644 --- a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java +++ b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java @@ -16,14 +16,10 @@ */ package spoon.refactoring; -import java.util.ArrayList; -import java.util.List; import java.util.regex.Pattern; import spoon.SpoonException; import spoon.reflect.declaration.CtNamedElement; -import spoon.reflect.reference.CtReference; -import spoon.reflect.visitor.chain.CtConsumer; public abstract class AbstractRenameRefactor implements Refactor { public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); @@ -44,45 +40,25 @@ public void refactor() { if (getNewName() == null) { throw new SpoonException("The new name of refactoring is not defined"); } - List issues = getIssues(); - if (issues.isEmpty() == false) { - throw new SpoonException("Refactoring cannot be processed. There are issues: " + issues.toString()); - } + detectIssues(); refactorNoCheck(); } - protected void refactorNoCheck() { - forEachReference(new CtConsumer() { - @Override - public void accept(CtReference t) { - t.setSimpleName(AbstractRenameRefactor.this.newName); - } - }); - target.setSimpleName(newName); - } - - protected abstract void forEachReference(CtConsumer consumer); - - @Override - public List getIssues() { - List issues = new ArrayList<>(); - detectIssues(issues); - return issues; - } + protected abstract void refactorNoCheck(); - protected void detectIssues(List issues) { - checkNewNameIsValid(issues); - detectNameConflicts(issues); + protected void detectIssues() { + checkNewNameIsValid(); + detectNameConflicts(); } /** * checks whether {@link #newName} is valid java identifier * @param issues */ - protected void checkNewNameIsValid(List issues) { + protected void checkNewNameIsValid() { } - protected void detectNameConflicts(List issues) { + protected void detectNameConflicts() { } diff --git a/src/main/java/spoon/refactoring/Issue.java b/src/main/java/spoon/refactoring/Issue.java deleted file mode 100644 index 011278c9805..00000000000 --- a/src/main/java/spoon/refactoring/Issue.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (C) 2006-2017 INRIA and contributors - * Spoon - http://spoon.gforge.inria.fr/ - * - * This software is governed by the CeCILL-C License under French law and - * abiding by the rules of distribution of free software. You can use, modify - * and/or redistribute the software under the terms of the CeCILL-C license as - * circulated by CEA, CNRS and INRIA at http://www.cecill.info. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-C license and that you accept its terms. - */ -package spoon.refactoring; - -public interface Issue { - String getMessage(); -} diff --git a/src/main/java/spoon/refactoring/IssueImpl.java b/src/main/java/spoon/refactoring/IssueImpl.java deleted file mode 100644 index 9161fc0575a..00000000000 --- a/src/main/java/spoon/refactoring/IssueImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (C) 2006-2017 INRIA and contributors - * Spoon - http://spoon.gforge.inria.fr/ - * - * This software is governed by the CeCILL-C License under French law and - * abiding by the rules of distribution of free software. You can use, modify - * and/or redistribute the software under the terms of the CeCILL-C license as - * circulated by CEA, CNRS and INRIA at http://www.cecill.info. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-C license and that you accept its terms. - */ -package spoon.refactoring; - -public class IssueImpl implements Issue { - - private String message; - - public IssueImpl(String message) { - this.message = message; - } - - @Override - public String getMessage() { - return message; - } - - @Override - public String toString() { - return getMessage(); - } -} diff --git a/src/main/java/spoon/refactoring/Refactor.java b/src/main/java/spoon/refactoring/Refactor.java index b6ab969d09c..2423510b45a 100644 --- a/src/main/java/spoon/refactoring/Refactor.java +++ b/src/main/java/spoon/refactoring/Refactor.java @@ -16,12 +16,18 @@ */ package spoon.refactoring; -import java.util.List; - /** - * + * Defines basic contract of all refactoring implementations.
+ * Usage:
+ *
+ * SomeRefactoring r = new SomeRefactoring();
+ * //configure refactoring by calling setters on `r`
+ * r.refactor();
+ * 
*/ public interface Refactor { - List getIssues(); + /** + * Process refactoring operation + */ void refactor(); } diff --git a/src/main/java/spoon/refactoring/RefactoringException.java b/src/main/java/spoon/refactoring/RefactoringException.java new file mode 100644 index 00000000000..31eb333a2cb --- /dev/null +++ b/src/main/java/spoon/refactoring/RefactoringException.java @@ -0,0 +1,23 @@ +package spoon.refactoring; + +import spoon.SpoonException; + +public class RefactoringException extends SpoonException { + private static final long serialVersionUID = 1L; + + public RefactoringException() { + } + + public RefactoringException(String msg) { + super(msg); + } + + public RefactoringException(Throwable e) { + super(e); + } + + public RefactoringException(String msg, Throwable e) { + super(msg, e); + } + +} diff --git a/src/main/java/spoon/refactoring/ChangeLocalVariableName.java b/src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java similarity index 76% rename from src/main/java/spoon/refactoring/ChangeLocalVariableName.java rename to src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java index 47bb5045123..6926a009efd 100644 --- a/src/main/java/spoon/refactoring/ChangeLocalVariableName.java +++ b/src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java @@ -17,7 +17,6 @@ package spoon.refactoring; import java.util.Collection; -import java.util.List; import java.util.regex.Pattern; import spoon.SpoonException; @@ -44,17 +43,38 @@ import spoon.reflect.visitor.filter.SiblingsFunction.Mode; import spoon.reflect.visitor.filter.VariableReferenceFunction; -public class ChangeLocalVariableName extends AbstractRenameRefactor> { +/** + * Spoon model refactoring function which renames `target` local variable to `newName`
+ * This refactoring will throw {@link RefactoringException} if the model would be not consistent after rename to new name. + * The exception is thrown before the model modificatons are started. + *
+ * CtLocalVariable anLocalVariable = ...
+ * RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor();
+ * refactor.setTarget(anLocalVariable);
+ * refactor.setNewName("someNewName");
+ * try {
+ *   refactor.refactor();
+ * } catch (RefactoringException e) {
+ *   //handle name conflict or name shadowing problem
+ * }
+ * 
+ */ +public class RenameLocalVariableRefactor extends AbstractRenameRefactor> { public static Pattern validVariableNameRE = javaIdentifierRE; - public ChangeLocalVariableName() { + public RenameLocalVariableRefactor() { super(validVariableNameRE); } - @Override - protected void forEachReference(CtConsumer consumer) { - getTarget().map(new VariableReferenceFunction()).forEach(consumer); + protected void refactorNoCheck() { + getTarget().map(new VariableReferenceFunction()).forEach(new CtConsumer() { + @Override + public void accept(CtReference t) { + t.setSimpleName(newName); + } + }); + target.setSimpleName(newName); } private static class QueryDriver implements CtScannerListener { @@ -99,7 +119,7 @@ public boolean isInContextOfLocalClass() { } @Override - protected void detectNameConflicts(final List issues) { + protected void detectNameConflicts() { /* * There can be these conflicts * 1) target variable would shadow before declared variable (parameter, localVariable, catchVariable) @@ -126,7 +146,7 @@ protected void detectNameConflicts(final List issues) { .map(new VariableReferenceFunction(var)).first(); if (shadowedVar != null) { //found variable reference, which would be shadowed by variable after rename. - createNameConflictIssue(issues, var, shadowedVar); + createNameConflictIssue(var, shadowedVar); } else { /* * there is no local variable reference, which would be shadowed by variable after rename. @@ -137,7 +157,7 @@ protected void detectNameConflicts(final List issues) { /* * the found variable is in conflict with target variable with newName */ - createNameConflictIssue(issues, var); + createNameConflictIssue(var); } } /* @@ -165,7 +185,7 @@ public boolean matches(CtElement element) { queryDriver.ignoreChildrenOf(element); CtLocalVariableReference shadowedVar = element.map(new LocalVariableReferenceFunction(target)).first(); if (shadowedVar != null) { - createNameConflictIssue(issues, fieldRef.getFieldDeclaration(), shadowedVar); + createNameConflictIssue(fieldRef.getFieldDeclaration(), shadowedVar); return true; } return false; @@ -204,7 +224,7 @@ public boolean matches(CtElement element) { CtLocalVariableReference shadowedVar = searchScope.map(new LocalVariableReferenceFunction(target)).first(); if (shadowedVar != null) { //found local variable reference, which would be shadowed by variable after rename. - createNameConflictIssue(issues, variable, shadowedVar); + createNameConflictIssue(variable, shadowedVar); return true; } //there is no local variable reference, which would be shadowed by variable after rename. @@ -214,7 +234,7 @@ public boolean matches(CtElement element) { * We are not in context of local class. * So this variable is in conflict. Return it */ - createNameConflictIssue(issues, variable); + createNameConflictIssue(variable); return true; } } else { @@ -228,11 +248,19 @@ public boolean matches(CtElement element) { }).first(); } - protected void createNameConflictIssue(List issues, CtVariable conflictVar) { - issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " is in conflict.")); + /** + * Override this method to get access to details about this refactoring issue + * @param conflictVar - variable which would be in conflict with the `targetVariable` after it's rename to new name + */ + protected void createNameConflictIssue(CtVariable conflictVar) { + throw new RefactoringException(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " is in conflict."); } - protected void createNameConflictIssue(List issues, CtVariable conflictVar, CtVariableReference shadowedVarRef) { - issues.add(new IssueImpl(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " would shadow local variable reference.")); + /** + * Override this method to get access to details about this refactoring issue + * @param conflictVar - variable which would shadow reference to `targetVariable` after it's rename to new name + * @param shadowedVarRef - the reference to `targetVariable`, which would be shadowed by `conflictVar` + */ + protected void createNameConflictIssue(CtVariable conflictVar, CtVariableReference shadowedVarRef) { + throw new RefactoringException(conflictVar.getClass().getSimpleName() + " with name " + conflictVar.getSimpleName() + " would shadow local variable reference."); } - } diff --git a/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java b/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java similarity index 60% rename from src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java rename to src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java index 5730c937b94..f336d7d73e7 100644 --- a/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java +++ b/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java @@ -3,18 +3,18 @@ import static org.junit.Assert.*; import java.io.File; +import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import org.junit.Before; import org.junit.Test; import spoon.Launcher; import spoon.OutputType; import spoon.SpoonException; import spoon.SpoonModelBuilder; -import spoon.refactoring.ChangeLocalVariableName; +import spoon.refactoring.RenameLocalVariableRefactor; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.declaration.CtAnnotation; import spoon.reflect.declaration.CtClass; @@ -22,54 +22,48 @@ import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtVariable; -import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtTypeReference; -import spoon.test.refactoring.testclasses.TryRename; -import spoon.test.refactoring.testclasses.VariableRename; +import spoon.test.refactoring.testclasses.TestTryRename; +import spoon.test.refactoring.testclasses.RenameLocalVariableRefactorTestSubject; import spoon.testing.utils.ModelUtils; -public class ChangeVariableNameTest +public class RenameLocalVariableRefactorTest { - Launcher launcher; - Factory factory; - CtClass varRenameClass; - CtTypeReference tryRename; - CtLocalVariable local1Var; - - @Before - public void setup() throws Exception { - varRenameClass = (CtClass)ModelUtils.buildClass(VariableRename.class); - launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/refactoring/testclasses/VariableRename.java"); - File outputBinDirectory = new File("./target/spooned-refactoring-test"); - if (!outputBinDirectory.exists()) { - outputBinDirectory.mkdirs(); - } - launcher.setBinaryOutputDirectory(outputBinDirectory); - launcher.setSourceOutputDirectory(outputBinDirectory); - launcher.buildModel(); - factory = launcher.getFactory(); - varRenameClass = factory.Class().get(VariableRename.class); - tryRename = factory.createCtTypeReference(TryRename.class); - local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); - } - @Test public void testModelConsistency() throws Throwable { - new VariableRename(); + //contract: check that all assertions in all methods of the RenameLocalVariableRefactorTestSubject are correct + new RenameLocalVariableRefactorTestSubject().checkModelConsistency(); } - String[] DEBUG = new String[]{/*"nestedClassMethodWithoutRefs", "var3", "var1"*/}; + /** + * If you need to debug behavior of refactoring on the exact method and variable in the {@link RenameLocalVariableRefactorTestSubject} model, + * then provide + * 1) name of method of {@link RenameLocalVariableRefactorTestSubject} + * 2) original name variable in the method + * 3) new name of variable in the method + * then put breakpoint on the line `this.getClass();` below and the debugger stops just before + * the to be inspected refactoring starts + */ + private String[] DEBUG = new String[]{/*"nestedClassMethodWithoutRefs", "var3", "var1"*/}; + /** + * The {@link RenameLocalVariableRefactorTestSubject} class is loaded as spoon model. Then: + * - It looks for each CtVariable and it's CtAnnotation and tries to rename that variable to the name defined by annotation. + * - If the annotation name is prefixed with "-", then that refactoring should fail. + * - If the annotation name is not prefixed, then that refactoring should pass. + * If it behaves different then expected, then this test fails + */ @Test - public void testRenameLocalVariableToUsedName() throws Exception { + public void testRenameAllLocalVariablesOfRenameTestSubject() throws Exception { + CtClass varRenameClass = (CtClass)ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtTypeReference tryRename = varRenameClass.getFactory().createCtTypeReference(TestTryRename.class); varRenameClass.getMethods().forEach(method->{ //debugging support if(DEBUG.length==3 && DEBUG[0].equals(method.getSimpleName())==false) return; method.filterChildren((CtVariable var)->true) .map((CtVariable var)->var.getAnnotation(tryRename)) - .forEach((CtAnnotation annotation)->{ + .forEach((CtAnnotation annotation)->{ String[] newNames = annotation.getActualAnnotation().value(); CtVariable targetVariable = (CtVariable)annotation.getAnnotatedElement(); for (String newName : newNames) { @@ -85,7 +79,7 @@ public void testRenameLocalVariableToUsedName() throws Exception { } checkLocalVariableRename((CtLocalVariable) targetVariable, newName, renameShouldPass); } else { - //TODO rename of other variables + //TODO test rename of other variables, e.g. parameters and catch... later } } }); @@ -95,7 +89,7 @@ public void testRenameLocalVariableToUsedName() throws Exception { protected void checkLocalVariableRename(CtLocalVariable targetVariable, String newName, boolean renameShouldPass) { String originName = targetVariable.getSimpleName(); - ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); refactor.setTarget(targetVariable); refactor.setNewName(newName); if(renameShouldPass) { @@ -122,7 +116,7 @@ protected void checkLocalVariableRename(CtLocalVariable targetVariable, Strin private void rollback(CtLocalVariable targetVariable, String originName) { String newName = targetVariable.getSimpleName(); - ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); refactor.setTarget(targetVariable); //rollback changes refactor.setNewName(originName); @@ -134,6 +128,14 @@ private void rollback(CtLocalVariable targetVariable, String originName) { } private void printModelAndTestConsistency(String refactoringDescription) { + Launcher launcher = new Launcher(); + File outputBinDirectory = new File("./target/spooned-refactoring-test"); + if (!outputBinDirectory.exists()) { + outputBinDirectory.mkdirs(); + } + launcher.setBinaryOutputDirectory(outputBinDirectory); + launcher.setSourceOutputDirectory(outputBinDirectory); + // 1) print modified model, try { launcher.getModelBuilder().generateProcessedSourceFiles(OutputType.CLASSES); @@ -151,17 +153,19 @@ private void printModelAndTestConsistency(String refactoringDescription) { // 3) create instance using that new model and test consistency try { // varRenameClass.newInstance(); - TestClassloader classLoader = new TestClassloader(); - Class testModelClass = classLoader.loadClass(VariableRename.class.getName()); - testModelClass.newInstance(); + TestClassloader classLoader = new TestClassloader(launcher); + Class testModelClass = classLoader.loadClass(RenameLocalVariableRefactorTestSubject.class.getName()); + testModelClass.getMethod("checkModelConsistency").invoke(testModelClass.newInstance()); + } catch (InvocationTargetException e) { + throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e.getTargetException()); } catch (Throwable e) { throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e); } } private class TestClassloader extends URLClassLoader { - TestClassloader() throws MalformedURLException { - super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, ChangeVariableNameTest.class.getClassLoader()); + TestClassloader(Launcher launcher) throws MalformedURLException { + super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, RenameLocalVariableRefactorTest.class.getClassLoader()); } @Override @@ -190,55 +194,62 @@ private String getParentMethodName(CtElement ele) { @Test - public void testRefactorWithoutTarget() throws Exception { + public void testRefactorWrongUsage() throws Exception { + CtType varRenameClass = ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtLocalVariable local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); - ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + //contract: a target variable is not defined. Throw SpoonException + RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); refactor.setNewName("local1"); try { refactor.refactor(); fail(); } catch(SpoonException e) { - + //should fail - OK } - } - - @Test - public void testRenameLocalVariableToSameName() throws Exception { - - ChangeLocalVariableName refactor = new ChangeLocalVariableName(); - refactor.setTarget(local1Var); - refactor.setNewName("local1"); - refactor.refactor(); - assertEquals("local1", local1Var.getSimpleName()); - } - - @Test - public void testRenameLocalVariableToInvalidName() throws Exception { - - ChangeLocalVariableName refactor = new ChangeLocalVariableName(); + //contract: invalid rename request to empty string. Throw SpoonException refactor.setTarget(local1Var); try { refactor.setNewName(""); fail(); } catch(SpoonException e) { + //should fail - OK } + //contract: invalid rename request to variable name which contains space. Throw SpoonException try { refactor.setNewName("x "); fail(); } catch(SpoonException e) { + //should fail - OK } + //contract: invalid rename request to variable name which contains space. Throw SpoonException try { refactor.setNewName("x y"); fail(); } catch(SpoonException e) { + //should fail - OK } + //contract: invalid rename request to variable name which contains character which is not allowed in variable name. Throw SpoonException try { refactor.setNewName("x("); fail(); } catch(SpoonException e) { + //should fail - OK } - } + } + + @Test + public void testRenameLocalVariableToSameName() throws Exception { + CtType varRenameClass = ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtLocalVariable local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); + + RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); + refactor.setTarget(local1Var); + refactor.setNewName("local1"); + refactor.refactor(); + assertEquals("local1", local1Var.getSimpleName()); + } } diff --git a/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java b/src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java similarity index 68% rename from src/test/java/spoon/test/refactoring/testclasses/VariableRename.java rename to src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java index 25278e13f7e..1750568ce6e 100644 --- a/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java +++ b/src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java @@ -8,14 +8,20 @@ import static org.junit.Assert.*; -public class VariableRename +public class RenameLocalVariableRefactorTestSubject { - public VariableRename() throws Throwable + public RenameLocalVariableRefactorTestSubject() { int local1 = 0; - //call all not private methods of this class automatically, to check assertions + } + + public void checkModelConsistency() throws Throwable { + //call all not private methods of this class automatically, to check assertions, which are there Method[] methods = getClass().getDeclaredMethods(); for (Method method : methods) { + if("checkModelConsistency".equals(method.getName())) { + continue; + } try { if(Modifier.isPrivate(method.getModifiers())) { continue; @@ -36,8 +42,8 @@ void callConflictWithParam() { /** * tests conflict of local variable with parameter */ - private void conflictWithParam(@TryRename("-var1") int var2) { - @TryRename("-var2") + private void conflictWithParam(@TestTryRename("-var1") int var2) { + @TestTryRename("-var2") int var1 = 1; assertTrue(var1 == 1); assertTrue(var2 == 2); @@ -47,15 +53,15 @@ private void conflictWithParam(@TryRename("-var1") int var2) { * tests conflict of local variable with CtCatchVariable */ private void conflictWithCatchVariable() { - @TryRename({"-var2", "-var3"}) + @TestTryRename({"-var2", "-var3"}) int var1 = 1; try { assertTrue(var1 == 1); - @TryRename({"-var1", "var3"}) + @TestTryRename({"-var1", "var3"}) int var2 = 2; assertTrue(var2 == 2); throw new NumberFormatException(); - } catch (@TryRename({"-var1", "var2"}) NumberFormatException var3) { + } catch (@TestTryRename({"-var1", "var2"}) NumberFormatException var3) { assertTrue(var1 == 1); } assertTrue(var1 == 1); @@ -66,23 +72,23 @@ private void conflictWithCatchVariable() { * and nested local variable references, which must would be shadowed */ void nestedClassMethodWithRefs() { - @TryRename({"-var2", "-var3", "-var4", "-var5", "-var6"}) + @TestTryRename({"-var2", "-var3", "-var4", "-var5", "-var6"}) int var1 = 1; new Consumer() { //must not rename to var1, because it would shadow var1 reference below - @TryRename({"-var1", "-var3", "-var4", "-var5", "-var6"}) + @TestTryRename({"-var1", "-var3", "-var4", "-var5", "-var6"}) int var2 = 2; @Override public void accept( //must not rename to var1, because reference to var1 below would be shadowed - @TryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 + @TestTryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 ) { //cannot rename to var1, because reference to var1 below would be shadowed - @TryRename({"-var1", "var2", "-var4", "-var5", "-var6"}) + @TestTryRename({"-var1", "var2", "-var4", "-var5", "-var6"}) int var3 = 3; try { //cannot rename to var1, because reference to var1 below would be shadowed - @TryRename({"-var1", "var2", "-var3", "-var4", "var6"}) + @TestTryRename({"-var1", "var2", "-var3", "-var4", "var6"}) int var5 = 5; assertTrue(var1 == 1); assertTrue(var2 == 2); @@ -92,7 +98,7 @@ public void accept( throw new NumberFormatException(); } catch ( //cannot rename to var1, because reference to var1 below would be shadowed - @TryRename({"-var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { + @TestTryRename({"-var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { assertTrue(var1 == 1); assertTrue(var2 == 2); assertTrue(var3 == 3); @@ -107,23 +113,23 @@ public void accept( * and no nested local variable references so rename is possible */ void nestedClassMethodWithoutRefs() { - @TryRename({"var2", "var3", "var4", "var5", "var6"}) + @TestTryRename({"var2", "var3", "var4", "var5", "var6"}) int var1 = 1; new Consumer() { //must not rename to var1, because it would shadow var1 reference below - @TryRename({"-var1", "var3", "var4", "var5", "var6"}) + @TestTryRename({"-var1", "var3", "var4", "var5", "var6"}) int var2 = 2; @Override public void accept( //must not rename to var1, because reference to var1 below would be shadowed - @TryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 + @TestTryRename({"-var1", "var2", "-var3", "-var5", "-var6"}) Integer var4 ) { //can rename to var1, because reference to var1 below is not shadowed - @TryRename({"var1", "var2", "-var4", "-var5", "-var6"}) + @TestTryRename({"var1", "var2", "-var4", "-var5", "-var6"}) int var3 = 3; try { //can rename to var1, because reference to var1 below is not shadowed - @TryRename({"var1", "var2", "-var3", "-var4", "var6"}) + @TestTryRename({"var1", "var2", "-var3", "-var4", "var6"}) int var5 = 5; // assertTrue(var1 == 1);//do not reference it in scope of other vars, so it can be renamed assertTrue(var2 == 2); @@ -133,7 +139,7 @@ public void accept( throw new NumberFormatException(); } catch ( //can rename to var1, because reference to var1 below is not shadowed - @TryRename({"var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { + @TestTryRename({"var1", "var2", "-var3", "-var4", "var5"}) NumberFormatException var6) { // assertTrue(var1 == 1);//do not reference it in scope of other vars, so it can be renamed assertTrue(var2 == 2); assertTrue(var3 == 3); @@ -145,18 +151,18 @@ public void accept( } void nestedClassMethodWithShadowVarWithRefs() { - @TryRename({"-var2", "var3"}) + @TestTryRename({"-var2", "var3"}) int var1 = 2; new Runnable() { - @TryRename({"var1", "var3"}) + @TestTryRename({"var1", "var3"}) int var2 = 3; @Override public void run() { assertTrue(var1 == 2); - @TryRename({"-var1", "var2"}) + @TestTryRename({"-var1", "var2"}) int var3 = 1; //this var1 shadows above defined var1. It can be renamed - @TryRename({"var2", "-var3"}) + @TestTryRename({"var2", "-var3"}) int var1 = 4; assertTrue(var1 == 4); assertTrue(var2 == 3); @@ -166,18 +172,18 @@ public void run() { assertTrue(var1 == 2); } void nestedClassMethodWithShadowVarWithoutRefs() { - @TryRename({"var2", "var3"}) + @TestTryRename({"var2", "var3"}) int var1 = 2; new Runnable() { - @TryRename({"var1", "var3"}) + @TestTryRename({"var1", "var3"}) int var2 = 3; @Override public void run() { - @TryRename({"-var1", "var2"}) + @TestTryRename({"-var1", "var2"}) // assertTrue(var1 == 2); //the var1 is not referenced so it can be renamed int var3 = 1; //this var1 shadows above defined var1. It can be renamed - @TryRename({"var2", "-var3"}) + @TestTryRename({"var2", "-var3"}) int var1 = 4; assertTrue(var1 == 4); assertTrue(var2 == 3); @@ -188,18 +194,18 @@ public void run() { } void nestedClassMethodWithShadowVarAndField() { - @TryRename({"var2", "var3"}) + @TestTryRename({"var2", "var3"}) int var1 = 2; new Runnable() { - @TryRename({"var2", "var3"}) + @TestTryRename({"var2", "var3"}) //this var1 shadows above defined var1. int var1 = 3; @Override public void run() { - @TryRename({"-var1", "var2"}) + @TestTryRename({"-var1", "var2"}) int var2 = 1; assertTrue(var1 == 3); - @TryRename({"-var2", "var3"}) + @TestTryRename({"-var2", "var3"}) int var1 = 4; assertTrue(var1 == 4); assertTrue(this.var1 == 3); @@ -210,11 +216,11 @@ public void run() { } void lambda() { - @TryRename({"-var2", "-var3"}) + @TestTryRename({"-var2", "-var3"}) int var1 = 1; assertTrue(var1 == 1); - Function fnc = (@TryRename({"-var1", "-var3"}) Integer var2)->{ - @TryRename({"-var1", "-var2"}) + Function fnc = (@TestTryRename({"-var1", "-var3"}) Integer var2)->{ + @TestTryRename({"-var1", "-var2"}) int var3 = 3; assertTrue(var1 == 1); assertTrue(var2 == 2); @@ -225,17 +231,17 @@ void lambda() { } void tryCatch() { - @TryRename({"-var2", "-var3", "-var4"}) + @TestTryRename({"-var2", "-var3", "-var4"}) int var1 = 1; assertTrue(var1 == 1); try { - @TryRename({"-var1","var3","var4"}) + @TestTryRename({"-var1","var3","var4"}) int var2 = 2; assertTrue(var1 == 1); assertTrue(var2 == 2); throw new Exception("ex2"); - } catch (@TryRename({"-var1", "var2", "-var4"}) Exception var3) { - @TryRename({"-var1", "var2", "-var3"}) + } catch (@TestTryRename({"-var1", "var2", "-var4"}) Exception var3) { + @TestTryRename({"-var1", "var2", "-var3"}) int var4 = 4; assertTrue(var1 == 1); assertTrue(var4 == 4); diff --git a/src/test/java/spoon/test/refactoring/testclasses/TryRename.java b/src/test/java/spoon/test/refactoring/testclasses/TestTryRename.java similarity index 91% rename from src/test/java/spoon/test/refactoring/testclasses/TryRename.java rename to src/test/java/spoon/test/refactoring/testclasses/TestTryRename.java index 60f550614ac..ba1bd732d6c 100644 --- a/src/test/java/spoon/test/refactoring/testclasses/TryRename.java +++ b/src/test/java/spoon/test/refactoring/testclasses/TestTryRename.java @@ -7,7 +7,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.LOCAL_VARIABLE, ElementType.PARAMETER, ElementType.FIELD}) -public @interface TryRename { +public @interface TestTryRename { /** * @return the list of names which should be tried by refactoring. * If the name starts with prefix "-", then this refactoring should fail on some validation issue From 89b613a8aa51b668353472ca3de224e990672277 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Sun, 19 Mar 2017 17:51:03 +0100 Subject: [PATCH 3/6] added license header and comment to the new RefactoringException --- .../refactoring/RefactoringException.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/spoon/refactoring/RefactoringException.java b/src/main/java/spoon/refactoring/RefactoringException.java index 31eb333a2cb..ddeef9e58a4 100644 --- a/src/main/java/spoon/refactoring/RefactoringException.java +++ b/src/main/java/spoon/refactoring/RefactoringException.java @@ -1,7 +1,27 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ + package spoon.refactoring; import spoon.SpoonException; +/** + * Thrown when required refactoring would cause model inconsistency + */ public class RefactoringException extends SpoonException { private static final long serialVersionUID = 1L; @@ -19,5 +39,4 @@ public RefactoringException(Throwable e) { public RefactoringException(String msg, Throwable e) { super(msg, e); } - } From d31543a85ae65507dc97a124d87eb2c9be8827c3 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Tue, 21 Mar 2017 19:13:49 +0100 Subject: [PATCH 4/6] new interface CtRenameRefactoring, class and method renaming --- .../refactoring/AbstractRenameRefactor.java | 8 ++-- .../{Refactor.java => CtRefactoring.java} | 4 +- .../refactoring/CtRenameRefactoring.java | 42 +++++++++++++++++++ .../java/spoon/refactoring/Refactoring.java | 14 +++++++ .../RenameLocalVariableRefactorTest.java | 4 +- 5 files changed, 66 insertions(+), 6 deletions(-) rename src/main/java/spoon/refactoring/{Refactor.java => CtRefactoring.java} (84%) create mode 100644 src/main/java/spoon/refactoring/CtRenameRefactoring.java diff --git a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java index 2510906ca6c..de3715fffbb 100644 --- a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java +++ b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java @@ -21,7 +21,7 @@ import spoon.SpoonException; import spoon.reflect.declaration.CtNamedElement; -public abstract class AbstractRenameRefactor implements Refactor { +public abstract class AbstractRenameRefactor implements CtRenameRefactoring { public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); protected T target; @@ -70,18 +70,20 @@ public T getTarget() { return target; } - public void setTarget(T target) { + public AbstractRenameRefactor setTarget(T target) { this.target = target; + return this; } public String getNewName() { return newName; } - public void setNewName(String newName) { + public AbstractRenameRefactor setNewName(String newName) { if (newNameValidationRE != null && newNameValidationRE.matcher(newName).matches() == false) { throw new SpoonException("New name \"" + newName + "\" is not valid name"); } this.newName = newName; + return this; } } diff --git a/src/main/java/spoon/refactoring/Refactor.java b/src/main/java/spoon/refactoring/CtRefactoring.java similarity index 84% rename from src/main/java/spoon/refactoring/Refactor.java rename to src/main/java/spoon/refactoring/CtRefactoring.java index 2423510b45a..31dfdc97dc4 100644 --- a/src/main/java/spoon/refactoring/Refactor.java +++ b/src/main/java/spoon/refactoring/CtRefactoring.java @@ -18,14 +18,16 @@ /** * Defines basic contract of all refactoring implementations.
+ * Contract: to process a required refactoring.
* Usage:
*
  * SomeRefactoring r = new SomeRefactoring();
  * //configure refactoring by calling setters on `r`
  * r.refactor();
  * 
+ * See child interfaces, which implements other supported refactoring methods */ -public interface Refactor { +public interface CtRefactoring { /** * Process refactoring operation */ diff --git a/src/main/java/spoon/refactoring/CtRenameRefactoring.java b/src/main/java/spoon/refactoring/CtRenameRefactoring.java new file mode 100644 index 00000000000..e84198847a8 --- /dev/null +++ b/src/main/java/spoon/refactoring/CtRenameRefactoring.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.refactoring; + +import spoon.reflect.declaration.CtNamedElement; + +/** + * The kind of refactoring, which renames a `target` element + * to the `newName`
+ * Usage:
+ *
+ * CtVariable someVariable = ...
+ * new SomeRenameRefactoring().setTarget(someVariable).setNewName("mutchBetterName").refactor();
+ * 
+ */ +public interface CtRenameRefactoring extends CtRefactoring { + /** + * @param target the model element, which has to be refactored. + * @return this to support fluent API + */ + CtRenameRefactoring setTarget(T target); + + /** + * @param newName the required name of the `target` model element + * @return this to support fluent API + */ + CtRenameRefactoring setNewName(String newName); +} diff --git a/src/main/java/spoon/refactoring/Refactoring.java b/src/main/java/spoon/refactoring/Refactoring.java index fce0c5fd249..aa003208b95 100644 --- a/src/main/java/spoon/refactoring/Refactoring.java +++ b/src/main/java/spoon/refactoring/Refactoring.java @@ -16,6 +16,7 @@ */ package spoon.refactoring; +import spoon.reflect.code.CtLocalVariable; import spoon.reflect.declaration.CtType; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.Query; @@ -52,4 +53,17 @@ public boolean matches(CtTypeReference reference) { reference.setSimpleName(name); } } + + /** + * Changes name of a {@link CtLocalVariable}. + * + * @param localVariable + * to be renamed {@link CtLocalVariable} in the AST. + * @param name + * New name of the element. + * @throws RefactoringException when rename to newName would cause model inconsistency, like ambiguity, shadowing of other variables, etc. + */ + public static void changeLocalVariableName(CtLocalVariable localVariable, String name) throws RefactoringException { + new RenameLocalVariableRefactor().setTarget(localVariable).setNewName(name).refactor(); + } } diff --git a/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java b/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java index f336d7d73e7..9644fc37963 100644 --- a/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java +++ b/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java @@ -99,7 +99,7 @@ protected void checkLocalVariableRename(CtLocalVariable targetVariable, Strin throw new AssertionError(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" should NOT fail when trying rename to \""+newName+"\"\n"+targetVariable.toString(), e); } assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but the name of variable was not changed", newName, targetVariable.getSimpleName()); - printModelAndTestConsistency(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\""); + assertCorrectModel(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\""); } else { try { refactor.refactor(); @@ -127,7 +127,7 @@ private void rollback(CtLocalVariable targetVariable, String originName) { } } - private void printModelAndTestConsistency(String refactoringDescription) { + private void assertCorrectModel(String refactoringDescription) { Launcher launcher = new Launcher(); File outputBinDirectory = new File("./target/spooned-refactoring-test"); if (!outputBinDirectory.exists()) { From 9888ff44671942fc4f76c57478d9c9e2c3596741 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Tue, 21 Mar 2017 20:19:52 +0100 Subject: [PATCH 5/6] improve javadoc --- .../refactoring/AbstractRenameRefactor.java | 22 ++++++++++++++++--- .../refactoring/CtRenameRefactoring.java | 8 +++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java index de3715fffbb..c916f4e2348 100644 --- a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java +++ b/src/main/java/spoon/refactoring/AbstractRenameRefactor.java @@ -21,6 +21,11 @@ import spoon.SpoonException; import spoon.reflect.declaration.CtNamedElement; +/** + * abstract implementation of rename element refactoring + * + * @param the type of target renamed element + */ public abstract class AbstractRenameRefactor implements CtRenameRefactoring { public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); @@ -52,33 +57,44 @@ protected void detectIssues() { } /** - * checks whether {@link #newName} is valid java identifier - * @param issues + * client may implement this method to check whether {@link #newName} is valid */ protected void checkNewNameIsValid() { } + /** + * client may implement this method to check whether {@link #newName} + * is in conflict with names of other model elements + */ protected void detectNameConflicts() { } - + /** + * Helper method, which can be used by the child classes to check if name is an java identifier + * @param name the to be checked name + * @return true if name is valid java identifier + */ protected boolean isJavaIdentifier(String name) { return javaIdentifierRE.matcher(name).matches(); } + @Override public T getTarget() { return target; } + @Override public AbstractRenameRefactor setTarget(T target) { this.target = target; return this; } + @Override public String getNewName() { return newName; } + @Override public AbstractRenameRefactor setNewName(String newName) { if (newNameValidationRE != null && newNameValidationRE.matcher(newName).matches() == false) { throw new SpoonException("New name \"" + newName + "\" is not valid name"); diff --git a/src/main/java/spoon/refactoring/CtRenameRefactoring.java b/src/main/java/spoon/refactoring/CtRenameRefactoring.java index e84198847a8..e68ebdbc291 100644 --- a/src/main/java/spoon/refactoring/CtRenameRefactoring.java +++ b/src/main/java/spoon/refactoring/CtRenameRefactoring.java @@ -28,12 +28,20 @@ * */ public interface CtRenameRefactoring extends CtRefactoring { + /** + * @return target model element, which has to be refactored. + */ + T getTarget(); /** * @param target the model element, which has to be refactored. * @return this to support fluent API */ CtRenameRefactoring setTarget(T target); + /** + * @return the required name of the `target` model element + */ + String getNewName(); /** * @param newName the required name of the `target` model element * @return this to support fluent API From 32587f36bdced84ad6e6b0b9b5700c6fe0f7670b Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Wed, 22 Mar 2017 18:28:05 +0100 Subject: [PATCH 6/6] rename RenameLocalVariableRefactor*->CtRenameLocalVariableRefactoring* --- ...or.java => AbstractRenameRefactoring.java} | 8 ++--- ... => CtRenameLocalVariableRefactoring.java} | 4 +-- .../java/spoon/refactoring/Refactoring.java | 6 ++-- ...CtRenameLocalVariableRefactoringTest.java} | 32 +++++++++---------- ...eLocalVariableRefactoringTestSubject.java} | 4 +-- 5 files changed, 27 insertions(+), 27 deletions(-) rename src/main/java/spoon/refactoring/{AbstractRenameRefactor.java => AbstractRenameRefactoring.java} (87%) rename src/main/java/spoon/refactoring/{RenameLocalVariableRefactor.java => CtRenameLocalVariableRefactoring.java} (96%) rename src/test/java/spoon/test/refactoring/{RenameLocalVariableRefactorTest.java => CtRenameLocalVariableRefactoringTest.java} (85%) rename src/test/java/spoon/test/refactoring/testclasses/{RenameLocalVariableRefactorTestSubject.java => CtRenameLocalVariableRefactoringTestSubject.java} (95%) diff --git a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java b/src/main/java/spoon/refactoring/AbstractRenameRefactoring.java similarity index 87% rename from src/main/java/spoon/refactoring/AbstractRenameRefactor.java rename to src/main/java/spoon/refactoring/AbstractRenameRefactoring.java index c916f4e2348..d0231265174 100644 --- a/src/main/java/spoon/refactoring/AbstractRenameRefactor.java +++ b/src/main/java/spoon/refactoring/AbstractRenameRefactoring.java @@ -26,14 +26,14 @@ * * @param the type of target renamed element */ -public abstract class AbstractRenameRefactor implements CtRenameRefactoring { +public abstract class AbstractRenameRefactoring implements CtRenameRefactoring { public static final Pattern javaIdentifierRE = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); protected T target; protected String newName; protected Pattern newNameValidationRE; - protected AbstractRenameRefactor(Pattern newNameValidationRE) { + protected AbstractRenameRefactoring(Pattern newNameValidationRE) { this.newNameValidationRE = newNameValidationRE; } @@ -84,7 +84,7 @@ public T getTarget() { } @Override - public AbstractRenameRefactor setTarget(T target) { + public AbstractRenameRefactoring setTarget(T target) { this.target = target; return this; } @@ -95,7 +95,7 @@ public String getNewName() { } @Override - public AbstractRenameRefactor setNewName(String newName) { + public AbstractRenameRefactoring setNewName(String newName) { if (newNameValidationRE != null && newNameValidationRE.matcher(newName).matches() == false) { throw new SpoonException("New name \"" + newName + "\" is not valid name"); } diff --git a/src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java b/src/main/java/spoon/refactoring/CtRenameLocalVariableRefactoring.java similarity index 96% rename from src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java rename to src/main/java/spoon/refactoring/CtRenameLocalVariableRefactoring.java index 6926a009efd..2e211ebcfe1 100644 --- a/src/main/java/spoon/refactoring/RenameLocalVariableRefactor.java +++ b/src/main/java/spoon/refactoring/CtRenameLocalVariableRefactoring.java @@ -59,11 +59,11 @@ * } * */ -public class RenameLocalVariableRefactor extends AbstractRenameRefactor> { +public class CtRenameLocalVariableRefactoring extends AbstractRenameRefactoring> { public static Pattern validVariableNameRE = javaIdentifierRE; - public RenameLocalVariableRefactor() { + public CtRenameLocalVariableRefactoring() { super(validVariableNameRE); } diff --git a/src/main/java/spoon/refactoring/Refactoring.java b/src/main/java/spoon/refactoring/Refactoring.java index aa003208b95..bc000fdab47 100644 --- a/src/main/java/spoon/refactoring/Refactoring.java +++ b/src/main/java/spoon/refactoring/Refactoring.java @@ -59,11 +59,11 @@ public boolean matches(CtTypeReference reference) { * * @param localVariable * to be renamed {@link CtLocalVariable} in the AST. - * @param name + * @param newName * New name of the element. * @throws RefactoringException when rename to newName would cause model inconsistency, like ambiguity, shadowing of other variables, etc. */ - public static void changeLocalVariableName(CtLocalVariable localVariable, String name) throws RefactoringException { - new RenameLocalVariableRefactor().setTarget(localVariable).setNewName(name).refactor(); + public static void changeLocalVariableName(CtLocalVariable localVariable, String newName) throws RefactoringException { + new CtRenameLocalVariableRefactoring().setTarget(localVariable).setNewName(newName).refactor(); } } diff --git a/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java b/src/test/java/spoon/test/refactoring/CtRenameLocalVariableRefactoringTest.java similarity index 85% rename from src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java rename to src/test/java/spoon/test/refactoring/CtRenameLocalVariableRefactoringTest.java index 9644fc37963..a9a150f2173 100644 --- a/src/test/java/spoon/test/refactoring/RenameLocalVariableRefactorTest.java +++ b/src/test/java/spoon/test/refactoring/CtRenameLocalVariableRefactoringTest.java @@ -14,7 +14,7 @@ import spoon.OutputType; import spoon.SpoonException; import spoon.SpoonModelBuilder; -import spoon.refactoring.RenameLocalVariableRefactor; +import spoon.refactoring.CtRenameLocalVariableRefactoring; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.declaration.CtAnnotation; import spoon.reflect.declaration.CtClass; @@ -24,21 +24,21 @@ import spoon.reflect.declaration.CtVariable; import spoon.reflect.reference.CtTypeReference; import spoon.test.refactoring.testclasses.TestTryRename; -import spoon.test.refactoring.testclasses.RenameLocalVariableRefactorTestSubject; +import spoon.test.refactoring.testclasses.CtRenameLocalVariableRefactoringTestSubject; import spoon.testing.utils.ModelUtils; -public class RenameLocalVariableRefactorTest +public class CtRenameLocalVariableRefactoringTest { @Test public void testModelConsistency() throws Throwable { //contract: check that all assertions in all methods of the RenameLocalVariableRefactorTestSubject are correct - new RenameLocalVariableRefactorTestSubject().checkModelConsistency(); + new CtRenameLocalVariableRefactoringTestSubject().checkModelConsistency(); } /** - * If you need to debug behavior of refactoring on the exact method and variable in the {@link RenameLocalVariableRefactorTestSubject} model, + * If you need to debug behavior of refactoring on the exact method and variable in the {@link CtRenameLocalVariableRefactoringTestSubject} model, * then provide - * 1) name of method of {@link RenameLocalVariableRefactorTestSubject} + * 1) name of method of {@link CtRenameLocalVariableRefactoringTestSubject} * 2) original name variable in the method * 3) new name of variable in the method * then put breakpoint on the line `this.getClass();` below and the debugger stops just before @@ -47,7 +47,7 @@ public void testModelConsistency() throws Throwable { private String[] DEBUG = new String[]{/*"nestedClassMethodWithoutRefs", "var3", "var1"*/}; /** - * The {@link RenameLocalVariableRefactorTestSubject} class is loaded as spoon model. Then: + * The {@link CtRenameLocalVariableRefactoringTestSubject} class is loaded as spoon model. Then: * - It looks for each CtVariable and it's CtAnnotation and tries to rename that variable to the name defined by annotation. * - If the annotation name is prefixed with "-", then that refactoring should fail. * - If the annotation name is not prefixed, then that refactoring should pass. @@ -55,7 +55,7 @@ public void testModelConsistency() throws Throwable { */ @Test public void testRenameAllLocalVariablesOfRenameTestSubject() throws Exception { - CtClass varRenameClass = (CtClass)ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtClass varRenameClass = (CtClass)ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class); CtTypeReference tryRename = varRenameClass.getFactory().createCtTypeReference(TestTryRename.class); varRenameClass.getMethods().forEach(method->{ @@ -89,7 +89,7 @@ public void testRenameAllLocalVariablesOfRenameTestSubject() throws Exception { protected void checkLocalVariableRename(CtLocalVariable targetVariable, String newName, boolean renameShouldPass) { String originName = targetVariable.getSimpleName(); - RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); + CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring(); refactor.setTarget(targetVariable); refactor.setNewName(newName); if(renameShouldPass) { @@ -116,7 +116,7 @@ protected void checkLocalVariableRename(CtLocalVariable targetVariable, Strin private void rollback(CtLocalVariable targetVariable, String originName) { String newName = targetVariable.getSimpleName(); - RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); + CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring(); refactor.setTarget(targetVariable); //rollback changes refactor.setNewName(originName); @@ -154,7 +154,7 @@ private void assertCorrectModel(String refactoringDescription) { try { // varRenameClass.newInstance(); TestClassloader classLoader = new TestClassloader(launcher); - Class testModelClass = classLoader.loadClass(RenameLocalVariableRefactorTestSubject.class.getName()); + Class testModelClass = classLoader.loadClass(CtRenameLocalVariableRefactoringTestSubject.class.getName()); testModelClass.getMethod("checkModelConsistency").invoke(testModelClass.newInstance()); } catch (InvocationTargetException e) { throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e.getTargetException()); @@ -165,7 +165,7 @@ private void assertCorrectModel(String refactoringDescription) { private class TestClassloader extends URLClassLoader { TestClassloader(Launcher launcher) throws MalformedURLException { - super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, RenameLocalVariableRefactorTest.class.getClassLoader()); + super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, CtRenameLocalVariableRefactoringTest.class.getClassLoader()); } @Override @@ -195,11 +195,11 @@ private String getParentMethodName(CtElement ele) { @Test public void testRefactorWrongUsage() throws Exception { - CtType varRenameClass = ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtType varRenameClass = ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class); CtLocalVariable local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); //contract: a target variable is not defined. Throw SpoonException - RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); + CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring(); refactor.setNewName("local1"); try { refactor.refactor(); @@ -243,10 +243,10 @@ public void testRefactorWrongUsage() throws Exception { @Test public void testRenameLocalVariableToSameName() throws Exception { - CtType varRenameClass = ModelUtils.buildClass(RenameLocalVariableRefactorTestSubject.class); + CtType varRenameClass = ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class); CtLocalVariable local1Var = varRenameClass.filterChildren((CtLocalVariable var)->var.getSimpleName().equals("local1")).first(); - RenameLocalVariableRefactor refactor = new RenameLocalVariableRefactor(); + CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring(); refactor.setTarget(local1Var); refactor.setNewName("local1"); refactor.refactor(); diff --git a/src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java b/src/test/java/spoon/test/refactoring/testclasses/CtRenameLocalVariableRefactoringTestSubject.java similarity index 95% rename from src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java rename to src/test/java/spoon/test/refactoring/testclasses/CtRenameLocalVariableRefactoringTestSubject.java index 1750568ce6e..de67ecc36f2 100644 --- a/src/test/java/spoon/test/refactoring/testclasses/RenameLocalVariableRefactorTestSubject.java +++ b/src/test/java/spoon/test/refactoring/testclasses/CtRenameLocalVariableRefactoringTestSubject.java @@ -8,9 +8,9 @@ import static org.junit.Assert.*; -public class RenameLocalVariableRefactorTestSubject +public class CtRenameLocalVariableRefactoringTestSubject { - public RenameLocalVariableRefactorTestSubject() + public CtRenameLocalVariableRefactoringTestSubject() { int local1 = 0; }