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
212 changes: 212 additions & 0 deletions JavaToCSharp.Tests/ConvertRecordTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
namespace JavaToCSharp.Tests;

public class ConvertRecordTests
{
[Fact]
public void Record_Is_Converted_To_Positional_Record()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {}
""";

var parsed = Convert(javaCode);

Assert.Contains("public record Point(int x, int y)", parsed);
}

[Fact]
public void Record_Component_Names_Are_Preserved()
{
// Bodies reference components directly (`x + y`) and those references are not rewritten,
// so renaming the components would produce code that does not compile.
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public int sum() { return x + y; }
}
""";

var parsed = Convert(javaCode);

Assert.Contains("(int x, int y)", parsed);
Assert.Contains("return x + y;", parsed);
}

[Fact]
public void Record_Implements_Interface()
{
const string javaCode = """
package com.example;
public record Circle(int radius) implements Shape {}
""";

var parsed = Convert(javaCode);

Assert.Contains("public record Circle(int radius) : Shape", parsed);
}

[Fact]
public void Generic_Record_Emits_Type_Parameters()
{
const string javaCode = """
package com.example;
public record Labeled<T>(String label, T value) {}
""";

var parsed = Convert(javaCode);

Assert.Contains("public record Labeled<T>(string label, T value)", parsed);
}

[Fact]
public void Nested_Record_Is_Converted()
{
const string javaCode = """
package com.example;
public class Holder {
public record Inner(int a) {}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("public record Inner(int a)", parsed);
}

[Fact]
public void Record_Static_Member_Is_Converted()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public static final Point ORIGIN = new Point(0, 0);
}
""";

var parsed = Convert(javaCode);

Assert.Contains("public static readonly Point ORIGIN", parsed);
}

[Fact]
public void Secondary_Constructor_Delegates_To_Primary_Constructor()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public Point(int v) { this(v, v); }
}
""";

var parsed = Convert(javaCode);

Assert.Contains("public Point(int v) : this(v, v)", parsed);
}

[Fact]
public void Compact_Constructor_Is_Lowered_To_An_Explicit_Constructor()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public Point {
if (x < 0) throw new IllegalArgumentException("neg");
}
}
""";

var warnings = new List<string>();
var parsed = Convert(javaCode, NewOptions(warnings));

Assert.Empty(warnings);
// The record drops its positional parameter list in favor of explicit properties.
Assert.DoesNotContain("record Point(", parsed);
Assert.Contains("public int x { get; init; }", parsed);
Assert.Contains("public Point(int x, int y)", parsed);
Assert.Contains("throw new ArgumentException(\"neg\")", parsed);
}

[Fact]
public void Compact_Constructor_Assigns_Components_After_Its_Body()
{
// Java runs the compact body against the parameters and assigns the fields afterwards, so
// a reassignment in the body must be reflected in the stored component value.
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public Point {
y = y * 2;
}
}
""";

var parsed = Convert(javaCode);

var bodyIndex = parsed.IndexOf("y = y * 2;", StringComparison.Ordinal);
var assignIndex = parsed.IndexOf("this.y = y;", StringComparison.Ordinal);

Assert.True(bodyIndex >= 0, "compact constructor body should be emitted");
Assert.True(assignIndex > bodyIndex, "component assignment should follow the compact body");
}

[Fact]
public void Canonical_Constructor_Is_Ported_Using_Explicit_Properties()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public Point(int x, int y) { this.x = x; this.y = y; }
}
""";

var warnings = new List<string>();
var parsed = Convert(javaCode, NewOptions(warnings));

Assert.Empty(warnings);
// A positional record would reject a same-signature constructor (CS0111).
Assert.DoesNotContain("record Point(", parsed);
Assert.Contains("public int x { get; init; }", parsed);
Assert.Contains("public Point(int x, int y)", parsed);
}

