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)