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: refactor: mutable collections of Spoon model handles parent and fire change events #1917
Conversation
78befc9
to
e5a3370
Compare
return list.lastIndexOf(o); | ||
} | ||
|
||
private void ensureModifiableStatementsList() { |
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 method should be changed to be generic
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.
why? It is private and it uses generic argument from ListModel 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.
Actually no you the name says it's for statements and you have this: list == CtElementImpl.<CtStatement>emptyList()
It should use a T
and have a name like ensureModifiableList
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.
Now I see it ;-), Thanks. I will fix it probably today in the evening
|
||
public void accept(CtVisitor visitor) { | ||
visitor.visitCtBlock(this); | ||
} | ||
|
||
@Override | ||
public List<CtStatement> getStatements() { | ||
ensureModifiableStatementsList(); | ||
return this.statements; |
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.
Why does this method return a mutable list? It seems on purpose regarding the previous call of ensureModifiableStatementsList
but it feels wrong...
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 returns a mutable ModelList, which can be mutated using List API and which assures that spoon model is kept consistent (parent-child is kept) and that modification events are sent.
This change is the CORE idea of this PR ;-)
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 change is the CORE idea of this PR ;-)
Hmmm. It looks a bit dangerous for me: old users won't change their API and won't beneficiate the changes, but it might disturb new users because they won't necessarily expect the API to return a pointer on the model.
WDYT about keeping a getStatements
that returns an immutable list, and adding a getModifiableStatements
like the one you proposed?
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.
Note: that this method already returned modifiable list before. I just assured that this modifiable list behaves correctly.
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.
that this method already returned modifiable list before
My point is that I believe we should have the same consistent behaviour everywhere in Spoon. And I think that almost everywhere - but maybe not here - this kind of method is currently returning a new ArrayList, detached from the model (see for example CtClass#getConstructors or CtType#getDeclaredExecutables): so they are mutable, but not linked to the model. If we make those methods returning a mutable element which is now linked to the model, it really can bring unexpected behaviours for the clients.
That's why I suggest to start here to fix a definitive behaviour that we'll use everywhere. And I'll definitively prefer to change those methods to return an immutable object and to create another methods which return explicitely a mutable linked to the model: we might break some clients, but the fix is easy and explicit.
@@ -163,79 +158,35 @@ private boolean shouldInsertAfterSuper() { | |||
@Override | |||
public <T extends CtStatementList> T setStatements(List<CtStatement> statements) { | |||
if (statements == null || statements.isEmpty()) { | |||
this.statements = CtElementImpl.emptyList(); | |||
this.statements.clear(); |
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.
We already call clear
below in the method: this could now be simlified by just adding a if not null around the final addAll
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.
you are right
There's some minor issues but it looks much more elegant than before ;) |
I understand the problem and agree that spoon should behave consistent. So there are two questions:
I vote for B), because it ends with lower maintenance, nicer API and good compatibility with old clients. Note: there are actually these implementations in spoon model: |
Maybe a good way to prepare this is to setup a test architecture to check what we want:
And to take into account the problem C an easy way is to release it as major release and document why. @monperrus won't be happy but it looks like the easiest way to avoid maintaining methods with mutable and immutable. |
Probably none of them if it's not the empty list. |
And I think that almost everywhere - but maybe not here - this kind of method is currently
returning a new ArrayList, detached from the model (see for example CtClass#getConstructors or
CtType#getDeclaredExecutables)
IMHO it's the opposite. It's mostly bound to the model but for a few specific cases such as the ones
you mention.
Anyway, we all agree that this should be 1) consistent and 2) specified and checked in a test.
Let's write the test to measure the quantity of immutable / mutable-detached / mutable-attached
collection non-derived getters.
|
https://github.com/INRIA/spoon/blob/0b45cf90bd380b8cdfc8dd2c2d29cf3f74a445d7/src/main/java/spoon/support/reflect/declaration/CtTypeImpl.java#L589
This is a derived getter. This is one point of confusion, here we are talking about non-derived
getters. (updated my previous comment accordingly)
|
Actually that's interesting: if all getters become mutable, how users will do the distinction between derived ones and the others, attached to the model? Without reading the doc I mean. CtType myType = // a way to get my type
// change nothing
myType.getAllFields().add(myNewField);
myType.getAllMethods().add(myNewMethod);
// change the model and fire events
myType.getAllTypeMembers().add(myNewField); |
interesting question... I vote to make immutable these methods (derived) whose mutation has no influence to the model. So it fails soon and client is immediately notified that it is not the correct way how to modify the model |
if all getters become mutable
almost all non-derived getters are already mutable :-) (my personal bet!)
the derived ones should be immutable, and we won't break anything serious if we do so.
|
The tests are actually failing on this old test code: CtBlock<?> body = ...
for (CtStatement s : body) {
body.removeStatement(s);
} the body contains two statements, but it processes only one of them, because concurrent modification is actually not check correctly in A) it should throw concurrent modification exception - standard java behavior if A), then we break compatibility of Spoon API I actually vote for A). WDYT? |
I have created a test (see #1922) which checks collection types. Here is the detailed results A summary:
Test actually fails on
|
I have fixed remaining non-derived mutable attached/detached collections of spoon model. There were
So now there should be not possible to create a model with children, which has unitialized parents. |
return list.lastIndexOf(o); | ||
} | ||
|
||
private static class InternalList<T> extends ArrayList<T> { |
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.
Could you document that this internalList purpose is to manage the ConcurrentModificationException through modCount, maybe linking the URL: https://docs.oracle.com/javase/7/docs/api/java/util/AbstractList.html#modCount
It will ease the future maintaining ;)
And more generally I'm not sure why you update this value only when calling clear
method: shouldn't you manage also the add/remove cases?
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.
Could you document
ok, I will do so.
And more generally I'm not sure why you update modCount only when calling
clear
method
I am directly changing it in clear
method because this one changes content of internal list, but doesn't calls any method of that internal list
. In all other cases the internal method of list
is called so the modCount of that internal list is managed automatically. Then I just call updateModCount
to read list.modCount and write it into this.modCount.
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 the explanation ;)
This PR now fails on many places because of this static void linkToParent(CtElement owner, CtElement element) {
if (owner.getFactory().getEnvironment().checksAreSkipped() == false && element.isParentInitialized() && element.getParent() != owner) {
//the `e` already has an different parent. Check if it is still linked to that parent
if (element.getRoleInParent() != null) {
throw new SpoonException("The element " + element + " is already used by another part of SpoonModel. Remove it from previous model or clone it before.");
}
}
element.setParent(owner);
} I think we should check whether added element is not already used on some other part of spoon model. Because otherwise you can add one element on two places and you will be not warned that you made invalid model.... WDYT? |
I think we should keep the contract explicit as it avoid unexpected side effects in Spoon model. So first let's try refactoring the current Spoon to assert that, and maybe then - if it makes the code harder to understand/maintain - add a flag to skip the contract in some very specific cases? |
The test
@monperrus you seems to be the author. Could you please fix this test somehow? |
Please have a look at
So there is question what is the intended behavior of WDYT? |
ping @monperrus, please have a look at failing tests here. I have no idea how to fix them |
ok, I will have a look at it. |
static void linkToParent(CtElement owner, CtElement element) { | ||
if (owner.getFactory().getEnvironment().checksAreSkipped() == false && element.isParentInitialized() && element.getParent() != owner) { | ||
//the `e` already has an different parent. Check if it is still linked to that parent | ||
if (element.getRoleInParent() != null) { |
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 change:
- is good for explicit behavior
- is bad for concise API (must always delete in parent before intercession)
- will break a lot of client code (it already breaks the test code, see the changes in
src/test
)
I would prefer to revert it, so that it remains fully backward compatible, and that while keeping the other awesome advantages of the rest of this PR.
WDYT?
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 exception was essential for finding errors. But should we keep it now? It will break a lot of client code.
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 already configurable here owner.getFactory().getEnvironment().checksAreSkipped()
so one can switch it off
break a lot of client code.
It breaks only client code which is already broken, because that code creates invalid AST, which can cause unexpected behavior of spoon and tricky issues.
Good news! when one removes:
Double win! To me that's the way to go. |
@monperrus, I copied the comments from above so it is easily visible what is the problem: The test
Test
So there is question what is the intended behavior of |
5950eb6
to
c6b108d
Compare
I would go for when can do this first in a separate baby PR. |
c6b108d
to
01305bd
Compare
The test in #1922 applied to this PR now reports
They should be transformed to attached collections to be consistent. But in different PRs. Test in #1922 was not able to check:
It is ready for merge from my point of view |
Thanks for the progress on this. To review a PR, there are several cases:
Here, this is clearly a case 2, and I am afraid that we break a lot of things. I suggest to document all the changes at the line level, in the tests:
Thanks, --Martin |
It skips assertion for these nodes:
no, it is not related. I made #2006 out of that
added comment: iterate on copy of list of statements, otherwise it fails with concurrent modification exception
I do not know why SignatureTest#testLiteralSignature expects that there exist some parent of the new element, ... I do not thing we should support that. So I removed that, because it failed with this PR.
the line above causes model inconsistency. The body of element is child of two nodes. So AST is no more tree ... This PR (by default) fails on such operations to warn about that inconsistency early. So test was fixed.
because they would fail following the contract you agreed in #2000. |
7cd3660
to
c890e0f
Compare
Thanks a lot for the explanation. AstCheckerTest: why do we need to skip those nodes now? RemoveTest: why this worked before? and this does not work anymore now? |
I still don't feel comfortable with this PR. It seems to me that we are trying to do two different things here:
Correct? |
yes, correct. |
c8f3b4f
to
63c3cd3
Compare
I moved the consistency check and related fixes to #2009, so this one is simpler now |
63c3cd3
to
f1697df
Compare
Thanks a lot. So now we only have two changed tests: AstCheckerTest: why do we need to skip those AST nodes now? RemoveTest: why iteration+remove worked before? why does this stop working now? |
AST checker test checks that each Spoon model property modifier (set/add/remove,....) method calls model change notification ... the new implementation with ModelList and ModelSet is now different case, so it must be ignored by this test remove test iterates over collection and then removes from this collection. Before it passed because it worked on the copy of the collection. Now it would fail because of concurrent modification exception. |
Now, I remember, AST checker works by pure static analysis.
We have other tests which check the behavior dynamically (which is better and less fragile), such as
ParentTest.
Do we also have a dynamic test for checking the presence of change events?
|
I do not know. But we will probably change all of them to ModelList and ModelSet, so they will be mutable attached... and then the correct behavior is tested by future #1922 |
Thanks a lot for this step towards #1922 |
First step towards #1633
... at the beginning only for CtBlock#statements
... later the same solution should be applied to each List, Set and Map of Spoon model
WDYT?