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
155 changes: 155 additions & 0 deletions JavaToCSharp.Tests/ConvertEnumTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
namespace JavaToCSharp.Tests;

public class ConvertEnumTests
{
[Fact]
public void Enum_In_Same_File_Emits_Static_Using()
{
const string javaCode = """
package com.example;
public enum Color { RED, GREEN }
""";

var parsed = Convert(javaCode);

Assert.Contains("using static Com.Example.Color;", parsed);
}

[Fact]
public void Static_Usings_Do_Not_Leak_Into_Later_Files_Sharing_Options()
{
const string enumCode = """
package com.example;
public enum Color { RED, GREEN }
""";
const string classCode = """
package com.example;
public class Foo {
}
""";

// The same options instance is reused across files, as the CLI and GUI do.
var options = NewOptions();

var enumResult = Convert(enumCode, options);
var classResult = Convert(classCode, options);

Assert.Contains("using static Com.Example.Color;", enumResult);
Assert.DoesNotContain("using static", classResult);
}

[Fact]
public void Static_Usings_Do_Not_Leak_Across_Namespaces()
{
const string enumCode = """
package com.foo;
public enum Color { RED, GREEN }
""";
const string classCode = """
package com.bar;
public class Foo {
}
""";

var options = NewOptions();

Convert(enumCode, options);
var classResult = Convert(classCode, options);

// Leaking here previously produced "using static Com.Bar.Color;", a
// reference to a type that does not exist in that namespace.
Assert.DoesNotContain("Color", classResult);
}

[Fact]
public void Nested_Enum_Static_Using_Includes_Declaring_Type()
{
const string javaCode = """
package com.example;
public class Holder {
public enum Inner { A, B }
}
""";

var parsed = Convert(javaCode);

// The C# type is Com.Example.Holder.Inner; a using naming only the enum
// would not resolve.
Assert.Contains("using static Com.Example.Holder.Inner;", parsed);
}

[Fact]
public void Deeply_Nested_Enum_Static_Using_Includes_Full_Path()
{
const string javaCode = """
package com.example;
public class Outer {
public static class Middle {
public enum Deep { A }
}
}
""";

var parsed = Convert(javaCode);

Assert.Contains("using static Com.Example.Outer.Middle.Deep;", parsed);
}

[Fact]
public void Enum_Nested_In_Interface_Static_Using_Includes_Declaring_Type()
{
const string javaCode = """
package com.example;
public interface Contract {
enum Kind { A, B }
}
""";

var parsed = Convert(javaCode);

Assert.Contains("using static Com.Example.Contract.Kind;", parsed);
}

[Fact]
public void Same_Enum_Name_At_Different_Scopes_Emits_Distinct_Static_Usings()
{
const string javaCode = """
package com.example;
public enum Color { RED }
public class Holder {
public enum Color { GREEN }
}
""";

var parsed = Convert(javaCode);

// These are two different C# types and each needs its own static using.
Assert.Contains("using static Com.Example.Color;", parsed);
Assert.Contains("using static Com.Example.Holder.Color;", parsed);
}

[Fact]
public void Static_Using_Is_Not_Duplicated_For_A_Single_Enum()
{
const string javaCode = """
package com.example;
public enum Color { RED, GREEN }
""";

var parsed = Convert(javaCode);

var occurrences = parsed.Split("using static Com.Example.Color;").Length - 1;
Assert.Equal(1, occurrences);
}

private static JavaConversionOptions NewOptions()
{
var options = new JavaConversionOptions { IncludeComments = false };
options.WarningEncountered += (_, eventArgs)
=> Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message);
return options;
}

