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)); + } +}