diff --git a/JavaToCSharp.Tests/ConvertLabeledBreakContinueTests.cs b/JavaToCSharp.Tests/ConvertLabeledBreakContinueTests.cs new file mode 100644 index 0000000..24dc7dc --- /dev/null +++ b/JavaToCSharp.Tests/ConvertLabeledBreakContinueTests.cs @@ -0,0 +1,189 @@ +namespace JavaToCSharp.Tests; + +public class ConvertLabeledBreakContinueTests +{ + private const string NestedLoops = """ + package com.example; + public class Grid { + public void scan() { + outer: for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (j == 1) { + continue outer; + } + if (i == 3) { + break outer; + } + } + } + } + } + """; + + [Fact] + public void Labeled_Jumps_Use_CSharp15_Syntax_By_Default() + { + var warnings = new List(); + var parsed = Convert(NestedLoops, NewOptions(warnings)); + + Assert.Contains("outer:", parsed); + Assert.Contains("break outer;", parsed); + Assert.Contains("continue outer;", parsed); + Assert.DoesNotContain("goto", parsed); + Assert.Empty(warnings); + } + + [Fact] + public void Labeled_Jumps_Fall_Back_To_Goto_When_Option_Disabled() + { + var warnings = new List(); + var parsed = Convert(NestedLoops, NewOptions(warnings, useLabeledJumps: false)); + + Assert.Contains("goto outer_break;", parsed); + Assert.Contains("goto outer_continue;", parsed); + Assert.Contains("outer_break:", parsed); + Assert.Contains("outer_continue:", parsed); + Assert.DoesNotContain("break outer;", parsed); + Assert.DoesNotContain("continue outer;", parsed); + Assert.Empty(warnings); + } + + [Fact] + public void Goto_Fallback_Emits_Continue_Target_Inside_Loop_And_Break_Target_After() + { + var parsed = Convert(NestedLoops, NewOptions(useLabeledJumps: false)); + + // The continue target must precede the break target: it belongs to the end of the loop body, + // while the break target must follow the loop entirely. + int continueTarget = parsed.IndexOf("outer_continue:", StringComparison.Ordinal); + int breakTarget = parsed.IndexOf("outer_break:", StringComparison.Ordinal); + + Assert.True(continueTarget > 0 && breakTarget > 0); + Assert.True(continueTarget < breakTarget); + } + + [Fact] + public void Goto_Fallback_Only_Emits_Targets_That_Are_Used() + { + const string breakOnly = """ + package com.example; + public class Grid { + public void scan() { + outer: for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (j == 1) { + break outer; + } + } + } + } + } + """; + + var parsed = Convert(breakOnly, NewOptions(useLabeledJumps: false)); + + Assert.Contains("outer_break:", parsed); + Assert.DoesNotContain("outer_continue", parsed); + } + + [Fact] + public void Unlabeled_Jumps_Are_Unaffected() + { + const string unlabeled = """ + package com.example; + public class Grid { + public void scan() { + for (int i = 0; i < 4; i++) { + if (i == 1) { + continue; + } + if (i == 3) { + break; + } + } + } + } + """; + + foreach (bool useLabeledJumps in new[] { true, false }) + { + var parsed = Convert(unlabeled, NewOptions(useLabeledJumps: useLabeledJumps)); + + Assert.Contains("break;", parsed); + Assert.Contains("continue;", parsed); + Assert.DoesNotContain("goto", parsed); + } + } + + [Fact] + public void Labeled_Continue_On_While_Loop_Is_Lowered_Into_Loop_Body() + { + const string whileLoop = """ + package com.example; + public class Counter { + public void count() { + int k = 0; + loop: while (k < 5) { + k++; + if (k < 5) { + continue loop; + } + break loop; + } + } + } + """; + + var parsed = Convert(whileLoop, NewOptions(useLabeledJumps: false)); + + int loopEnd = parsed.IndexOf("loop_break:", StringComparison.Ordinal); + int continueTarget = parsed.IndexOf("loop_continue:", StringComparison.Ordinal); + + // The continue target belongs inside the while body, so it appears before the break target. + Assert.True(continueTarget > 0); + Assert.True(continueTarget < loopEnd); + } + + [Fact] + public void Nested_Labels_Each_Get_Their_Own_Targets() + { + const string nestedLabels = """ + package com.example; + public class Grid { + public void scan() { + outer: for (int i = 0; i < 4; i++) { + inner: for (int j = 0; j < 4; j++) { + if (j == 1) { + break inner; + } + if (i == 3) { + break outer; + } + } + } + } + } + """; + + var parsed = Convert(nestedLabels, NewOptions(useLabeledJumps: false)); + + Assert.Contains("goto inner_break;", parsed); + Assert.Contains("goto outer_break;", parsed); + Assert.Contains("inner_break:", parsed); + Assert.Contains("outer_break:", parsed); + } + + private static JavaConversionOptions NewOptions(List? warnings = null, bool useLabeledJumps = true) + { + var options = new JavaConversionOptions + { + IncludeComments = false, + UseLabeledBreakAndContinue = useLabeledJumps, + }; + options.WarningEncountered += (_, eventArgs) => warnings?.Add(eventArgs.Message); + return options; + } + + private static string Convert(string javaCode, JavaConversionOptions? options = null) + => JavaToCSharpConverter.ConvertText(javaCode, options ?? NewOptions()) ?? ""; +} diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index 58dfd83..9b6eb34 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -87,12 +87,26 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/Java16LocalRecords.java")] [InlineData("Resources/InstanceInitializers.java")] [InlineData("Resources/StaticImports.java")] + [InlineData("Resources/LabeledBreakContinue.java")] public void FullIntegrationTests(string filePath, bool allowWarnings = false) + => RunFullIntegrationTest(filePath, allowWarnings); + + /// + /// Runs the labeled break/continue sample through the goto fallback, to confirm it behaves + /// identically to the C# 15 labeled jumps covered by . + /// + [Theory] + [InlineData("Resources/LabeledBreakContinue.java")] + public void FullIntegrationTestsWithGotoFallback(string filePath) + => RunFullIntegrationTest(filePath, allowWarnings: false, useLabeledBreakAndContinue: false); + + private void RunFullIntegrationTest(string filePath, bool allowWarnings, bool useLabeledBreakAndContinue = true) { var options = new JavaConversionOptions { ConvertSystemOutToConsole = true, IncludeComments = false, + UseLabeledBreakAndContinue = useLabeledBreakAndContinue, }; options.AddUsing("System"); @@ -113,7 +127,10 @@ public void FullIntegrationTests(string filePath, bool allowWarnings = false) testOutputHelper.WriteLine(parsed); - var fileName = Path.GetFileNameWithoutExtension(filePath); + // The suffix keeps the two option variants in separate assemblies, because Assembly.LoadFile + // caches by path and would otherwise reuse the first variant's compiled output. + var fileName = Path.GetFileNameWithoutExtension(filePath) + + (useLabeledBreakAndContinue ? "" : "_Goto"); var assembly = CompileAssembly(fileName, parsed); var expectation = ParseExpectation(javaText); @@ -168,7 +185,10 @@ public void FullIntegrationTests(string filePath, bool allowWarnings = false) private static Assembly CompileAssembly(string assemblyName, string cSharpLanguageText) { - var syntaxTree = CSharpSyntaxTree.ParseText(cSharpLanguageText); + // Preview is required for the C# 15 features the converter emits, such as labeled break/continue. + var syntaxTree = CSharpSyntaxTree.ParseText( + cSharpLanguageText, + new CSharpParseOptions(LanguageVersion.Preview)); var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication) .WithOverflowChecks(true) diff --git a/JavaToCSharp.Tests/Resources/LabeledBreakContinue.java b/JavaToCSharp.Tests/Resources/LabeledBreakContinue.java new file mode 100644 index 0000000..3ded629 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/LabeledBreakContinue.java @@ -0,0 +1,42 @@ +/// - output: "0,0\n1,0\n2,0\nsearch=1,2\ndone\n" +package example; + +public class Program { + public static void main(String[] args) { + outer: for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (j == 1) { + continue outer; + } + if (i == 3) { + break outer; + } + System.out.println(i + "," + j); + } + } + + int foundRow = -1; + int foundCol = -1; + search: for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + if (row + col == 3) { + foundRow = row; + foundCol = col; + break search; + } + } + } + System.out.println("search=" + foundRow + "," + foundCol); + + int k = 0; + loop: while (k < 5) { + k++; + if (k < 5) { + continue loop; + } + break loop; + } + + System.out.println("done"); + } +} diff --git a/JavaToCSharp/ConversionContext.cs b/JavaToCSharp/ConversionContext.cs index 6b2d5ac..250e15f 100644 --- a/JavaToCSharp/ConversionContext.cs +++ b/JavaToCSharp/ConversionContext.cs @@ -42,6 +42,18 @@ public class ConversionContext(JavaConversionOptions options) /// internal string? YieldTarget { get; set; } + /// + /// Java labels targeted by a labeled break that was lowered to a goto. The labeled statement + /// visitor emits a matching target label after the loop for each one. + /// + internal ISet LabelsNeedingBreakTarget { get; } = new HashSet(); + + /// + /// Java labels targeted by a labeled continue that was lowered to a goto. The labeled + /// statement visitor emits a matching target label at the end of the loop body for each one. + /// + internal ISet LabelsNeedingContinueTarget { get; } = new HashSet(); + private int _uniqueLocalCounter; /// diff --git a/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs b/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs index c91ee90..c33074d 100644 --- a/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs @@ -242,8 +242,10 @@ public static ClassDeclarationSyntax VisitClassDeclaration(ConversionContext con { if (context.Options.UseClosedForSealedClasses) { - // Roslyn has no ClosedKeyword token, as `closed` is a C# 15 feature. - classSyntax = classSyntax.AddModifiers(SyntaxFactory.ParseToken("closed ")); + // The `closed` keyword is still marked experimental in Roslyn. +#pragma warning disable RSEXPERIMENTAL006 + classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.ClosedKeyword)); +#pragma warning restore RSEXPERIMENTAL006 } else { diff --git a/JavaToCSharp/JavaConversionOptions.cs b/JavaToCSharp/JavaConversionOptions.cs index 805cd1c..a5ddb5b 100644 --- a/JavaToCSharp/JavaConversionOptions.cs +++ b/JavaToCSharp/JavaConversionOptions.cs @@ -39,6 +39,13 @@ public class JavaConversionOptions /// public bool UseClosedForSealedClasses { get; set; } = true; + /// + /// Translates Java labeled break and continue using the C# 15 labeled jump syntax + /// (break outer;), which requires .NET 11 or later. + /// When disabled, the jumps are lowered to an equivalent goto and generated target labels instead. + /// + public bool UseLabeledBreakAndContinue { get; set; } = true; + public SyntaxMapping SyntaxMappings { get; set; } = new SyntaxMapping(); public JavaConversionOptions AddPackageReplacement(string pattern, string replacement, RegexOptions options = RegexOptions.None) diff --git a/JavaToCSharp/JavaToCSharp.csproj b/JavaToCSharp/JavaToCSharp.csproj index f409195..0d96571 100644 --- a/JavaToCSharp/JavaToCSharp.csproj +++ b/JavaToCSharp/JavaToCSharp.csproj @@ -18,7 +18,12 @@ - + + + diff --git a/JavaToCSharp/Statements/BreakStatementVisitor.cs b/JavaToCSharp/Statements/BreakStatementVisitor.cs index e84deb9..b70b3f1 100644 --- a/JavaToCSharp/Statements/BreakStatementVisitor.cs +++ b/JavaToCSharp/Statements/BreakStatementVisitor.cs @@ -1,4 +1,4 @@ -using com.github.javaparser; +using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,8 +9,10 @@ public class BreakStatementVisitor : StatementVisitor { public override StatementSyntax Visit(ConversionContext context, BreakStmt brk) { - if (brk.getLabel().isPresent()) - context.Options.Warning("Break with label detected, using plain break instead. Check for correctness.", brk.getBegin().FromRequiredOptional().line); + var label = brk.getLabel().FromOptional(); + + if (label is not null) + return LabeledJumpHelper.CreateJump(context, label.asString(), isBreak: true); return SyntaxFactory.BreakStatement(); } diff --git a/JavaToCSharp/Statements/ContinueStatementVisitor.cs b/JavaToCSharp/Statements/ContinueStatementVisitor.cs index aee2a3a..5f2a482 100644 --- a/JavaToCSharp/Statements/ContinueStatementVisitor.cs +++ b/JavaToCSharp/Statements/ContinueStatementVisitor.cs @@ -1,4 +1,4 @@ -using com.github.javaparser; +using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,8 +9,10 @@ public class ContinueStatementVisitor : StatementVisitor { public override StatementSyntax Visit(ConversionContext context, ContinueStmt cnt) { - if (cnt.getLabel().isPresent()) - context.Options.Warning("Continue with label detected, using plain continue instead. Check for correctness.", cnt.getBegin().FromRequiredOptional().line); + var label = cnt.getLabel().FromOptional(); + + if (label is not null) + return LabeledJumpHelper.CreateJump(context, label.asString(), isBreak: false); return SyntaxFactory.ContinueStatement(); } diff --git a/JavaToCSharp/Statements/LabeledJumpHelper.cs b/JavaToCSharp/Statements/LabeledJumpHelper.cs new file mode 100644 index 0000000..859a9ab --- /dev/null +++ b/JavaToCSharp/Statements/LabeledJumpHelper.cs @@ -0,0 +1,63 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace JavaToCSharp.Statements; + +/// +/// Lowers Java's labeled break/continue into C#. +/// +/// +/// Two shapes are supported, selected by : +/// +/// C# 15 labeled jumps (break outer;), which map one-to-one onto the Java syntax. +/// A goto to a generated target label, which is valid in every C# version. +/// +/// +internal static class LabeledJumpHelper +{ + /// + /// The label a goto jumps to in order to leave the loop labeled . + /// Emitted immediately after the loop. + /// + internal static string BreakTargetLabel(string label) => $"{label}_break"; + + /// + /// The label a goto jumps to in order to start the next iteration of the loop labeled + /// . Emitted as the last statement of the loop body. + /// + internal static string ContinueTargetLabel(string label) => $"{label}_continue"; + + /// + /// Creates the statement for a labeled break or continue, recording on the context which + /// target labels the goto fallback must emit. + /// + internal static StatementSyntax CreateJump(ConversionContext context, string label, bool isBreak) + { + if (context.Options.UseLabeledBreakAndContinue) + { + var name = SyntaxFactory.IdentifierName(label); + + // The labeled break/continue factory overloads are still marked experimental in Roslyn. +#pragma warning disable RSEXPERIMENTAL006 + return isBreak + ? SyntaxFactory.BreakStatement(name) + : SyntaxFactory.ContinueStatement(name); +#pragma warning restore RSEXPERIMENTAL006 + } + + var target = isBreak ? BreakTargetLabel(label) : ContinueTargetLabel(label); + + if (isBreak) + { + context.LabelsNeedingBreakTarget.Add(label); + } + else + { + context.LabelsNeedingContinueTarget.Add(label); + } + + return SyntaxFactory.GotoStatement( + SyntaxKind.GotoStatement, + SyntaxFactory.IdentifierName(target)); + } +} diff --git a/JavaToCSharp/Statements/LabeledStatementVisitor.cs b/JavaToCSharp/Statements/LabeledStatementVisitor.cs index dbba533..c9feb37 100644 --- a/JavaToCSharp/Statements/LabeledStatementVisitor.cs +++ b/JavaToCSharp/Statements/LabeledStatementVisitor.cs @@ -1,4 +1,4 @@ -using com.github.javaparser.ast.stmt; +using com.github.javaparser.ast.stmt; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -8,9 +8,98 @@ public class LabeledStatementVisitor : StatementVisitor { public override StatementSyntax? Visit(ConversionContext context, LabeledStmt labeledStmt) { + var label = labeledStmt.getLabel().asString(); var statement = labeledStmt.getStatement(); + + // Only this label's own jumps matter here; an enclosing labeled loop handles its own. + bool neededBreakTarget = context.LabelsNeedingBreakTarget.Remove(label); + bool neededContinueTarget = context.LabelsNeedingContinueTarget.Remove(label); + var syntax = VisitStatement(context, statement); - return syntax is null ? null : SyntaxFactory.LabeledStatement(labeledStmt.getLabel().asString(), syntax); + if (syntax is null) + { + return null; + } + + if (context.Options.UseLabeledBreakAndContinue) + { + // C# 15 places the label directly on the loop, exactly as Java does. + return SyntaxFactory.LabeledStatement(label, syntax); + } + + bool usesBreak = context.LabelsNeedingBreakTarget.Remove(label); + bool usesContinue = context.LabelsNeedingContinueTarget.Remove(label); + + if (usesContinue) + { + syntax = AppendContinueTarget(context, syntax, label); + } + + if (!usesBreak) + { + // Restore any state belonging to an enclosing statement with the same label. + RestoreOuter(context, label, neededBreakTarget, neededContinueTarget); + return syntax; + } + + // The break target must follow the loop, so the loop and the label become a block. An empty + // statement is required because a C# label cannot be the last statement in a block. + var result = SyntaxFactory.Block( + syntax, + SyntaxFactory.LabeledStatement( + LabeledJumpHelper.BreakTargetLabel(label), + SyntaxFactory.EmptyStatement())); + + RestoreOuter(context, label, neededBreakTarget, neededContinueTarget); + + return result; + } + + /// + /// Appends the continue target label to the end of the loop body, so that a goto to it + /// finishes the current iteration and lets the loop advance normally. + /// + private static StatementSyntax AppendContinueTarget(ConversionContext context, StatementSyntax syntax, string label) + { + var targetLabel = SyntaxFactory.LabeledStatement( + LabeledJumpHelper.ContinueTargetLabel(label), + SyntaxFactory.EmptyStatement()); + + return syntax switch + { + ForStatementSyntax f => f.WithStatement(WithTrailing(f.Statement, targetLabel)), + ForEachStatementSyntax f => f.WithStatement(WithTrailing(f.Statement, targetLabel)), + WhileStatementSyntax w => w.WithStatement(WithTrailing(w.Statement, targetLabel)), + DoStatementSyntax d => d.WithStatement(WithTrailing(d.Statement, targetLabel)), + _ => Unsupported(context, syntax, label), + }; + } + + private static StatementSyntax Unsupported(ConversionContext context, StatementSyntax syntax, string label) + { + context.Options.Warning( + $"Labeled continue targeting `{label}` could not be lowered because the labeled statement is not a loop. Check for correctness.", + 0); + + return syntax; + } + + private static BlockSyntax WithTrailing(StatementSyntax body, StatementSyntax target) => + body is BlockSyntax block + ? block.AddStatements(target) + : SyntaxFactory.Block(body, target); + + private static void RestoreOuter(ConversionContext context, string label, bool neededBreak, bool neededContinue) + { + if (neededBreak) + { + context.LabelsNeedingBreakTarget.Add(label); + } + + if (neededContinue) + { + context.LabelsNeedingContinueTarget.Add(label); + } } } diff --git a/JavaToCSharpCli/Program.cs b/JavaToCSharpCli/Program.cs index 9d18b17..b2978b6 100644 --- a/JavaToCSharpCli/Program.cs +++ b/JavaToCSharpCli/Program.cs @@ -72,6 +72,12 @@ public class Program DefaultValueFactory = _ => false, }; + private static readonly Option _noLabeledBreakAndContinueOption = new("--no-labeled-break-and-continue") + { + Description = "Do not use C# 15 labeled break/continue; fall back to `goto` when converting Java labeled jumps", + DefaultValueFactory = _ => false, + }; + private static readonly Option _clearDefaultUsingsOption = new("--clear-usings") { Description = "Remove all default usings provided by this app", @@ -116,6 +122,7 @@ public static async Task Main(string[] args) rootCommand.Options.Add(_systemOutToConsoleOption); rootCommand.Options.Add(_fileScopedNamespacesOption); rootCommand.Options.Add(_noClosedForSealedClassesOption); + rootCommand.Options.Add(_noLabeledBreakAndContinueOption); rootCommand.Options.Add(_clearDefaultUsingsOption); rootCommand.Options.Add(_addUsingsOption); rootCommand.Options.Add(_mappingsFileNameOption); @@ -172,6 +179,7 @@ private static JavaConversionOptions GetJavaConversionOptions(ParseResult contex UseUnrecognizedCodeToComment = context.GetValue(_commentUnrecognizedCodeOption), UseFileScopedNamespaces = context.GetValue(_fileScopedNamespacesOption), UseClosedForSealedClasses = !context.GetValue(_noClosedForSealedClassesOption), + UseLabeledBreakAndContinue = !context.GetValue(_noLabeledBreakAndContinueOption), }; if (context.GetValue(_clearDefaultUsingsOption)) diff --git a/JavaToCSharpGui/App.config b/JavaToCSharpGui/App.config index e44869e..518b957 100644 --- a/JavaToCSharpGui/App.config +++ b/JavaToCSharpGui/App.config @@ -34,6 +34,9 @@ True + + True + \ No newline at end of file diff --git a/JavaToCSharpGui/CurrentOptions.cs b/JavaToCSharpGui/CurrentOptions.cs index 0a22eec..c2639db 100644 --- a/JavaToCSharpGui/CurrentOptions.cs +++ b/JavaToCSharpGui/CurrentOptions.cs @@ -15,6 +15,7 @@ static CurrentOptions() Options.ConvertSystemOutToConsole = Settings.Default.ConvertSystemOutToConsole; Options.UseFileScopedNamespaces = Settings.Default.UseFileScopedNamespaces; Options.UseClosedForSealedClasses = Settings.Default.UseClosedForSealedClasses; + Options.UseLabeledBreakAndContinue = Settings.Default.UseLabeledBreakAndContinue; Options.SetUsings(Settings.Default.Usings.Split(';')); } @@ -31,6 +32,7 @@ public static void Persist() Settings.Default.ConvertSystemOutToConsole = Options.ConvertSystemOutToConsole; Settings.Default.UseFileScopedNamespaces = Options.UseFileScopedNamespaces; Settings.Default.UseClosedForSealedClasses = Options.UseClosedForSealedClasses; + Settings.Default.UseLabeledBreakAndContinue = Options.UseLabeledBreakAndContinue; Settings.Default.Usings = string.Join(";", Options.Usings); Settings.Default.Save(); diff --git a/JavaToCSharpGui/Properties/Settings.Designer.cs b/JavaToCSharpGui/Properties/Settings.Designer.cs index a7530ae..c96b6c3 100644 --- a/JavaToCSharpGui/Properties/Settings.Designer.cs +++ b/JavaToCSharpGui/Properties/Settings.Designer.cs @@ -130,5 +130,17 @@ public bool UseClosedForSealedClasses { this["UseClosedForSealedClasses"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool UseLabeledBreakAndContinue { + get { + return ((bool)(this["UseLabeledBreakAndContinue"])); + } + set { + this["UseLabeledBreakAndContinue"] = value; + } + } } } diff --git a/JavaToCSharpGui/Properties/Settings.settings b/JavaToCSharpGui/Properties/Settings.settings index 1b69f21..b13aefe 100644 --- a/JavaToCSharpGui/Properties/Settings.settings +++ b/JavaToCSharpGui/Properties/Settings.settings @@ -29,5 +29,8 @@ True + + True + diff --git a/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs b/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs index 15d9af8..2fba5b2 100644 --- a/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs +++ b/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs @@ -28,6 +28,8 @@ public partial class SettingsWindowViewModel : ViewModelBase [ObservableProperty] private bool _useClosedForSealedClasses = CurrentOptions.Options.UseClosedForSealedClasses; + [ObservableProperty] private bool _useLabeledBreakAndContinue = CurrentOptions.Options.UseLabeledBreakAndContinue; + public event EventHandler? CloseRequested; [RelayCommand] @@ -57,6 +59,7 @@ private void Save() CurrentOptions.Options.ConvertSystemOutToConsole = ConvertSystemOutToConsole; CurrentOptions.Options.UseFileScopedNamespaces = UseFileScopedNamespaces; CurrentOptions.Options.UseClosedForSealedClasses = UseClosedForSealedClasses; + CurrentOptions.Options.UseLabeledBreakAndContinue = UseLabeledBreakAndContinue; CurrentOptions.Options.SetUsings(Usings); diff --git a/JavaToCSharpGui/Views/SettingsWindow.axaml b/JavaToCSharpGui/Views/SettingsWindow.axaml index 0284fab..9044109 100644 --- a/JavaToCSharpGui/Views/SettingsWindow.axaml +++ b/JavaToCSharpGui/Views/SettingsWindow.axaml @@ -46,6 +46,10 @@ IsChecked="{CompiledBinding UseClosedForSealedClasses}"> Use C# 15 `closed` for Java sealed classes + + Use C# 15 labeled break/continue (otherwise goto) + Include comments in output