diff --git a/JavaToCSharp.Tests/ConvertEnumTests.cs b/JavaToCSharp.Tests/ConvertEnumTests.cs new file mode 100644 index 00000000..7e6a2f54 --- /dev/null +++ b/JavaToCSharp.Tests/ConvertEnumTests.cs @@ -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()) ?? ""; +} diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index f12723d5..ec753c81 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -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 diff --git a/JavaToCSharp.Tests/Resources/NestedEnumStaticUsing.java b/JavaToCSharp.Tests/Resources/NestedEnumStaticUsing.java new file mode 100644 index 00000000..909bd73c --- /dev/null +++ b/JavaToCSharp.Tests/Resources/NestedEnumStaticUsing.java @@ -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); + } +} diff --git a/JavaToCSharp/ConversionContext.cs b/JavaToCSharp/ConversionContext.cs index 2f565f24..1360bba8 100644 --- a/JavaToCSharp/ConversionContext.cs +++ b/JavaToCSharp/ConversionContext.cs @@ -8,9 +8,23 @@ public class ConversionContext(JavaConversionOptions options) public ISet UsedAnonymousTypeNames { get; } = new HashSet(); + public ISet StaticUsingEnumNames { get; } = new HashSet(); + public JavaConversionOptions Options { get; } = options; + public ConversionState ConversionState { get; private set; } + public string? RootTypeName { get; set; } public string? LastTypeName { get; set; } + + /// + /// Records the new conversion state and raises . + /// + internal void ConversionStateChanged(ConversionState newState) + { + ConversionState = newState; + + Options.ConversionStateChanged(newState); + } } diff --git a/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs b/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs index 97a700cc..22869022 100644 --- a/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs @@ -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); @@ -118,4 +118,26 @@ EnumMemberDeclarationSyntax MembersToCommentTrivia(EnumMemberDeclarationSyntax l return lastMemberDecl; } } + + /// + /// 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. + /// + private static string GetTypeNameWithinPackage(EnumDeclaration enumDecl, string name) + { + var parts = new List { name }; + + for (var parent = enumDecl.getParentNode().FromOptional(); + parent is not null; + parent = parent.getParentNode().FromOptional()) + { + if (parent is TypeDeclaration typeDecl) + { + parts.Insert(0, typeDecl.getNameAsString()); + } + } + + return string.Join(".", parts); + } } diff --git a/JavaToCSharp/JavaConversionOptions.cs b/JavaToCSharp/JavaConversionOptions.cs index 85795cdf..4d78abca 100644 --- a/JavaToCSharp/JavaConversionOptions.cs +++ b/JavaToCSharp/JavaConversionOptions.cs @@ -12,8 +12,6 @@ public class JavaConversionOptions public IList Usings { get; } = new List(); - public IList StaticUsingEnumNames { get; } = new List(); - public bool IncludeSubdirectories { get; set; } = true; public bool IncludeUsings { get; set; } = true; @@ -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)); @@ -75,9 +71,5 @@ public JavaConversionOptions SetUsings(IEnumerable 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)); } diff --git a/JavaToCSharp/JavaToCSharpConverter.cs b/JavaToCSharp/JavaToCSharpConverter.cs index 676dbacf..eba282d7 100644 --- a/JavaToCSharp/JavaToCSharpConverter.cs +++ b/JavaToCSharp/JavaToCSharpConverter.cs @@ -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); @@ -47,7 +47,7 @@ public static class JavaToCSharpConverter var result = parsed.getResult().FromRequiredOptional(); - options.ConversionStateChanged(ConversionState.BuildingCSharpAst); + context.ConversionStateChanged(ConversionState.BuildingCSharpAst); var types = result.getTypes().ToList() ?? []; var imports = result.getImports()?.ToList() ?? []; @@ -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) ); @@ -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(); } diff --git a/JavaToCSharp/UsingsHelper.cs b/JavaToCSharp/UsingsHelper.cs index a4630ff6..f358633f 100644 --- a/JavaToCSharp/UsingsHelper.cs +++ b/JavaToCSharp/UsingsHelper.cs @@ -9,9 +9,9 @@ public static class UsingsHelper { public static IEnumerable GetUsings(ConversionContext context, IEnumerable imports, - JavaConversionOptions? options, NameSyntax? namespaceNameSyntax) { + var options = context.Options; var usings = new List(); foreach (var import in imports) @@ -25,7 +25,7 @@ public static IEnumerable 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)) { @@ -44,7 +44,7 @@ public static IEnumerable GetUsings(ConversionContext cont usings.Add(usingSyntax.NormalizeWhitespace().WithTrailingNewLines()); } - if (options?.IncludeUsings == true) + if (options.IncludeUsings) { usings.AddRange(options.Usings .Where(x => !string.IsNullOrWhiteSpace(x)) @@ -53,7 +53,7 @@ public static IEnumerable 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}"))