diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs
index 8d121e1..c9663f0 100644
--- a/JavaToCSharp.Tests/IntegrationTests.cs
+++ b/JavaToCSharp.Tests/IntegrationTests.cs
@@ -67,6 +67,9 @@ 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")]
@@ -74,7 +77,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
[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
{
@@ -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);
diff --git a/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYield.java b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYield.java
new file mode 100644
index 0000000..1f838e0
--- /dev/null
+++ b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYield.java
@@ -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);
+ }
+}
diff --git a/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldAssign.java b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldAssign.java
new file mode 100644
index 0000000..41436bd
--- /dev/null
+++ b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldAssign.java
@@ -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);
+ }
+}
diff --git a/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldReturn.java b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldReturn.java
new file mode 100644
index 0000000..c110f6f
--- /dev/null
+++ b/JavaToCSharp.Tests/Resources/Java14SwitchExpressionsYieldReturn.java
@@ -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));
+ }
+}
diff --git a/JavaToCSharp.Tests/SwitchExpressionLoweringTests.cs b/JavaToCSharp.Tests/SwitchExpressionLoweringTests.cs
new file mode 100644
index 0000000..f409a07
--- /dev/null
+++ b/JavaToCSharp.Tests/SwitchExpressionLoweringTests.cs
@@ -0,0 +1,90 @@
+using com.github.javaparser;
+using com.github.javaparser.ast.expr;
+using JavaToCSharp.Expressions;
+
+namespace JavaToCSharp.Tests;
+
+public class SwitchExpressionLoweringTests
+{
+ ///
+ /// Single-expression arms must keep using C# switch expressions rather than being lowered,
+ /// so that existing conversions are unaffected.
+ ///
+ [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);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// Parsed as a whole file rather than via parseExpression, which does not recognise `yield`
+ /// as a yield statement outside of statement context.
+ ///
+ [Fact]
+ public void MultiStatementArms_NestedInExpression_ConvertToInvokedLambda()
+ {
+ var warnings = new List();
+ 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", 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"));
+ }
+
+ ///
+ /// 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.
+ ///
+ [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();
+ var context = new ConversionContext(options ?? new JavaConversionOptions());
+
+ return ExpressionVisitor.VisitExpression(context, parsedExpr)?.ToString();
+ }
+}
diff --git a/JavaToCSharp/ConversionContext.cs b/JavaToCSharp/ConversionContext.cs
index 1360bba..fbc4fca 100644
--- a/JavaToCSharp/ConversionContext.cs
+++ b/JavaToCSharp/ConversionContext.cs
@@ -18,6 +18,26 @@ public class ConversionContext(JavaConversionOptions options)
public string? LastTypeName { get; set; }
+ ///
+ /// 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 .
+ ///
+ internal List PendingStatements { get; } = [];
+
+ ///
+ /// The identifier that a Java yield statement should assign to, when a multi-statement
+ /// switch expression has been lowered into a switch statement. Null when not inside such a lowering.
+ ///
+ internal string? YieldTarget { get; set; }
+
+ private int _uniqueLocalCounter;
+
+ ///
+ /// Creates a local variable name that will not collide with other generated locals.
+ ///
+ internal string CreateUniqueLocalName(string prefix) => $"__{prefix}{_uniqueLocalCounter++}";
+
///
/// Records the new conversion state and raises .
///
diff --git a/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs
index 5087195..1a11764 100644
--- a/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs
+++ b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs
@@ -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;
@@ -72,19 +74,79 @@ private static ExpressionSyntax GetArmExpressionSyntax(ConversionContext context
{
var statements = entry.getStatements().ToList() ?? [];
- 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() 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");
}
+
+ ///
+ /// Builds ((Func<SPECIFY_ME>)(() => { ... }))() 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.
+ ///
+ private static ExpressionSyntax GetArmInvokedLambdaSyntax(
+ ConversionContext context,
+ SwitchEntry entry,
+ List 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().line);
+
+ // Inside a lambda, Java's `yield` becomes `return`. A null YieldTarget selects that behaviour.
+ var previousTarget = context.YieldTarget;
+ context.YieldTarget = null;
+
+ List 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())
+ : StatementVisitor.VisitStatements(context, statements);
+ }
+ finally
+ {
+ context.YieldTarget = previousTarget;
+ }
+
+ var lambda = ParenthesizedLambdaExpression().WithBlock(Block(body));
+
+ var funcType = GenericName(Identifier("Func"))
+ .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(IdentifierName("SPECIFY_ME"))));
+
+ return InvocationExpression(
+ ParenthesizedExpression(
+ CastExpression(funcType, ParenthesizedExpression(lambda))));
+ }
}
diff --git a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs
index 12635f6..ab9dfdf 100644
--- a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs
+++ b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs
@@ -1,4 +1,5 @@
-using com.github.javaparser.ast.body;
+using com.github.javaparser;
+using com.github.javaparser.ast.body;
using com.github.javaparser.ast.expr;
using com.github.javaparser.ast.stmt;
using JavaToCSharp.Expressions;
@@ -19,6 +20,24 @@ public class ExpressionStatementVisitor : StatementVisitor
return VisitVariableDeclarationStatement(context, expr);
}
+ // `target = switch (...) { ... yield ... }` lowers to a switch statement assigning to the target.
+ if (expression is AssignExpr { } assignExpr
+ && assignExpr.getValue() is SwitchExpr assignedSwitch
+ && assignExpr.getOperator() == AssignExpr.Operator.ASSIGN
+ && SwitchExpressionLowering.RequiresLowering(assignedSwitch))
+ {
+ var targetSyntax = ExpressionVisitor.VisitExpression(context, assignExpr.getTarget());
+
+ if (targetSyntax is IdentifierNameSyntax identifier)
+ {
+ context.Options.Warning(
+ "Multi-statement switch expression converted to a switch statement. Review the generated code carefully.",
+ assignedSwitch.getBegin().FromRequiredOptional().line);
+
+ return SwitchExpressionLowering.Lower(context, assignedSwitch, identifier.Identifier.Text);
+ }
+ }
+
var expressionSyntax = ExpressionVisitor.VisitExpression(context, expression);
return expressionSyntax is null ? null : SyntaxFactory.ExpressionStatement(expressionSyntax);
@@ -30,6 +49,7 @@ private static StatementSyntax VisitVariableDeclarationStatement(ConversionConte
int? arrayRank = null;
var variables = new List();
+ var loweredSwitches = new List();
var variableDeclarators = varExpr.getVariables()?.ToList() ?? [];
@@ -57,6 +77,23 @@ private static StatementSyntax VisitVariableDeclarationStatement(ConversionConte
var initExpr = item.getInitializer().FromOptional();
+ if (initExpr is SwitchExpr switchExpr && SwitchExpressionLowering.RequiresLowering(switchExpr))
+ {
+ // C# switch expressions cannot contain multiple statements per arm, so lower this
+ // into a switch statement that assigns to the declared variable. The declaration is
+ // emitted without an initializer and the switch statement precedes the current statement.
+ context.Options.Warning(
+ "Multi-statement switch expression converted to a switch statement. Review the generated code carefully.",
+ switchExpr.getBegin().FromRequiredOptional().line);
+
+ variables.Add(SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(name)));
+
+ loweredSwitches.Add(
+ SwitchExpressionLowering.Lower(context, switchExpr, TypeHelper.EscapeIdentifier(name)));
+
+ continue;
+ }
+
if (initExpr is not null)
{
var initSyntax = ExpressionVisitor.VisitExpression(context, initExpr);
@@ -74,7 +111,19 @@ private static StatementSyntax VisitVariableDeclarationStatement(ConversionConte
var typeSyntax = TypeHelper.ConvertTypeSyntax(commonType, arrayRank ?? 0);
- return SyntaxFactory.LocalDeclarationStatement(
+ var declaration = SyntaxFactory.LocalDeclarationStatement(
SyntaxFactory.VariableDeclaration(typeSyntax, SyntaxFactory.SeparatedList(variables, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), variables.Count - 1))));
+
+ if (loweredSwitches.Count == 0)
+ {
+ return declaration;
+ }
+
+ // The declaration must precede the switch statements that assign to it, so emit it first
+ // and return the final switch as this statement's syntax.
+ context.PendingStatements.Add(declaration);
+ context.PendingStatements.AddRange(loweredSwitches[..^1]);
+
+ return loweredSwitches[^1];
}
}
diff --git a/JavaToCSharp/Statements/ReturnStatementVisitor.cs b/JavaToCSharp/Statements/ReturnStatementVisitor.cs
index 3b92de1..40c1350 100644
--- a/JavaToCSharp/Statements/ReturnStatementVisitor.cs
+++ b/JavaToCSharp/Statements/ReturnStatementVisitor.cs
@@ -1,4 +1,6 @@
-using com.github.javaparser.ast.expr;
+using com.github.javaparser;
+using com.github.javaparser.ast.body;
+using com.github.javaparser.ast.expr;
using com.github.javaparser.ast.stmt;
using JavaToCSharp.Expressions;
using Microsoft.CodeAnalysis.CSharp;
@@ -17,8 +19,55 @@ public override StatementSyntax Visit(ConversionContext context, ReturnStmt retu
return SyntaxFactory.ReturnStatement(); // i.e. "return" in a void method
}
+ if (expr is SwitchExpr switchExpr && SwitchExpressionLowering.RequiresLowering(switchExpr))
+ {
+ // C# switch expressions cannot contain multiple statements per arm. Lower into a switch
+ // statement over a temporary, then return that temporary.
+ context.Options.Warning(
+ "Multi-statement switch expression converted to a switch statement. Review the generated code carefully.",
+ switchExpr.getBegin().FromRequiredOptional().line);
+
+ var temp = context.CreateUniqueLocalName("switchResult");
+
+ // `var` needs an initializer, so use the enclosing method's declared return type.
+ var methodDecl = FindEnclosingMethod(returnStmt)
+ ?? throw new InvalidOperationException(
+ "Multi-statement switch expression in a return statement outside of a method is not supported");
+
+ var returnTypeNode = methodDecl.getType();
+ var tempType = TypeHelper.ConvertTypeSyntax(returnTypeNode, returnTypeNode.getArrayLevel());
+
+ context.PendingStatements.Add(
+ SyntaxFactory.LocalDeclarationStatement(
+ SyntaxFactory.VariableDeclaration(
+ tempType,
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.VariableDeclarator(temp)))));
+
+ context.PendingStatements.Add(SwitchExpressionLowering.Lower(context, switchExpr, temp));
+
+ return SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName(temp));
+ }
+
var exprSyntax = ExpressionVisitor.VisitExpression(context, expr);
return SyntaxFactory.ReturnStatement(exprSyntax);
}
+
+ private static MethodDeclaration? FindEnclosingMethod(com.github.javaparser.ast.Node node)
+ {
+ var current = node.getParentNode().FromOptional();
+
+ while (current is not null)
+ {
+ if (current is MethodDeclaration methodDecl)
+ {
+ return methodDecl;
+ }
+
+ current = current.getParentNode().FromOptional();
+ }
+
+ return null;
+ }
}
diff --git a/JavaToCSharp/Statements/StatementVisitor.cs b/JavaToCSharp/Statements/StatementVisitor.cs
index 196cb9c..36e3737 100644
--- a/JavaToCSharp/Statements/StatementVisitor.cs
+++ b/JavaToCSharp/Statements/StatementVisitor.cs
@@ -38,6 +38,7 @@ static StatementVisitor()
{ typeof(ThrowStmt), new ThrowStatementVisitor() },
{ typeof(TryStmt), new TryStatementVisitor() },
{ typeof(WhileStmt), new WhileStatementVisitor() },
+ { typeof(YieldStmt), new YieldStatementVisitor() },
{ typeof(EmptyStmt), new EmptyStatementVisitor() },
{ typeof(LocalClassDeclarationStmt), new TypeDeclarationStatementVisitor() }
};
@@ -46,11 +47,38 @@ static StatementVisitor()
protected abstract StatementSyntax? Visit(ConversionContext context, Statement statement);
public static List VisitStatements(ConversionContext context, IEnumerable? statements)
- => statements is null
- ? []
- : statements.Select(statement => VisitStatement(context, statement))
- .OfType() // filter out nulls
- .ToList();
+ {
+ if (statements is null)
+ {
+ return [];
+ }
+
+ var results = new List();
+
+ // Statements pending from an outer statement list must not be drained here; this list is
+ // only responsible for the statements its own children produce.
+ var outerPending = context.PendingStatements.Count;
+
+ foreach (var statement in statements)
+ {
+ var syntax = VisitStatement(context, statement);
+
+ // A visitor may have lowered part of this statement into statements that must precede it,
+ // for example a multi-statement switch expression becoming a switch statement.
+ if (context.PendingStatements.Count > outerPending)
+ {
+ results.AddRange(context.PendingStatements.Skip(outerPending));
+ context.PendingStatements.RemoveRange(outerPending, context.PendingStatements.Count - outerPending);
+ }
+
+ if (syntax is not null)
+ {
+ results.Add(syntax);
+ }
+ }
+
+ return results;
+ }
public static StatementSyntax? VisitStatement(ConversionContext context, Statement statement)
{
diff --git a/JavaToCSharp/Statements/SwitchExpressionLowering.cs b/JavaToCSharp/Statements/SwitchExpressionLowering.cs
new file mode 100644
index 0000000..5ac1e37
--- /dev/null
+++ b/JavaToCSharp/Statements/SwitchExpressionLowering.cs
@@ -0,0 +1,146 @@
+using com.github.javaparser.ast.expr;
+using com.github.javaparser.ast.stmt;
+using JavaToCSharp.Expressions;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace JavaToCSharp.Statements;
+
+///
+/// Lowers Java 14 multi-statement switch expressions (those using yield) into C# switch
+/// statements that assign into a target variable. C# switch expressions only permit a single
+/// expression per arm, so multi-statement arms cannot be represented directly.
+///
+internal static class SwitchExpressionLowering
+{
+ ///
+ /// Determines whether the given switch expression requires lowering to a switch statement,
+ /// i.e. whether any arm contains more than a single expression or throw.
+ ///
+ public static bool RequiresLowering(SwitchExpr expr)
+ {
+ foreach (var entry in expr.getEntries().ToList() ?? [])
+ {
+ var statements = entry.getStatements().ToList() ?? [];
+
+ // Zero statements is a fallthrough label in the colon form, which is representable.
+ if (statements.Count > 1)
+ {
+ return true;
+ }
+
+ if (statements.Count == 1 && statements[0] is not (ThrowStmt or ExpressionStmt))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Builds a switch statement equivalent to , assigning each arm's
+ /// yielded value to .
+ ///
+ public static StatementSyntax Lower(ConversionContext context, SwitchExpr expr, string target)
+ {
+ var selector = ExpressionVisitor.VisitExpression(context, expr.getSelector())
+ ?? throw new InvalidOperationException("Switch expression selector cannot be null");
+
+ var entries = expr.getEntries().ToList() ?? [];
+ var sections = new List();
+ var pendingLabels = new List();
+
+ var previousTarget = context.YieldTarget;
+ context.YieldTarget = target;
+
+ try
+ {
+ foreach (var entry in entries)
+ {
+ var labels = entry.getLabels().ToList() ?? [];
+
+ if (labels.Count == 0)
+ {
+ pendingLabels.Add(SyntaxFactory.DefaultSwitchLabel());
+ }
+ else
+ {
+ foreach (var label in labels)
+ {
+ var labelExpr = ExpressionVisitor.VisitExpression(context, label)
+ ?? throw new InvalidOperationException("Switch expression label must contain an expression");
+
+ pendingLabels.Add(SyntaxFactory.CaseSwitchLabel(labelExpr));
+ }
+ }
+
+ var statements = entry.getStatements().ToList() ?? [];
+
+ // A label with no statements falls through to the next entry's labels.
+ if (statements.Count == 0)
+ {
+ continue;
+ }
+
+ var body = BuildSectionBody(context, statements, target);
+
+ sections.Add(SyntaxFactory.SwitchSection(
+ SyntaxFactory.List(pendingLabels),
+ SyntaxFactory.List(body)));
+
+ pendingLabels = [];
+ }
+ }
+ finally
+ {
+ context.YieldTarget = previousTarget;
+ }
+
+ return SyntaxFactory.SwitchStatement(selector, SyntaxFactory.List(sections));
+ }
+
+ private static List BuildSectionBody(
+ ConversionContext context,
+ List statements,
+ string target)
+ {
+ List body;
+
+ // The arrow form wraps the arm body in a block; flatten it so the assignment and break
+ // sit directly in the switch section rather than inside a nested scope.
+ if (statements is [BlockStmt block])
+ {
+ body = StatementVisitor.VisitStatements(context, block.getStatements().ToList());
+ }
+ else if (statements is [ExpressionStmt exprStmt])
+ {
+ // Arrow form with a bare expression: `case X -> value` yields that value.
+ var value = ExpressionVisitor.VisitExpression(context, exprStmt.getExpression())
+ ?? throw new InvalidOperationException("Switch expression arm must contain an expression");
+
+ body =
+ [
+ SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ SyntaxFactory.IdentifierName(target),
+ value))
+ ];
+ }
+ else
+ {
+ body = StatementVisitor.VisitStatements(context, statements);
+ }
+
+ if (!EndsControlFlow(body))
+ {
+ body.Add(SyntaxFactory.BreakStatement());
+ }
+
+ return body;
+ }
+
+ private static bool EndsControlFlow(List body)
+ => body.Count > 0 && body[^1] is BreakStatementSyntax or ReturnStatementSyntax or ThrowStatementSyntax;
+}
diff --git a/JavaToCSharp/Statements/YieldStatementVisitor.cs b/JavaToCSharp/Statements/YieldStatementVisitor.cs
new file mode 100644
index 0000000..5c229c2
--- /dev/null
+++ b/JavaToCSharp/Statements/YieldStatementVisitor.cs
@@ -0,0 +1,39 @@
+using com.github.javaparser.ast.expr;
+using com.github.javaparser.ast.stmt;
+using JavaToCSharp.Expressions;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace JavaToCSharp.Statements;
+
+///
+/// Converts a Java yield statement. Java's yield produces a value from a switch
+/// expression arm; the C# equivalent depends on how the enclosing switch expression was lowered,
+/// which is communicated via . When a target is set the
+/// value is assigned to it within a switch statement; otherwise the arm became an invoked lambda
+/// and the value is returned.
+///
+public class YieldStatementVisitor : StatementVisitor
+{
+ public override StatementSyntax Visit(ConversionContext context, YieldStmt yieldStmt)
+ {
+ var expr = yieldStmt.getExpression();
+ var exprSyntax = ExpressionVisitor.VisitExpression(context, expr)
+ ?? throw new InvalidOperationException("Yield statement must contain an expression");
+
+ var target = context.YieldTarget;
+
+ if (target is null)
+ {
+ // Inside an invoked lambda, yielding a value is a return.
+ return SyntaxFactory.ReturnStatement(exprSyntax);
+ }
+
+ // Assign to the temporary that receives the switch expression's value, then leave the section.
+ return SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ SyntaxFactory.IdentifierName(target),
+ exprSyntax));
+ }
+}