Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,15 @@ private Optional<Expression> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) ->}.
*
* <p>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<TypeUsage> type,
final String name) {
this(marshaller.unmarshal(type), name);
}

@Marshal
public void destructor(final Marshaller marshaller,
final Out<Marshalled<TypeUsage>> type,
final Out<String> 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<Pattern> components) implements Pattern {

public Record {
components = List.copyOf(components);
}

@Unmarshal
public Record(final Marshaller marshaller,
final Marshalled<TypeUsage> type,
final Stream<Marshalled<Pattern>> components) {
this(marshaller.unmarshal(type), components.map(marshaller::unmarshal).toList());
}

@Marshal
public void destructor(final Marshaller marshaller,
final Out<Marshalled<TypeUsage>> type,
final Out<Stream<Marshalled<Pattern>>> 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.
*/
Expand All @@ -63,13 +143,21 @@ public final class InstanceOf
*/
private final Optional<String> bindingVariable;

/**
* The nested component {@link Pattern}s of a record-deconstruction pattern
* (Java 21+), empty unless this {@link InstanceOf} represents one.
*/
private final List<Pattern> components;

private InstanceOf(final Expression expression,
final TypeUsage type,
final Optional<String> bindingVariable) {
final Optional<String> bindingVariable,
final List<Pattern> 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
Expand All @@ -78,23 +166,27 @@ public InstanceOf(@Bound final CodeModel codeModel,
final Stream<Marshalled<Trait>> traits,
final Marshalled<Expression> expression,
final Marshalled<TypeUsage> type,
final Optional<String> bindingVariable) {
final Optional<String> bindingVariable,
final Stream<Marshalled<Pattern>> 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
public void destructor(final Marshaller marshaller,
final Out<Stream<Marshalled<Trait>>> traits,
final Out<Marshalled<Expression>> expression,
final Out<Marshalled<TypeUsage>> type,
final Out<Optional<String>> bindingVariable) {
final Out<Optional<String>> bindingVariable,
final Out<Stream<Marshalled<Pattern>>> 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));
}

/**
Expand Down Expand Up @@ -124,9 +216,29 @@ public Optional<String> 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<Pattern> components() {
return this.components;
}

@Override
public Stream<? extends Composite> compositeChildren() {
return Stream.of(expression, type);
return Stream.concat(Stream.of(expression, type), components.stream().flatMap(InstanceOf::patternTypes));
}

private static Stream<TypeUsage> 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
Expand All @@ -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);
}

Expand All @@ -149,7 +262,7 @@ public boolean equals(final Object object) {
public static InstanceOf of(final Expression expression,
final TypeUsage type,
final Optional<String> bindingVariable) {
return new InstanceOf(expression, type, bindingVariable);
return new InstanceOf(expression, type, bindingVariable, List.of());
}

/**
Expand All @@ -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<Pattern> components) {
return new InstanceOf(expression, type, Optional.empty(), components);
}

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading