From 20db5d7ee4ba911347e11a3fd2ffb5e6ebd5bba5 Mon Sep 17 00:00:00 2001
From: Pavel Vojtechovsky
Date: Tue, 7 Feb 2017 19:24:32 +0100
Subject: [PATCH] feature: ChangeLocalVariableName + tests
---
.../refactoring/AbstractRenameRefactor.java | 111 ++++++++
.../refactoring/ChangeLocalVariableName.java | 222 ++++++++++++++++
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 | 173 +++++++++++++
.../refactoring/testclasses/TryRename.java | 16 ++
.../testclasses/VariableRename.java | 236 ++++++++++++++++++
8 files changed, 842 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..a36e51f1ed3
--- /dev/null
+++ b/src/main/java/spoon/refactoring/ChangeLocalVariableName.java
@@ -0,0 +1,222 @@
+/**
+ * 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.CtCatch;
+import spoon.reflect.code.CtCatchVariable;
+import spoon.reflect.code.CtLambda;
+import spoon.reflect.code.CtLocalVariable;
+import spoon.reflect.declaration.CtElement;
+import spoon.reflect.declaration.CtExecutable;
+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.CtReference;
+import spoon.reflect.visitor.Filter;
+import spoon.reflect.visitor.chain.CtConsumer;
+import spoon.reflect.visitor.filter.VariableReferencePossibleDeclarationFunction;
+import spoon.reflect.visitor.filter.LocalVariableScopeFunction;
+import spoon.reflect.visitor.filter.ParentFunction;
+import spoon.reflect.visitor.filter.VariableReferenceFunction;
+import spoon.reflect.visitor.filter.VariableScopeFunction;
+
+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);
+ }
+
+ @Override
+ protected void detectNameConflicts(List issues) {
+ //There can these conflicts
+ //1) target variable would shadow before declared variable (parameter, localVariable, catchVariable)
+ //in this case we do not return fields, because it is allowed to shadow them - the fields can use "this." prefix to keep their reference valid
+ CtVariable> conflictVar = getTarget().map(new VariableReferencePossibleDeclarationFunction().includingFields(false))
+ .select(new Filter>() {
+ @Override
+ public boolean matches(CtVariable> var) {
+ return newName.equals(var.getSimpleName());
+ }
+ }).first();
+ if (conflictVar != null) {
+ Issue issue = createNameConflictIssue(conflictVar);
+ if (issue != null) {
+ issues.add(issue);
+ }
+ }
+
+ //2) target variable is shadowed by later declared variable
+ //skip evaluation of children of local classes, their nested variables can shadow this variable, so they are not in conflict
+ conflictVar = getTarget().map(new LocalVariableScopeFunction()).select(new Filter() {
+ @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 shadows input local variable.
+ * So there cannot be conflict with input local variable in nested nodes of localClass.
+ * Skip scanning of children, or at least ignore all conflicts in children of localClass
+ */
+ //TODO!! Skip scanning of children of this element
+ throw new RuntimeException("TODO");
+ }
+ }
+ } else 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!");
+ } else if (variable instanceof CtParameter) {
+ /*
+ * we have found a parameter with new name. It already shadows input local variable,
+ * so there cannot be conflict with input local variable in nested nodes of parent executable.
+ */
+ //TODO!! Skip scanning of next children of element.getParent()
+ throw new RuntimeException("TODO");
+ } else if (variable instanceof CtCatchVariable || variable instanceof CtLocalVariable) {
+ /*
+ * we have found a catch variable or local variable with new name.
+ */
+ if (isInContextOfLocalClass) {
+ /*
+ * We are in context of local class.
+ * This variable already shadows input local variable,
+ * so there cannot be conflict with input local variable in nested nodes of parent executable.
+ */
+ //TODO!! Skip scanning of next children of element.getParent()
+ throw new RuntimeException("TODO");
+ } else {
+ /*
+ * We are not in context of local class.
+ * So this is conflict. Return it
+ */
+ 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();
+ if (conflictVar != null) {
+ Issue issue = createNameConflictIssue(conflictVar);
+ if (issue != null) {
+ issues.add(issue);
+ }
+ }
+ //search for parent element, which represents scope of variables, which might be in conflict
+ //it means first Method, Constructor or AnonymousExecutable. The Lambda is not scope for variables.
+ CtExecutable> l_scope = getTarget().map(new ParentFunction()).select(new Filter>() {
+ @Override
+ public boolean matches(CtExecutable> element) {
+ if (element instanceof CtLambda) {
+ return false;
+ }
+ return true;
+ }
+ }).first();
+
+ //1) search for all variable declarations whose name is equal to newName and which contains target variable in their visibility scope
+ l_scope.filterChildren(new Filter>() {
+ @Override
+ public boolean matches(CtVariable> var) {
+ if (newName.equals(var.getSimpleName())) {
+ /*
+ * We have found a CtVariable whose simple name is equal to newName of target variable.
+ * Check if these variables are in conflict by
+ * visiting all elements in scope of input variable
+ * and match them if they are target variable.
+ * In other words, search for declaration of target variable in scope of input variable.
+ * If it is found, then there is conflict
+ */
+ return isDeclaredInScopeOfSource(var, target);
+ }
+ return false;
+ }
+ })
+ //called for each variable declaration which is in conflict with newName
+ .forEach(new CtConsumer>() {
+ @Override
+ public void accept(CtVariable> conflictVar) {
+ Issue issue = createNameConflictIssue(conflictVar);
+ if (issue != null) {
+ issues.add(issue);
+ }
+ }
+ });
+
+ //2) search for all variable declarations whose name is equal to newName and which are in visibility scope of target variable
+ target.map(new VariableScopeFunction()).select(new Filter>() {
+ @Override
+ public boolean matches(CtVariable> var) {
+ return newName.equals(var.getSimpleName());
+ }
+ }).forEach(new CtConsumer>() {
+ @Override
+ public void accept(CtVariable> conflictVar) {
+ Issue issue = createNameConflictIssue(conflictVar);
+ if (issue != null) {
+ issues.add(issue);
+ }
+ }
+ });
+ }
+
+ /**
+ * Detects whether target variable is declared in scope of source variable
+ *
+ * @param source - source variable declaration
+ * @param target - target variable declaration
+ * @return true if target variable is declared in visibility scope of source variable
+ */
+ protected boolean isDeclaredInScopeOfSource(CtVariable> source, CtVariable> target) {
+ return source.map(new VariableScopeFunction()).select(new Filter() {
+ public boolean matches(CtElement element) {
+ return element == target;
+ };
+ }).first() != null;
+ }
+
+ protected Issue createNameConflictIssue(CtVariable> conflictVar) {
+ return new IssueImpl(conflictVar.getSimpleName() + " with name " + conflictVar.getSimpleName() + " already exists.");
+ }
+}
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..e4a0f6b6615
--- /dev/null
+++ b/src/test/java/spoon/test/refactoring/ChangeVariableNameTest.java
@@ -0,0 +1,173 @@
+package spoon.test.refactoring;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import spoon.SpoonException;
+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
+{
+ Factory factory;
+ CtClass> varRenameClass;
+ CtTypeReference tryRename;
+ CtLocalVariable> local1Var;
+
+ @Before
+ public void setup() throws Exception {
+ varRenameClass = (CtClass>)ModelUtils.buildClass(VariableRename.class);
+ factory = varRenameClass.getFactory();
+ tryRename = factory.createCtTypeReference(TryRename.class);
+ local1Var = varRenameClass.filterChildren((CtLocalVariable> var)->true).first();
+ }
+
+ @Test
+ public void testModelConsistency() throws Exception {
+ new VariableRename();
+ }
+
+ @Test
+ public void testRenameLocalVariableToUsedName() throws Exception {
+
+ varRenameClass.getMethods().forEach(method->{
+ 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>) {
+ 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+"\"", e);
+ }
+ assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but the name of variable was not changed", newName, targetVariable.getSimpleName());
+ } 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());
+ }
+ printModelAndTestConsistency();
+ if(renameShouldPass) {
+ //rollback changes
+ refactor.setNewName(originName);
+ refactor.refactor();
+ }
+ assertEquals(originName, targetVariable.getSimpleName());
+ }
+
+ private void printModelAndTestConsistency() {
+ /*
+ * TODO
+ * 1) print modified model,
+ * 2) build it
+ * 3) create instance using that new model
+ * 4) test consistency of the class
+ */
+ }
+
+ private String getParentMethodName(CtElement ele) {
+ return ele.getParent(CtType.class).getSimpleName()+"#"+ele.getParent(CtMethod.class).getSimpleName();
+ }
+
+
+ @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();
+ try {
+ refactor.setNewName("");
+ fail();
+ } catch(SpoonException e) {
+ }
+ assertEquals("local1", local1Var.getSimpleName());
+
+ 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) {
+ }
+ }
+
+ @Test
+ public void testRenameLocalVariableToValidName() throws Exception {
+ ChangeLocalVariableName refactor = new ChangeLocalVariableName();
+ refactor.setTarget(local1Var);
+ refactor.setNewName("local3");
+ refactor.refactor();
+ assertEquals("local3", local1Var.getSimpleName());
+ }
+}
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..8f418d22600
--- /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 whose rename should pass
+ */
+ 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..296c43e5dd9
--- /dev/null
+++ b/src/test/java/spoon/test/refactoring/testclasses/VariableRename.java
@@ -0,0 +1,236 @@
+package spoon.test.refactoring.testclasses;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.junit.Assert.*;
+
+public class VariableRename
+{
+ public VariableRename()
+ {
+ //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 (IllegalAccessException | IllegalArgumentException | InvocationTargetException 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 conflictWithParam() {
+ @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 which must not be shadowed
+ */
+ void nestedClassMethod() {
+ @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);
+ }
+
+ void nestedClassMethodWithShadowVar() {
+ @TryRename({"var2", "var3"})
+ int var1 = 2;
+ new Runnable() {
+ @TryRename({"var1", "var3"})
+ int var2 = 3;
+ @Override
+ public void run() {
+ @TryRename({"-var1", "var2"})
+ int var3 = 1;
+ assertTrue(var1 == 2);
+ //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);
+ }
+ }
+
+ /*
+ private String[] method(String param1) {
+ List values = new ArrayList<>();
+ new Runnable() {
+ @Override
+ public void run() {
+ String local9 = "l9";
+ values.add(local9);
+ Function fnc = (String local7)->{
+ String local8 = "l8";
+ try {
+ throw new Exception("ex2");
+ } catch (Exception local6) {
+ values.add(member1);
+ values.add(static1);
+ values.add(param1);
+ {
+ String local0 = "l0";
+ values.add(local0);
+ }
+ String local1 = "l1";
+ values.add(local1);
+ String local2 = "l2";
+ values.add(local2);
+ for(int local3 = 0; local3<1; local3++)
+ {
+ values.add("l3_"+String.valueOf(local3));
+ String local4 = "l4";
+ values.add(local4);
+ }
+ values.add(method1());
+ values.add(staticMethod1());
+ try {
+ throw new Exception("ex1");
+ } catch (Exception local5) {
+ values.add(local5.getMessage());
+ }
+ values.add(local6.getMessage());
+ }
+ values.add(local7);
+ return local8;
+ };
+ values.add(fnc.apply("l7"));
+ }
+ }.run();
+ return values.toArray(new String[0]);
+ }
+
+ private String method1() {
+ return "met1";
+ }
+ private static String staticMethod1() {
+ return "statMet1";
+ }
+ */
+}