[Fact]
public void Explicit_Accessor_Warns_Because_It_Conflicts_With_Generated_Property()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {
public int x() { return Math.abs(x); }
}
""";

var warnings = new List<string>();
var parsed = Convert(javaCode, NewOptions(warnings));

Assert.Contains(warnings, w => w.Contains("Accessor `x()`"));
Assert.DoesNotContain("int X()", parsed);
}

[Fact]
public void Record_Without_Members_Ends_With_Semicolon()
{
const string javaCode = """
package com.example;
public record Point(int x, int y) {}
""";

var parsed = Convert(javaCode);

Assert.Contains("public record Point(int x, int y);", parsed);
}

private static JavaConversionOptions NewOptions(List<string>? warnings = null)
{
var options = new JavaConversionOptions { IncludeComments = false };
options.WarningEncountered += (_, eventArgs) => warnings?.Add(eventArgs.Message);
return options;
}

private static string Convert(string javaCode, JavaConversionOptions? options = null)
=> JavaToCSharpConverter.ConvertText(javaCode, options ?? NewOptions()) ?? "";
}
1 change: 1 addition & 0 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
[InlineData("Resources/Java14SwitchExpressionsYieldReturn.java", true)]
[InlineData("Resources/Java14SwitchExpressionsYieldAssign.java", true)]
[InlineData("Resources/Java15TextBlocks.java")]
[InlineData("Resources/Java16Records.java")]
[InlineData("Resources/NewArrayLiteralBug.java")]
[InlineData("Resources/OctalLiteralBug.java")]
[InlineData("Resources/DeprecatedAnnotation.java")]
Expand Down
69 changes: 69 additions & 0 deletions JavaToCSharp.Tests/Resources/Java16Records.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// Expect:
/// - output: "1, 2\n3\nsame=True\ndiff=False\norigin=0\nCircle r=2\nlabel=P\nrange=1..50\ncaught=yes\n"
package example;

// https://docs.oracle.com/en/java/javase/16/language/records.html

interface Shape {
public String describe();
}

record Circle(int radius) implements Shape {
public String describe() {
return "Circle r=" + radius;
}
}

public class Program {
// Members are declared public because Java's package-private default maps to C# private,
// which is a pre-existing converter behavior unrelated to records.
public record Point(int x, int y) {
public static final Point ORIGIN = new Point(0, 0);

public int sum() {
return x + y;
}
}

public record Labeled<T>(String label, T value) {
}

// The compact constructor validates and normalizes the components. Its body runs against the
// parameters, and the components are assigned from them afterwards.
public record Range(int low, int high) {
public Range {
if (low > high) {
throw new IllegalArgumentException("low > high");
}
high = high * 10;
}
}

public static void main(String[] args) {
Point p = new Point(1, 2);
System.out.println(p.x + ", " + p.y);
System.out.println(p.sum());

// Records have value equality in both languages.
System.out.println("same=" + p.equals(new Point(1, 2)));
System.out.println("diff=" + p.equals(new Point(3, 4)));

System.out.println("origin=" + Point.ORIGIN.sum());

Shape s = new Circle(2);
System.out.println(s.describe());

Labeled<Integer> labeled = new Labeled<Integer>("P", 42);
System.out.println("label=" + labeled.label);

Range r = new Range(1, 5);
System.out.println("range=" + r.low + ".." + r.high);

try {
new Range(9, 2);
System.out.println("caught=no");
} catch (IllegalArgumentException e) {
System.out.println("caught=yes");
}
}
}
1 change: 1 addition & 0 deletions JavaToCSharp/Declarations/BodyDeclarationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ static BodyDeclarationVisitor()
{ typeof(InitializerDeclaration), new InitializerDeclarationVisitor() },
{ typeof(ClassOrInterfaceDeclaration), new ClassOrInterfaceDeclarationVisitor() },
{ typeof(AnnotationDeclaration), new AnnotationDeclarationVisitor() },
{ typeof(RecordDeclaration), new RecordDeclarationVisitor() },
};
}

Expand Down
Loading
Loading