diff --git a/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs
new file mode 100644
index 0000000..058a660
--- /dev/null
+++ b/JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs
@@ -0,0 +1,134 @@
+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);
+
+ // 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));
+ }
+
+ 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..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")]
@@ -90,6 +93,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.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]);
+ }
+}
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)