Skip to content

Support Java 21 record patterns (#67) - #167

Merged
paulirwin merged 2 commits into
masterfrom
issue/67-record-patterns
Aug 16, 2026
Merged

Support Java 21 record patterns (#67)#167
paulirwin merged 2 commits into
masterfrom
issue/67-record-patterns

Conversation

@paulirwin

@paulirwin paulirwin commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Fixes #67.

Converts Java 21 record patterns (JEP 440) into the equivalent C# patterns.

What's supported

Java C#
o instanceof Point(int x, int y) o is Point (int x, int y)
o instanceof String s o is string s
case Point(int x, int y) -> case Point (int x, int y):
case Point(int x, int y) when x > y -> case Point (int x, int y) when x > y:
Line(Point(var ax, var ay), Point end) Line (Point (var ax, var ay), Point end)

Nested deconstruction, var components, and when guards all work, in instanceof tests as well as both switch expressions and switch statements.

Java's type patterns map onto C# declaration patterns and its record deconstruction patterns map onto C# positional patterns, so the conversion is largely structural. New PatternExpressionVisitor holds that logic so the instanceof and both switch paths share it.

Unblocking the parse

The issue was labelled blocked pending javaparser support. That's resolved — javaparser 3.27.0 (already referenced here) has RecordPatternExpr and TypePatternExpr.

However, the parser language level was pinned to JAVA_17, so record patterns failed at parse time with "Record patterns are not supported... the language level must be configured" before conversion was ever reached. This raises it to JAVA_21.

Drive-by fix: switch statement break handling

Adding pattern labels surfaced a latent bug. Java's arrow form never falls through, but only the default case was getting an implicit break. That was invisible while every arrow case was a constant, but an arrow-form pattern case emitted C# that fails to compile:

CS0163: Control cannot fall through from one case label ('case string s:') to another

Breaks are now added for every arrow-form entry, and suppressed when the section already ends in a jump statement. That last part also removes unreachable breaks the old code emitted after a return — the previous logic scanned all statements for a break rather than just the last one.

Colon-form entries are untouched and keep Java's explicit fallthrough semantics.

Testing

Java21RecordPatterns.java is wired into FullIntegrationTests, which compiles the generated C# with Roslyn, executes it, and asserts on the runtime output declared in the resource's /// - output: header. So the test verifies the patterns actually match correctly at runtime, not just that plausible-looking text was emitted.

The expected output is the verbatim output of running the Java source under a JDK, which pins the converted C# to Java's real behaviour rather than to a hand-written guess. (Writing it by hand got one value wrong; running the Java caught it.)

That's backed by 10 unit tests in ConvertRecordPatternTests covering the emitted syntax for each form, including the break/fallthrough behaviour.

Full suite: 329 passed, 0 failed (318 before this change).

Note on generics

Box<?>(String value) converts to Box<TWildcardTodo> (string value). That's this repo's existing placeholder for Java wildcards (TypeNameParser), not specific to record patterns, so it's left as-is and out of the test resource.

🤖 Generated with Claude Code

Converts Java 21 record patterns (JEP 440) into C# patterns:

- `instanceof` with a type or record pattern becomes a C# `is` pattern,
  so `o instanceof Point(int x, int y)` converts to a positional pattern.
- Pattern labels in switch expressions and switch statements convert to
  C# patterns, including nested deconstruction and `var` components.
- Java 21 `when` guards convert to C# `when` clauses.

The parser language level is raised from Java 17 to Java 21, without
which record patterns fail to parse at all.

Switch statement break handling is also corrected. Java's arrow form
never falls through, but previously only the default case received an
implicit `break`, so an arrow-form pattern case would emit C# that
fails to compile with CS0163. Breaks are now added for every arrow-form
entry and suppressed when the section already ends in a jump statement,
which additionally removes some unreachable breaks the old code emitted
after a `return`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulirwin
paulirwin marked this pull request as ready for review August 16, 2026 21:09
The record pattern resource was in GeneralSuccessfulConversionTest, which
only asserts the result is non-null. That catches conversion throwing but
says nothing about whether the emitted C# is correct.

Move it to FullIntegrationTests, which compiles the generated C# with
Roslyn, executes it, and asserts on the runtime output declared in the
resource's `/// - output:` header. The resource is rewritten as a runnable
program covering instanceof, switch expressions, both arrow and colon
switch statements, guards, and nested deconstruction.

The expected output is the verbatim output of the Java source run under a
JDK, so the test pins the converted C# to Java's actual behavior rather
than to a hand-written guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulirwin
paulirwin merged commit 5191335 into master Aug 16, 2026
5 checks passed
@paulirwin
paulirwin deleted the issue/67-record-patterns branch August 16, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Java 21 Record Patterns

1 participant