Skip to content

Commit 64fbb2a

Browse files
paulirwinclaude
andauthored
Split mixed array ranks into separate variable declarations (#100) (#177)
Fixes #100. ## Problem Java allows C-style array brackets on individual declarators, so one declaration can mix array ranks: ```java int multi[][] = new int[2][2], single[] = new int[2]; ``` This failed conversion outright with `AssertionError: The variables do not have a common type.` The error comes from JavaParser's `getCommonType()`, which asserts every declarator shares a type. Because it was called before the visitor's own array-level check, the friendlier `InvalidOperationException` on the next lines was unreachable. ## Fix Group declarators by array level and emit one C# declaration per distinct rank. The groups are emitted as **flat sibling statements** through the existing `PendingStatements` mechanism rather than a nested block — a block would put the variables in an inner scope and break every later reference to them. Declarators that share a rank stay together in a single declaration, and declaration order is preserved. ```csharp // int single[] = new int[2], scalar = 7, other[] = {8, 9}; int[] single = new int[2], other = new[] { 8, 9 }; int scalar = 7; ``` ## Scope This covers **local variable declarations**, matching the issue's repro. `FieldDeclarationVisitor` has the same limitation, but `VisitForClass` returns a single `MemberDeclarationSyntax`, so splitting fields would require changing that signature across every body-declaration visitor. That felt out of scope here; fields remain unsupported, as already noted in `ArrayField.java`. Note the pre-existing, separate limitation that jagged arrays (`int[][]`) are emitted as rectangular (`int[,]`) while indexing stays `[0][0]` — that's the known issue behind `MultidimensionalArrays.java` being conversion-only, and is untouched by this PR. ## Testing **Unit tests** — new `ConvertMixedArrayRankDeclarationTests` covers the split, same-rank grouping, declaration order and absence of a nested scope, uninitialized declarators, the #100 repro, and a regression guard that single-rank declarations are unchanged. **Integration test** — new `MixedArrayRankDeclarations.java` registered in `FullIntegrationTests`, which compiles the generated C# with Roslyn, runs it, and asserts on stdout. It converts with **no warnings**. Full suite: 389 passed, 0 failed. I also confirmed 6 of the 7 new tests fail without the fix and pass with it (the 7th is the unchanged-behavior guard, which correctly passes both ways). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ec22810 commit 64fbb2a

5 files changed

Lines changed: 223 additions & 10 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
namespace JavaToCSharp.Tests;
2+
3+
/// <summary>
4+
/// Java permits C-style array brackets on individual declarators, so a single declaration can mix
5+
/// array ranks. C# has no equivalent, so these must be split into one declaration per rank.
6+
/// </summary>
7+
public class ConvertMixedArrayRankDeclarationTests
8+
{
9+
[Fact]
10+
public void Mixed_Ranks_Split_Into_Separate_Declarations()
11+
{
12+
var parsed = Convert("""
13+
package com.example;
14+
public class Program {
15+
public void run() {
16+
int single[] = new int[2], scalar = 7;
17+
}
18+
}
19+
""");
20+
21+
Assert.Contains("int[] single = new int[2];", parsed);
22+
Assert.Contains("int scalar = 7;", parsed);
23+
}
24+
25+
[Fact]
26+
public void Declarators_Of_The_Same_Rank_Stay_In_One_Declaration()
27+
{
28+
var parsed = Convert("""
29+
package com.example;
30+
public class Program {
31+
public void run() {
32+
int a[] = new int[1], b = 0, c[] = new int[2];
33+
}
34+
}
35+
""");
36+
37+
// `a` and `c` share a rank, so they must remain a single declaration rather than being
38+
// split one-per-declarator.
39+
Assert.Contains("int[] a = new int[1], c = new int[2];", parsed);
40+
Assert.Contains("int b = 0;", parsed);
41+
}
42+
43+
[Fact]
44+
public void Declaration_Groups_Are_Emitted_As_Siblings_In_Declaration_Order()
45+
{
46+
var parsed = Convert("""
47+
package com.example;
48+
public class Program {
49+
public void run() {
50+
int single[] = new int[2], scalar = 7;
51+
}
52+
}
53+
""");
54+
55+
int arrayDecl = parsed.IndexOf("int[] single", StringComparison.Ordinal);
56+
int scalarDecl = parsed.IndexOf("int scalar", StringComparison.Ordinal);
57+
58+
Assert.True(arrayDecl > 0 && scalarDecl > 0);
59+
Assert.True(arrayDecl < scalarDecl, "Groups must preserve the original declaration order.");
60+
61+
// The split must not introduce a nested scope, which would put the variables out of reach
62+
// of later statements in the enclosing block.
63+
Assert.DoesNotContain("{\n {", parsed.ReplaceLineEndings("\n"));
64+
}
65+
66+
[Fact]
67+
public void Uninitialized_Declarators_Are_Preserved_When_Split()
68+
{
69+
var parsed = Convert("""
70+
package com.example;
71+
public class Program {
72+
public void run() {
73+
int values[], count = 0;
74+
}
75+
}
76+
""");
77+
78+
Assert.Contains("int[] values;", parsed);
79+
Assert.Contains("int count = 0;", parsed);
80+
}
81+
82+
[Fact]
83+
public void Single_Rank_Declarations_Are_Unaffected()
84+
{
85+
var parsed = Convert("""
86+
package com.example;
87+
public class Program {
88+
public void run() {
89+
int x = 1, y = 2;
90+
}
91+
}
92+
""");
93+
94+
Assert.Contains("int x = 1, y = 2;", parsed);
95+
}
96+
97+
[Fact]
98+
public void Mixed_Ranks_Convert_Without_Error()
99+
{
100+
// Regression test for #100: asking JavaParser for a common type across mixed ranks
101+
// threw "The variables do not have a common type."
102+
var warnings = new List<string>();
103+
104+
var parsed = Convert("""
105+
package com.example;
106+
public class Program {
107+
public void run() {
108+
int multi[][] = new int[2][2], single[] = new int[2];
109+
}
110+
}
111+
""", warnings);
112+
113+
// The two ranks must land in separate declarations. Note the 2-D array is emitted as a
114+
// rectangular `int[,]` rather than a jagged `int[][]`; that is a pre-existing limitation
115+
// independent of the mixed-rank split under test here.
116+
Assert.Contains("int[, ] multi = new int[2, 2];", parsed);
117+
Assert.Contains("int[] single = new int[2];", parsed);
118+
119+
// The only warning permitted here is the pre-existing multi-dimensional array caveat.
120+
Assert.All(warnings, w => Assert.Contains("Multi-dimensional arrays", w));
121+
}
122+
123+
private static string Convert(string javaCode, List<string>? warnings = null)
124+
{
125+
var options = new JavaConversionOptions
126+
{
127+
IncludeComments = false,
128+
};
129+
130+
options.WarningEncountered += (_, eventArgs) => warnings?.Add(eventArgs.Message);
131+
132+
return JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";
133+
}
134+
}

JavaToCSharp.Tests/IntegrationTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class IntegrationTests(ITestOutputHelper testOutputHelper)
2121
[InlineData("Resources/Java9DiamondOperatorInnerClass.java")]
2222
[InlineData("Resources/Java11LambdaInference.java")]
2323
[InlineData("Resources/MultidimensionalArrays.java", true)]
24+
// Warnings are expected: jagged arrays are still emitted as rectangular C# arrays, so this
25+
// converts but cannot be compiled and run. See the note in the resource file.
26+
[InlineData("Resources/MixedArrayRankMultidimensional.java", true)]
2427
[InlineData("Resources/Java17SealedClasses.java", true)]
2528
// Conversion-only: java.util.function has no BCL delegate mapping, so the output cannot be run.
2629
[InlineData("Resources/Java8MethodReferences.java")]
@@ -90,6 +93,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
9093
[InlineData("Resources/LabeledBreakContinue.java")]
9194
[InlineData("Resources/ExceptionGetMessage.java")]
9295
[InlineData("Resources/LongLiterals.java")]
96+
[InlineData("Resources/MixedArrayRankDeclarations.java")]
9397
public void FullIntegrationTests(string filePath, bool allowWarnings = false)
9498
=> RunFullIntegrationTest(filePath, allowWarnings);
9599

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// Expect:
2+
/// - output: "5\n7\n9\n2\n0\n"
3+
package example;
4+
5+
public class Program {
6+
public static void main(String[] args) {
7+
// Java allows C-style array brackets per declarator, so one declaration can mix ranks.
8+
// These must split into separate C# declarations, preserving declaration order.
9+
int single[] = new int[2], scalar = 7, other[] = {8, 9};
10+
11+
single[0] = 5;
12+
13+
System.out.println(single[0]);
14+
System.out.println(scalar);
15+
System.out.println(other[1]);
16+
17+
// A rank group with more than one declarator, and an uninitialized declarator.
18+
int a[] = {1, 2}, b = 0, c[];
19+
c = new int[1];
20+
21+
System.out.println(a[1]);
22+
System.out.println(c[0] + b);
23+
}
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// NOTE: this test case only parses and converts successfully, it does not yet run.
2+
// The mixed-rank declaration is split correctly by this test's coverage, but jagged arrays are
3+
// still emitted as rectangular C# arrays (`int[,]`) while indexing stays `multi[0][0]`, so the
4+
// generated code does not compile. That is the pre-existing limitation tracked by
5+
// MultidimensionalArrays.java, not by the mixed-rank split.
6+
package example;
7+
8+
public class Program {
9+
public static void main(String[] args) {
10+
// The example from issue #100: mixing a 2-D and a 1-D declarator in one declaration.
11+
int multi[][] = new int[2][2],
12+
single[] = new int[2];
13+
multi[0][0] = 1;
14+
multi[0][1] = 2;
15+
multi[1][0] = 3;
16+
multi[1][1] = 4;
17+
single[0] = 5;
18+
19+
System.out.println(multi[0][0]);
20+
System.out.println(multi[0][1]);
21+
System.out.println(multi[1][0]);
22+
System.out.println(multi[1][1]);
23+
System.out.println(single[0]);
24+
}
25+
}

JavaToCSharp/Statements/ExpressionStatementVisitor.cs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,54 @@ public class ExpressionStatementVisitor : StatementVisitor<ExpressionStmt>
4343
return expressionSyntax is null ? null : SyntaxFactory.ExpressionStatement(expressionSyntax);
4444
}
4545

46-
private static StatementSyntax VisitVariableDeclarationStatement(ConversionContext context, VariableDeclarationExpr varExpr)
46+
private static StatementSyntax? VisitVariableDeclarationStatement(ConversionContext context, VariableDeclarationExpr varExpr)
47+
{
48+
var variableDeclarators = varExpr.getVariables()?.ToList<VariableDeclarator>() ?? [];
49+
50+
// Java allows C-style array brackets on individual declarators, so a single declaration can mix
51+
// ranks (`int multi[][] = ..., single[] = ...;`). C# has no equivalent, and asking JavaParser for
52+
// a common type throws in that case, so emit one C# declaration per distinct array rank. The
53+
// groups stay flat siblings rather than a nested block so the variables remain in the same scope.
54+
var declaratorGroups = variableDeclarators
55+
.GroupBy(item => item.getType().getArrayLevel())
56+
.ToList();
57+
58+
if (declaratorGroups.Count > 1)
59+
{
60+
StatementSyntax? last = null;
61+
62+
foreach (var group in declaratorGroups)
63+
{
64+
if (last is not null)
65+
{
66+
context.PendingStatements.Add(last);
67+
}
68+
69+
last = VisitVariableDeclarationGroup(context, group.First().getType(), group.ToList());
70+
}
71+
72+
return last;
73+
}
74+
75+
return VisitVariableDeclarationGroup(context, varExpr.getCommonType(), variableDeclarators);
76+
}
77+
78+
private static StatementSyntax? VisitVariableDeclarationGroup(
79+
ConversionContext context,
80+
com.github.javaparser.ast.type.Type commonType,
81+
List<VariableDeclarator> variableDeclarators)
4782
{
48-
var commonType = varExpr.getCommonType();
4983
int? arrayRank = null;
5084

5185
var variables = new List<VariableDeclaratorSyntax>();
5286
var loweredSwitches = new List<StatementSyntax>();
5387

54-
var variableDeclarators = varExpr.getVariables()?.ToList<VariableDeclarator>() ?? [];
55-
5688
foreach (var item in variableDeclarators)
5789
{
5890
var type = item.getType();
5991

60-
if (arrayRank is not null && type.getArrayLevel() != arrayRank)
61-
{
62-
throw new InvalidOperationException("Different array levels in the same field declaration are not yet supported");
63-
}
64-
6592
arrayRank ??= type.getArrayLevel();
6693

67-
var id = item.getType();
6894
string name = item.getNameAsString();
6995

7096
if (type.getArrayLevel() > 0)

0 commit comments

Comments
 (0)