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
14 changes: 11 additions & 3 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
[InlineData("Resources/Java9PrivateInterfaceMethods.java")]
[InlineData("Resources/Java10TypeInference.java")]
[InlineData("Resources/Java14SwitchExpressions.java")]
[InlineData("Resources/Java14SwitchExpressionsYield.java", true)]
[InlineData("Resources/Java14SwitchExpressionsYieldReturn.java", true)]
[InlineData("Resources/Java14SwitchExpressionsYieldAssign.java", true)]
[InlineData("Resources/Java15TextBlocks.java")]
[InlineData("Resources/NewArrayLiteralBug.java")]
[InlineData("Resources/OctalLiteralBug.java")]
[InlineData("Resources/DeprecatedAnnotation.java")]
[InlineData("Resources/BooleanArrays.java")]
[InlineData("Resources/BinaryLiterals.java")]
[InlineData("Resources/NestedEnumStaticUsing.java")]
public void FullIntegrationTests(string filePath)
public void FullIntegrationTests(string filePath, bool allowWarnings = false)
{
var options = new JavaConversionOptions
{
Expand All @@ -84,8 +87,13 @@ public void FullIntegrationTests(string filePath)

options.AddUsing("System");

options.WarningEncountered += (_, eventArgs)
=> throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}");
options.WarningEncountered += (_, eventArgs) =>
{
if (!allowWarnings)
{
throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}");
}
};

var javaText = File.ReadAllText(filePath);

Expand Down
60 changes: 60 additions & 0 deletions JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYield.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// Expect:
/// - output: "9\n9\n8\n"
package example;

// https://docs.oracle.com/en/java/javase/14/language/switch-expressions.html#GUID-BA4F63E3-4823-43C6-A5F3-BAA4A2EF3ADC__GUID-4900EB1C-3832-4CB8-ACAE-A87675260B75

enum Day { SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; }

public class Program {
public static void main(String[] args) {
Day day = Day.WEDNESDAY;

// colon form with yield
int numLetters = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
yield 6;
case TUESDAY:
System.out.println(7);
yield 7;
case THURSDAY:
case SATURDAY:
System.out.println(8);
yield 8;
case WEDNESDAY:
yield 9;
default:
throw new IllegalStateException("Invalid day: " + day);
};
System.out.println(numLetters);

// arrow form with a block body and yield
int arrowLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> {
int nine = 9;
yield nine;
}
default -> throw new IllegalStateException("Invalid day: " + day);
};
System.out.println(arrowLetters);

