Skip to content

Commit af4a559

Browse files
paulirwinclaude
andcommitted
Use the typed Roslyn API for C# 15 labeled jumps and closed
Upgrades Microsoft.CodeAnalysis.CSharp from 5.6.0 to 5.9.0, which adds real support for both C# 15 features this project emits. Labeled break/continue now use the new BreakStatement(IdentifierNameSyntax) and ContinueStatement(IdentifierNameSyntax) overloads, replacing the `goto` placeholder that was substituted into the final text. The sealed class `closed` modifier now uses SyntaxKind.ClosedKeyword instead of fabricating the token with ParseToken("closed "). Both APIs are still marked experimental, so RSEXPERIMENTAL006 is suppressed at each use site. The integration test harness now parses generated code with LanguageVersion.Preview, so the C# 15 labeled jump output is compiled and executed rather than only string-asserted. LabeledBreakContinue.java runs through both the default C# 15 path and the goto fallback, asserting the same output for each. Roslyn 5.9.0 depends on an unpublished Microsoft.CodeAnalysis.Analyzers prerelease, so the released 5.9.0 is referenced explicitly to avoid NU1603. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d05e1eb commit af4a559

5 files changed

Lines changed: 42 additions & 35 deletions

File tree

‎JavaToCSharp.Tests/IntegrationTests.cs‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,26 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
8787
[InlineData("Resources/Java16LocalRecords.java")]
8888
[InlineData("Resources/InstanceInitializers.java")]
8989
[InlineData("Resources/StaticImports.java")]
90-
// Uses the `goto` fallback: the C# 15 labeled jump syntax cannot be compiled by the Roslyn version here.
9190
[InlineData("Resources/LabeledBreakContinue.java")]
9291
public void FullIntegrationTests(string filePath, bool allowWarnings = false)
92+
=> RunFullIntegrationTest(filePath, allowWarnings);
93+
94+
/// <summary>
95+
/// Runs the labeled break/continue sample through the <c>goto</c> fallback, to confirm it behaves
96+
/// identically to the C# 15 labeled jumps covered by <see cref="FullIntegrationTests"/>.
97+
/// </summary>
98+
[Theory]
99+
[InlineData("Resources/LabeledBreakContinue.java")]
100+
public void FullIntegrationTestsWithGotoFallback(string filePath)
101+
=> RunFullIntegrationTest(filePath, allowWarnings: false, useLabeledBreakAndContinue: false);
102+
103+
private void RunFullIntegrationTest(string filePath, bool allowWarnings, bool useLabeledBreakAndContinue = true)
93104
{
94105
var options = new JavaConversionOptions
95106
{
96107
ConvertSystemOutToConsole = true,
97108
IncludeComments = false,
98-
UseLabeledBreakAndContinue = false,
109+
UseLabeledBreakAndContinue = useLabeledBreakAndContinue,
99110
};
100111

101112
options.AddUsing("System");
@@ -116,7 +127,10 @@ public void FullIntegrationTests(string filePath, bool allowWarnings = false)
116127

117128
testOutputHelper.WriteLine(parsed);
118129

119-
var fileName = Path.GetFileNameWithoutExtension(filePath);
130+
// The suffix keeps the two option variants in separate assemblies, because Assembly.LoadFile
131+
// caches by path and would otherwise reuse the first variant's compiled output.
132+
var fileName = Path.GetFileNameWithoutExtension(filePath)
133+
+ (useLabeledBreakAndContinue ? "" : "_Goto");
120134
var assembly = CompileAssembly(fileName, parsed);
121135

122136
var expectation = ParseExpectation(javaText);
@@ -171,7 +185,10 @@ public void FullIntegrationTests(string filePath, bool allowWarnings = false)
171185

172186
private static Assembly CompileAssembly(string assemblyName, string cSharpLanguageText)
173187
{
174-
var syntaxTree = CSharpSyntaxTree.ParseText(cSharpLanguageText);
188+
// Preview is required for the C# 15 features the converter emits, such as labeled break/continue.
189+
var syntaxTree = CSharpSyntaxTree.ParseText(
190+
cSharpLanguageText,
191+
new CSharpParseOptions(LanguageVersion.Preview));
175192

176193
var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication)
177194
.WithOverflowChecks(true)

‎JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ public static ClassDeclarationSyntax VisitClassDeclaration(ConversionContext con
242242
{
243243
if (context.Options.UseClosedForSealedClasses)
244244
{
245-
// Roslyn has no ClosedKeyword token, as `closed` is a C# 15 feature.
246-
classSyntax = classSyntax.AddModifiers(SyntaxFactory.ParseToken("closed "));
245+
// The `closed` keyword is still marked experimental in Roslyn.
246+
#pragma warning disable RSEXPERIMENTAL006
247+
classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.ClosedKeyword));
248+
#pragma warning restore RSEXPERIMENTAL006
247249
}
248250
else
249251
{

‎JavaToCSharp/JavaToCSharp.csproj‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
<ItemGroup>
1919
<PackageReference Include="IKVM" Version="8.15.0"/>
2020
<PackageReference Include="IKVM.Maven.Sdk" Version="1.12.0"/>
21-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
21+
<!--
22+
Microsoft.CodeAnalysis.CSharp 5.9.0 depends on an unpublished Microsoft.CodeAnalysis.Analyzers
23+
prerelease. Referencing the released 5.9.0 explicitly satisfies that range and avoids NU1603.
24+
-->
25+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
26+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
2227
<PackageReference Include="Nullable.Extended.Analyzer" Version="1.15.6581"/>
2328
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.11" />
2429
<PackageReference Include="YamlDotNet" Version="18.1.0" />

‎JavaToCSharp/JavaToCSharpConverter.cs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,6 @@ public static class JavaToCSharpConverter
156156

157157
context.ConversionStateChanged(ConversionState.Done);
158158

159-
// C# 15 labeled jumps cannot be represented in a Roslyn tree, so they are emitted as placeholders
160-
// and substituted into the final text here. See LabeledJumpHelper.
161-
return Statements.LabeledJumpHelper.RewritePlaceholders(tree.GetText().ToString());
159+
return tree.GetText().ToString();
162160
}
163161
}