private static string Convert(string javaCode, JavaConversionOptions? options = null)
=> JavaToCSharpConverter.ConvertText(javaCode, options ?? NewOptions()) ?? "";
}
1 change: 1 addition & 0 deletions JavaToCSharp.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath)
[InlineData("Resources/DeprecatedAnnotation.java")]
[InlineData("Resources/BooleanArrays.java")]
[InlineData("Resources/BinaryLiterals.java")]
[InlineData("Resources/NestedEnumStaticUsing.java")]
public void FullIntegrationTests(string filePath)
{
var options = new JavaConversionOptions
Expand Down
11 changes: 11 additions & 0 deletions JavaToCSharp.Tests/Resources/NestedEnumStaticUsing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Expect:
/// - output: "RED\n"
package example;

public class Program {
public enum Color { RED, GREEN }

public static void main(String[] args) {
System.out.println(Color.RED);
}
}
14 changes: 14 additions & 0 deletions JavaToCSharp/ConversionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@ public class ConversionContext(JavaConversionOptions options)

public ISet<string> UsedAnonymousTypeNames { get; } = new HashSet<string>();

public ISet<string> StaticUsingEnumNames { get; } = new HashSet<string>();

public JavaConversionOptions Options { get; } = options;

public ConversionState ConversionState { get; private set; }

public string? RootTypeName { get; set; }

public string? LastTypeName { get; set; }

/// <summary>
/// Records the new conversion state and raises <see cref="JavaConversionOptions.StateChanged"/>.
/// </summary>
internal void ConversionStateChanged(ConversionState newState)
{
ConversionState = newState;

Options.ConversionStateChanged(newState);
}
}
24 changes: 23 additions & 1 deletion JavaToCSharp/Declarations/EnumDeclarationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static EnumDeclarationSyntax VisitEnumDeclaration(ConversionContext conte
if (mods.Contains(Modifier.Keyword.PUBLIC))
enumSyntax = enumSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword));

context.Options.StaticUsingEnumNames.Add(name);
context.StaticUsingEnumNames.Add(GetTypeNameWithinPackage(enumDecl, name));

return enumSyntax.WithJavaComments(context, enumDecl);

Expand All @@ -118,4 +118,26 @@ EnumMemberDeclarationSyntax MembersToCommentTrivia(EnumMemberDeclarationSyntax l
return lastMemberDecl;
}
}

/// <summary>
/// Builds the enum's name qualified by any types it is nested within, excluding the package
/// (e.g. "Holder.Inner" for an enum "Inner" declared inside class "Holder"). A static using
/// must name the enum's declaring type, not just the enum itself, to resolve.
/// </summary>
private static string GetTypeNameWithinPackage(EnumDeclaration enumDecl, string name)
{
var parts = new List<string> { name };

for (var parent = enumDecl.getParentNode().FromOptional<Node>();
parent is not null;
parent = parent.getParentNode().FromOptional<Node>())
{
if (parent is TypeDeclaration typeDecl)
{
parts.Insert(0, typeDecl.getNameAsString());
}
}

return string.Join(".", parts);
}
}
10 changes: 1 addition & 9 deletions JavaToCSharp/JavaConversionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ public class JavaConversionOptions

public IList<string> Usings { get; } = new List<string>();

public IList<string> StaticUsingEnumNames { get; } = new List<string>();

public bool IncludeSubdirectories { get; set; } = true;

public bool IncludeUsings { get; set; } = true;
Expand All @@ -37,8 +35,6 @@ public class JavaConversionOptions

public SyntaxMapping SyntaxMappings { get; set; } = new SyntaxMapping();

public ConversionState ConversionState { get; set; }

