Fix: Correctly resolve in scope variables of instanceof patterns #6645#6647
Fix: Correctly resolve in scope variables of instanceof patterns #6645#6647Luro02 wants to merge 1 commit intoINRIA:masterfrom
Conversation
|
@Luro02 mind using our assertions and testing utils? Take a look at https://github.com/INRIA/spoon/blob/master/CONTRIBUTING.md or ask the next mistral instance. |
Are you referring to that |
src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java
Outdated
Show resolved
Hide resolved
src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java
Outdated
Show resolved
Hide resolved
src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java
Outdated
Show resolved
Hide resolved
|
This looks somewhat incomplete, especially with more complex if conditions. For example:
Could you expand the tests to cover these scenarios? It feels like we need full DA/DU analysis here to properly support that feature... |
|
This is likely going to be more involved than I expected, so I am converting this to a draft for now. |
|
I went through the JLS https://docs.oracle.com/javase/specs/jls/se25/html/jls-6.html#jls-6.3.1 and implemented the pattern variable resolution accordingly. The pushed implementation seems to be mostly working, except for switches, which I have not implemented yet. There are a lot of tests missing, many things are not covered, but I am unsure how these should look like. The Contributing Guidelines suggest using The main problem would be how to get the declarations and associated references from a piece of code. I am tempted to simply fetch a list of all declarations, and all references, then hardcode which indices of the references should resolve to which declaration, but maybe there is a cleaner way? |
src/test/java/spoon/test/reference/InstanceOfReferenceTest.java
Outdated
Show resolved
Hide resolved
|
Here is the current state of the PR: The original code starts at the given element, checks if the desired variable is in scope, and if not, moves up to the parent of the element. Same repeats until it reaches the package declaration or runs out of parents. For instanceof patterns, the scope changes through the parents, e.g. a pattern that was in scope will not be in scope anymore when negated (it would then match on false). The final scope of the pattern is defined by the statement containing the expression with the instanceof pattern. To figure out which instanceof pattern is in scope, one has to start at the pattern definition, then walk up the element tree. My implementation does the following:The loop that moves up the tree keeps track of the current scopes. When it moves up to the parent, it updates the scopes with new variables that have become available (like one declared in a sibling) or removes ones that no longer apply. The code can then look at the scopes to find any variable that is in scope. When moving to the parent, it only knows the variables from the child, but there might be siblings that introduce variables too. The code currently hardcodes which siblings are explored, but I think always applying the siblings function might be more sensible here. For siblings it will try to find one variable declaration (a leaf in the tree), then move up from there. Given that it uses the same code that is used for the child, it will automatically explore the other branches. The code has become rather complicated, so the next steps would be to simplify where applicable. I found a few bugs in the old code as well, e.g. it used filterChildren to get the variables of a case, but then a variable declared in the block |
|
It has been a while since I last worked on the PR, because I was busy. I think the implementation is far enough that it is ready for an initial review. It looks like I am not supposed to add custom test assertions? Will have to investigate where I should add them. What remains to be done?
What I need feedback on:
|
I-Al-Istannen
left a comment
There was a problem hiding this comment.
Thank you very much! I added some nitpicks and some thoughts, but I don't think I have completely grasped it.
I think I found one omission (if without a then but a non-terminating else) and some other small comments.
| if (parent instanceof CtModifiable ctModifiable) { | ||
| isInStaticScope = isInStaticScope || ctModifiable.hasModifier(ModifierKind.STATIC); | ||
| } |
There was a problem hiding this comment.
| if (parent instanceof CtModifiable ctModifiable) { | |
| isInStaticScope = isInStaticScope || ctModifiable.hasModifier(ModifierKind.STATIC); | |
| } | |
| if (parent instanceof CtModifiable ctModifiable && ctModifiable.hasModifier(ModifierKind.STATIC)) { | |
| isInStaticScope = true; | |
| } |
?
There was a problem hiding this comment.
That part is from the original code, but will update this. I think my change was just some whitespace adjustments
| /** | ||
| * The variable this scope applies to. | ||
| * | ||
| * @return the variable | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * The variable this scope applies to. | |
| * | |
| * @return the variable | |
| */ | |
| /** | |
| * {@return the variable this scope applies to} | |
| */ |
| /** | ||
| * The element in which the variable can be referenced. | ||
| * | ||
| * @return the element | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * The element in which the variable can be referenced. | |
| * | |
| * @return the element | |
| */ | |
| /** | |
| * {@return the element in which the variable can be referenced} | |
| */ |
| CtElement ctElement(); | ||
|
|
||
| /** | ||
| * Checks whether the scope applies to the given element. |
There was a problem hiding this comment.
I can glance that from the method name, but what does "apply" mean?
| * | ||
| * @return the variable | ||
| */ | ||
| CtVariable<?> ctVariable(); |
There was a problem hiding this comment.
Is there any reason for prefixing everything with ct? I think the rest of the code doesn't do that.
There was a problem hiding this comment.
No, just my personal preference naming things after their types
| } | ||
|
|
||
| /** | ||
| * Updates the child scopes for the parent. |
There was a problem hiding this comment.
What does this mean?
| // TODO: With the current implementation any new CtBodyHolders will be resolved by the below code, potentially causing | ||
| // wrong variable resolution. Instead one could constrain the below if to | ||
| // if (parent instanceof CtCatch || parent instanceof CtExecutable || ...) | ||
| // Then in any new implementation the variables would resolve to null instead of a wrong variable (trickier to discover) |
There was a problem hiding this comment.
CtBodyHolder is a somewhat odd interface that doesn't really correspond to a JLS concept, so I'd rather cover the specific cases.
That also brings me to synchronized blocks, which is not a CtBodyHolder but has a similar shape. Is that handled correctly here?
There was a problem hiding this comment.
CtBodyHolderis a somewhat odd interface that doesn't really correspond to a JLS concept, so I'd rather cover the specific cases.That also brings me to
synchronizedblocks, which is not aCtBodyHolderbut has a similar shape. Is that handled correctly here?
CtSynchronized isn't mentioned in the JLS for instanceof patterns, so I assume synchronized (a instanceof B b) { ... } is not a thing.
Other than that the CtSynchronized always has a CtBlock, and the variable can not escape the block. The CtBlock is handled correctly
I will add some tests for this
There was a problem hiding this comment.
CtSynchronizedisn't mentioned in the JLS for instanceof patterns, so I assumesynchronized (a instanceof B b) { ... }is not a thing.Other than that the CtSynchronized always has a
CtBlock, and the variable can not escape the block. The CtBlock is handled correctly
Oh so e.g., CtCatch is relevant here because it introduces a variable?
There was a problem hiding this comment.
Yes, it only cares about elements that introduce variables. Then defines where in their element the variables are valid.
When moving to the parent, these will no longer apply, so they are discarded. The instanceof patterns are the special case for which we need the scopes, because they can still be valid in their parent element.
| private static <T> List<T> castList(List<?> sourceList) { | ||
| return (List<T>) sourceList; | ||
| } |
There was a problem hiding this comment.
This feels... risky? I would inline that logic and add an explanatory comment ("Will only create VariableScope if branch is a CtVariable, which it isn't"), if you need that property
| } | ||
|
|
||
| // If the branch itself is a variable declaration, then this variable is in scope for the branch. | ||
| // The updateChildScopesForParent would have introduced it, but it was not called, because of the loop conidition |
There was a problem hiding this comment.
| // The updateChildScopesForParent would have introduced it, but it was not called, because of the loop conidition | |
| // The updateChildScopesForParent would have introduced it, but it was not called, because of the loop condition |
| } | ||
|
|
||
| private static boolean completesNormally(CtStatement statement) { | ||
| // FIXME: The JLS has a definition for what "cannot complete normally" is |
There was a problem hiding this comment.
We should under-approximate this, if the JLS implementation requires too much. I asked some AI and it spit out 60 lines of fun code, so I guess technically it is doable, but if you have a save under-approximation that's fine by me. Do keep a FIXME there then, though :)
I have not looked at the JLS, so I might have missed something.
The code in the
SiblingsFunctionintroduced in #6444 has been removed, because I think it does not belong there? Seems wrong to return the instanceof variables there.