Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions JavaToCSharp.Tests/ConvertMixedArrayRankDeclarationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
namespace JavaToCSharp.Tests;

/// <summary>
/// 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.
/// </summary>
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<string>();

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<string>? warnings = null)
{
var options = new JavaConversionOptions
{
IncludeComments = false,
};

options.WarningEncountered += (_, eventArgs) => warnings?.Add(eventArgs.Message);

return JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";
}
}
4 changes: 4 additions & 0 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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);

Expand Down
24 changes: 24 additions & 0 deletions JavaToCSharp.Tests/Resources/MixedArrayRankDeclarations.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 25 additions & 0 deletions JavaToCSharp.Tests/Resources/MixedArrayRankMultidimensional.java
Original file line number Diff line number Diff line change
@@ -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]);
}
}
46 changes: 36 additions & 10 deletions JavaToCSharp/Statements/ExpressionStatementVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,54 @@ public class ExpressionStatementVisitor : StatementVisitor<ExpressionStmt>
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<VariableDeclarator>() ?? [];

// 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<VariableDeclarator> variableDeclarators)
{
var commonType = varExpr.getCommonType();
int? arrayRank = null;

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

var variableDeclarators = varExpr.getVariables()?.ToList<VariableDeclarator>() ?? [];

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)
Expand Down
Loading