From f3143c4fb720dbc693ab291f59a120a133038b33 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Mon, 17 Aug 2026 16:23:40 -0600 Subject: [PATCH 1/2] Split mixed array ranks into separate variable declarations (#100) Java allows C-style array brackets on individual declarators, so a single declaration can mix array ranks: int multi[][] = new int[2][2], single[] = new int[2]; C# has no equivalent. Conversion failed outright with the JavaParser assertion "The variables do not have a common type.", thrown by getCommonType() before the visitor's own array-level check could run. Group the declarators by array level and emit one C# declaration per distinct rank. The groups are emitted as flat sibling statements via PendingStatements rather than a nested block, so the declared variables remain in the enclosing scope and stay visible to later statements. Declarators that share a rank stay together in one declaration, and declaration order is preserved. This covers local variable declarations, matching the issue's repro. Field declarations have the same limitation but the body-declaration visitor returns a single member, so splitting them needs a wider refactor; that remains unsupported, as noted in ArrayField.java. Co-Authored-By: Claude Opus 5 (1M context) --- .../ConvertMixedArrayRankDeclarationTests.cs | 131 ++++++++++++++++++ JavaToCSharp.Tests/IntegrationTests.cs | 1 + .../Resources/MixedArrayRankDeclarations.java | 24 ++++ .../Statements/ExpressionStatementVisitor.cs | 46 ++++-- 4 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs create mode 100644 JavaToCSharp.Tests/Resources/MixedArrayRankDeclarations.java diff --git a/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs new file mode 100644 index 0000000..72a3fd0 --- /dev/null +++ b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs @@ -0,0 +1,131 @@ +namespace JavaToCSharp.Tests; + +/// +/// Java permits C-style array brackets on individual declarators, so a single declaration can mix +/// array ranks. C# has no equivalent, so these must be split into one declaration per rank. +/// +public class ConvertMixedArrayRankDeclarationTests +{ + [Fact] + public void Mixed_Ranks_Split_Into_Separate_Declarations() + { + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int single[] = new int[2], scalar = 7; + } + } + """); + + Assert.Contains("int[] single = new int[2];", parsed); + Assert.Contains("int scalar = 7;", parsed); + } + + [Fact] + public void Declarators_Of_The_Same_Rank_Stay_In_One_Declaration() + { + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int a[] = new int[1], b = 0, c[] = new int[2]; + } + } + """); + + // `a` and `c` share a rank, so they must remain a single declaration rather than being + // split one-per-declarator. + Assert.Contains("int[] a = new int[1], c = new int[2];", parsed); + Assert.Contains("int b = 0;", parsed); + } + + [Fact] + public void Declaration_Groups_Are_Emitted_As_Siblings_In_Declaration_Order() + { + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int single[] = new int[2], scalar = 7; + } + } + """); + + int arrayDecl = parsed.IndexOf("int[] single", StringComparison.Ordinal); + int scalarDecl = parsed.IndexOf("int scalar", StringComparison.Ordinal); + + Assert.True(arrayDecl > 0 && scalarDecl > 0); + Assert.True(arrayDecl < scalarDecl, "Groups must preserve the original declaration order."); + + // The split must not introduce a nested scope, which would put the variables out of reach + // of later statements in the enclosing block. + Assert.DoesNotContain("{\n {", parsed.ReplaceLineEndings("\n")); + } + + [Fact] + public void Uninitialized_Declarators_Are_Preserved_When_Split() + { + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int values[], count = 0; + } + } + """); + + Assert.Contains("int[] values;", parsed); + Assert.Contains("int count = 0;", parsed); + } + + [Fact] + public void Single_Rank_Declarations_Are_Unaffected() + { + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int x = 1, y = 2; + } + } + """); + + Assert.Contains("int x = 1, y = 2;", parsed); + } + + [Fact] + public void Mixed_Ranks_Convert_Without_Error() + { + // Regression test for #100: asking JavaParser for a common type across mixed ranks + // threw "The variables do not have a common type." + var warnings = new List(); + + var parsed = Convert(""" + package com.example; + public class Program { + public void run() { + int multi[][] = new int[2][2], single[] = new int[2]; + } + } + """, warnings); + + Assert.Contains("multi", parsed); + Assert.Contains("single", parsed); + + // The only warning permitted here is the pre-existing multi-dimensional array caveat. + Assert.All(warnings, w => Assert.Contains("Multi-dimensional arrays", w)); + } + + private static string Convert(string javaCode, List? warnings = null) + { + var options = new JavaConversionOptions + { + IncludeComments = false, + }; + + options.WarningEncountered += (_, eventArgs) => warnings?.Add(eventArgs.Message); + + return JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + } +} diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index 5c72214..b1e18eb 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -90,6 +90,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/LabeledBreakContinue.java")] [InlineData("Resources/ExceptionGetMessage.java")] [InlineData("Resources/LongLiterals.java")] + [InlineData("Resources/MixedArrayRankDeclarations.java")] public void FullIntegrationTests(string filePath, bool allowWarnings = false) => RunFullIntegrationTest(filePath, allowWarnings); diff --git a/JavaToCSharp.Tests/Resources/MixedArrayRankDeclarations.java b/JavaToCSharp.Tests/Resources/MixedArrayRankDeclarations.java new file mode 100644 index 0000000..59172b6 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/MixedArrayRankDeclarations.java @@ -0,0 +1,24 @@ +/// Expect: +/// - output: "5\n7\n9\n2\n0\n" +package example; + +public class Program { + public static void main(String[] args) { + // Java allows C-style array brackets per declarator, so one declaration can mix ranks. + // These must split into separate C# declarations, preserving declaration order. + int single[] = new int[2], scalar = 7, other[] = {8, 9}; + + single[0] = 5; + + System.out.println(single[0]); + System.out.println(scalar); + System.out.println(other[1]); + + // A rank group with more than one declarator, and an uninitialized declarator. + int a[] = {1, 2}, b = 0, c[]; + c = new int[1]; + + System.out.println(a[1]); + System.out.println(c[0] + b); + } +} diff --git a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs index ab9dfdf..0659b35 100644 --- a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs +++ b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs @@ -43,28 +43,54 @@ public class ExpressionStatementVisitor : StatementVisitor return expressionSyntax is null ? null : SyntaxFactory.ExpressionStatement(expressionSyntax); } - private static StatementSyntax VisitVariableDeclarationStatement(ConversionContext context, VariableDeclarationExpr varExpr) + private static StatementSyntax? VisitVariableDeclarationStatement(ConversionContext context, VariableDeclarationExpr varExpr) + { + var variableDeclarators = varExpr.getVariables()?.ToList() ?? []; + + // Java allows C-style array brackets on individual declarators, so a single declaration can mix + // ranks (`int multi[][] = ..., single[] = ...;`). C# has no equivalent, and asking JavaParser for + // a common type throws in that case, so emit one C# declaration per distinct array rank. The + // groups stay flat siblings rather than a nested block so the variables remain in the same scope. + var declaratorGroups = variableDeclarators + .GroupBy(item => item.getType().getArrayLevel()) + .ToList(); + + if (declaratorGroups.Count > 1) + { + StatementSyntax? last = null; + + foreach (var group in declaratorGroups) + { + if (last is not null) + { + context.PendingStatements.Add(last); + } + + last = VisitVariableDeclarationGroup(context, group.First().getType(), group.ToList()); + } + + return last; + } + + return VisitVariableDeclarationGroup(context, varExpr.getCommonType(), variableDeclarators); + } + + private static StatementSyntax? VisitVariableDeclarationGroup( + ConversionContext context, + com.github.javaparser.ast.type.Type commonType, + List variableDeclarators) { - var commonType = varExpr.getCommonType(); int? arrayRank = null; var variables = new List(); var loweredSwitches = new List(); - var variableDeclarators = varExpr.getVariables()?.ToList() ?? []; - foreach (var item in variableDeclarators) { var type = item.getType(); - if (arrayRank is not null && type.getArrayLevel() != arrayRank) - { - throw new InvalidOperationException("Different array levels in the same field declaration are not yet supported"); - } - arrayRank ??= type.getArrayLevel(); - var id = item.getType(); string name = item.getNameAsString(); if (type.getArrayLevel() > 0) From 772a5f36cd6ff26b7881637c8458c6453e0bdaf6 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Tue, 18 Aug 2026 08:29:21 -0600 Subject: [PATCH 2/2] Cover the issue's multi-dimensional example in the integration tests Add MixedArrayRankMultidimensional.java with the exact `int multi[][], single[]` declaration from issue #100. It is registered in GeneralSuccessfulConversionTest rather than FullIntegrationTests because jagged arrays are still emitted as rectangular C# arrays (`int[,]`) while indexing stays `multi[0][0]`, so the generated code converts but does not compile. That is the pre-existing limitation already tracked by MultidimensionalArrays.java, independent of the mixed-rank split. The runnable coverage in MixedArrayRankDeclarations.java is unchanged. Also tighten the multi-dimensional unit test to assert the two ranks land in separate declarations, instead of only checking that conversion no longer throws. Co-Authored-By: Claude Opus 5 (1M context) --- .../ConvertMixedArrayRankDeclarationTests.cs | 7 ++++-- JavaToCSharp.Tests/IntegrationTests.cs | 3 +++ .../MixedArrayRankMultidimensional.java | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 JavaToCSharp.Tests/Resources/MixedArrayRankMultidimensional.java diff --git a/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs index 72a3fd0..058a660 100644 --- a/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs +++ b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs @@ -110,8 +110,11 @@ public void run() { } """, warnings); - Assert.Contains("multi", parsed); - Assert.Contains("single", parsed); + // The two ranks must land in separate declarations. Note the 2-D array is emitted as a + // rectangular `int[,]` rather than a jagged `int[][]`; that is a pre-existing limitation + // independent of the mixed-rank split under test here. + Assert.Contains("int[, ] multi = new int[2, 2];", parsed); + Assert.Contains("int[] single = new int[2];", parsed); // The only warning permitted here is the pre-existing multi-dimensional array caveat. Assert.All(warnings, w => Assert.Contains("Multi-dimensional arrays", w)); diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index b1e18eb..954079e 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -21,6 +21,9 @@ public class IntegrationTests(ITestOutputHelper testOutputHelper) [InlineData("Resources/Java9DiamondOperatorInnerClass.java")] [InlineData("Resources/Java11LambdaInference.java")] [InlineData("Resources/MultidimensionalArrays.java", true)] + // Warnings are expected: jagged arrays are still emitted as rectangular C# arrays, so this + // converts but cannot be compiled and run. See the note in the resource file. + [InlineData("Resources/MixedArrayRankMultidimensional.java", true)] [InlineData("Resources/Java17SealedClasses.java", true)] // Conversion-only: java.util.function has no BCL delegate mapping, so the output cannot be run. [InlineData("Resources/Java8MethodReferences.java")] diff --git a/JavaToCSharp.Tests/Resources/MixedArrayRankMultidimensional.java b/JavaToCSharp.Tests/Resources/MixedArrayRankMultidimensional.java new file mode 100644 index 0000000..dc70a91 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/MixedArrayRankMultidimensional.java @@ -0,0 +1,25 @@ +// NOTE: this test case only parses and converts successfully, it does not yet run. +// The mixed-rank declaration is split correctly by this test's coverage, but jagged arrays are +// still emitted as rectangular C# arrays (`int[,]`) while indexing stays `multi[0][0]`, so the +// generated code does not compile. That is the pre-existing limitation tracked by +// MultidimensionalArrays.java, not by the mixed-rank split. +package example; + +public class Program { + public static void main(String[] args) { + // The example from issue #100: mixing a 2-D and a 1-D declarator in one declaration. + int multi[][] = new int[2][2], + single[] = new int[2]; + multi[0][0] = 1; + multi[0][1] = 2; + multi[1][0] = 3; + multi[1][1] = 4; + single[0] = 5; + + System.out.println(multi[0][0]); + System.out.println(multi[0][1]); + System.out.println(multi[1][0]); + System.out.println(multi[1][1]); + System.out.println(single[0]); + } +}