From d53377a8c3720e933d122b18cf861cc33cc9de68 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:48:22 +0200 Subject: [PATCH 1/5] feat: add minimal support for instanceof patterns This approach only works for variables declared in a parent node. Note that this does not reflect the actual flow scope rules the Java compiler uses. --- .../PotentialVariableDeclarationFunction.java | 3 ++- .../reflect/visitor/filter/SiblingsFunction.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java index c03fef1ac48..4496b37e978 100644 --- a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java @@ -12,6 +12,7 @@ import spoon.reflect.code.CtCase; import spoon.reflect.code.CtCatch; import spoon.reflect.code.CtCatchVariable; +import spoon.reflect.code.CtIf; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtStatementList; @@ -137,7 +138,7 @@ public void apply(CtElement input, CtConsumer outputConsumer) { } } } - } else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList) { + } else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList || parent instanceof CtIf) { //visit all previous CtVariable siblings of scopeElement element in parent BodyHolder or Statement list siblingsQuery.setInput(scopeElement).forEach(outputConsumer); if (query.isTerminated()) { diff --git a/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java b/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java index 491837821ca..08fe9f7a652 100644 --- a/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java @@ -7,6 +7,9 @@ */ package spoon.reflect.visitor.filter; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.code.CtTypePattern; import spoon.reflect.declaration.CtElement; import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.chain.CtConsumableFunction; @@ -72,7 +75,15 @@ public void scan(CtElement element) { canVisit = includingSelf; } if (canVisit) { - outputConsumer.accept(element); + if (element instanceof CtLocalVariable) { + outputConsumer.accept(element); + } else if (element instanceof CtExpression) { + element.filterChildren(new TypeFilter<>(CtTypePattern.class)) + .forEach(typePattern -> { + var var = ((CtTypePattern) typePattern).getVariable(); + outputConsumer.accept(var); + }); + } } } } From 2a70cf9f7408550a9c00bfad43162584aaab3e29 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:25:51 +0200 Subject: [PATCH 2/5] test: add test cases for pattern matching declarations --- .../reference/InstanceOfReferenceTest.java | 208 ++++++++++++++++++ .../PatternMatchingReferenceTest.java | 35 +++ 2 files changed, 243 insertions(+) create mode 100644 src/test/java/spoon/test/reference/InstanceOfReferenceTest.java create mode 100644 src/test/java/spoon/test/reference/PatternMatchingReferenceTest.java diff --git a/src/test/java/spoon/test/reference/InstanceOfReferenceTest.java b/src/test/java/spoon/test/reference/InstanceOfReferenceTest.java new file mode 100644 index 00000000000..bf1efdf6a1f --- /dev/null +++ b/src/test/java/spoon/test/reference/InstanceOfReferenceTest.java @@ -0,0 +1,208 @@ +package spoon.test.reference; + +import org.junit.jupiter.api.Test; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.code.CtTypePattern; +import spoon.reflect.reference.CtLocalVariableReference; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.testing.utils.GitHubIssue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static spoon.test.SpoonTestHelpers.createModelFromString; + +/** + * Tests that references to pattern variables declared using the instanceof operator can be resolved. + * Pattern matching for instanceof was introduced in Java 16, cf. JEP 394. + * Variables declared in pattern matches have flow scope semantics. + */ +public class InstanceOfReferenceTest { + @Test + public void testVariableDeclaredInIf() { + String code = """ + class X { + String typePattern(Object obj) { + boolean someCondition = true; + if (someCondition && obj instanceof String s) { + return s; + } + return ""; + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(1); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testVariableDeclaredInWhileLoop() { + String code = """ + class X { + public void processShapes(List shapes) { + var iter = 0; + while (iter < shapes.size() && shapes.get(iter) instanceof String shape) { + iter++; + System.out.println(shape); + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(3); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testVariableDeclaredInForLoop() { + String code = """ + class X { + public void processShapes(List shapes) { + for (var iter = 0; iter < shapes.size() && shapes.get(iter) instanceof String shape; iter++) { + System.out.println(shape); + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(3); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testDeclaredVariableUsedInSameCondition() { + String code = """ + class X { + public void processShapes(Object obj) { + if (obj instanceof String s && s.length() > 5) { + // NOP + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testDeclaredVariableUsedInSameCondition2() { + String code = """ + class X { + public void hasRightSize(Shape s) throws MyException { + return s instanceof Circle c && c.getRadius() > 10; + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testFlowScope() { + String code = """ + class X { + public void onlyForStrings(Object o) throws MyException { + if (!(o instanceof String s)) + throw new MyException(); + // s is in scope + System.out.println(s); + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testFlowScope2() { + String code = """ + class X { + String s = "abc"; + + public void method2(Object o) { + if (!(o instanceof String s)) { + System.out.println("not a string"); + } else { + System.out.println(s); // The local variable is in scope here! + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testFlowScope3() { + String code = """ + class X { + String typePattern(Object obj) { + if (obj instanceof String s) { + System.out.println("It's a string"); + } else { + throw new RuntimeException("It's not a string"); + } + return s; // We can still access s here! + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } + + @Test + public void testCorrectScoping() { + String code = """ + class Example2 { + Point p; + + void test2(Object o) { + if (o instanceof Point p) { + // p refers to the pattern variable + System.out.println(p); + } else { + // p refers to the field + System.out.println(p); + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + var refs = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)); + assertEquals(1, refs.size()); + var decl = refs.get(0).getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } +} diff --git a/src/test/java/spoon/test/reference/PatternMatchingReferenceTest.java b/src/test/java/spoon/test/reference/PatternMatchingReferenceTest.java new file mode 100644 index 00000000000..58bb4e9f16a --- /dev/null +++ b/src/test/java/spoon/test/reference/PatternMatchingReferenceTest.java @@ -0,0 +1,35 @@ +package spoon.test.reference; + +import org.junit.jupiter.api.Test; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.code.CtTypePattern; +import spoon.reflect.reference.CtLocalVariableReference; +import spoon.reflect.visitor.filter.TypeFilter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static spoon.test.SpoonTestHelpers.createModelFromString; + +public class PatternMatchingReferenceTest { + @Test + public void testCasePatternReference() { + String code = """ + class X { + public void processShape(Shape shape) { + switch (shape) { + case Circle c -> { + System.out.println("This is a circle with radius: " + c.getRadius()); + } + } + } + } + """; + CtModel model = createModelFromString(code, 21); + CtLocalVariable variable = model.getElements(new TypeFilter<>(CtTypePattern.class)).get(0).getVariable(); + CtLocalVariableReference ref = model.getElements(new TypeFilter<>(CtLocalVariableReference.class)).get(0); + var decl = ref.getDeclaration(); + assertNotNull(decl); + assertEquals(variable, decl); + } +} From 28b4a8648bb23b835ef119e1883dcd3ca3be6af4 Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:50:11 +0200 Subject: [PATCH 3/5] feat: add support for pattern matching in binary expressions --- .../filter/PotentialVariableDeclarationFunction.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java index 4496b37e978..1ece39f8cbf 100644 --- a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java @@ -8,6 +8,7 @@ package spoon.reflect.visitor.filter; import spoon.reflect.code.CaseKind; +import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtBodyHolder; import spoon.reflect.code.CtCase; import spoon.reflect.code.CtCatch; @@ -158,6 +159,11 @@ public void apply(CtElement input, CtConsumer outputConsumer) { } } } + } else if (parent instanceof CtBinaryOperator) { + siblingsQuery.setInput(scopeElement).forEach(outputConsumer); + if (query.isTerminated()) { + return; + } } if (parent instanceof CtModifiable) { isInStaticScope = isInStaticScope || ((CtModifiable) parent).hasModifier(ModifierKind.STATIC); From 2b072455dcdb5bf0f17645b78df50911d616e1ff Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:34:24 +0200 Subject: [PATCH 4/5] fix: restore previous SiblingsFunction --- .../PotentialVariableDeclarationFunction.java | 37 ++++++++++++++++--- .../visitor/filter/SiblingsFunction.java | 13 +------ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java index 1ece39f8cbf..e16c6035c23 100644 --- a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java @@ -13,11 +13,15 @@ import spoon.reflect.code.CtCase; import spoon.reflect.code.CtCatch; import spoon.reflect.code.CtCatchVariable; +import spoon.reflect.code.CtFor; import spoon.reflect.code.CtIf; +import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtStatementList; import spoon.reflect.code.CtSwitch; +import spoon.reflect.code.CtTypePattern; +import spoon.reflect.code.CtWhile; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtField; @@ -139,7 +143,16 @@ public void apply(CtElement input, CtConsumer outputConsumer) { } } } - } else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList || parent instanceof CtIf) { + } else if (parent instanceof CtIf ifElement) { + var cond = ifElement.getCondition(); + searchTypePattern(outputConsumer, cond); + } else if(parent instanceof CtWhile whileElement) { + var expr = whileElement.getLoopingExpression(); + searchTypePattern(outputConsumer, expr); + } else if(parent instanceof CtFor forElement) { + var expr = forElement.getExpression(); + searchTypePattern(outputConsumer, expr); + } else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList || parent instanceof CtExpression) { //visit all previous CtVariable siblings of scopeElement element in parent BodyHolder or Statement list siblingsQuery.setInput(scopeElement).forEach(outputConsumer); if (query.isTerminated()) { @@ -158,11 +171,13 @@ public void apply(CtElement input, CtConsumer outputConsumer) { return; } } - } - } else if (parent instanceof CtBinaryOperator) { - siblingsQuery.setInput(scopeElement).forEach(outputConsumer); - if (query.isTerminated()) { - return; + } else if (parent instanceof CtBinaryOperator op) { + //search for type pattern in binary operator + var left = op.getLeftHandOperand(); + searchTypePattern(outputConsumer, left); + if (query.isTerminated()) { + return; + } } } if (parent instanceof CtModifiable) { @@ -172,6 +187,16 @@ public void apply(CtElement input, CtConsumer outputConsumer) { } } + private void searchTypePattern(CtConsumer outputConsumer, CtExpression cond) { + cond.filterChildren(new TypeFilter<>(CtTypePattern.class)) + .forEach(typePattern -> { + var var = ((CtTypePattern) typePattern).getVariable(); + if (var != null && (variableName == null || variableName.equals(var.getSimpleName()))) { + outputConsumer.accept(var); + } + }); + } + /** * @param var * @param output diff --git a/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java b/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java index 08fe9f7a652..491837821ca 100644 --- a/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/SiblingsFunction.java @@ -7,9 +7,6 @@ */ package spoon.reflect.visitor.filter; -import spoon.reflect.code.CtExpression; -import spoon.reflect.code.CtLocalVariable; -import spoon.reflect.code.CtTypePattern; import spoon.reflect.declaration.CtElement; import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.chain.CtConsumableFunction; @@ -75,15 +72,7 @@ public void scan(CtElement element) { canVisit = includingSelf; } if (canVisit) { - if (element instanceof CtLocalVariable) { - outputConsumer.accept(element); - } else if (element instanceof CtExpression) { - element.filterChildren(new TypeFilter<>(CtTypePattern.class)) - .forEach(typePattern -> { - var var = ((CtTypePattern) typePattern).getVariable(); - outputConsumer.accept(var); - }); - } + outputConsumer.accept(element); } } } From 8ffb98e781816047f558a5ecfd7e3204d76be3ac Mon Sep 17 00:00:00 2001 From: MaxAtoms <7847075+MaxAtoms@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:03:44 +0200 Subject: [PATCH 5/5] fix: lint --- .../visitor/filter/PotentialVariableDeclarationFunction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java index e16c6035c23..bbdef635e89 100644 --- a/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java +++ b/src/main/java/spoon/reflect/visitor/filter/PotentialVariableDeclarationFunction.java @@ -146,10 +146,10 @@ public void apply(CtElement input, CtConsumer outputConsumer) { } else if (parent instanceof CtIf ifElement) { var cond = ifElement.getCondition(); searchTypePattern(outputConsumer, cond); - } else if(parent instanceof CtWhile whileElement) { + } else if (parent instanceof CtWhile whileElement) { var expr = whileElement.getLoopingExpression(); searchTypePattern(outputConsumer, expr); - } else if(parent instanceof CtFor forElement) { + } else if (parent instanceof CtFor forElement) { var expr = forElement.getExpression(); searchTypePattern(outputConsumer, expr); } else if (parent instanceof CtBodyHolder || parent instanceof CtStatementList || parent instanceof CtExpression) {