From f8266326946b48c6f1a58ffd3630957ed4b08db9 Mon Sep 17 00:00:00 2001 From: Reed von Redwitz Date: Fri, 10 Jul 2026 11:34:56 +0200 Subject: [PATCH] fix(jdk-codemodel): resolve Symbol.PatternBinding for record-deconstruction pattern components --- .../codemodel/jdk/JdkExpressionConverter.java | 52 ++++++- .../codemodel/jdk/JdkStatementConverter.java | 10 +- .../codemodel/jdk/expression/InstanceOf.java | 140 +++++++++++++++++- .../jdk/SwitchPatternCaptureTests.java | 66 +++++++++ .../codemodel/jdk/SymbolResolutionTests.java | 91 ++++++++++++ 5 files changed, 347 insertions(+), 12 deletions(-) diff --git a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkExpressionConverter.java b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkExpressionConverter.java index 5c63319..1874f32 100644 --- a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkExpressionConverter.java +++ b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkExpressionConverter.java @@ -103,6 +103,7 @@ import com.sun.source.tree.NewClassTree; import com.sun.source.tree.ParameterizedTypeTree; import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.PatternTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.SwitchExpressionTree; import com.sun.source.tree.Tree; @@ -736,14 +737,21 @@ public Expression visitTypeCast(final TypeCastTree t, final Void v) { @Override public Expression visitInstanceOf(final InstanceOfTree t, final Void v) { + if (t.getPattern() instanceof DeconstructionPatternTree dp) { + final var type = resolveTypeUsage(dp.getDeconstructor()); + addSourceLocation(dp.getDeconstructor()).ifPresent(type::addTrait); + final var components = dp.getNestedPatterns().stream() + .map(this::convertPattern) + .toList(); + final var instanceOf = InstanceOf.ofDeconstruction(convert(t.getExpression()), type, components); + dp.getNestedPatterns().forEach(nested -> registerNestedPatternBindings(nested, instanceOf)); + return instanceOf; + } final Optional bindingVariable; final Tree typeTree; if (t.getPattern() instanceof BindingPatternTree bp) { bindingVariable = Optional.of(bp.getVariable().getName().toString()); typeTree = t.getType(); - } else if (t.getPattern() instanceof DeconstructionPatternTree dp) { - bindingVariable = Optional.empty(); - typeTree = dp.getDeconstructor(); } else { bindingVariable = Optional.empty(); typeTree = t.getType(); @@ -760,6 +768,44 @@ public Expression visitInstanceOf(final InstanceOfTree t, final Void v) { return instanceOf; } + /** + * Recursively converts a nested {@link PatternTree} (a component of a record-deconstruction + * pattern) to an {@link InstanceOf.Pattern}. A component is either a {@link BindingPatternTree} + * (a simple type-pattern variable) or another {@link DeconstructionPatternTree} nested + * arbitrarily deep. + */ + InstanceOf.Pattern convertPattern(final PatternTree pattern) { + if (pattern instanceof BindingPatternTree binding) { + final var type = resolveTypeUsage(binding.getVariable().getType()); + addSourceLocation(binding.getVariable().getType()).ifPresent(type::addTrait); + return new InstanceOf.Pattern.Binding(type, binding.getVariable().getName().toString()); + } + if (pattern instanceof DeconstructionPatternTree deconstruction) { + final var type = resolveTypeUsage(deconstruction.getDeconstructor()); + addSourceLocation(deconstruction.getDeconstructor()).ifPresent(type::addTrait); + final var components = deconstruction.getNestedPatterns().stream() + .map(this::convertPattern) + .toList(); + return new InstanceOf.Pattern.Record(type, components); + } + throw new IllegalStateException("Unsupported nested pattern kind: " + pattern.getClass()); + } + + /** + * Recursively registers every {@link BindingPatternTree} nested within a record-deconstruction + * pattern against {@code instanceOf}, so that later {@link Symbol.PatternBinding} resolution + * can link identifier usages of those component variables back to the pattern test that + * declared them. + */ + void registerNestedPatternBindings(final PatternTree pattern, final InstanceOf instanceOf) { + if (pattern instanceof BindingPatternTree binding) { + registerPatternBinding(binding.getVariable(), instanceOf); + } else if (pattern instanceof DeconstructionPatternTree deconstruction) { + deconstruction.getNestedPatterns() + .forEach(nested -> registerNestedPatternBindings(nested, instanceOf)); + } + } + @Override public Expression visitLambdaExpression(final LambdaExpressionTree t, final Void v) { final Statement body; diff --git a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkStatementConverter.java b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkStatementConverter.java index 8bb5c58..4d65cb5 100644 --- a/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkStatementConverter.java +++ b/jdk-codemodel/src/main/java/build/codemodel/jdk/JdkStatementConverter.java @@ -339,11 +339,15 @@ private Optional convertPatternLabel(final PatternTree pattern, return Optional.of(instanceOf); } if (pattern instanceof DeconstructionPatternTree deconstruction) { - // Nested binding variables of a record deconstruction pattern are not yet captured; - // only the deconstructed type is preserved. final var type = exprConverter.resolveTypeUsage(deconstruction.getDeconstructor()); exprConverter.addSourceLocation(deconstruction.getDeconstructor()).ifPresent(type::addTrait); - return Optional.of(InstanceOf.of(selector, type)); + final var components = deconstruction.getNestedPatterns().stream() + .map(exprConverter::convertPattern) + .toList(); + final var instanceOf = InstanceOf.ofDeconstruction(selector, type, components); + deconstruction.getNestedPatterns() + .forEach(nested -> exprConverter.registerNestedPatternBindings(nested, instanceOf)); + return Optional.of(instanceOf); } return Optional.empty(); } diff --git a/jdk-codemodel/src/main/java/build/codemodel/jdk/expression/InstanceOf.java b/jdk-codemodel/src/main/java/build/codemodel/jdk/expression/InstanceOf.java index 62da61b..c4fea28 100644 --- a/jdk-codemodel/src/main/java/build/codemodel/jdk/expression/InstanceOf.java +++ b/jdk-codemodel/src/main/java/build/codemodel/jdk/expression/InstanceOf.java @@ -35,6 +35,7 @@ import build.codemodel.foundation.usage.TypeUsage; import java.lang.invoke.MethodHandles; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -48,6 +49,85 @@ public final class InstanceOf extends AbstractExpression { + /** + * A component of a record deconstruction pattern (Java 21+ record patterns), e.g. each of + * {@code int x}, {@code int y} in {@code case Point(int x, int y) ->}. + * + *

A component is either a simple {@link Binding} (a type-pattern variable) or another + * nested {@link Record} deconstruction, recursively. + */ + public sealed interface Pattern + permits Pattern.Binding, Pattern.Record { + + /** + * The type this component's value is checked/deconstructed as. + * + * @return the {@link TypeUsage} + */ + TypeUsage type(); + + /** + * A simple binding component, e.g. {@code int x}. + * + * @param type the declared type of the binding + * @param name the binding variable name + */ + record Binding(TypeUsage type, String name) implements Pattern { + + @Unmarshal + public Binding(final Marshaller marshaller, + final Marshalled type, + final String name) { + this(marshaller.unmarshal(type), name); + } + + @Marshal + public void destructor(final Marshaller marshaller, + final Out> type, + final Out name) { + type.set(marshaller.marshal(this.type)); + name.set(this.name); + } + + static { + Marshalling.register(Binding.class, MethodHandles.lookup()); + } + } + + /** + * A nested record-deconstruction component, e.g. {@code Point(int x, int y)} nested + * inside another deconstruction pattern. + * + * @param type the deconstructed record type + * @param components the nested component {@link Pattern}s, in declaration order + */ + record Record(TypeUsage type, List components) implements Pattern { + + public Record { + components = List.copyOf(components); + } + + @Unmarshal + public Record(final Marshaller marshaller, + final Marshalled type, + final Stream> components) { + this(marshaller.unmarshal(type), components.map(marshaller::unmarshal).toList()); + } + + @Marshal + public void destructor(final Marshaller marshaller, + final Out> type, + final Out>> components) { + type.set(marshaller.marshal(this.type)); + components.set(this.components.stream().map(marshaller::marshal)); + } + + static { + Marshalling.register(Record.class, MethodHandles.lookup()); + } + } + } + /** * The expression being tested. */ @@ -63,13 +143,21 @@ public final class InstanceOf */ private final Optional bindingVariable; + /** + * The nested component {@link Pattern}s of a record-deconstruction pattern + * (Java 21+), empty unless this {@link InstanceOf} represents one. + */ + private final List components; + private InstanceOf(final Expression expression, final TypeUsage type, - final Optional bindingVariable) { + final Optional bindingVariable, + final List components) { super(Objects.requireNonNull(expression, "expression must not be null").codeModel()); this.expression = expression; this.type = Objects.requireNonNull(type, "type must not be null"); this.bindingVariable = bindingVariable == null ? Optional.empty() : bindingVariable; + this.components = components == null ? List.of() : List.copyOf(components); } @Unmarshal @@ -78,11 +166,13 @@ public InstanceOf(@Bound final CodeModel codeModel, final Stream> traits, final Marshalled expression, final Marshalled type, - final Optional bindingVariable) { + final Optional bindingVariable, + final Stream> components) { super(codeModel, marshaller, traits); this.expression = marshaller.unmarshal(expression); this.type = marshaller.unmarshal(type); this.bindingVariable = bindingVariable == null ? Optional.empty() : bindingVariable; + this.components = components == null ? List.of() : components.map(marshaller::unmarshal).toList(); } @Marshal @@ -90,11 +180,13 @@ public void destructor(final Marshaller marshaller, final Out>> traits, final Out> expression, final Out> type, - final Out> bindingVariable) { + final Out> bindingVariable, + final Out>> components) { super.destructor(marshaller, traits); expression.set(marshaller.marshal(this.expression)); type.set(marshaller.marshal(this.type)); bindingVariable.set(this.bindingVariable); + components.set(this.components.stream().map(marshaller::marshal)); } /** @@ -124,9 +216,29 @@ public Optional bindingVariable() { return this.bindingVariable; } + /** + * Obtains the nested component {@link Pattern}s of a record-deconstruction pattern + * (Java 21+ record patterns), e.g. {@code int x}, {@code int y} in + * {@code case Point(int x, int y) ->}. Empty unless this {@link InstanceOf} represents a + * deconstruction pattern. + * + * @return the component {@link Pattern}s, in declaration order + */ + public List components() { + return this.components; + } + @Override public Stream compositeChildren() { - return Stream.of(expression, type); + return Stream.concat(Stream.of(expression, type), components.stream().flatMap(InstanceOf::patternTypes)); + } + + private static Stream patternTypes(final Pattern pattern) { + return switch (pattern) { + case Pattern.Binding(var type, var name) -> Stream.of(type); + case Pattern.Record(var type, var nested) -> + Stream.concat(Stream.of(type), nested.stream().flatMap(InstanceOf::patternTypes)); + }; } @Override @@ -135,6 +247,7 @@ public boolean equals(final Object object) { && Objects.equals(this.expression, other.expression) && Objects.equals(this.type, other.type) && Objects.equals(this.bindingVariable, other.bindingVariable) + && Objects.equals(this.components, other.components) && super.equals(other); } @@ -149,7 +262,7 @@ public boolean equals(final Object object) { public static InstanceOf of(final Expression expression, final TypeUsage type, final Optional bindingVariable) { - return new InstanceOf(expression, type, bindingVariable); + return new InstanceOf(expression, type, bindingVariable, List.of()); } /** @@ -160,7 +273,22 @@ public static InstanceOf of(final Expression expression, * @return a new {@link InstanceOf} */ public static InstanceOf of(final Expression expression, final TypeUsage type) { - return new InstanceOf(expression, type, Optional.empty()); + return new InstanceOf(expression, type, Optional.empty(), List.of()); + } + + /** + * Creates a record-deconstruction {@link InstanceOf} expression (Java 21+ record patterns), + * e.g. {@code case Point(int x, int y) ->}. + * + * @param expression the expression being tested + * @param type the deconstructed record {@link TypeUsage} + * @param components the nested component {@link Pattern}s, in declaration order + * @return a new {@link InstanceOf} + */ + public static InstanceOf ofDeconstruction(final Expression expression, + final TypeUsage type, + final List components) { + return new InstanceOf(expression, type, Optional.empty(), components); } static { diff --git a/jdk-codemodel/src/test/java/build/codemodel/jdk/SwitchPatternCaptureTests.java b/jdk-codemodel/src/test/java/build/codemodel/jdk/SwitchPatternCaptureTests.java index 07eb95b..9e35272 100644 --- a/jdk-codemodel/src/test/java/build/codemodel/jdk/SwitchPatternCaptureTests.java +++ b/jdk-codemodel/src/test/java/build/codemodel/jdk/SwitchPatternCaptureTests.java @@ -109,6 +109,72 @@ void shouldConvertGuardedPatternLabel() { assertThat(label.bindingVariable()).contains("i"); } + @Test + void shouldConvertDeconstructionPatternLabelWithComponentBindings() { + final var sw = switchStatementIn(""" + record Point(int x, int y) {} + switch (input) { + case Point(int x, int y) -> { + return "point:" + x + "," + y; + } + default -> { + return "none"; + } + } + """); + + final var patternCase = sw.cases().findFirst().orElseThrow(); + final var label = (InstanceOf) patternCase.labels().findFirst().orElseThrow(); + assertThat(label.checkedType().toString()).contains("Point"); + assertThat(label.bindingVariable()).isEmpty(); + + final var components = label.components(); + assertThat(components).hasSize(2); + + final var x = (InstanceOf.Pattern.Binding) components.get(0); + assertThat(x.name()).isEqualTo("x"); + assertThat(x.type().toString()).contains("int"); + + final var y = (InstanceOf.Pattern.Binding) components.get(1); + assertThat(y.name()).isEqualTo("y"); + assertThat(y.type().toString()).contains("int"); + } + + @Test + void shouldConvertNestedDeconstructionPatternComponents() { + final var sw = switchStatementIn(""" + record Point(int x, int y) {} + record Line(Point start, Point end) {} + switch (input) { + case Line(Point(int x1, int y1), Point end) -> { + return "line:" + x1 + "," + y1 + "," + end; + } + default -> { + return "none"; + } + } + """); + + final var patternCase = sw.cases().findFirst().orElseThrow(); + final var label = (InstanceOf) patternCase.labels().findFirst().orElseThrow(); + assertThat(label.checkedType().toString()).contains("Line"); + + final var components = label.components(); + assertThat(components).hasSize(2); + + final var start = (InstanceOf.Pattern.Record) components.get(0); + assertThat(start.type().toString()).contains("Point"); + assertThat(start.components()).hasSize(2); + final var x1 = (InstanceOf.Pattern.Binding) start.components().get(0); + assertThat(x1.name()).isEqualTo("x1"); + final var y1 = (InstanceOf.Pattern.Binding) start.components().get(1); + assertThat(y1.name()).isEqualTo("y1"); + + final var end = (InstanceOf.Pattern.Binding) components.get(1); + assertThat(end.name()).isEqualTo("end"); + assertThat(end.type().toString()).contains("Point"); + } + @Test void shouldNotDropCaseWhenLabelIsAPattern() { // Regression test: JCCase#getExpressions() filters to CONSTANTCASELABEL only, so a diff --git a/jdk-codemodel/src/test/java/build/codemodel/jdk/SymbolResolutionTests.java b/jdk-codemodel/src/test/java/build/codemodel/jdk/SymbolResolutionTests.java index 3976d71..e5ae256 100644 --- a/jdk-codemodel/src/test/java/build/codemodel/jdk/SymbolResolutionTests.java +++ b/jdk-codemodel/src/test/java/build/codemodel/jdk/SymbolResolutionTests.java @@ -448,4 +448,95 @@ public int bar(Object o) { assertThat(patternBinding.declaredType().toString()).contains("String"); assertThat(patternBinding.declaration()).contains(instanceOf); } + + @Test + void shouldResolveInstanceOfDeconstructionPatternComponentBinding() { + final var source = JavaFileObjects.forSourceString("com.example.Foo", """ + package com.example; + public class Foo { + record Point(int x, int y) {} + public int bar(Object o) { + if (o instanceof Point(int x, int y)) { + return x + y; + } + return 0; + } + } + """); + final var codeModel = JdkInitializerTests.runInternal( + new JdkInitializer(List.of(), List.of(), List.of(source))); + + final var typeName = codeModel.getEmptyModuleTypeName("com.example.Foo"); + final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow(); + final var method = descriptor.traits(MethodDescriptor.class) + .filter(m -> m.methodName().name().toString().equals("bar")) + .findFirst().orElseThrow(); + final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body(); + + final var ifStatement = (If) body.statements() + .filter(s -> s instanceof If) + .findFirst().orElseThrow(); + final var instanceOf = (InstanceOf) ((build.codemodel.jdk.expression.Parenthesized) ifStatement.condition()).inner(); + final var innerReturn = (Return) ((build.codemodel.imperative.Block) ifStatement.thenStatement()).statements() + .filter(s -> s instanceof Return) + .findFirst().orElseThrow(); + + final var addition = (build.codemodel.expression.Addition) innerReturn.expression().orElseThrow(); + final var x = (Identifier) addition.left(); + final var y = (Identifier) addition.right(); + + final var xSymbol = (Symbol.PatternBinding) x.getTrait(Symbol.class).orElseThrow(); + assertThat(xSymbol.declaration()).contains(instanceOf); + final var ySymbol = (Symbol.PatternBinding) y.getTrait(Symbol.class).orElseThrow(); + assertThat(ySymbol.declaration()).contains(instanceOf); + } + + @Test + void shouldResolveSwitchDeconstructionPatternComponentBinding() { + final var source = JavaFileObjects.forSourceString("com.example.Foo", """ + package com.example; + public class Foo { + record Point(int x, int y) {} + public int bar(Object o) { + switch (o) { + case Point(int x, int y) -> { + return x + y; + } + default -> { + return 0; + } + } + } + } + """); + final var codeModel = JdkInitializerTests.runInternal( + new JdkInitializer(List.of(), List.of(), List.of(source))); + + final var typeName = codeModel.getEmptyModuleTypeName("com.example.Foo"); + final var descriptor = codeModel.getTypeDescriptor(typeName).orElseThrow(); + final var method = descriptor.traits(MethodDescriptor.class) + .filter(m -> m.methodName().name().toString().equals("bar")) + .findFirst().orElseThrow(); + final var body = method.getTrait(MethodBodyDescriptor.class).orElseThrow().body(); + + final var switchStatement = (SwitchStatement) body.statements() + .filter(s -> s instanceof SwitchStatement) + .findFirst().orElseThrow(); + final var pointCase = switchStatement.cases() + .filter(c -> c.labels().findFirst().orElseThrow() instanceof InstanceOf) + .findFirst().orElseThrow(); + final var instanceOf = (InstanceOf) pointCase.labels().findFirst().orElseThrow(); + final var innerReturn = (Return) pointCase.statements() + .filter(s -> s instanceof Return) + .findFirst().orElseThrow(); + + final var addition = (build.codemodel.expression.Addition) innerReturn.expression().orElseThrow(); + final var x = (Identifier) addition.left(); + final var y = (Identifier) addition.right(); + + final var xSymbol = (Symbol.PatternBinding) x.getTrait(Symbol.class).orElseThrow(); + assertThat(xSymbol.declaration()).contains(instanceOf); + final var ySymbol = (Symbol.PatternBinding) y.getTrait(Symbol.class).orElseThrow(); + assertThat(ySymbol.declaration()).contains(instanceOf); + } }