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
4 changes: 2 additions & 2 deletions JavaToCSharp.Tests/ConcurrencyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public void ConvertType_IsThreadSafe()
("String", "string"),
("Integer", "int"),
("List<String>", "IList<string>"),
("Map<String, Integer>", "Dictionary<string, int>"),
("Map<String, Integer>", "IDictionary<string, int>"),
("int[]", "int[]"),
("List<Map<String, Object>>", "IList<Dictionary<string, object>>"),
("List<Map<String, Object>>", "IList<IDictionary<string, object>>"),
];

var failures = new System.Collections.Concurrent.ConcurrentBag<string>();
Expand Down
63 changes: 63 additions & 0 deletions JavaToCSharp.Tests/ConvertTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Integer>", "IDictionary<string, int>")]
[InlineData("Set<String>", "ISet<string>")]
[InlineData("Collection<String>", "ICollection<string>")]
[InlineData("Iterable<String>", "IEnumerable<string>")]
[InlineData("SortedMap<String, Integer>", "IDictionary<string, int>")]
// ...while the concrete java implementations map to instantiable .NET types.
[InlineData("HashMap<String, Integer>", "Dictionary<string, int>")]
[InlineData("LinkedHashMap<String, Integer>", "Dictionary<string, int>")]
[InlineData("TreeMap<String, Integer>", "SortedDictionary<string, int>")]
[InlineData("TreeSet<String>", "SortedSet<string>")]
[InlineData("LinkedHashSet<String>", "HashSet<string>")]
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<String, Integer> counts = new HashMap<String, Integer>();
}
""";
var options = new JavaConversionOptions
{
IncludeUsings = false,
IncludeNamespace = false,
};
var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";

Assert.Contains("private IDictionary<string, int> counts = new Dictionary<string, int>();", parsed);
}

[Fact]
public void ConvertType_GenericSingleParameter()
{
Expand Down
6 changes: 6 additions & 0 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) =>
{
Expand Down Expand Up @@ -251,6 +256,7 @@ private static IEnumerable<MetadataReference> 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"));
}
Expand Down
29 changes: 29 additions & 0 deletions JavaToCSharp.Tests/Resources/CollectionTypeMappings.java
Original file line number Diff line number Diff line change
@@ -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<String, Integer> counts = new HashMap<String, Integer>();
counts.put("a", 1);
System.out.println(counts.get("a"));

Map<String, Integer> sorted = new TreeMap<String, Integer>();
sorted.put("b", 2);
System.out.println(sorted.get("b"));

Set<String> set = new HashSet<String>();
set.add("x");
System.out.println(set.contains("x"));

Set<String> sortedSet = new TreeSet<String>();
sortedSet.add("y");
System.out.println(sortedSet.contains("y"));

List<String> list = new ArrayList<String>();
list.add("a");
System.out.println(list.get(0));
}
}
74 changes: 57 additions & 17 deletions JavaToCSharp/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,74 @@ public static class TypeHelper
// so this must be a concurrent collection to keep parallel conversions safe.
private static readonly ConcurrentDictionary<string, string> _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)
Expand Down Expand Up @@ -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);
Expand Down
Loading