‎JavaToCSharp/Statements/LabeledJumpHelper.cs‎

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Text.RegularExpressions;
21
using Microsoft.CodeAnalysis.CSharp;
32
using Microsoft.CodeAnalysis.CSharp.Syntax;
43

@@ -10,24 +9,12 @@ namespace JavaToCSharp.Statements;
109
/// <remarks>
1110
/// Two shapes are supported, selected by <see cref="JavaConversionOptions.UseLabeledBreakAndContinue"/>:
1211
/// <list type="bullet">
13-
/// <item>C# 15 labeled jumps (<c>break outer;</c>). Roslyn cannot model these — there is no label operand on
14-
/// <see cref="BreakStatementSyntax"/> and the parser rejects the syntax even at
15-
/// <c>LanguageVersion.Preview</c> — so a <c>goto</c> placeholder is emitted instead and rewritten to the real
16-
/// text after whitespace normalization by <see cref="SanitizingSyntaxRewriter"/>.</item>
12+
/// <item>C# 15 labeled jumps (<c>break outer;</c>), which map one-to-one onto the Java syntax.</item>
1713
/// <item>A <c>goto</c> to a generated target label, which is valid in every C# version.</item>
1814
/// </list>
1915
/// </remarks>
20-
internal static partial class LabeledJumpHelper
16+
internal static class LabeledJumpHelper
2117
{
22-
/// <summary>
23-
/// Prefix for the placeholder <c>goto</c> target that stands in for a C# 15 labeled jump.
24-
/// Chosen to be a valid C# identifier so the placeholder tree parses and normalizes cleanly.
25-
/// </summary>
26-
private const string PlaceholderPrefix = "__javaToCSharp_labeled_";
27-
28-
[GeneratedRegex($@"goto {PlaceholderPrefix}(break|continue)_(\w+);")]
29-
private static partial Regex PlaceholderRegex { get; }
30-
3118
/// <summary>
3219
/// The label a <c>goto</c> jumps to in order to leave the loop labeled <paramref name="label"/>.
3320
/// Emitted immediately after the loop.
@@ -48,9 +35,14 @@ internal static StatementSyntax CreateJump(ConversionContext context, string lab
4835
{
4936
if (context.Options.UseLabeledBreakAndContinue)
5037
{
51-
// Placeholder for `break <label>;` / `continue <label>;`, rewritten post-normalization.
52-
return SyntaxFactory.ParseStatement(
53-
$"goto {PlaceholderPrefix}{(isBreak ? "break" : "continue")}_{label};");
38+
var name = SyntaxFactory.IdentifierName(label);
39+
40+
// The labeled break/continue factory overloads are still marked experimental in Roslyn.
41+
#pragma warning disable RSEXPERIMENTAL006
42+
return isBreak
43+
? SyntaxFactory.BreakStatement(name)
44+
: SyntaxFactory.ContinueStatement(name);
45+
#pragma warning restore RSEXPERIMENTAL006
5446
}
5547

5648
var target = isBreak ? BreakTargetLabel(label) : ContinueTargetLabel(label);
@@ -68,11 +60,4 @@ internal static StatementSyntax CreateJump(ConversionContext context, string lab
6860
SyntaxKind.GotoStatement,
6961
SyntaxFactory.IdentifierName(target));
7062
}
71-
72-
/// <summary>
73-
/// Replaces the placeholder <c>goto</c> statements with real C# 15 labeled jumps. Must run on the final
74-
/// text, because the resulting syntax cannot be represented in a Roslyn syntax tree.
75-
/// </summary>
76-
internal static string RewritePlaceholders(string text) =>
77-
PlaceholderRegex.Replace(text, "$1 $2;");
7863
}

0 commit comments

Comments
 (0)