Support Java 16 record classes (#65) - #163
Merged
Merged
Conversation
Java records are converted to C# positional records: components become positional parameters, `implements` becomes the base list, and members are converted through the existing class member pipeline. Previously a top-level record was silently dropped, because the type dispatch in JavaToCSharpConverter had no fallback branch, and a nested record threw from BodyDeclarationVisitor's exact-type lookup. Add an `else` branch so any future unsupported type declaration warns instead of disappearing. Record component names are kept verbatim rather than capitalized. Bodies refer to components directly (`x + y`) and those references are not rewritten, so renaming the components would emit code that does not compile. Three Java constructs have no positional-record equivalent in C# and are skipped with a warning rather than emitting code that fails to compile: - Compact constructors, which have no C# counterpart. - Explicit canonical constructors, which collide with the generated primary constructor (CS0111). - Explicit component accessors, which collide with the generated property (CS0102). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A compact constructor's body runs against the constructor parameters and may
reassign them, with the component fields assigned from those parameters
afterwards. Verified against Java: given `y = y * 2` in the body, the stored
component holds the doubled value, so the body cannot be lowered to property
initializers, which would capture the original arguments.
Emit such records in non-positional form instead: explicit `{ get; init; }`
properties plus a constructor holding the converted compact body followed by
the component assignments. Value equality, `ToString`, and `with` still come
from the record itself; only the implicit `Deconstruct` is lost, and record
deconstruction is not converted today.
An explicit canonical constructor needs the same non-positional form to avoid
colliding with the generated primary constructor, and already assigns the
components itself, so it is now ported rather than dropped with a warning.
Java forbids declaring both a compact and a canonical constructor, so at most
one is ever present.
The integration fixture executes the converted code and its output matches
that of the equivalent Java program, including the reassigned component.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #65.
Converts Java 16 record classes to C# records.
Covers components,
implements, generics, static members, nested records, secondary constructors (emitted as: this(v, v)), and value equality.Also fixes a silent data-loss bug
The type dispatch in
JavaToCSharpConverterhad noelsebranch, so a top-level record was silently dropped — empty output, no warning, no error. Nested records instead threw fromBodyDeclarationVisitor's exact-type lookup. Records already parsed, since the language level isJAVA_17; nothing consumed them.The new
elsebranch means any future unsupported type declaration warns rather than disappearing.Compact constructors
A compact constructor's body runs against the constructor parameters and may reassign them, with the components assigned from those parameters afterwards. Verified against real Java:
So the body cannot be lowered to property initializers — those would capture the original arguments and silently produce
y=5. Such records are emitted in non-positional form instead:Value equality,
ToString, andwithstill come from the record. Only the implicitDeconstructis lost, and record deconstruction is not converted today.Explicit canonical constructors need the same non-positional form to avoid colliding with the generated primary constructor (CS0111), and already assign the components themselves, so they are ported too. Java forbids declaring both a compact and a canonical constructor, so at most one is ever present.
Known limitation
Explicit accessor overrides (
public int x()) are skipped with a warning. The component property exists in either form, so emitting the method would be a duplicate member (CS0102).Separately, and not addressed here: accessor call sites convert incorrectly.
MethodCallExpressionVisitorcapitalizes every method call unconditionally, so Javap.x()becomesp.X(), matching neither the property's name nor its kind. Distinguishing an accessor call from an ordinary one needs type resolution. Worth a follow-up issue.Component names are deliberately kept verbatim rather than capitalized to C# convention: bodies refer to components directly (
x + y) and those references are not rewritten, so renaming them would emit code that does not compile.Testing
Java16Records.javaruns through the integration harness that compiles and executes the generated C#. Its output is byte-identical to that of the equivalent Java program run underjava, including the reassigned component (range=1..50).Plus 12 focused unit tests in
ConvertRecordTests.cs. Full suite: 311 passing, clean build underTreatWarningsAsErrors.Two fixture details work around pre-existing behaviors unrelated to records, rather than expanding this PR's scope: members are explicitly
public(Java's package-private default maps to C#private, reproducible on a plain class), ande.getMessage()is avoided (it converts to.GetMessage()rather than.Message).🤖 Generated with Claude Code