-
-
Notifications
You must be signed in to change notification settings - Fork 352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
review 1: Change local variable name refactoring #1005
review 1: Change local variable name refactoring #1005
Conversation
} | ||
}); | ||
} | ||
|
||
public static void changeVariableName(CtVariable<?> variable, String name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadoc
that's a very useful contribution
an exception. we also need strong tests. |
d8f30aa
to
02a8082
Compare
I like the idea of usage of complex thirdparty library (like guava), with good jUnit tests, as a test environment. The test would look like this:
I came with this idea, hoping that I can avoid writing small jUnit test for each refactoring feature. But now I see these small tests are useful when detecting where is the problem in the code. So the best will be combination of small tests at beginning and at the end that thirparty library refactoring test, which founds the most tricky things. |
So the best will be combination of small tests at beginning and at the
end that thirparty library refactoring test, which founds the most
tricky things.
I agree. For the complex ones, we run them through `build.sh`.
|
The refactoring request validity checks are missing. These checks can be quite tricky. For example rename of local variable should check
Do you think that we should continue with refactoring functions, which are implemented as static methods of one class? I have feeling like it would be better to have one class for each refactoring. The API might be like this: interface CtRefactoring {
boolean isValid();
void refactor() throws SpoonInvalidRequest;
} And sometime there are probably some refactorings which cannot be done without temporary inconsistency in the model, so then we should be able to switch OFF the validation check. I have implemented nothing yet, so I welcome your suggestions. It is easy to accept any design if there is no code yet. ;-) |
Do you think that we should continue with refactoring functions, which are implemented as static
methods of one class?
Spoon's design has always been very much OO, So I really support the introduction of OO for refactoring.
|
02a8082
to
aa5badd
Compare
It is POC of Refactoring class, which does rename of local variable. CtLocalVariable target = ...
ChangeLocalVariableName rename = new ChangeLocalVariableName();
rename.setTarget(target);
rename.setNewName("someName");
if(rename.getIssues().size()>0) {
... handle conflicts, etc...
}
rename.refactor(); I will add fluent API or a constructor of course. But please give me feedback mainly to
Please ignore other classes, which comes from other PRs or are obsolete ... I will clean it after your feedback about API |
aa5badd
to
54c734b
Compare
Hi Pavel, This is very interesting but this already contains a lot of different things to be discussed, they may be split in several PRs (hence in several separate discussions, one after the other). According to your long-term plan, what do you prefer to discuss and merge first? maybe |
Hi Martin, I am aware that there is a lot of things, many baby steps, some of them may be in wrong direction... I develop it this way as a prototype to see what code/structures I need to implement refactoring algorithms in short, efficient and maintainable way. And note, I do not need VariableScopeQuery or VariableReferenceQuery. I need refactorings, which are based on these queries. So I started with refactorings and as a side effect there are VariableScopeQuery and VariableReferenceQuery.
It is good topic for discussion, because it is one of the core components of that refactorings. So yes, let's discuss it first. I am just not sure if it makes sense to make a PR only for that, because VariableScopeQuery is not much usable on it's own. The better is combination with VariableReferenceQuery. These two are tightly coupled. If you agree then we can move discussion to #1114, which contains only these two queries. |
OK |
54c734b
to
f5e67a3
Compare
20db5d7
to
9857e30
Compare
During implementation of local variable rename refactoring, class
I will prepare another PR for that soon. Do you have any preference/ideas how to do it? You can influence the solution before it is implemented - so less effort is needed. If you want to see context where I need it, then have a look at |
Sounds interesting.
Do you have any preference/ideas how to do it?
not really. I look forward to your solution for this.
|
9857e30
to
0c7d49d
Compare
I am testing ChangeLocalVariableName refactoring method using ChangeVariableNameTest. OK, there are still some todos, but I stuck on one thing. The testing algorithm is like this:
The problem is that step 7) does not work. I have no idea why class loader loads some other implementation of the class ... void nestedClassMethodWithShadowVar() {
@TryRename({"var2", "var3"})
int var1 = 2; //<--------- the "var1" is renamed to "var2"
new Runnable() {
@TryRename({"var1", "var3"})
int var2 = 3;
@Override
public void run() {
@TryRename({"-var1", "var2"})
int var3 = 1;
assertTrue(var1 == 2); //<-----------this reference is renamed too
//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);
} by mistake passes (and it should not pass), and pretty printer produces this valid java code: void nestedClassMethodWithShadowVar() {
@spoon.test.refactoring.testclasses.TryRename(value = { "var2" , "var3" })
int var2 = 2; //<!----------- is renamed to "var2" - OK
new java.lang.Runnable() {
@spoon.test.refactoring.testclasses.TryRename(value = { "var1" , "var3" })
int var2 = 3;
@java.lang.Override
public void run() {
@spoon.test.refactoring.testclasses.TryRename(value = { "-var1" , "var2" })
int var3 = 1;
org.junit.Assert.assertTrue((var2 == 2)); //<-------- is renamed to var2,
//but the value should be 3, because it is no more local variable reference, but it is now the reference to the class field,
//which shadows origin local variable! So it should fail here!!!
@spoon.test.refactoring.testclasses.TryRename(value = { "var2" , "-var3" })
int var1 = 4;
org.junit.Assert.assertTrue((var1 == 4));
org.junit.Assert.assertTrue(((var2) == 3));
org.junit.Assert.assertTrue((var3 == 1));
}
}.run();
org.junit.Assert.assertTrue((var2 == 2));
} but execution of that code does not fail on AssertionError, but it should fail :-( Any idea why my test class loader does not load classes compiled from refactored code? |
I could be wrong (I did not read all your changes). Change: SpoonModelBuilder.InputType.FILES to SpoonModelBuilder.InputType.CTTYPES |
@tdurieux thanks! It really compiled origin not refactored sources. Now the test fails as expected :-) This PR is still not finished. There are several bugs ... working on it. |
Revapi Analysis resultsOld API: fr.inria.gforge.spoon:spoon-core:jar:5.6.0-20170315.164827-83 New API: fr.inria.gforge.spoon:spoon-core:jar:5.6.0-SNAPSHOT Detected changes: 8. Change 1
Change 1
Change 2
Change 2
Change 2
Change 3
Change 3
Change 3
Change 4
Change 4
Change 5
Change 5
Change 5
Change 6
Change 7
Change 8
|
rebase to have a cleaner diff? |
7516392
to
69b5996
Compare
Here you are :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this brand-new feature, which looks promising.
I've tried to understand the design and usage from the tests and the interface documentation.
Here is a first set of comments.
|
||
@Test | ||
public void testModelConsistency() throws Throwable { | ||
new VariableRename(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this have side effects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has no side effects, but it throws an exception if the tested model is inconsistent. It is similar to VariableReferencesModelTest
, where I first tested model consistency by call of constructor, which internally called all other methods of model to check if they contain consistent assertions. Then you came with good idea to switch it to jUnit test class too. So VariableRename
is very similar to that, but in this case I cannot easily switch it to jUnit test class, because I am testing model consistency before and after each model refactoring test action.
|
||
@Test | ||
public void testRefactorWithoutTarget() throws Exception { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contract: a name refactoring where the given variable name does not exist results in a SpoonException
} | ||
|
||
@Test | ||
public void testRenameLocalVariableToInvalidName() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge all exception tests (incl testRefactorWithoutTarget) in one single test
|
||
import static org.junit.Assert.*; | ||
|
||
public class VariableRename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better name? such as RenameTestSubject
?
public VariableRename() throws Throwable | ||
{ | ||
int local1 = 0; | ||
//call all not private methods of this class automatically, to check assertions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting. would be event better in a separate clear method.
new RenameTestSubject().callAllMethods()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
public void testRenameLocalVariableToSameName() throws Exception { | ||
|
||
ChangeLocalVariableName refactor = new ChangeLocalVariableName(); | ||
refactor.setTarget(local1Var); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
local1var
: what is this?
tests that are self-contained and that can be understood by just looking at the method (without scrolling) are easier to understand an maintain in the long term.
|
||
@Test | ||
public void testRenameLocalVariableToUsedName() throws Exception { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the contract tested here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is the main test method (...bad named...), which uses spoon model of VariableRename
class. 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.
*/ | ||
package spoon.refactoring; | ||
|
||
public interface Issue { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to RefactoringIssue
? If it's an issue, what about using a RefactoringException
directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean to throw exception directly instead of collecting of issues and then throw an exception?
I would like to support use case where list of issues is better then one exception on the first issue.
The idea is like this:
- create an refactoring
- call Refactoring#getIssues()
- if there are some issues, then solve them automatically, by applying other refactorings
- call Refactoring#getIssues() again
- if there are still some issues then skip this refactoring
- if there are no issues, then process refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it is no problem to remove issues for now and add them later with some mature inheritance hierarchy of issues. So KISS wins here.
/** | ||
* | ||
*/ | ||
public interface Refactor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interface and methods must be documented. Can you give a usage snippet? I don't really see here what can be done.
target.setSimpleName(newName); | ||
} | ||
|
||
protected abstract void forEachReference(CtConsumer<CtReference> consumer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadoc? contract for the implementors?
Your ides/change suggestions were implemented. Please review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a second set of comments.
I'm really impressed by the test :-)
* r.refactor(); | ||
* </pre> | ||
*/ | ||
public interface Refactor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to Refactoring?
add method setTarget(CtElement)
to really show in the interface that one always refactors something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to Refactoring
but there is already class spoon.refactoring.Refactoring
, so ?
add method setTarget(CtElement)
Does all possible refactorings accepts exactly one CtElement? Does all possible refactoring have input called target
? I am not sure. I vote to let setTarget
method defined on level of more specific refactoring class
import spoon.SpoonException; | ||
import spoon.reflect.declaration.CtNamedElement; | ||
|
||
public abstract class AbstractRenameRefactor<T extends CtNamedElement> implements Refactor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently we have only one subclass. what other subclasses do you envision?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RenameTypeRefactor
, RenameParameterRefactor
, RenameFieldRefactor
, RenameMethodRefactor
, ...
} | ||
} | ||
|
||
private void printModelAndTestConsistency(String refactoringDescription) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is an assert-like method, it should be named accordingly assert*
, eg assertCorrectModel
* If it behaves different then expected, then this test fails | ||
*/ | ||
@Test | ||
public void testRenameAllLocalVariablesOfRenameTestSubject() throws Exception { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test is super powerful, it's beautiful.
However, I would suggest to add before a simpler test, which does one simple refactoring, it would show a small and clear snippet on how to use RenameLocalVariableRefactor
, and it would help to understand this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by the way, it's in essence a parametrized test, where RenameLocalVariableRefactorTestSubject contains the test parameters. Really clever. It would be even more explicit and maintainable written as @Parametrized
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have experience with parametrized test? Me not. I can learn it, but if you would have time to rewrite this test to parametrized, I might focus on resolving of type parameters. ;-)
* } | ||
* </pre> | ||
*/ | ||
public class RenameLocalVariableRefactor extends AbstractRenameRefactor<CtLocalVariable<?>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a static method in Refactoring (eg Refactoring#renameLocalVariable(CtNamedElement target, String newName)
) to help discoverability and usage of this class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea. I will do so tomorrow evening.
but there is already class |spoon.refactoring.Refactoring|, so ?
right. Then "CtRefactoring"?
|
Does all possible refactorings accepts exactly one CtElement?
correct, setTarget(CtElement...) may be a better option.
I vote to let |setTarget| method defined on level of more specific refactoring class
The problem with that is that the interface cannot be understood without knowing concrete
implementations, which is an issue
|
Do you have experience with parametrized test? Me not. I can learn it, but if you would have time
to rewrite this test to parametrized
No problem, we keep it like this. I wanted to point to an interesting thing...
|
I would prefer to keep CtRefactor as it is. I can imagine refactoring methods, which has no
But I understand that Spoon is interface based, so I have added new interface, where setTarget fits better. What about this one? public interface CtRenameRefactoring<T extends CtNamedElement> extends CtRefactoring {
CtRenameRefactoring<T> setTarget(T target);
CtRenameRefactoring<T> setNewName(String newName);
}
I have added public static void changeLocalVariableName(CtLocalVariable<?> localVariable, String name) whose name and parameters fits more to the existing method public static void changeTypeName(final CtType<?> type, String name) What about rename of |
Revapi Analysis resultsOld API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-20170321.120037-10 New API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-SNAPSHOT Detected changes: 1. Change 1
|
Revapi Analysis resultsOld API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-20170321.120037-10 New API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-SNAPSHOT Detected changes: 1. Change 1
|
* @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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beautiful line
I have added
public static void changeLocalVariableName(CtLocalVariable<?> localVariable, String name)
thanks, rename parameter name to newName?
I guess the only think which can be "understood" here is that "client can run refactoring after
s/he configures it by refactoring specific methods"
I would prefer to keep CtRefactoring as it is.
I'm not convinced, I prefer strong and consistent interfaces, but it's no reason to argue about this
for now :-)
What about rename of |RenameLocalVariableRefactor| to
A) |CtRenameLocalVariableRefactor|
B) |CtRenameLocalVariableRefactoring|
|Good idea, ||CtRenameLocalVariableRefactoring is perfect. ||Let's move |AbstractRenameRefactor
<https://github.com/INRIA/spoon/pull/1005/files#diff-4ae10ecea7772cfbf5caccd21ff3b831>to
AbstractRenameRefactoring also
|
Revapi Analysis resultsOld API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-20170321.234528-11 New API: fr.inria.gforge.spoon:spoon-core:jar:5.7.0-SNAPSHOT Detected changes: 1. Change 1
|
thanks! |
You are welcome ;-) |
This code is not tested and may be even wrong somewhere. I will work on it, because I will need such refactoring in my project.
Just have a short look at class
Refactoring
, please. Whether you want such methods in Spoon or it is out of it's scope.What about checking of validity of such refactoring requests? For example if I have
then the class B will be twice in the model. Do you have some preferences how to report such invalid requests?
A) to throw exception
B) to call some listener and silently skip the operation
C) ?
I am just sure that in all cases the validity should be checked before the model is modified.