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

/// <summary>
/// Tests for Java 21 record patterns (JEP 440), which convert to C# positional patterns.
/// </summary>
public class ConvertRecordPatternTests
{
[Fact]
public void InstanceOf_Record_Pattern_Is_Converted_To_Positional_Pattern()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
public boolean test(Object obj) {
return obj instanceof Point(int x, int y) && x > y;
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("obj is Point (int x, int y)", parsed);
}

[Fact]
public void InstanceOf_Type_Pattern_Binds_The_Variable()
{
const string javaCode = """
package com.example;
public class Shapes {
public String test(Object obj) {
if (obj instanceof String s) {
return s;
}
return "";
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("obj is string s", parsed);
}

[Fact]
public void InstanceOf_Without_Pattern_Remains_A_Plain_Type_Test()
{
const string javaCode = """
package com.example;
public class Shapes {
public boolean test(Object obj) {
return obj instanceof String;
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("obj is string", parsed);
Assert.DoesNotContain("is string s", parsed);
}

[Fact]
public void Nested_Record_Pattern_Is_Converted()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
record Line(Point start, Point end) {}
public boolean test(Object obj) {
return obj instanceof Line(Point(var ax, var ay), Point end);
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("obj is Line (Point (var ax, var ay), Point end)", parsed);
}

[Fact]
public void Switch_Expression_Record_Pattern_Is_Converted()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
public String test(Object obj) {
return switch (obj) {
case Point(int x, int y) -> "point";
default -> "other";
};
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("Point (int x, int y) =>", parsed);
}

[Fact]
public void Switch_Expression_Guard_Is_Converted_To_When_Clause()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
public String test(Object obj) {
return switch (obj) {
case Point(int x, int y) when x > y -> "wide";
default -> "other";
};
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("when x > y =>", parsed);
}

[Fact]
public void Switch_Statement_Record_Pattern_Is_Converted_With_Guard()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
public String test(Object obj) {
switch (obj) {
case Point(int x, int y) when x > y:
return "wide";
default:
return "other";
}
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("case Point (int x, int y)when x > y:", parsed);
}

/// <summary>
/// Java's arrow form never falls through, but C# requires each switch section to end with a
/// jump statement, so the conversion has to supply the break that Java leaves implicit.
/// </summary>
[Fact]
public void Arrow_Switch_Statement_Cases_Get_An_Implicit_Break()
{
const string javaCode = """
package com.example;
public class Shapes {
record Point(int x, int y) {}
public void test(Object obj) {
switch (obj) {
case Point(int x, int y) -> System.out.println("point");
default -> System.out.println("other");
}
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("case Point (int x, int y):", parsed);
// Both the pattern case and the default case need a break.
Assert.Equal(2, parsed.Split("break;").Length - 1);
}

/// <summary>
/// A break after a return would be unreachable, which C# rejects as an error.
/// </summary>
[Fact]
public void Cases_Ending_In_A_Jump_Statement_Do_Not_Get_An_Extra_Break()
{
const string javaCode = """
package com.example;
public class Shapes {
public String test(Object obj) {
switch (obj) {
case String s -> { return s; }
default -> { return "other"; }
}
}
}
""";

var parsed = Convert(javaCode);

Assert.DoesNotContain("break;", parsed);
}

/// <summary>
/// Colon-form entries keep Java's explicit fallthrough semantics, so no break is invented.
/// </summary>
[Fact]
public void Colon_Switch_Statement_Fallthrough_Is_Preserved()
{
const string javaCode = """
package com.example;
public class Shapes {
public String test(int i) {
switch (i) {
case 1:
case 2:
return "low";
default:
return "high";
}
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("case 1:", parsed);
Assert.Contains("case 2:", parsed);
Assert.DoesNotContain("break;", parsed);
}

private static string Convert(string javaCode)
{
var options = new JavaConversionOptions { IncludeComments = false };
options.WarningEncountered += (_, eventArgs)
=> throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}");

return JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";
}
}
1 change: 1 addition & 0 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
[InlineData("Resources/Java14SwitchExpressionsYieldAssign.java", true)]
[InlineData("Resources/Java15TextBlocks.java")]
[InlineData("Resources/Java16Records.java")]
[InlineData("Resources/Java21RecordPatterns.java")]
[InlineData("Resources/NewArrayLiteralBug.java")]
[InlineData("Resources/OctalLiteralBug.java")]
[InlineData("Resources/DeprecatedAnnotation.java")]
Expand Down
98 changes: 98 additions & 0 deletions JavaToCSharp.Tests/Resources/Java21RecordPatterns.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/// Expect:
/// - output: "point 1,2\nline 0,0\nstring hi\nunknown\nwide point\npoint 1,2\nline\nstring hi\nunknown\narrow point\narrow line\narrow unknown\ncolon point\ncolon hi\ncolon unknown\nnested 4\n"
package example;

// https://openjdk.org/jeps/440

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 record patterns.
public record Point(int x, int y) {
}

public record Line(Point start, Point end) {
}

// instanceof with type and record patterns, including a nested deconstruction.
public static String describeInstanceOf(Object obj) {
if (obj instanceof Point(int x, int y)) {
return "point " + x + "," + y;
}

if (obj instanceof Line(Point(var ax, var ay), Point end)) {
return "line " + ax + "," + ay;
}

if (obj instanceof String s) {
return "string " + s;
}

return "unknown";
}

// Switch expression with pattern labels and a guard.
public static String describeSwitchExpression(Object obj) {
return switch (obj) {
case Point(int x, int y) when x > y -> "wide point";
case Point(int x, int y) -> "point " + x + "," + y;
case Line(Point start, Point end) -> "line";
case String s -> "string " + s;
default -> "unknown";
};
}

// Arrow-form switch statement: each case has an implicit break in Java.
public static String describeArrowSwitch(Object obj) {
String result;

switch (obj) {
case Point(int x, int y) -> result = "arrow point";
case Line(Point start, Point end) -> result = "arrow line";
default -> result = "arrow unknown";
}

return result;
}

// Colon-form switch statement with pattern labels.
public static String describeColonSwitch(Object obj) {
switch (obj) {
case Point(int x, int y):
return "colon point";
case String s:
return "colon " + s;
default:
return "colon unknown";
}
}

public static void main(String[] args) {
Point point = new Point(1, 2);
Line line = new Line(new Point(0, 0), new Point(4, 5));

System.out.println(describeInstanceOf(point));
System.out.println(describeInstanceOf(line));
System.out.println(describeInstanceOf("hi"));
System.out.println(describeInstanceOf(42));

// The guard selects the first arm only when x > y.
System.out.println(describeSwitchExpression(new Point(9, 1)));
System.out.println(describeSwitchExpression(point));
System.out.println(describeSwitchExpression(line));
System.out.println(describeSwitchExpression("hi"));
System.out.println(describeSwitchExpression(42));

System.out.println(describeArrowSwitch(point));
System.out.println(describeArrowSwitch(line));
System.out.println(describeArrowSwitch(42));

System.out.println(describeColonSwitch(point));
System.out.println(describeColonSwitch("hi"));
System.out.println(describeColonSwitch(42));

// Bindings from a nested pattern are usable in the matched branch.
if (line instanceof Line(Point(var ax, var ay), Point(var bx, var by))) {
System.out.println("nested " + (bx - ax));
}
}
}
8 changes: 8 additions & 0 deletions JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public class InstanceOfExpressionVisitor : ExpressionVisitor<InstanceOfExpr>
return null;
}

// Java 16+ allows `x instanceof Shape s` and Java 21 `x instanceof Point(int a, int b)`.
// Both become C# `is` patterns; without a pattern this is a plain type test.
if (expr.getPattern().FromOptional<PatternExpr>() is { } pattern
&& PatternExpressionVisitor.ConvertPattern(context, pattern) is { } patternSyntax)
{
return SyntaxFactory.IsPatternExpression(exprSyntax, patternSyntax);
}

var type = TypeHelper.ConvertTypeOf(expr);

return SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, exprSyntax, SyntaxFactory.IdentifierName(type));
Expand Down
Loading
Loading