diff --git a/JavaToCSharp.Tests/ConcurrencyTests.cs b/JavaToCSharp.Tests/ConcurrencyTests.cs index bc2beef..d904012 100644 --- a/JavaToCSharp.Tests/ConcurrencyTests.cs +++ b/JavaToCSharp.Tests/ConcurrencyTests.cs @@ -23,9 +23,9 @@ public void ConvertType_IsThreadSafe() ("String", "string"), ("Integer", "int"), ("List", "IList"), - ("Map", "Dictionary"), + ("Map", "IDictionary"), ("int[]", "int[]"), - ("List>", "IList>"), + ("List>", "IList>"), ]; var failures = new System.Collections.Concurrent.ConcurrentBag(); diff --git a/JavaToCSharp.Tests/ConvertTypeTests.cs b/JavaToCSharp.Tests/ConvertTypeTests.cs index c9f56ce..4e52e83 100644 --- a/JavaToCSharp.Tests/ConvertTypeTests.cs +++ b/JavaToCSharp.Tests/ConvertTypeTests.cs @@ -58,6 +58,69 @@ public interface Lemmatizer { Assert.Contains("string[] Lemmatize(string[] toks, string[] tags);", parsed); } + [Theory] + // Collection interfaces keep their abstraction rather than binding to a concrete type (#134). + [InlineData("Map", "IDictionary")] + [InlineData("Set", "ISet")] + [InlineData("Collection", "ICollection")] + [InlineData("Iterable", "IEnumerable")] + [InlineData("SortedMap", "IDictionary")] + // ...while the concrete java implementations map to instantiable .NET types. + [InlineData("HashMap", "Dictionary")] + [InlineData("LinkedHashMap", "Dictionary")] + [InlineData("TreeMap", "SortedDictionary")] + [InlineData("TreeSet", "SortedSet")] + [InlineData("LinkedHashSet", "HashSet")] + public void ConvertType_Collections(string javaType, string expected) + { + Assert.Equal(expected, TypeHelper.ConvertType(javaType)); + } + + [Theory] + [InlineData("Character", "char")] + [InlineData("Double", "double")] + [InlineData("Short", "short")] + [InlineData("Byte", "sbyte")] // java's byte is signed + [InlineData("BigDecimal", "decimal")] + [InlineData("StringBuffer", "StringBuilder")] + public void ConvertType_SimpleTypes(string javaType, string expected) + { + Assert.Equal(expected, TypeHelper.ConvertType(javaType)); + } + + [Theory] + [InlineData("Throwable", "Exception")] + [InlineData("ClassCastException", "InvalidCastException")] + [InlineData("NumberFormatException", "FormatException")] + [InlineData("IndexOutOfBoundsException", "IndexOutOfRangeException")] + [InlineData("ArrayIndexOutOfBoundsException", "IndexOutOfRangeException")] + [InlineData("NoSuchElementException", "InvalidOperationException")] + [InlineData("OutOfMemoryError", "OutOfMemoryException")] + public void ConvertType_Exceptions(string javaType, string expected) + { + Assert.Equal(expected, TypeHelper.ConvertType(javaType)); + } + + [Fact] + public void ConvertType_MapDeclaration_AssignedFromHashMap() + { + const string javaCode = """ + import java.util.*; + + public class Holder { + private Map counts = new HashMap(); + } + """; + var options = new JavaConversionOptions + { + IncludeUsings = false, + IncludeNamespace = false, + }; + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + Assert.Contains("private IDictionary counts = new Dictionary();", parsed); + } + [Fact] public void ConvertType_GenericSingleParameter() { diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index 954079e..b53ea43 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -94,6 +94,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/ExceptionGetMessage.java")] [InlineData("Resources/LongLiterals.java")] [InlineData("Resources/MixedArrayRankDeclarations.java")] + [InlineData("Resources/CollectionTypeMappings.java")] public void FullIntegrationTests(string filePath, bool allowWarnings = false) => RunFullIntegrationTest(filePath, allowWarnings); @@ -115,7 +116,11 @@ private void RunFullIntegrationTest(string filePath, bool allowWarnings, bool us UseLabeledBreakAndContinue = useLabeledBreakAndContinue, }; + // Mirror the CLI's default usings so the compiled sample sees what a real conversion would. options.AddUsing("System"); + options.AddUsing("System.Collections.Generic"); + options.AddUsing("System.Linq"); + options.AddUsing("System.Text"); options.WarningEncountered += (_, eventArgs) => { @@ -251,6 +256,7 @@ private static IEnumerable GetMetadataReferencesForBcl() { yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll")); yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Console.dll")); + yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Collections.dll")); yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Linq.dll")); yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")); } diff --git a/JavaToCSharp.Tests/Resources/CollectionTypeMappings.java b/JavaToCSharp.Tests/Resources/CollectionTypeMappings.java new file mode 100644 index 0000000..6eb64aa --- /dev/null +++ b/JavaToCSharp.Tests/Resources/CollectionTypeMappings.java @@ -0,0 +1,29 @@ +/// Expect: +/// - output: "1\n2\nTrue\nTrue\na\n" +package example; + +public class Program { + public static void main(String[] args) { + // A variable declared against the java interface must convert to the .NET interface, + // while the concrete implementation it is assigned from must stay instantiable. + Map counts = new HashMap(); + counts.put("a", 1); + System.out.println(counts.get("a")); + + Map sorted = new TreeMap(); + sorted.put("b", 2); + System.out.println(sorted.get("b")); + + Set set = new HashSet(); + set.add("x"); + System.out.println(set.contains("x")); + + Set sortedSet = new TreeSet(); + sortedSet.add("y"); + System.out.println(sortedSet.contains("y")); + + List list = new ArrayList(); + list.add("a"); + System.out.println(list.get(0)); + } +} diff --git a/JavaToCSharp/TypeHelper.cs b/JavaToCSharp/TypeHelper.cs index bcaafcf..34b8908 100644 --- a/JavaToCSharp/TypeHelper.cs +++ b/JavaToCSharp/TypeHelper.cs @@ -17,37 +17,74 @@ public static class TypeHelper // so this must be a concurrent collection to keep parallel conversions safe. private static readonly ConcurrentDictionary _typeNameConversions = new() { - // Simple types + // Primitives and their boxed counterparts. Java's boxed types are nullable references while + // the C# equivalents are value types, so a null-valued Java variable will need manual review. ["boolean"] = "bool", ["Boolean"] = "bool", - ["ICloseable"] = "IDisposable", + ["Byte"] = "sbyte", // java's byte is signed, unlike C#'s + ["Character"] = "char", + ["Double"] = "double", + ["Float"] = "float", ["Integer"] = "int", ["Long"] = "long", - ["Float"] = "float", - ["String"] = "string", - ["Object"] = "object", - ["AutoCloseable"] = "IDisposable", + ["Short"] = "short", - // Generic types - ["ArrayList"] = "List", - ["List"] = "IList", - ["Map"] = "Dictionary", - ["Set"] = "HashSet", + // Other simple types + ["AutoCloseable"] = "IDisposable", + ["BigDecimal"] = "decimal", + ["Closeable"] = "IDisposable", + ["ICloseable"] = "IDisposable", + ["Object"] = "object", + ["String"] = "string", + ["StringBuffer"] = "StringBuilder", + + // Collection interfaces map to the .NET interfaces so that variables declared against an + // abstraction stay abstract; the concrete java implementations below supply the `new` types. + ["Collection"] = "ICollection", + ["Comparable"] = "IComparable", + ["Comparator"] = "IComparer", + ["Iterable"] = "IEnumerable", ["Iterator"] = "IEnumerator", + ["List"] = "IList", + ["Map"] = "IDictionary", + ["NavigableMap"] = "IDictionary", + ["NavigableSet"] = "ISet", + ["Set"] = "ISet", + ["SortedMap"] = "IDictionary", + ["SortedSet"] = "ISet", + + // Concrete collection implementations. These are what `new Foo<>()` expressions resolve to, + // so they must name instantiable .NET types rather than interfaces. + ["ArrayList"] = "List", + ["HashMap"] = "Dictionary", + ["LinkedHashMap"] = "Dictionary", + ["LinkedHashSet"] = "HashSet", + ["TreeMap"] = "SortedDictionary", + ["TreeSet"] = "SortedSet", // Exceptions + ["AccessDeniedException"] = "UnauthorizedAccessException", ["AlreadyClosedException"] = "ObjectDisposedException", + ["ArrayIndexOutOfBoundsException"] = "IndexOutOfRangeException", + ["AssertionError"] = "InvalidOperationException", + ["ClassCastException"] = "InvalidCastException", + ["CloneNotSupportedException"] = "NotSupportedException", + ["EOFException"] = "EndOfStreamException", ["Error"] = "Exception", ["IllegalArgumentException"] = "ArgumentException", ["IllegalStateException"] = "InvalidOperationException", - ["UnsupportedOperationException"] = "NotSupportedException", - ["RuntimeException"] = "Exception", - ["AccessDeniedException"] = "UnauthorizedAccessException", - ["AssertionError"] = "InvalidOperationException", + ["IndexOutOfBoundsException"] = "IndexOutOfRangeException", + ["InterruptedException"] = "OperationCanceledException", + ["NoSuchElementException"] = "InvalidOperationException", + ["NoSuchFileException"] = "FileNotFoundException", ["NullPointerException"] = "NullReferenceException", + ["NumberFormatException"] = "FormatException", + ["OutOfMemoryError"] = "OutOfMemoryException", + ["RuntimeException"] = "Exception", + ["StackOverflowError"] = "StackOverflowException", + ["Throwable"] = "Exception", ["UncheckedIOException"] = "IOException", - ["EOFException"] = "EndOfStreamException", - ["NoSuchFileException"] = "FileNotFoundException", + ["UnsupportedOperationException"] = "NotSupportedException", }; public static void AddOrUpdateTypeNameConversions(string key, string value) @@ -293,6 +330,9 @@ public static bool TryTransformMethodCall(ConversionContext context, MethodCallE return true; } + // Java's put returns the previous value, which an index assignment discards. That + // matches the existing handling of List.set, whose return value is dropped too. + case "put" when args.size() == 2: case "set" when args.size() == 2: { var scopeSyntaxSet = ExpressionVisitor.VisitExpression(context, scope);