// arrow form with a block body, yield, and multiple statements
Day saturday = Day.SATURDAY;
int blockLetters = switch (saturday) {
case WEDNESDAY -> 9;
default -> {
int eight = 4;
eight = eight * 2;
yield eight;
}
};
System.out.println(blockLetters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// Expect:
/// - output: "20\n99\n"
package example;

enum Mode { ON, OFF; }

public class Program {
public static void main(String[] args) {
int result = 0;
Mode mode = Mode.ON;

// assignment to an already-declared variable
result = switch (mode) {
case ON: {
int doubled = 10 * 2;
yield doubled;
}
default:
yield -1;
};
System.out.println(result);

// reassignment, exercising that the lowering can run more than once
mode = Mode.OFF;
result = switch (mode) {
case ON:
yield 1;
default: {
int big = 100;
yield big - 1;
}
};
System.out.println(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// Expect:
/// - output: "7\n42\n11\n"
package example;

enum Size { SMALL, MEDIUM, LARGE; }

public class Program {
// switch expression yielded directly from a return statement
static int describe(Size size) {
return switch (size) {
case SMALL: {
int base = 3;
yield base + 4;
}
case MEDIUM:
yield 42;
default: {
int a = 5;
int b = 6;
yield a + b;
}
};
}

public static void main(String[] args) {
System.out.println(describe(Size.SMALL));
System.out.println(describe(Size.MEDIUM));
System.out.println(describe(Size.LARGE));
}
}
90 changes: 90 additions & 0 deletions JavaToCSharp.Tests/SwitchExpressionLoweringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using com.github.javaparser;
using com.github.javaparser.ast.expr;
using JavaToCSharp.Expressions;

namespace JavaToCSharp.Tests;

public class SwitchExpressionLoweringTests
{
/// <summary>
/// Single-expression arms must keep using C# switch expressions rather than being lowered,
/// so that existing conversions are unaffected.
/// </summary>
[Theory]
[InlineData("switch (x) { case 1 -> 10; default -> 20; }")]
[InlineData("switch (x) { case 1 -> 10; default -> throw new RuntimeException(); }")]
// NOTE: the colon/yield form cannot be parsed by parseExpression, only in statement context,
// so it is covered by the integration tests rather than here.
public void SingleExpressionArms_ConvertToSwitchExpression(string javaExpr)
{
var csharp = Convert(javaExpr);

Assert.Contains("switch", csharp);
Assert.Contains("=>", csharp);
}

/// <summary>
/// Arms with more than one statement cannot be represented as a C# switch expression arm.
/// These are lowered by the statement visitors, so converting the expression alone throws.
/// </summary>
/// <summary>
/// In positions where the statements cannot be hoisted out — nested inside a larger expression —
/// the arm is emitted as an immediately-invoked lambda instead. The return type cannot be
/// inferred without a symbol solver, so a placeholder is emitted for the user to replace.
/// </summary>
/// <remarks>
/// Parsed as a whole file rather than via parseExpression, which does not recognise `yield`
/// as a yield statement outside of statement context.
/// </remarks>
[Fact]
public void MultiStatementArms_NestedInExpression_ConvertToInvokedLambda()
{
var warnings = new List<string>();
var options = new JavaConversionOptions { IncludeComments = false };
options.WarningEncountered += (_, e) => warnings.Add(e.Message);

var csharp = JavaToCSharpConverter.ConvertText(
"""
package example;
public class Program {
static int foo(int v) { return v; }
public static void main(String[] args) {
int x = 1;
foo(switch (x) { case 1 -> { int a = 1; yield a; } default -> 20; });
}
}
""", options);

Assert.NotNull(csharp);
Assert.Contains("Func<SPECIFY_ME>", csharp);
// yield becomes return inside the lambda
Assert.Contains("return a;", csharp);
// the lambda must actually be invoked, not merely constructed
Assert.Contains("))()", csharp);
// arms that are already a single expression are left alone
Assert.Contains("_ => 20", csharp);

Assert.Contains(warnings, w => w.Contains("SPECIFY_ME"));
}

/// <summary>
/// A label with no statements falls through to the next label. This previously crashed with an
/// index-out-of-range because the arm body was read before checking that one existed.
/// </summary>
[Fact]
public void FallthroughLabels_DoNotCrash()
{
var csharp = Convert("switch (x) { case 1, 2 -> 10; default -> 20; }");

Assert.Contains("=>", csharp);
}

private static string? Convert(string javaExpr, JavaConversionOptions? options = null)
{
var parseResult = new JavaParser().parseExpression(javaExpr);
var parsedExpr = parseResult.getResult().FromRequiredOptional<Expression>();
var context = new ConversionContext(options ?? new JavaConversionOptions());

return ExpressionVisitor.VisitExpression(context, parsedExpr)?.ToString();
}
}
20 changes: 20 additions & 0 deletions JavaToCSharp/ConversionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ public class ConversionContext(JavaConversionOptions options)

public string? LastTypeName { get; set; }

/// <summary>
/// Statements that must be emitted immediately before the statement currently being visited.
/// Used to lower constructs that cannot be expressed as a single statement, such as multi-statement
/// switch expressions. Drained by <see cref="Statements.StatementVisitor.VisitStatements"/>.
/// </summary>
internal List<StatementSyntax> PendingStatements { get; } = [];

/// <summary>
/// The identifier that a Java <c>yield</c> statement should assign to, when a multi-statement
/// switch expression has been lowered into a switch statement. Null when not inside such a lowering.
/// </summary>
internal string? YieldTarget { get; set; }

private int _uniqueLocalCounter;

/// <summary>
/// Creates a local variable name that will not collide with other generated locals.
/// </summary>
internal string CreateUniqueLocalName(string prefix) => $"__{prefix}{_uniqueLocalCounter++}";

/// <summary>
/// Records the new conversion state and raises <see cref="JavaConversionOptions.StateChanged"/>.
/// </summary>
Expand Down
66 changes: 64 additions & 2 deletions JavaToCSharp/Expressions/SwitchExpressionVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using com.github.javaparser;
using com.github.javaparser.ast.expr;
using com.github.javaparser.ast.stmt;
using JavaToCSharp.Statements;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
Expand Down Expand Up @@ -72,19 +74,79 @@ private static ExpressionSyntax GetArmExpressionSyntax(ConversionContext context
{
var statements = entry.getStatements().ToList<Statement>() ?? [];

if (statements.Count > 1)
if (statements.Count == 0)
{
throw new InvalidOperationException("Switch expressions with multiple statements are not supported");
throw new InvalidOperationException("Switch expression entry must contain at least one statement");
}

// Reaching here with multiple statements, a block, or a yield means the enclosing statement
// visitor could not hoist the statements out, i.e. the switch expression is nested inside a
// larger expression. Emit an immediately-invoked lambda so the arm can still hold statements.
// A block containing nothing but a yield is equivalent to that single expression.
if (statements is [BlockStmt onlyBlock]
&& onlyBlock.getStatements().ToList<Statement>() is [YieldStmt blockYield])
{
statements = [blockYield];
}

if (statements.Count > 1 || statements[0] is BlockStmt)
{
return GetArmInvokedLambdaSyntax(context, entry, statements);
}

var armExpr = statements[0] switch
{
ThrowStmt throwStmt => throwStmt.getExpression(),
ExpressionStmt exprStmt => exprStmt.getExpression(),
// A lone yield is just the value it yields.
YieldStmt yieldStmt => yieldStmt.getExpression(),
_ => throw new InvalidOperationException("Only throw and expression statements are supported in switch expressions")
};

return VisitExpression(context, armExpr)
?? throw new InvalidOperationException("Switch expression entry must contain a single expression statement");
}

/// <summary>
/// Builds <c>((Func&lt;SPECIFY_ME&gt;)(() =&gt; { ... }))()</c> for an arm whose statements cannot be
/// hoisted into the enclosing block. The return type cannot be inferred without a symbol solver,
/// so a placeholder is emitted for the user to replace.
/// </summary>
private static ExpressionSyntax GetArmInvokedLambdaSyntax(
ConversionContext context,
SwitchEntry entry,
List<Statement> statements)
{
context.Options.Warning(
"Switch expression arm with multiple statements is nested within another expression and was "
+ "converted to an invoked lambda. Replace SPECIFY_ME with the appropriate return type.",
entry.getBegin().FromRequiredOptional<Position>().line);

// Inside a lambda, Java's `yield` becomes `return`. A null YieldTarget selects that behaviour.
var previousTarget = context.YieldTarget;
context.YieldTarget = null;

List<StatementSyntax> body;

try
{
// The arrow form wraps the arm in a block; use its statements directly to avoid nesting.
body = statements is [BlockStmt block]
? StatementVisitor.VisitStatements(context, block.getStatements().ToList<Statement>())
: StatementVisitor.VisitStatements(context, statements);
}
finally
{
context.YieldTarget = previousTarget;
}

var lambda = ParenthesizedLambdaExpression().WithBlock(Block(body));

var funcType = GenericName(Identifier("Func"))
.WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList<TypeSyntax>(IdentifierName("SPECIFY_ME"))));

return InvocationExpression(
ParenthesizedExpression(
CastExpression(funcType, ParenthesizedExpression(lambda))));
}
}
Loading
Loading