Skip to content

Commit 4cf5e94

Browse files
authored
fix: resolve references to variables declared in type patterns (#6444)
1 parent d31a05e commit 4cf5e94

File tree

4 files changed

+359
-19
lines changed

4 files changed

+359
-19
lines changed

src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@
88
package spoon.reflect.visitor.filter;
99

1010
import spoon.reflect.code.CaseKind;
11+
import spoon.reflect.code.CtBinaryOperator;
1112
import spoon.reflect.code.CtBodyHolder;
1213
import spoon.reflect.code.CtCase;
1314
import spoon.reflect.code.CtCatch;
1415
import spoon.reflect.code.CtCatchVariable;
16+
import spoon.reflect.code.CtFor;
17+
import spoon.reflect.code.CtIf;
18+
import spoon.reflect.code.CtExpression;
1519
import spoon.reflect.code.CtLocalVariable;
1620
import spoon.reflect.code.CtStatement;
1721
import spoon.reflect.code.CtStatementList;
1822
import spoon.reflect.code.CtSwitch;
23+
import spoon.reflect.code.CtTypePattern;
24+
import spoon.reflect.code.CtWhile;
1925
import spoon.reflect.declaration.CtElement;
2026
import spoon.reflect.declaration.CtExecutable;
2127
import spoon.reflect.declaration.CtField;
@@ -120,42 +126,61 @@ public void apply(CtElement input, CtConsumer<Object> outputConsumer) {
120126
if (query.isTerminated()) {
121127
return;
122128
}
123-
} else if (parent instanceof CtSwitch
124-
&& scopeElement instanceof CtCase && ((CtCase<?>) scopeElement).getCaseKind() == CaseKind.COLON) {
125-
SiblingsFunction siblingsFunction = new SiblingsFunction().mode(SiblingsFunction.Mode.PREVIOUS);
126-
List<CtCase<?>> list = input.getFactory().createQuery()
127-
.map(siblingsFunction)
128-
.setInput(scopeElement)
129-
.filterChildren(new TypeFilter<>(CtCase.class))
130-
.list();
129+
} else if (parent instanceof CtSwitch && scopeElement instanceof CtCase<?> caseElement) {
130+
if (caseElement.getCaseKind() == CaseKind.COLON) {
131+
SiblingsFunction siblingsFunction = new SiblingsFunction().mode(SiblingsFunction.Mode.PREVIOUS);
132+
List<CtCase<?>> list = input.getFactory().createQuery()
133+
.map(siblingsFunction)
134+
.setInput(scopeElement)
135+
.filterChildren(new TypeFilter<>(CtCase.class))
136+
.list();
131137

132-
for (CtCase<?> c : list) {
133-
for (CtStatement statement : c.getStatements()) {
134-
if (statement instanceof CtLocalVariable && ((CtLocalVariable<?>) statement).getSimpleName().equals(variableName)) {
135-
sendToOutput((CtVariable<?>) statement, outputConsumer);
136-
return;
138+
for (CtCase<?> c : list) {
139+
for (CtStatement statement : c.getStatements()) {
140+
if (statement instanceof CtLocalVariable && ((CtLocalVariable<?>) statement).getSimpleName().equals(variableName)) {
141+
sendToOutput((CtVariable<?>) statement, outputConsumer);
142+
return;
143+
}
137144
}
138145
}
139146
}
140-
} else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList) {
147+
148+
// search for variable declaration in case
149+
var expr = caseElement.getCaseExpression();
150+
searchTypePattern(outputConsumer, expr);
151+
} else if (parent instanceof CtIf ifElement) {
152+
// search for variable declaration in if expression
153+
var cond = ifElement.getCondition();
154+
searchTypePattern(outputConsumer, cond);
155+
} else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList || parent instanceof CtExpression<?>) {
141156
//visit all previous CtVariable siblings of scopeElement element in parent BodyHolder or Statement list
142157
siblingsQuery.setInput(scopeElement).forEach(outputConsumer);
143158
if (query.isTerminated()) {
144159
return;
145160
}
146161
//visit parameters of CtCatch and CtExecutable (method, lambda)
147-
if (parent instanceof CtCatch) {
148-
CtCatch ctCatch = (CtCatch) parent;
162+
if (parent instanceof CtCatch ctCatch) {
149163
if (sendToOutput(ctCatch.getParameter(), outputConsumer)) {
150164
return;
151165
}
152-
} else if (parent instanceof CtExecutable) {
153-
CtExecutable<?> exec = (CtExecutable<?>) parent;
166+
} else if (parent instanceof CtExecutable<?> exec) {
154167
for (CtParameter<?> param : exec.getParameters()) {
155168
if (sendToOutput(param, outputConsumer)) {
156169
return;
157170
}
158171
}
172+
} else if (parent instanceof CtFor forElement) {
173+
// search for variable declaration in for loop expression
174+
var expr = forElement.getExpression();
175+
searchTypePattern(outputConsumer, expr);
176+
} else if (parent instanceof CtWhile whileElement) {
177+
// search for variable declaration in while loop expression
178+
var expr = whileElement.getLoopingExpression();
179+
searchTypePattern(outputConsumer, expr);
180+
} else if (parent instanceof CtBinaryOperator<?> op) {
181+
// search for type pattern in binary operator
182+
var left = op.getLeftHandOperand();
183+
searchTypePattern(outputConsumer, left);
159184
}
160185
}
161186
if (parent instanceof CtModifiable) {
@@ -165,6 +190,19 @@ public void apply(CtElement input, CtConsumer<Object> outputConsumer) {
165190
}
166191
}
167192

193+
/**
194+
* Search for the variable declaration in type patterns and send matches to outputConsumer
195+
*/
196+
private void searchTypePattern(CtConsumer<Object> outputConsumer, CtExpression<?> expr) {
197+
expr.filterChildren(new TypeFilter<>(CtTypePattern.class))
198+
.forEach(typePattern -> {
199+
var var = ((CtTypePattern) typePattern).getVariable();
200+
if (var != null && (variableName == null || variableName.equals(var.getSimpleName()))) {
201+
outputConsumer.accept(var);
202+
}
203+
});
204+
}
205+
168206
/**
169207
* @param var
170208
* @param output

src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88
package spoon.reflect.visitor.filter;
99

10+
import spoon.reflect.code.CtIf;
1011
import spoon.reflect.declaration.CtElement;
12+
import spoon.reflect.declaration.CtVariable;
1113
import spoon.reflect.visitor.CtScanner;
1214
import spoon.reflect.visitor.chain.CtConsumableFunction;
1315
import spoon.reflect.visitor.chain.CtConsumer;
@@ -72,7 +74,15 @@ public void scan(CtElement element) {
7274
canVisit = includingSelf;
7375
}
7476
if (canVisit) {
75-
outputConsumer.accept(element);
77+
if (element instanceof CtIf ctIf) {
78+
// check for flow scoped variable declarations in if conditions
79+
var vars = ctIf.getCondition().getElements(new TypeFilter<>(CtVariable.class));
80+
for (var var : vars) {
81+
outputConsumer.accept(var);
82+
}
83+
} else {
84+
outputConsumer.accept(element);
85+
}
7686
}
7787
}
7888
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package spoon.test.reference;
2+
3+
import org.junit.jupiter.api.Test;
4+
import spoon.reflect.CtModel;
5+
import spoon.reflect.code.CtLocalVariable;
6+
import spoon.reflect.code.CtTypePattern;
7+
import spoon.reflect.reference.CtFieldReference;
8+
import spoon.reflect.reference.CtLocalVariableReference;
9+
import spoon.reflect.visitor.filter.TypeFilter;
10+
import spoon.testing.utils.GitHubIssue;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNotNull;
14+
import static spoon.test.SpoonTestHelpers.createModelFromString;
15+
16+
/**
17+
* Tests that references to pattern variables declared using the <code>instanceof</code> operator can be resolved.
18+
* Pattern matching for instanceof was introduced in Java 16, cf. <a href=https://openjdk.java.net/jeps/394>JEP 394</a>.
19+
* Variables declared in pattern matches have <a href="https://openjdk.org/projects/amber/design-notes/patterns/pattern-match-semantics">flow scope semantics</a>.
20+
*/
21+
public class InstanceOfReferenceTest {
22+
@Test
23+
public void testVariableDeclaredInIf() {
24+
String code = """
25+
class X {
26+
String typePattern(Object obj) {
27+
boolean someCondition = true;
28+
if (someCondition && obj instanceof String s) {
29+
return s;
30+
}
31+
return "";
32+
}
33+
}
34+
""";
35+
CtModel model = createModelFromString(code, 21);
36+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
37+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(1);
38+
var decl = ref.getDeclaration();
39+
assertNotNull(decl);
40+
assertEquals(variable, decl);
41+
}
42+
43+
@Test
44+
public void testVariableDeclaredInWhileLoop() {
45+
String code = """
46+
class X {
47+
public void processShapes(List<Object> shapes) {
48+
var iter = 0;
49+
while (iter < shapes.size() && shapes.get(iter) instanceof String shape) {
50+
iter++;
51+
System.out.println(shape);
52+
}
53+
}
54+
}
55+
""";
56+
CtModel model = createModelFromString(code, 21);
57+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
58+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(3);
59+
var decl = ref.getDeclaration();
60+
assertNotNull(decl);
61+
assertEquals(variable, decl);
62+
}
63+
64+
@Test
65+
public void testVariableDeclaredInForLoop() {
66+
String code = """
67+
class X {
68+
public void processShapes(List<Object> shapes) {
69+
for (var iter = 0; iter < shapes.size() && shapes.get(iter) instanceof String shape; iter++) {
70+
System.out.println(shape);
71+
}
72+
}
73+
}
74+
""";
75+
CtModel model = createModelFromString(code, 21);
76+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
77+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(3);
78+
var decl = ref.getDeclaration();
79+
assertNotNull(decl);
80+
assertEquals(variable, decl);
81+
}
82+
83+
@Test
84+
public void testDeclaredVariableUsedInSameCondition() {
85+
String code = """
86+
class X {
87+
public void processShapes(Object obj) {
88+
if (obj instanceof String s && s.length() > 5) {
89+
// NOP
90+
}
91+
}
92+
}
93+
""";
94+
CtModel model = createModelFromString(code, 21);
95+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
96+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0);
97+
var decl = ref.getDeclaration();
98+
assertNotNull(decl);
99+
assertEquals(variable, decl);
100+
}
101+
102+
@Test
103+
public void testDeclaredVariableUsedInSameCondition2() {
104+
String code = """
105+
class X {
106+
public void hasRightSize(Shape s) throws MyException {
107+
return s instanceof Circle c && c.getRadius() > 10;
108+
}
109+
}
110+
""";
111+
CtModel model = createModelFromString(code, 21);
112+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
113+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0);
114+
var decl = ref.getDeclaration();
115+
assertNotNull(decl);
116+
assertEquals(variable, decl);
117+
}
118+
119+
@Test
120+
public void testFlowScope() {
121+
String code = """
122+
class X {
123+
public void onlyForStrings(Object o) throws MyException {
124+
if (!(o instanceof String s))
125+
throw new MyException();
126+
// s is in scope
127+
System.out.println(s);
128+
}
129+
}
130+
""";
131+
CtModel model = createModelFromString(code, 21);
132+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
133+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0);
134+
var decl = ref.getDeclaration();
135+
assertNotNull(decl);
136+
assertEquals(variable, decl);
137+
}
138+
139+
@Test
140+
public void testFlowScope2() {
141+
String code = """
142+
class X {
143+
String s = "abc";
144+
145+
public void method2(Object o) {
146+
if (!(o instanceof String s)) {
147+
System.out.println("not a string");
148+
} else {
149+
System.out.println(s); // The local variable is in scope here!
150+
}
151+
}
152+
}
153+
""";
154+
CtModel model = createModelFromString(code, 21);
155+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
156+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0);
157+
var decl = ref.getDeclaration();
158+
assertNotNull(decl);
159+
assertEquals(variable, decl);
160+
}
161+
162+
@Test
163+
public void testFlowScope3() {
164+
String code = """
165+
class X {
166+
String typePattern(Object obj) {
167+
if (obj instanceof String s) {
168+
System.out.println("It's a string");
169+
} else {
170+
throw new RuntimeException("It's not a string");
171+
}
172+
return s; // We can still access s here!
173+
}
174+
}
175+
""";
176+
CtModel model = createModelFromString(code, 21);
177+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
178+
CtLocalVariableReference<?> ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0);
179+
var decl = ref.getDeclaration();
180+
assertNotNull(decl);
181+
assertEquals(variable, decl);
182+
}
183+
184+
@Test
185+
public void testCorrectScoping() {
186+
String code = """
187+
class Example2 {
188+
Point p;
189+
190+
void test2(Object o) {
191+
if (o instanceof Point p) {
192+
// p refers to the pattern variable
193+
System.out.println(p);
194+
} else {
195+
// p refers to the field
196+
System.out.println(p);
197+
}
198+
}
199+
}
200+
""";
201+
CtModel model = createModelFromString(code, 21);
202+
CtLocalVariable<?> variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
203+
var refs = model.getElements(new TypeFilter<>(CtLocalVariableReference.class));
204+
assertEquals(1, refs.size());
205+
var decl = refs.get(0).getDeclaration();
206+
assertNotNull(decl);
207+
assertEquals(variable, decl);
208+
}
209+
210+
@Test
211+
public void testRecordPatterns() {
212+
String code = """
213+
record Point(int x, int y) {}
214+
record Circle(Point center, int radius) {}
215+
216+
public class Y {
217+
public void test() {
218+
Object obj = new Circle(new Point(10, 20), 5);
219+
if (obj instanceof Circle(Point (int x, int y), int r)) {
220+
System.out.println("Object is a Circle at center (" + x + ", " + y + ") with radius " + r);
221+
}
222+
}
223+
}
224+
""";
225+
CtModel model = createModelFromString(code, 21);
226+
CtLocalVariable<?> varX = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable();
227+
CtLocalVariable<?> varY = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(1).getVariable();
228+
CtLocalVariable<?> varR = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(2).getVariable();
229+
var refs = model.getElements(new TypeFilter<>(CtLocalVariableReference.class));
230+
assertEquals(4, refs.size()); // includes reference to 'obj'
231+
var declX = refs.get(1).getDeclaration();
232+
var declY = refs.get(2).getDeclaration();
233+
var declR = refs.get(3).getDeclaration();
234+
assertEquals(varX, declX);
235+
assertEquals(varY, declY);
236+
assertEquals(varR, declR);
237+
}
238+
}

0 commit comments

Comments
 (0)