public JavaConversionOptions AddPackageReplacement(string pattern, string replacement, RegexOptions options = RegexOptions.None)
{
PackageReplacements.Add(new Replacement(pattern, replacement, options));
Expand Down Expand Up @@ -75,9 +71,5 @@ public JavaConversionOptions SetUsings(IEnumerable<string> usings)
internal void Warning(string message, int javaLineNumber) => WarningEncountered?.Invoke(this, new ConversionWarningEventArgs(message, javaLineNumber));

internal void ConversionStateChanged(ConversionState newState)
{
ConversionState = newState;

StateChanged?.Invoke(this, new ConversionStateChangedEventArgs(newState));
}
=> StateChanged?.Invoke(this, new ConversionStateChangedEventArgs(newState));
}
12 changes: 6 additions & 6 deletions JavaToCSharp/JavaToCSharpConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ public static class JavaToCSharpConverter
{
options ??= new JavaConversionOptions();

options.ConversionStateChanged(ConversionState.Starting);

var context = new ConversionContext(options);

context.ConversionStateChanged(ConversionState.Starting);

var textBytes = Encoding.UTF8.GetBytes(javaText ?? string.Empty);

using var memoryStream = new MemoryStream(textBytes);
using var wrapper = new InputStreamWrapper(memoryStream);

options.ConversionStateChanged(ConversionState.ParsingJavaAst);
context.ConversionStateChanged(ConversionState.ParsingJavaAst);

var parser = new JavaParser();
parser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17);
Expand All @@ -47,7 +47,7 @@ public static class JavaToCSharpConverter

var result = parsed.getResult().FromRequiredOptional<CompilationUnit>();

options.ConversionStateChanged(ConversionState.BuildingCSharpAst);
context.ConversionStateChanged(ConversionState.BuildingCSharpAst);

var types = result.getTypes().ToList<TypeDeclaration>() ?? [];
var imports = result.getImports()?.ToList<ImportDeclaration>() ?? [];
Expand Down Expand Up @@ -127,7 +127,7 @@ public static class JavaToCSharpConverter

var root = SyntaxFactory.CompilationUnit(
externs: [],
usings: SyntaxFactory.List(UsingsHelper.GetUsings(context, imports, options, namespaceNameSyntax)),
usings: SyntaxFactory.List(UsingsHelper.GetUsings(context, imports, namespaceNameSyntax)),
attributeLists: [],
members: SyntaxFactory.List(rootMembers)
);
Expand All @@ -144,7 +144,7 @@ public static class JavaToCSharpConverter

var tree = SyntaxFactory.SyntaxTree(sanitizedRoot);

options.ConversionStateChanged(ConversionState.Done);
context.ConversionStateChanged(ConversionState.Done);

return tree.GetText().ToString();
}
Expand Down
8 changes: 4 additions & 4 deletions JavaToCSharp/UsingsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public static class UsingsHelper
{
public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext context,
IEnumerable<ImportDeclaration> imports,
JavaConversionOptions? options,
NameSyntax? namespaceNameSyntax)
{
var options = context.Options;
var usings = new List<UsingDirectiveSyntax>();

foreach (var import in imports)
Expand All @@ -25,7 +25,7 @@ public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext cont
var nameSpace = TypeHelper.Capitalize(importNameWithoutClassName);

// Override namespace if a non empty mapping is found (mapping to empty string removes the import)
if (options is not null && options.SyntaxMappings.ImportMappings.TryGetValue(importName, out var mappedNamespace))
if (options.SyntaxMappings.ImportMappings.TryGetValue(importName, out var mappedNamespace))
{
if (string.IsNullOrEmpty(mappedNamespace))
{
Expand All @@ -44,7 +44,7 @@ public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext cont
usings.Add(usingSyntax.NormalizeWhitespace().WithTrailingNewLines());
}

if (options?.IncludeUsings == true)
if (options.IncludeUsings)
{
usings.AddRange(options.Usings
.Where(x => !string.IsNullOrWhiteSpace(x))
Expand All @@ -53,7 +53,7 @@ public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext cont

if (namespaceNameSyntax is not null)
{
foreach (var staticUsing in options?.StaticUsingEnumNames ?? [])
foreach (var staticUsing in context.StaticUsingEnumNames)
{
var usingSyntax = SyntaxFactory
.UsingDirective(SyntaxFactory.ParseName($"{namespaceNameSyntax}.{staticUsing}"))
Expand Down
Loading