diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..af719b2e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: paulirwin +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6e0d0a8..cdff9d1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,10 +11,10 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v1 - - name: Setup .NET 7 + - name: Setup .NET 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.x + dotnet-version: 10.x - name: Build JavaToCSharp run: dotnet build ./JavaToCSharp/JavaToCSharp.csproj --configuration Release @@ -30,10 +30,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: Setup .NET 7 + - name: Setup .NET 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.x + dotnet-version: 10.x - name: Build JavaToCSharp run: dotnet build ./JavaToCSharp/JavaToCSharp.csproj --configuration Release diff --git a/JavaToCSharp.Tests/CommentTests.cs b/JavaToCSharp.Tests/CommentTests.cs new file mode 100644 index 00000000..3bd5514f --- /dev/null +++ b/JavaToCSharp.Tests/CommentTests.cs @@ -0,0 +1,168 @@ +using Xunit.Abstractions; + +namespace JavaToCSharp.Tests; + +public class CommentTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public void CommentsBeforePackage_ShouldRemainAtTopOfFile() + { + const string javaCode = """ + // 1 comment + // 2 comment + // Red comment + // Blue comment + package com.example.code; + + import java.something.Bar; + + public class Foo { + } + """; + var options = new JavaConversionOptions + { + StartInterfaceNamesWithI = true, + }; + + options.Usings.Clear(); + + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + testOutputHelper.WriteLine(parsed); + + const string expected = """ + // 1 comment + // 2 comment + // Red comment + // Blue comment + using Java.Something; + + namespace Com.Example.Code + { + public class Foo + { + } + } + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } + + [Fact] + public void CommentsBeforePackage_ShouldRemainAtTopOfFile_WithUsings() + { + const string javaCode = """ + // 1 comment + // 2 comment + // Red comment + // Blue comment + package com.example.code; + + // Import some package + import java.something.Bar; + + // Import some other package + import java.something.other.Baz; + + // Define some class + public class Foo { + } + """; + var options = new JavaConversionOptions + { + StartInterfaceNamesWithI = true, + }; + + options.Usings.Clear(); + + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + testOutputHelper.WriteLine(parsed); + + // TODO.PI: improve whitespace + const string expected = """ + // 1 comment + // 2 comment + // Red comment + // Blue comment + // Import some package + using Java.Something; + // Import some other package + using Java.Something.Other; + + namespace Com.Example.Code + { + // Define some class + public class Foo + { + } + } + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } + + [Theory] + [InlineData("Child", "Child")] + [InlineData("Child extends Parent", "Child : Parent")] + [InlineData("Child implements Parent", "Child : Parent")] + [InlineData("Child extends Parent implements IParent", "Child : Parent, IParent")] + + [InlineData("Parent", "Parent")] + [InlineData("Child>", "Child\n where T : BoundType")] + [InlineData("Child extends Parent", "Child : Parent")] + public void CommentsInsideClass_ShouldNotBeDuplicated_Fix_88(string javaClass, string csharpClass) + { + string javaCode = $$""" + //class comment + public class {{javaClass}} { + //before comment 1 + public void method1() { + doSomething(); //after comment1 + } + //before comment 2 + public void method1() { + doSomething(); //after comment2 + } + //before comment 3 + public void method1() { + } + } + """; + var options = new JavaConversionOptions + { + IncludeUsings = false, + IncludeNamespace = false, + }; + + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + testOutputHelper.WriteLine(parsed); + + string expected = $$""" + //class comment + public class {{csharpClass}} + { + //before comment 1 + public virtual void Method1() + { + DoSomething(); //after comment1 + } + + //before comment 2 + public virtual void Method1() + { + DoSomething(); //after comment2 + } + + //before comment 3 + public virtual void Method1() + { + } + } + + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } +} diff --git a/JavaToCSharp.Tests/ConvertExpressionTests.cs b/JavaToCSharp.Tests/ConvertExpressionTests.cs new file mode 100644 index 00000000..1c7098bf --- /dev/null +++ b/JavaToCSharp.Tests/ConvertExpressionTests.cs @@ -0,0 +1,31 @@ +using com.github.javaparser; +using com.github.javaparser.ast.expr; +using JavaToCSharp.Expressions; + +namespace JavaToCSharp.Tests; + +public class ConvertExpressionTests +{ + [Theory] + [InlineData("lst.size()", "lst.Count")] + [InlineData("lst.get(i)", "lst[i]")] + [InlineData("lst.set(i, value)", "lst[i] = value")] + [InlineData("str.length()", "str.Length")] + [InlineData("arr.length", "arr.Length")] + + //Conversion not done if param number does not match the required + [InlineData("obj.size(i)", "obj.Size(i)")] + [InlineData("obj.get()", "obj.Get()")] + [InlineData("obj.get(i, j)", "obj.Get(i,j)")] + [InlineData("obj.set(i)", "obj.Set(i)")] + [InlineData("obj.set(i, j ,k)", "obj.Set(i,j,k)")] + [InlineData("obj.length(i)", "obj.Length(i)")] + public void Convert_DesignatedMethods_Into_PropertyAccessors(string javaExpr, string expectedCSharpExpr) + { + ParseResult parseResult = new JavaParser().parseExpression(javaExpr); + Expression parsedExpr = parseResult.getResult().FromRequiredOptional(); + var expr = ExpressionVisitor.VisitExpression(new ConversionContext(new JavaConversionOptions()), parsedExpr); + Assert.Equal(expectedCSharpExpr, expr?.ToString()); + } + +} diff --git a/JavaToCSharp.Tests/ConvertInterfaceTests.cs b/JavaToCSharp.Tests/ConvertInterfaceTests.cs index 8c5699a1..96f5a425 100644 --- a/JavaToCSharp.Tests/ConvertInterfaceTests.cs +++ b/JavaToCSharp.Tests/ConvertInterfaceTests.cs @@ -5,61 +5,56 @@ public class ConvertInterfaceTests [Fact] public void Verify_Method_Param_Types_Use_New_Interface_Name_Types() { - var javaCode = """ - import com.github.javaparser.resolution.Context; - import com.github.javaparser.resolution.Test; - public interface ResolvedType { - default boolean isArray() { - return false; - } - } + const string javaCode = """ + import com.github.javaparser.resolution.Context; + import com.github.javaparser.resolution.Test; + public interface ResolvedType { + default boolean isArray() { + return false; + } + } - public class InferenceVariableType implements ResolvedType { - public void registerEquivalentType(ResolvedType type) { - } - } + public class InferenceVariableType implements ResolvedType { + public void registerEquivalentType(ResolvedType type) { + } + } - public interface ResolvedValueDeclaration extends ResolvedDeclaration { - ResolvedType getType(); - } + public interface ResolvedValueDeclaration extends ResolvedDeclaration { + ResolvedType getType(); + } - """; + """; var options = new JavaConversionOptions { StartInterfaceNamesWithI = true }; options.WarningEncountered += (_, eventArgs) - => Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message); + => Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message); var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; - var expectedCSharpCode = """ - using Com.Github.Javaparser.Resolution; - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - using System.Text; - - namespace MyApp - { - public interface IResolvedType - { - bool IsArray() - { - return false; - } - } - - public class InferenceVariableType : IResolvedType - { - public virtual void RegisterEquivalentType(IResolvedType type) - { - } - } - - public interface IResolvedValueDeclaration : ResolvedDeclaration - { - IResolvedType GetType(); - } - } - """; + const string expectedCSharpCode = """ + using Com.Github.Javaparser.Resolution; + + namespace MyApp + { + public interface IResolvedType + { + bool IsArray() + { + return false; + } + } + + public class InferenceVariableType : IResolvedType + { + public virtual void RegisterEquivalentType(IResolvedType type) + { + } + } + + public interface IResolvedValueDeclaration : ResolvedDeclaration + { + IResolvedType GetType(); + } + } + """; Assert.Equal(expectedCSharpCode.ReplaceLineEndings(), parsed.ReplaceLineEndings()); } @@ -67,32 +62,24 @@ public interface IResolvedValueDeclaration : ResolvedDeclaration [Fact] public void Verify_Interface_Extends_Are_Converted() { - var javaCode = """ - public interface CharTermAttribute extends Attribute, CharSequence, Appendable { - } - """; + const string javaCode = """ + public interface CharTermAttribute extends Attribute, CharSequence, Appendable { + } + """; var options = new JavaConversionOptions { StartInterfaceNamesWithI = true }; options.WarningEncountered += (_, eventArgs) - => Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message); + => Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message); var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; - var expectedCSharpCode = """ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - using System.Text; + const string expected = """ + namespace MyApp + { + public interface ICharTermAttribute : Attribute, CharSequence, Appendable + { + } + } + """; - namespace MyApp - { - public interface ICharTermAttribute : Attribute, CharSequence, Appendable - { - } - } - """; - - - - Assert.Equal(expectedCSharpCode.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); } -} \ No newline at end of file +} diff --git a/JavaToCSharp.Tests/ConvertTypeTests.cs b/JavaToCSharp.Tests/ConvertTypeTests.cs index ae499545..558a8efc 100644 --- a/JavaToCSharp.Tests/ConvertTypeTests.cs +++ b/JavaToCSharp.Tests/ConvertTypeTests.cs @@ -1,3 +1,5 @@ +using com.github.javaparser.ast.type; + namespace JavaToCSharp.Tests; public class ConvertTypeTests @@ -43,4 +45,103 @@ public void ConvertType_WithGenericWildcard_ShouldReplaceToken() { Assert.Equal("MyType", TypeHelper.ConvertType("MyType")); } + + [Fact] + public void ConvertTypeSyntax_GivenNonArrayType_ShouldReturnCorrectSyntax() + { + var type = new PrimitiveType(PrimitiveType.Primitive.INT); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 0); + + Assert.Equal("int", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenNonArrayTypeWithRank_ShouldReturnCorrectSyntax() + { + var type = new PrimitiveType(PrimitiveType.Primitive.INT); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 1); + + Assert.Equal("int[]", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenNonArrayTypeWithMultidimensionalRank_ShouldReturnCorrectSyntax() + { + var type = new PrimitiveType(PrimitiveType.Primitive.INT); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 2); + + Assert.Equal("int[,]", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenArrayTypeWithoutRank_ShouldReturnCorrectSyntax() + { + var type = new ArrayType(new PrimitiveType(PrimitiveType.Primitive.INT)); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 0); + + Assert.Equal("int[]", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenArrayTypeWithRank_ShouldReturnCorrectSyntax() + { + var type = new ArrayType(new PrimitiveType(PrimitiveType.Primitive.INT)); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 1); + + Assert.Equal("int[]", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenArrayTypeWithMultidimensionalRank_ShouldReturnCorrectSyntax() + { + var type = new ArrayType(new ArrayType(new PrimitiveType(PrimitiveType.Primitive.INT))); + + var syntax = TypeHelper.ConvertTypeSyntax(type, 2); + + Assert.Equal("int[,]", syntax.ToString()); + } + + [Fact] + public void ConvertTypeSyntax_GivenMismatchedRank_ShouldThrowException() + { + var type = new ArrayType(new ArrayType(new PrimitiveType(PrimitiveType.Primitive.INT))); + + Assert.Throws(() => TypeHelper.ConvertTypeSyntax(type, 1)); + } + + [Theory] + [InlineData("GenericClass", "GenericClass")] + [InlineData("GenericClass>", "GenericClass\n where T : BoundType")] + [InlineData("GenericClass", "GenericClass\n where T : BoundType")] + + [InlineData("GenericClass", "GenericClass")] + [InlineData("GenericClass>", "GenericClass\n where U : BoundType")] + [InlineData("GenericClass, U extends BoundType2>", "GenericClass\n where T : BoundType1 where U : BoundType2")] + + [InlineData("GenericClass & BoundType2>", "GenericClass\n where T : BoundType1, BoundType2")] + public void ConvertClassTypeBoundedParameters(string javaClass, string csharpClass) + { + string javaCode = $$""" + public class {{javaClass}} { } + """; + var options = new JavaConversionOptions + { + IncludeUsings = false, + IncludeNamespace = false, + }; + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + string expected = $$""" + public class {{csharpClass}} + { + } + + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } } diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index 28f67d4e..f12723d5 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Xunit.Abstractions; namespace JavaToCSharp.Tests; @@ -11,7 +12,7 @@ namespace JavaToCSharp.Tests; /// /// Uses some BSD-2-Clause licensed code from Jaktnat. License: https://github.com/paulirwin/jaktnat/blob/main/LICENSE /// -public class IntegrationTests +public class IntegrationTests(ITestOutputHelper testOutputHelper) { [Theory] [InlineData("Resources/ArrayField.java")] @@ -19,18 +20,27 @@ public class IntegrationTests [InlineData("Resources/TestNumericDocValuesUpdates.java")] [InlineData("Resources/Java9DiamondOperatorInnerClass.java")] [InlineData("Resources/Java11LambdaInference.java")] - public void GeneralSuccessfulConversionTest(string filePath) + [InlineData("Resources/MultidimensionalArrays.java", true)] + public void GeneralSuccessfulConversionTest(string filePath, bool allowWarnings = false) { var options = new JavaConversionOptions { IncludeComments = false, }; - - options.WarningEncountered += (_, eventArgs) - => throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}"); - + + options.WarningEncountered += (_, eventArgs) => + { + if (!allowWarnings) + { + throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}"); + } + }; + var parsed = JavaToCSharpConverter.ConvertText(File.ReadAllText(filePath), options); + Assert.NotNull(parsed); + + testOutputHelper.WriteLine(parsed); } [Theory] @@ -41,10 +51,11 @@ public void GeneralUnsuccessfulConversionTest(string filePath) { IncludeComments = false, }; - + options.WarningEncountered += (_, eventArgs) - => throw new InvalidOperationException($"Encountered a warning in conversion when we expected a failure: {eventArgs.Message}"); - + => throw new InvalidOperationException( + $"Encountered a warning in conversion when we expected a failure: {eventArgs.Message}"); + Assert.ThrowsAny(() => JavaToCSharpConverter.ConvertText(File.ReadAllText(filePath), options)); } @@ -55,8 +66,12 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/Java9TryWithResources.java")] [InlineData("Resources/Java9PrivateInterfaceMethods.java")] [InlineData("Resources/Java10TypeInference.java")] + [InlineData("Resources/Java14SwitchExpressions.java")] [InlineData("Resources/NewArrayLiteralBug.java")] [InlineData("Resources/OctalLiteralBug.java")] + [InlineData("Resources/DeprecatedAnnotation.java")] + [InlineData("Resources/BooleanArrays.java")] + [InlineData("Resources/BinaryLiterals.java")] public void FullIntegrationTests(string filePath) { var options = new JavaConversionOptions @@ -64,20 +79,25 @@ public void FullIntegrationTests(string filePath) ConvertSystemOutToConsole = true, IncludeComments = false, }; - + + options.AddUsing("System"); + options.WarningEncountered += (_, eventArgs) => throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}"); var javaText = File.ReadAllText(filePath); - + var parsed = JavaToCSharpConverter.ConvertText(javaText, options); + Assert.NotNull(parsed); + testOutputHelper.WriteLine(parsed); + var fileName = Path.GetFileNameWithoutExtension(filePath); var assembly = CompileAssembly(fileName, parsed); - + var expectation = ParseExpectation(javaText); - + // NOTE: examples must have a class name of Program in the example package var programType = assembly.GetType("Example.Program"); @@ -85,17 +105,17 @@ public void FullIntegrationTests(string filePath) { throw new InvalidOperationException("Cannot find expected Program type in assembly"); } - + var mainMethod = programType.GetMethod("Main", BindingFlags.Static | BindingFlags.Public); if (mainMethod is null) { throw new InvalidOperationException("Cannot find expected Main method in assembly"); } - + using var sw = new StringWriter(); Console.SetOut(sw); - + try { mainMethod.Invoke(null, new object[] { Array.Empty() }); @@ -109,7 +129,7 @@ public void FullIntegrationTests(string filePath) return; } - + var output = sw.ToString().ReplaceLineEndings("\n"); if (expectation.Output != null) @@ -129,14 +149,14 @@ public void FullIntegrationTests(string filePath) private static Assembly CompileAssembly(string assemblyName, string cSharpLanguageText) { var syntaxTree = CSharpSyntaxTree.ParseText(cSharpLanguageText); - + var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication) .WithOverflowChecks(true) .WithOptimizationLevel(OptimizationLevel.Debug); - - var compilation = CSharpCompilation.Create(assemblyName, - new List { syntaxTree }, - GetMetadataReferencesForBcl(), + + var compilation = CSharpCompilation.Create(assemblyName, + new List { syntaxTree }, + GetMetadataReferencesForBcl(), options); var outputDir = Path.Join(Environment.CurrentDirectory, "bin"); @@ -150,7 +170,8 @@ private static Assembly CompileAssembly(string assemblyName, string cSharpLangua if (!emitResult.Success) { - throw new InvalidOperationException($"Failed to emit Roslyn assembly: {string.Join(", ", emitResult.Diagnostics)}"); + throw new InvalidOperationException( + $"Failed to emit Roslyn assembly: {string.Join(", ", emitResult.Diagnostics)}"); } ms.Position = 0; @@ -188,7 +209,7 @@ private static IEnumerable GetMetadataReferencesForBcl() yield return MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")); } } - + private static Expectation ParseExpectation(string contents) { using var sr = new StringReader(contents); @@ -216,7 +237,7 @@ private static Expectation ParseExpectation(string contents) var literal = FindLiteralExpressionSyntax(root); - if (literal != null) + if (literal != null) { expectation.Output = literal.Token.ValueText; } @@ -235,7 +256,7 @@ private static Expectation ParseExpectation(string contents) return expectation; } - + private static LiteralExpressionSyntax? FindLiteralExpressionSyntax(SyntaxNode node) { if (node is LiteralExpressionSyntax literal) @@ -254,4 +275,4 @@ private class Expectation public string? Error { get; set; } } -} \ No newline at end of file +} diff --git a/JavaToCSharp.Tests/JavaToCSharp.Tests.csproj b/JavaToCSharp.Tests/JavaToCSharp.Tests.csproj index 7ede5601..51827f4d 100644 --- a/JavaToCSharp.Tests/JavaToCSharp.Tests.csproj +++ b/JavaToCSharp.Tests/JavaToCSharp.Tests.csproj @@ -1,32 +1,32 @@  - net7.0 + net10.0 false enable latest enable - nullable + true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - + diff --git a/JavaToCSharp.Tests/Resources/BinaryLiterals.java b/JavaToCSharp.Tests/Resources/BinaryLiterals.java new file mode 100644 index 00000000..cdb11f1b --- /dev/null +++ b/JavaToCSharp.Tests/Resources/BinaryLiterals.java @@ -0,0 +1,13 @@ +/// Expect: +/// - output: "5\n5\n" +package example; + +// see issue #83 +public class Program { + public static void main(String[] args) { + int binary = 0b101; + long longBinary = 0b101L; + System.out.println(binary); + System.out.println(longBinary); + } +} diff --git a/JavaToCSharp.Tests/Resources/BooleanArrays.java b/JavaToCSharp.Tests/Resources/BooleanArrays.java new file mode 100644 index 00000000..36ed7da3 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/BooleanArrays.java @@ -0,0 +1,17 @@ +/// Expect: +/// - output: "2\n" +package example; + +// see issue #98 +public class Program { + static boolean[] myBooleans = new boolean[] { true, false, true, false }; + public static void main(String[] args) { + int trueCount = 0; + for (boolean b : myBooleans) { + if (b) { + trueCount++; + } + } + System.out.println(trueCount); + } +} diff --git a/JavaToCSharp.Tests/Resources/DeprecatedAnnotation.java b/JavaToCSharp.Tests/Resources/DeprecatedAnnotation.java new file mode 100644 index 00000000..45f1874c --- /dev/null +++ b/JavaToCSharp.Tests/Resources/DeprecatedAnnotation.java @@ -0,0 +1,10 @@ +/// Expect: +/// - output: "Hello world!\n" +package example; + +@Deprecated +public class Program { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} diff --git a/JavaToCSharp.Tests/Resources/Java14SwitchExpressions.java b/JavaToCSharp.Tests/Resources/Java14SwitchExpressions.java new file mode 100644 index 00000000..9354ce3e --- /dev/null +++ b/JavaToCSharp.Tests/Resources/Java14SwitchExpressions.java @@ -0,0 +1,23 @@ +/// Expect: +/// - output: "9\n" +package example; + +// https://docs.oracle.com/en/java/javase/14/language/switch-expressions.html#GUID-BA4F63E3-4823-43C6-A5F3-BAA4A2EF3ADC + +enum Day { SUNDAY, MONDAY, TUESDAY, + WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; } + +public class Program { + public static void main(String[] args) { + Day day = Day.WEDNESDAY; + System.out.println( + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> 6; + case TUESDAY -> 7; + case THURSDAY, SATURDAY -> 8; + case WEDNESDAY -> 9; + default -> throw new IllegalStateException("Invalid day: " + day); + } + ); + } +} diff --git a/JavaToCSharp.Tests/Resources/MultidimensionalArrays.java b/JavaToCSharp.Tests/Resources/MultidimensionalArrays.java new file mode 100644 index 00000000..79ef5acb --- /dev/null +++ b/JavaToCSharp.Tests/Resources/MultidimensionalArrays.java @@ -0,0 +1,47 @@ +// NOTE: this test case only parses and converts successfully, it does not yet work. +// The multidimensional array indexers do not translate to the correct syntax. +package example; + +public class Program { + private static int[][] multiField = new int[2][2]; + private static int[] singleField = new int[2]; + + static { + multiField[0][0] = 10; + multiField[0][1] = 20; + multiField[1][0] = 30; + multiField[1][1] = 40; + + singleField[0] = 11; + singleField[1] = 21; + } + + public static void main(String[] args) { + int multi[][] = new int[2][2]; + int single[] = new int[2]; + + multi[0][0] = 1; + multi[0][1] = 2; + multi[1][0] = 3; + multi[1][1] = 4; + + System.out.println(multi[0][0]); + System.out.println(multi[0][1]); + System.out.println(multi[1][0]); + System.out.println(multi[1][1]); + + single[0] = 1; + single[1] = 2; + + System.out.println(single[0]); + System.out.println(single[1]); + + System.out.println(multiField[0][0]); + System.out.println(multiField[0][1]); + System.out.println(multiField[1][0]); + System.out.println(multiField[1][1]); + + System.out.println(singleField[0]); + System.out.println(singleField[1]); + } +} diff --git a/JavaToCSharp.Tests/SyntaxMappingTests.cs b/JavaToCSharp.Tests/SyntaxMappingTests.cs new file mode 100644 index 00000000..17117739 --- /dev/null +++ b/JavaToCSharp.Tests/SyntaxMappingTests.cs @@ -0,0 +1,116 @@ +using YamlDotNet.Core; + +namespace JavaToCSharp.Tests; + +public class SyntaxMappingTests +{ + [Fact] + public void Deserialize_Mappings() + { + var mappingString = """ + ImportMappings: + org.junit.Test : XUnit + java.util.List : "" + """; + + var mappings = SyntaxMapping.Deserialize(mappingString); + Assert.NotNull(mappings); + Assert.Equal(2, mappings.ImportMappings.Count); + Assert.Equal("XUnit", mappings.ImportMappings["org.junit.Test"]); + Assert.Equal("", mappings.ImportMappings["java.util.List"]); + Assert.False(mappings.ImportMappings.ContainsKey("other.Clazz")); + Assert.Empty(mappings.VoidMethodMappings); + Assert.Empty(mappings.NonVoidMethodMappings); + Assert.Empty(mappings.AnnotationMappings); + } + + [Fact] + public void Conversion_Options_Defaults_To_Empty_Mappings() + { + var options = new JavaConversionOptions(); + Assert.Empty(options.SyntaxMappings.ImportMappings); + Assert.Empty(options.SyntaxMappings.VoidMethodMappings); + Assert.Empty(options.SyntaxMappings.NonVoidMethodMappings); + Assert.Empty(options.SyntaxMappings.AnnotationMappings); + } + + [Theory] + [InlineData("VoidMethodMappings:\n org.junit.Assert.assertTrue : Assert.True")] + [InlineData("VoidMethodMappings:\n assertTrue : \"\"")] + [InlineData("NonVoidMethodMappings:\n org.junit.Assert.assertTrue : Assert.True")] + [InlineData("NonVoidMethodMappings:\n assertTrue : \"\"")] + public void Validation_Exceptions(string mappingString) + { + Assert.Throws(() => SyntaxMapping.Deserialize(mappingString)); + } + + // The use of mappings is tested using a typical JUnit4 test converted to Xunit: + // - Multiple java imports: rewritten and removed (empty value) + // - Multiple java methods: rewritten (void) and not rewritten (non void) + // - Multiple Java method annotations: rewritten and removed (no mapping) + // Not tested: + // - Qualified java method/annotation names (this would require a more elaborated handling of the scope) + // - No qualified CSharp methods (this would require CSharp static imports, that are not implemented) + // - Annotations with parameters + [Fact] + public void Conversion_With_Import_Method_And_Annotation_Mappings() + { + const string javaCode = """ + import static org.junit.Assert.assertEquals; + import static org.junit.Assert.assertTrue; + import org.junit.Test; + public class MappingsTest { + @Test @CustomJava @NotMapped + public void testAsserts() { + assertEquals("a", "a"); + assertTrue(true); + va.assertTrue(true); // non void is not mapped + } + } + + """; + var mappingsYaml = """ + ImportMappings: + org.junit.Test : Xunit + #to remove static imports + org.junit.Assert.assertEquals : "" + org.junit.Assert.assertTrue : "" + VoidMethodMappings: + assertEquals : Assert.Equal + assertTrue : Assert.True + AnnotationMappings: + Test : Fact + CustomJava : CustomCs + + """; + const string expectedCSharpCode = """ + using Xunit; + + public class MappingsTest + { + [Fact] + [CustomCs] + public virtual void TestAsserts() + { + Assert.Equal("a", "a"); + Assert.True(true); + va.AssertTrue(true); // non void is not mapped + } + } + + """; + + var parsed = GetParsed(javaCode, mappingsYaml); + Assert.Equal(expectedCSharpCode.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } + + private static string GetParsed(string javaCode, string mappingsYaml) + { + var mappings = SyntaxMapping.Deserialize(mappingsYaml); + var options = new JavaConversionOptions { IncludeNamespace = false, IncludeUsings = false, SyntaxMappings = mappings }; + options.WarningEncountered += (_, eventArgs) + => Console.WriteLine("Line {0}: {1}", eventArgs.JavaLineNumber, eventArgs.Message); + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + return parsed; + } +} diff --git a/JavaToCSharp.sln b/JavaToCSharp.sln deleted file mode 100644 index 13adf14d..00000000 --- a/JavaToCSharp.sln +++ /dev/null @@ -1,58 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33829.357 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaToCSharp", "JavaToCSharp\JavaToCSharp.csproj", "{EDCE2F7C-299E-4FEA-870F-87C201FB2E43}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{7A18D136-8A66-4B98-8D8E-9C210E0C8D2D}" - ProjectSection(SolutionItems) = preProject - Lib\javaparser-core-3.25.4.jar = Lib\javaparser-core-3.25.4.jar - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaToCSharpCli", "JavaToCSharpCli\JavaToCSharpCli.csproj", "{8C56AE52-8E0F-438F-9B5B-87B891D70861}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaToCSharpGui", "JavaToCSharpGui\JavaToCSharpGui.csproj", "{DAA3F412-0460-40C3-98F7-3244649820F9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E8E0DCFE-ED61-442C-9F4B-36433D6949DF}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaToCSharp.Tests", "JavaToCSharp.Tests\JavaToCSharp.Tests.csproj", "{0CBBEF05-FD79-474A-A5F0-25B341B8B77D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{DE56E074-10C7-47BA-8E8A-DAB0F3F92181}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EDCE2F7C-299E-4FEA-870F-87C201FB2E43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDCE2F7C-299E-4FEA-870F-87C201FB2E43}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDCE2F7C-299E-4FEA-870F-87C201FB2E43}.Release|Any CPU.Build.0 = Release|Any CPU - {EDCE2F7C-299E-4FEA-870F-87C201FB2E43}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C56AE52-8E0F-438F-9B5B-87B891D70861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C56AE52-8E0F-438F-9B5B-87B891D70861}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C56AE52-8E0F-438F-9B5B-87B891D70861}.Release|Any CPU.Build.0 = Release|Any CPU - {8C56AE52-8E0F-438F-9B5B-87B891D70861}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DAA3F412-0460-40C3-98F7-3244649820F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DAA3F412-0460-40C3-98F7-3244649820F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DAA3F412-0460-40C3-98F7-3244649820F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DAA3F412-0460-40C3-98F7-3244649820F9}.Release|Any CPU.Build.0 = Release|Any CPU - {0CBBEF05-FD79-474A-A5F0-25B341B8B77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0CBBEF05-FD79-474A-A5F0-25B341B8B77D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0CBBEF05-FD79-474A-A5F0-25B341B8B77D}.Release|Any CPU.Build.0 = Release|Any CPU - {0CBBEF05-FD79-474A-A5F0-25B341B8B77D}.Debug|Any CPU.Build.0 = Debug|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {0CBBEF05-FD79-474A-A5F0-25B341B8B77D} = {DE56E074-10C7-47BA-8E8A-DAB0F3F92181} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {EB538484-5025-4753-B0A5-2C66A6C06193} - EndGlobalSection -EndGlobal diff --git a/JavaToCSharp.slnx b/JavaToCSharp.slnx new file mode 100644 index 00000000..a964be56 --- /dev/null +++ b/JavaToCSharp.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/JavaToCSharp.sln.DotSettings b/JavaToCSharp.slnx.DotSettings similarity index 65% rename from JavaToCSharp.sln.DotSettings rename to JavaToCSharp.slnx.DotSettings index 9e8d4492..fb2552b8 100644 --- a/JavaToCSharp.sln.DotSettings +++ b/JavaToCSharp.slnx.DotSettings @@ -1,5 +1,7 @@  <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></Policy> + True True True True diff --git a/JavaToCSharp/CommentsHelper.cs b/JavaToCSharp/CommentsHelper.cs index 1ff289ac..06da9c9f 100644 --- a/JavaToCSharp/CommentsHelper.cs +++ b/JavaToCSharp/CommentsHelper.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; - +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,11 +6,10 @@ using JavaAst = com.github.javaparser.ast; using JavaComments = com.github.javaparser.ast.comments; using JavaParser = com.github.javaparser; -using SysRegex = System.Text.RegularExpressions; namespace JavaToCSharp; -public static class CommentsHelper +public static partial class CommentsHelper { private enum CommentPosition { @@ -22,8 +18,8 @@ private enum CommentPosition } // Regex: Optional *, capture optional @par, capture optional text (keep leading whitespaces, trim end). - private static readonly SysRegex.Regex _analyzeDocString = - new(@"^(\s*\*)?(\s*(?@[a-z]+))?\s?(?.*?)\s*$", SysRegex.RegexOptions.Compiled); + [GeneratedRegex(@"^(\s*\*)?(\s*(?@[a-z]+))?\s?(?.*?)\s*$", RegexOptions.Compiled)] + private static partial Regex AnalyzeDocStringRegex { get; } private static readonly Dictionary _knownTagsDict = new() { @@ -33,21 +29,79 @@ private enum CommentPosition ["@throws"] = "exception" }; - public static TSyntax? AddCommentsTrivias(TSyntax? syntax, JavaAst.Node? node, string? commentEnding) where TSyntax : SyntaxNode + public static UsingDirectiveSyntax AddUsingComments(UsingDirectiveSyntax syntax, JavaAst.ImportDeclaration import) + { + var comments = GatherComments(import); + + if (comments.Count == 0) + { + return syntax; + } + + var leadingTriviaList = new List(); + + foreach (var (comment, _) in comments) + { + var (kind, pre, post) = GetCommentInfo(comment); + var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post); + leadingTriviaList.Add(commentTrivia); + } + + return syntax.WithUsingKeyword(SyntaxFactory.Token(SyntaxKind.UsingKeyword).WithLeadingTrivia(leadingTriviaList)); + } + + public static CompilationUnitSyntax AddPackageComments(CompilationUnitSyntax syntax, + JavaAst.CompilationUnit compilationUnit, + JavaAst.PackageDeclaration? packageDeclaration) + { + var originalLeadingTrivia = syntax.GetLeadingTrivia(); + var leadingTriviaList = new List(); + + if (compilationUnit.getComment().FromOptional() is { } compilationUnitComment) + { + var (kind, pre, post) = GetCommentInfo(compilationUnitComment); + var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + compilationUnitComment.getContent() + post + Environment.NewLine); + leadingTriviaList.Add(commentTrivia); + } + + if (packageDeclaration is not null) + { + var packageComments = GatherComments(packageDeclaration); + + if (packageComments.Count > 0) + { + foreach (var (comment, _) in packageComments) + { + var (kind, pre, post) = GetCommentInfo(comment); + var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post + Environment.NewLine); + leadingTriviaList.Add(commentTrivia); + } + } + } + + leadingTriviaList.AddRange(originalLeadingTrivia); + + return leadingTriviaList.Count > 0 ? syntax.WithLeadingTrivia(leadingTriviaList) : syntax; + } + + public static TSyntax? AddCommentsTrivias(TSyntax? syntax, JavaAst.Node? node) where TSyntax : SyntaxNode { if (syntax is null) { return null; } - + var comments = GatherComments(node); + if (comments.Count > 0) { var leadingTriviaList = new List(); var trailingTriviaList = new List(); + foreach (var (comment, pos) in comments) { - var (kind, pre, post) = GetCommentInfo(comment, commentEnding); + var (kind, pre, post) = GetCommentInfo(comment); + if (kind == SyntaxKind.XmlComment) { leadingTriviaList.AddRange(ConvertDocComment(comment, post)); @@ -75,23 +129,27 @@ private enum CommentPosition } private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( - JavaComments.Comment comment, - string? commentEnding) + JavaComments.Comment comment) { return comment switch { - JavaComments.BlockComment => (SyntaxKind.MultiLineCommentTrivia, "/*", "*/" + commentEnding), - JavaComments.JavadocComment => (SyntaxKind.XmlComment, null, commentEnding), - _ => (SyntaxKind.SingleLineCommentTrivia, "//", commentEnding), + JavaComments.BlockComment => (SyntaxKind.MultiLineCommentTrivia, "/*", "*/"), + JavaComments.JavadocComment => (SyntaxKind.XmlComment, null, null), + _ => (SyntaxKind.SingleLineCommentTrivia, "//", null), }; } private static List<(JavaComments.Comment c, CommentPosition pos)> GatherComments(JavaAst.Node? node) { var result = new List<(JavaComments.Comment c, CommentPosition pos)>(); - if (node == null) return result; + + if (node is null) + { + return result; + } var parentNode = node.getParentNode().FromOptional(); + if (parentNode is null) { if (node.getComment().FromOptional() is { } comment) @@ -102,6 +160,7 @@ private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( else { var unsortedComments = parentNode.getAllContainedComments(); + if (unsortedComments.size() != 0) { var comments = unsortedComments.OfType() @@ -110,7 +169,7 @@ private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( .ToList(); // Find leading comments - var nodeBegin = node.getBegin().FromOptional() + var nodeBegin = node.getBegin().FromOptional() ?? throw new InvalidOperationException("Node did not have a begin position"); var previousSibling = GetPreviousSibling(parentNode, nodeBegin); int previousPos = previousSibling?.getEnd().FromOptional()?.line ?? 0; @@ -120,7 +179,7 @@ private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( // Find trailing comments. // We consider only comments either appearing on the same line or, if no sibling nodes follow, // then also comments on the succeeding lines (because otherwise they belong to the next sibling). - var nodeEnd = node.getEnd().FromOptional() + var nodeEnd = node.getEnd().FromOptional() ?? throw new InvalidOperationException("Node did not have an end position"); var trailingComments = HasNextSibling(parentNode, nodeEnd) @@ -138,7 +197,7 @@ private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( comments.Where(c => { var commentBegin = c.getBegin().FromOptional(); - return commentBegin != null && (commentBegin.line == nodeEnd.line && commentBegin.column > nodeEnd.column || commentBegin.line > nodeEnd.line); + return commentBegin is not null && (commentBegin.line == nodeEnd.line && commentBegin.column > nodeEnd.column || commentBegin.line > nodeEnd.line); }) .Select(c => (c, CommentPosition.Trailing)); @@ -147,7 +206,7 @@ private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( .Where(c => { var commentBegin = c.getBegin().FromOptional(); - return commentBegin != null && commentBegin.line == nodeEnd.line && commentBegin.column > nodeEnd.column; + return commentBegin is not null && commentBegin.line == nodeEnd.line && commentBegin.column > nodeEnd.column; }) .Select(c => (c, CommentPosition.Trailing)); @@ -159,7 +218,7 @@ private static bool HasNextSibling(JavaAst.Node parentNode, JavaParser.Position .Any(sibling => { var siblingBegin = sibling.getBegin().FromOptional(); - return siblingBegin != null && (siblingBegin.line > nodeEnd.line || siblingBegin.line == nodeEnd.line && siblingBegin.column > nodeEnd.column); + return siblingBegin is not null && (siblingBegin.line > nodeEnd.line || siblingBegin.line == nodeEnd.line && siblingBegin.column > nodeEnd.column); }); } @@ -169,7 +228,7 @@ private static bool HasNextSibling(JavaAst.Node parentNode, JavaParser.Position { var commentBegin = c.getBegin().FromOptional(); var commentEnd = c.getEnd().FromOptional(); - return commentBegin != null && commentEnd != null && commentBegin.line > previousPos && (commentEnd.line < nodeBegin.line || commentEnd.line == nodeBegin.line && commentEnd.column < nodeBegin.column); + return commentBegin is not null && commentEnd is not null && commentBegin.line > previousPos && (commentEnd.line < nodeBegin.line || commentEnd.line == nodeBegin.line && commentEnd.column < nodeBegin.column); }) .Select(c => (c, CommentPosition.Leading)); } @@ -179,10 +238,11 @@ private static bool HasNextSibling(JavaAst.Node parentNode, JavaParser.Position return parentNode.getChildNodes() .OfType() .Where(sibling => sibling is not JavaComments.Comment) + .OrderBy(sibling => sibling.getEnd().FromOptional()) // fix #88 .LastOrDefault(sibling => { var siblingEnd = sibling.getEnd().FromOptional(); - return siblingEnd != null && (siblingEnd.line < nodeBegin.line || siblingEnd.line == nodeBegin.line && siblingEnd.column < nodeBegin.column); + return siblingEnd is not null && (siblingEnd.line < nodeBegin.line || siblingEnd.line == nodeBegin.line && siblingEnd.column < nodeBegin.column); }); } @@ -198,7 +258,7 @@ public static IEnumerable ConvertToComment(IEnumerable(); foreach (var code in codes) { - string[] input = code.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); + string[] input = code.ToString().Split([Environment.NewLine], StringSplitOptions.None); outputs.AddRange(input); } @@ -223,14 +283,14 @@ public static IEnumerable ConvertToComment(IEnumerable ConvertDocComment(JavaComments.Comment comment, string? post) { - string[] input = comment.getContent().Split(new[] { Environment.NewLine }, StringSplitOptions.None); + string[] input = comment.getContent().Split([Environment.NewLine], StringSplitOptions.None); var output = new List(); var remarks = new List(); // For Java tags unknown in C# var currentOutput = output; string? tag = null; foreach (string inputLine in input) { - var match = _analyzeDocString.Match(inputLine); + var match = AnalyzeDocStringRegex.Match(inputLine); if (match.Success) { string paramName = match.Groups["param"].Value; @@ -250,7 +310,7 @@ private static IEnumerable ConvertDocComment(JavaComments.Comment remarks.Add(paramName + text); tag = "remarks"; } - else if (tag == null) + else if (tag is null) { tag = "summary"; OpenSection(output, tag, text); @@ -304,14 +364,14 @@ private static void CloseSection(IList output, string? tag) } else { - output[output.Count - 1] += xmlEndTag; + output[^1] += xmlEndTag; } } } private static void TrimTrailingEmptyLines(IList lines) { - while (lines.Count > 0 && lines[lines.Count - 1].Trim() == "") + while (lines.Count > 0 && lines[^1].Trim() == "") { lines.RemoveAt(lines.Count - 1); } @@ -395,7 +455,7 @@ static SyntaxNode InsertEmptyLineBeforeComment(SyntaxNode node) } node = statement.InsertTriviaBefore(leading[index], - Enumerable.Repeat(SyntaxFactory.CarriageReturnLineFeed, 1)); + Enumerable.Repeat(Whitespace.NewLine, 1)); } } @@ -414,7 +474,7 @@ private static SyntaxNode AdjustBlockCommentIndentation(SyntaxNode node) if (t.IsKind(SyntaxKind.MultiLineCommentTrivia)) { int indentation = GetIndentation(leading, i) + 1; // Add one to align stars. - string[] lines = t.ToFullString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); + string[] lines = t.ToFullString().Split([Environment.NewLine], StringSplitOptions.None); string indentString = new(' ', indentation); for (int l = 1; l < lines.Length; l++) { @@ -422,7 +482,7 @@ private static SyntaxNode AdjustBlockCommentIndentation(SyntaxNode node) lines[l] = indentString + lines[l].TrimStart(); } - node = node.ReplaceTrivia(t, SyntaxFactory.Comment(String.Join(Environment.NewLine, lines).TrimEnd(' '))); + node = node.ReplaceTrivia(t, SyntaxFactory.Comment(string.Join(Environment.NewLine, lines).TrimEnd(' '))); } } @@ -451,4 +511,4 @@ private static int GetIndentation(SyntaxTriviaList leading, int commentIndex) return s.All(c => c == ' ') ? s.Length : 0; } -} \ No newline at end of file +} diff --git a/JavaToCSharp/ConversionContext.cs b/JavaToCSharp/ConversionContext.cs index a93053b7..2f565f24 100644 --- a/JavaToCSharp/ConversionContext.cs +++ b/JavaToCSharp/ConversionContext.cs @@ -1,22 +1,14 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace JavaToCSharp; -public class ConversionContext +public class ConversionContext(JavaConversionOptions options) { - public ConversionContext(JavaConversionOptions options) - { - PendingAnonymousTypes = new Queue(); - UsedAnonymousTypeNames = new HashSet(); - Options = options; - } + public Queue PendingAnonymousTypes { get; } = new(); - public Queue PendingAnonymousTypes { get; } + public ISet UsedAnonymousTypeNames { get; } = new HashSet(); - public ISet UsedAnonymousTypeNames { get; } - - public JavaConversionOptions Options { get; } + public JavaConversionOptions Options { get; } = options; public string? RootTypeName { get; set; } diff --git a/JavaToCSharp/ConversionStateChangedEventArgs.cs b/JavaToCSharp/ConversionStateChangedEventArgs.cs index d632e0b9..b69d8e2d 100644 --- a/JavaToCSharp/ConversionStateChangedEventArgs.cs +++ b/JavaToCSharp/ConversionStateChangedEventArgs.cs @@ -1,13 +1,6 @@ -using System; +namespace JavaToCSharp; -namespace JavaToCSharp; - -public sealed class ConversionStateChangedEventArgs : EventArgs +public sealed class ConversionStateChangedEventArgs(ConversionState newState) : EventArgs { - public ConversionStateChangedEventArgs(ConversionState newState) - { - NewState = newState; - } - - public ConversionState NewState { get; } + public ConversionState NewState { get; } = newState; } diff --git a/JavaToCSharp/ConversionWarningEventArgs.cs b/JavaToCSharp/ConversionWarningEventArgs.cs index 5313a511..dca58057 100644 --- a/JavaToCSharp/ConversionWarningEventArgs.cs +++ b/JavaToCSharp/ConversionWarningEventArgs.cs @@ -1,16 +1,8 @@ -using System; +namespace JavaToCSharp; -namespace JavaToCSharp; - -public class ConversionWarningEventArgs : EventArgs +public class ConversionWarningEventArgs(string message, int javaLineNumber) : EventArgs { - public ConversionWarningEventArgs(string message, int javaLineNumber) - { - Message = message; - JavaLineNumber = javaLineNumber; - } - - public string Message { get; } + public string Message { get; } = message; - public int JavaLineNumber { get; } + public int JavaLineNumber { get; } = javaLineNumber; } diff --git a/JavaToCSharp/Declarations/AnnotationDeclarationVisitor.cs b/JavaToCSharp/Declarations/AnnotationDeclarationVisitor.cs index 493a490c..dfe52342 100644 --- a/JavaToCSharp/Declarations/AnnotationDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/AnnotationDeclarationVisitor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using com.github.javaparser; +using com.github.javaparser; using com.github.javaparser.ast.body; using com.github.javaparser.ast.type; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/JavaToCSharp/Declarations/BodyDeclarationVisitor.cs b/JavaToCSharp/Declarations/BodyDeclarationVisitor.cs index c5d7a382..a840de3b 100644 --- a/JavaToCSharp/Declarations/BodyDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/BodyDeclarationVisitor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.type; using Microsoft.CodeAnalysis.CSharp.Syntax; using Range = com.github.javaparser.Range; diff --git a/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs b/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs index 580eb454..fe18368d 100644 --- a/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/ClassOrInterfaceDeclarationVisitor.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast; +using com.github.javaparser.ast; using com.github.javaparser.ast.body; +using com.github.javaparser.ast.expr; using com.github.javaparser.ast.type; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -11,7 +10,7 @@ namespace JavaToCSharp.Declarations; public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor { public override MemberDeclarationSyntax? VisitForClass( - ConversionContext context, + ConversionContext context, ClassDeclarationSyntax classSyntax, ClassOrInterfaceDeclaration declaration, IReadOnlyList extends, @@ -20,16 +19,20 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor(); + var typeParams = interfaceDecl.getTypeParameters().ToList(); - if (typeParams is {Count: > 0}) + if (typeParams is { Count: > 0 }) { - classSyntax = classSyntax.AddTypeParameterListParameters(typeParams.Select(i => SyntaxFactory.TypeParameter(i.getNameAsString())).ToArray()); + classSyntax = classSyntax.AddTypeParameterListParameters(typeParams + .Select(i => SyntaxFactory.TypeParameter(i.getNameAsString())).ToArray()); } - var mods = javai.getModifiers().ToModifierKeywordSet(); + var mods = interfaceDecl.getModifiers().ToModifierKeywordSet(); if (mods.Contains(Modifier.Keyword.PRIVATE)) classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); @@ -61,8 +67,9 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor(); - if (extends != null) + var extends = interfaceDecl.getExtendedTypes().ToList(); + + if (extends is not null) { foreach (var extend in extends) { @@ -70,8 +77,9 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor(); - if (implements != null) + var implements = interfaceDecl.getImplementedTypes().ToList(); + + if (implements is not null) { foreach (var implement in implements) { @@ -79,41 +87,49 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor(); + var members = interfaceDecl.getMembers()?.ToList(); if (members is not null) + { foreach (var member in members) { var syntax = VisitBodyDeclarationForInterface(context, classSyntax, member); var memberWithComments = syntax?.WithJavaComments(context, member); - if (memberWithComments != null) + + if (memberWithComments is not null) { classSyntax = classSyntax.AddMembers(memberWithComments); } } + } - return classSyntax.WithJavaComments(context, javai); + return classSyntax.WithJavaComments(context, interfaceDecl); } - public static ClassDeclarationSyntax? VisitClassDeclaration(ConversionContext context, ClassOrInterfaceDeclaration javac, bool isNested = false) + public static ClassDeclarationSyntax VisitClassDeclaration(ConversionContext context, + ClassOrInterfaceDeclaration classDecl, bool isNested = false) { - string name = javac.getNameAsString(); + string name = classDecl.getNameAsString(); if (!isNested) + { context.RootTypeName = name; + } context.LastTypeName = name; var classSyntax = SyntaxFactory.ClassDeclaration(name); - var typeParams = javac.getTypeParameters().ToList(); + var typeParams = classDecl.getTypeParameters().ToList(); - if (typeParams is {Count: > 0}) + if (typeParams is { Count: > 0 }) { - classSyntax = classSyntax.AddTypeParameterListParameters(typeParams.Select(i => SyntaxFactory.TypeParameter(i.getNameAsString())).ToArray()); + classSyntax = classSyntax.AddTypeParameterListParameters(typeParams + .Select(i => SyntaxFactory.TypeParameter(i.getNameAsString())).ToArray()); + classSyntax = classSyntax.AddConstraintClauses(TypeHelper.GetTypeParameterListConstraints(typeParams).ToArray()); } - var mods = javac.getModifiers().ToModifierKeywordSet(); + var mods = classDecl.getModifiers().ToModifierKeywordSet(); if (mods.Contains(Modifier.Keyword.PRIVATE)) classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); @@ -126,23 +142,24 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor() ?? new List(); + var extends = classDecl.getExtendedTypes().ToList() ?? []; foreach (var extend in extends) { classSyntax = classSyntax.AddBaseListTypes(SyntaxFactory.SimpleBaseType(TypeHelper.GetSyntaxFromType(extend))); } - var implements = javac.getImplementedTypes().ToList() ?? new List(); + var implements = classDecl.getImplementedTypes().ToList() ?? []; foreach (var implement in implements) { classSyntax = classSyntax.AddBaseListTypes(SyntaxFactory.SimpleBaseType(TypeHelper.GetSyntaxFromType(implement))); } - var members = javac.getMembers()?.ToList(); + var members = classDecl.getMembers()?.ToList(); if (members is not null) + { foreach (var member in members) { if (member is ClassOrInterfaceDeclaration childType) @@ -150,25 +167,22 @@ public class ClassOrInterfaceDeclarationVisitor : BodyDeclarationVisitor(); + + if (annotations is { Count: > 0 }) + { + foreach (var annotation in annotations) + { + string annotationName = annotation.getNameAsString(); + const string annotationText = "Obsolete"; // TODO parse from java comment + + if (annotationName == "Deprecated") + { + classSyntax = classSyntax.AddAttributeLists(SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Obsolete")) + .WithArgumentList( + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(annotationText))))))))); + + break; + } + } + } - return classSyntax.WithJavaComments(context, javac); + return classSyntax.WithJavaComments(context, classDecl); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Declarations/ConstructorDeclarationVisitor.cs b/JavaToCSharp/Declarations/ConstructorDeclarationVisitor.cs index 8c109c1e..5566c8f6 100644 --- a/JavaToCSharp/Declarations/ConstructorDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/ConstructorDeclarationVisitor.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast; +using com.github.javaparser.ast; using com.github.javaparser.ast.body; using com.github.javaparser.ast.stmt; using com.github.javaparser.ast.type; using JavaToCSharp.Statements; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -26,9 +22,8 @@ public class ConstructorDeclarationVisitor : BodyDeclarationVisitor(); @@ -77,13 +72,13 @@ public class ConstructorDeclarationVisitor : BodyDeclarationVisitor 0) + if (initArgs is not null && initArgs.size() > 0) { argsSyntax = TypeHelper.GetSyntaxFromArguments(context, initArgs); } - var constructorInitSyntax = SyntaxFactory.ConstructorInitializer(ctorInvStmt.isThis() - ? SyntaxKind.ThisConstructorInitializer + var constructorInitSyntax = SyntaxFactory.ConstructorInitializer(ctorInvStmt.isThis() + ? SyntaxKind.ThisConstructorInitializer : SyntaxKind.BaseConstructorInitializer, argsSyntax); ctorSyntax = ctorSyntax.WithInitializer(constructorInitSyntax); diff --git a/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs b/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs index 59baaea2..97a700cc 100644 --- a/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/EnumDeclarationVisitor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser; +using com.github.javaparser; using com.github.javaparser.ast; using com.github.javaparser.ast.body; using com.github.javaparser.ast.type; @@ -12,7 +10,7 @@ namespace JavaToCSharp.Declarations; public class EnumDeclarationVisitor : BodyDeclarationVisitor { - public override MemberDeclarationSyntax? VisitForClass( + public override MemberDeclarationSyntax VisitForClass( ConversionContext context, ClassDeclarationSyntax? classSyntax, EnumDeclaration enumDecl, @@ -22,19 +20,21 @@ public class EnumDeclarationVisitor : BodyDeclarationVisitor return VisitEnumDeclaration(context, enumDecl); } - public override MemberDeclarationSyntax? VisitForInterface(ConversionContext context, InterfaceDeclarationSyntax interfaceSyntax, EnumDeclaration declaration) + public override MemberDeclarationSyntax VisitForInterface(ConversionContext context, + InterfaceDeclarationSyntax interfaceSyntax, EnumDeclaration declaration) { return VisitEnumDeclaration(context, declaration); } - public static EnumDeclarationSyntax? VisitEnumDeclaration(ConversionContext context, EnumDeclaration javai) + public static EnumDeclarationSyntax VisitEnumDeclaration(ConversionContext context, EnumDeclaration enumDecl) { - var name = javai.getNameAsString(); + var name = enumDecl.getNameAsString(); context.LastTypeName = name; - var classSyntax = SyntaxFactory.EnumDeclaration(name); + var enumSyntax = SyntaxFactory.EnumDeclaration(name); + + var typeConstants = enumDecl.getEntries().ToList(); - var typeConstants = javai.getEntries().ToList(); if (typeConstants is { Count: > 0 }) { var useCodeToComment = context.Options.UseUnrecognizedCodeToComment; @@ -42,11 +42,12 @@ public class EnumDeclarationVisitor : BodyDeclarationVisitor var enumMembers = new List(membersCount); var lastMembersIndex = membersCount - 1; var showNoPortedWarning = false; + for (int i = 0; i < membersCount; i++) { var itemConst = typeConstants[i]; var memberDecl = SyntaxFactory.EnumMemberDeclaration(itemConst.getNameAsString()) - .WithJavaComments(context, itemConst); + .WithJavaComments(context, itemConst); if (useCodeToComment) { @@ -55,60 +56,66 @@ public class EnumDeclarationVisitor : BodyDeclarationVisitor var classBody = itemConst.getClassBody(); if (!constArgs.isEmpty() || !classBody.isEmpty()) { - var bodyCodes = CommentsHelper.ConvertToComment(new[] { itemConst }, "enum member body", false); + var bodyCodes = CommentsHelper.ConvertToComment([itemConst], "enum member body", false); - if (memberDecl is not null && memberDecl.HasLeadingTrivia) + if (memberDecl.HasLeadingTrivia) { var firstLeadingTrivia = memberDecl.GetLeadingTrivia().Last(); memberDecl = memberDecl.InsertTriviaAfter(firstLeadingTrivia, bodyCodes); } else { - memberDecl = memberDecl?.WithLeadingTrivia(bodyCodes); + memberDecl = memberDecl.WithLeadingTrivia(bodyCodes); } showNoPortedWarning = true; } //java-enum `method-body` to `code Comment` - if (i == lastMembersIndex && memberDecl != null) + if (i == lastMembersIndex) + { memberDecl = MembersToCommentTrivia(memberDecl, ref showNoPortedWarning); + } } - if (memberDecl is not null) enumMembers.Add(memberDecl); + enumMembers.Add(memberDecl); } if (showNoPortedWarning) - context.Options.Warning($"Members found in enum {name} will not be ported. Check for correctness.", javai.getBegin().FromRequiredOptional().line); + context.Options.Warning($"Members found in enum {name} will not be ported. Check for correctness.", + enumDecl.getBegin().FromRequiredOptional().line); - classSyntax = classSyntax.AddMembers(enumMembers.ToArray()); + enumSyntax = enumSyntax.AddMembers(enumMembers.ToArray()); } - var mods = javai.getModifiers().ToModifierKeywordSet(); - + var mods = enumDecl.getModifiers().ToModifierKeywordSet(); + if (mods.Contains(Modifier.Keyword.PRIVATE)) - classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); + enumSyntax = enumSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); if (mods.Contains(Modifier.Keyword.PROTECTED)) - classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword)); + enumSyntax = enumSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword)); if (mods.Contains(Modifier.Keyword.PUBLIC)) - classSyntax = classSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); + enumSyntax = enumSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); + + context.Options.StaticUsingEnumNames.Add(name); - return classSyntax.WithJavaComments(context, javai); + return enumSyntax.WithJavaComments(context, enumDecl); - EnumMemberDeclarationSyntax MembersToCommentTrivia(EnumMemberDeclarationSyntax lastMemberDecl, ref bool showNoPortedWarning) + EnumMemberDeclarationSyntax MembersToCommentTrivia(EnumMemberDeclarationSyntax lastMemberDecl, + ref bool showNoPortedWarning) { - var members = javai.getMembers().ToList(); + var members = enumDecl.getMembers().ToList(); if (members is { Count: > 0 }) { var todoCodes = CommentsHelper.ConvertToComment(members, "enum body members"); - var lastMemberTrailingTrivia = lastMemberDecl.GetTrailingTrivia(); - lastMemberDecl = lastMemberTrailingTrivia.Count > 0 ? - lastMemberDecl.InsertTriviaAfter(lastMemberTrailingTrivia.Last(), todoCodes) : - lastMemberDecl.WithTrailingTrivia(todoCodes); - showNoPortedWarning = true; + var lastMemberTrailingTrivia = lastMemberDecl.GetTrailingTrivia(); + lastMemberDecl = lastMemberTrailingTrivia.Count > 0 + ? lastMemberDecl.InsertTriviaAfter(lastMemberTrailingTrivia.Last(), todoCodes) + : lastMemberDecl.WithTrailingTrivia(todoCodes); + showNoPortedWarning = true; } return lastMemberDecl; } } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs b/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs index 28876014..d37cc9ed 100644 --- a/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast; +using com.github.javaparser.ast; using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using com.github.javaparser.ast.type; -using com.sun.org.apache.bcel.@internal.classfile; using JavaToCSharp.Expressions; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,35 +11,46 @@ namespace JavaToCSharp.Declarations; public class FieldDeclarationVisitor : BodyDeclarationVisitor { public override MemberDeclarationSyntax VisitForClass( - ConversionContext context, - ClassDeclarationSyntax? classSyntax, + ConversionContext context, + ClassDeclarationSyntax? classSyntax, FieldDeclaration fieldDecl, IReadOnlyList extends, IReadOnlyList implements) { var variables = new List(); - string typeName = fieldDecl.getCommonType().toString(); + var commonType = fieldDecl.getCommonType(); + int? arrayRank = null; + + var variableDeclarators = fieldDecl.getVariables()?.ToList() ?? []; - var variableDeclarators = fieldDecl.getVariables()?.ToList() ?? new List(); 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(); + string name = item.getNameAsString(); if (type.getArrayLevel() > 0) { - if (!typeName.EndsWith("[]")) - typeName += "[]"; - if (name.EndsWith("[]")) + while (name.EndsWith("[]")) + { name = name[..^2]; + } } var initExpr = item.getInitializer().FromOptional(); - if (initExpr != null) + if (initExpr is not null) { var initSyntax = ExpressionVisitor.VisitExpression(context, initExpr); + if (initSyntax is not null) { var varDeclarationSyntax = SyntaxFactory.VariableDeclarator(name).WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)); @@ -53,11 +61,11 @@ public override MemberDeclarationSyntax VisitForClass( variables.Add(SyntaxFactory.VariableDeclarator(name)); } - typeName = TypeHelper.ConvertType(typeName); + var typeSyntax = TypeHelper.ConvertTypeSyntax(commonType, arrayRank ?? 0); var fieldSyntax = SyntaxFactory.FieldDeclaration( SyntaxFactory.VariableDeclaration( - SyntaxFactory.ParseTypeName(typeName), + typeSyntax, SyntaxFactory.SeparatedList(variables, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), variables.Count - 1)))); var mods = fieldDecl.getModifiers().ToModifierKeywordSet(); diff --git a/JavaToCSharp/Declarations/InitializerDeclarationVisitor.cs b/JavaToCSharp/Declarations/InitializerDeclarationVisitor.cs index 99f4f115..1cbb03f8 100644 --- a/JavaToCSharp/Declarations/InitializerDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/InitializerDeclarationVisitor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using com.github.javaparser; +using com.github.javaparser; using com.github.javaparser.ast.body; using com.github.javaparser.ast.type; using JavaToCSharp.Statements; diff --git a/JavaToCSharp/Declarations/MethodDeclarationVisitor.cs b/JavaToCSharp/Declarations/MethodDeclarationVisitor.cs index c2e42a5e..645757a2 100644 --- a/JavaToCSharp/Declarations/MethodDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/MethodDeclarationVisitor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast; +using com.github.javaparser.ast; using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; @@ -17,8 +14,8 @@ namespace JavaToCSharp.Declarations; public class MethodDeclarationVisitor : BodyDeclarationVisitor { public override MemberDeclarationSyntax VisitForClass( - ConversionContext context, - ClassDeclarationSyntax classSyntax, + ConversionContext context, + ClassDeclarationSyntax classSyntax, MethodDeclaration methodDecl, IReadOnlyList extends, IReadOnlyList implements) @@ -26,8 +23,8 @@ public override MemberDeclarationSyntax VisitForClass( return VisitInternal(context, false, classSyntax.Identifier.Text, classSyntax.Modifiers, methodDecl, extends); } - public override MemberDeclarationSyntax VisitForInterface(ConversionContext context, - InterfaceDeclarationSyntax interfaceSyntax, + public override MemberDeclarationSyntax VisitForInterface(ConversionContext context, + InterfaceDeclarationSyntax interfaceSyntax, MethodDeclaration methodDecl) { // If there is a body, mostly treat it like a class method @@ -36,7 +33,7 @@ public override MemberDeclarationSyntax VisitForInterface(ConversionContext cont return VisitInternal(context, true, interfaceSyntax.Identifier.Text, interfaceSyntax.Modifiers, methodDecl, ArraySegment.Empty); } - + var returnType = methodDecl.getType(); var returnTypeName = TypeHelper.ConvertType(returnType.toString()); @@ -58,7 +55,7 @@ public override MemberDeclarationSyntax VisitForInterface(ConversionContext cont { var paramSyntax = parameters.Select(i => SyntaxFactory.Parameter( - attributeLists: new SyntaxList(), + attributeLists: [], modifiers: SyntaxFactory.TokenList(), type: SyntaxFactory.ParseTypeName(TypeHelper.ConvertTypeOf(i)), identifier: SyntaxFactory.ParseToken(TypeHelper.EscapeIdentifier(i.getNameAsString())), @@ -113,19 +110,27 @@ private static MemberDeclarationSyntax VisitInternal( bool isOverride = false; // TODO: figure out how to check for a non-interface base type - if (annotations is {Count: > 0}) + if (annotations is { Count: > 0 }) { foreach (var annotation in annotations) { string name = annotation.getNameAsString(); - + // ignore @Override annotation on interface-only classes. Unfortunately this is as good as we can get for now. - if (name == "Override" + if (name == "Override" && extends.Count > 0) { methodSyntax = methodSyntax.AddModifiers(SyntaxFactory.Token(SyntaxKind.OverrideKeyword)); isOverride = true; } + // add annotation if a mapping is found + else if (context.Options is not null && context.Options.SyntaxMappings.AnnotationMappings.TryGetValue(name, out var mappedAnnotation)) + { + var attributeList = SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute(SyntaxFactory.ParseName(mappedAnnotation)))); + methodSyntax = methodSyntax.AddAttributeLists(attributeList); + } } } @@ -146,11 +151,14 @@ private static MemberDeclarationSyntax VisitInternal( foreach (var param in parameters) { - string typeName = TypeHelper.ConvertTypeOf(param); + var type = param.getType(); + int arrayLevel = type.getArrayLevel(); string identifier = TypeHelper.EscapeIdentifier(param.getNameAsString()); - if ((param.getType().getArrayLevel() > 0 && !typeName.EndsWith("[]")) || param.isVarArgs()) - typeName += "[]"; + if (param.isVarArgs() && arrayLevel == 0) + { + arrayLevel = 1; + } var modifiers = SyntaxFactory.TokenList(); @@ -158,9 +166,9 @@ private static MemberDeclarationSyntax VisitInternal( modifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ParamsKeyword)); var paramSyntax = SyntaxFactory.Parameter( - attributeLists: new SyntaxList(), + attributeLists: [], modifiers: modifiers, - type: SyntaxFactory.ParseTypeName(typeName), + type: TypeHelper.ConvertTypeSyntax(type, arrayLevel), identifier: SyntaxFactory.ParseToken(identifier), @default: null); @@ -172,7 +180,7 @@ private static MemberDeclarationSyntax VisitInternal( var block = methodDecl.getBody().FromOptional(); - if (block == null) + if (block is null) { // i.e. abstract method methodSyntax = methodSyntax.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); @@ -188,20 +196,11 @@ private static MemberDeclarationSyntax VisitInternal( { var lockBlock = SyntaxFactory.Block(statementSyntax); - LockStatementSyntax? lockSyntax = null; - if (mods.Contains(Modifier.Keyword.STATIC)) - { - lockSyntax = SyntaxFactory.LockStatement(SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(typeIdentifier)), lockBlock); - } - else - { - lockSyntax = SyntaxFactory.LockStatement(SyntaxFactory.ThisExpression(), lockBlock); - } + var lockSyntax = mods.Contains(Modifier.Keyword.STATIC) + ? SyntaxFactory.LockStatement(SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(typeIdentifier)), lockBlock) + : SyntaxFactory.LockStatement(SyntaxFactory.ThisExpression(), lockBlock); - if (lockSyntax is not null) - { - methodSyntax = methodSyntax.AddBodyStatements(lockSyntax); - } + methodSyntax = methodSyntax.AddBodyStatements(lockSyntax); } else { diff --git a/JavaToCSharp/Expressions/ArrayAccessExpressionVisitor.cs b/JavaToCSharp/Expressions/ArrayAccessExpressionVisitor.cs index 85f20e2b..3c16710e 100644 --- a/JavaToCSharp/Expressions/ArrayAccessExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ArrayAccessExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class ArrayAccessExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, ArrayAccessExpr expr) + protected override ExpressionSyntax? Visit(ConversionContext context, ArrayAccessExpr expr) { var nameExpr = expr.getName(); var nameSyntax = VisitExpression(context, nameExpr); @@ -22,9 +22,9 @@ public class ArrayAccessExpressionVisitor : ExpressionVisitor return null; } - return SyntaxFactory.ElementAccessExpression(nameSyntax, SyntaxFactory.BracketedArgumentList(SyntaxFactory.SeparatedList(new[] - { + return SyntaxFactory.ElementAccessExpression(nameSyntax, SyntaxFactory.BracketedArgumentList(SyntaxFactory.SeparatedList( + [ SyntaxFactory.Argument(indexSyntax) - }))); + ]))); } } diff --git a/JavaToCSharp/Expressions/ArrayCreationExpressionVisitor.cs b/JavaToCSharp/Expressions/ArrayCreationExpressionVisitor.cs index c781ac90..89f9d4f8 100644 --- a/JavaToCSharp/Expressions/ArrayCreationExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ArrayCreationExpressionVisitor.cs @@ -1,4 +1,5 @@ -using com.github.javaparser.ast; +using com.github.javaparser; +using com.github.javaparser.ast; using com.github.javaparser.ast.expr; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -7,46 +8,58 @@ namespace JavaToCSharp.Expressions; public class ArrayCreationExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, ArrayCreationExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, ArrayCreationExpr expr) { var type = TypeHelper.ConvertType(expr.getElementType()); + var typeSyntax = SyntaxFactory.ParseTypeName(type); + var arrayTypeSyntax = SyntaxFactory.ArrayType(typeSyntax); var rankDimensions = expr.getLevels().ToList(); + if (rankDimensions is { Count: > 1 }) + { + context.Options.Warning("Multi-dimensional arrays will likely have conversion issues. Review the generated code carefully.", + expr.getBegin().FromRequiredOptional().line); + } + var initializer = expr.getInitializer().FromOptional(); var rankSyntaxes = new List(); - if (rankDimensions != null) + if (rankDimensions is not null) { - var expressionSyntaxes = rankDimensions.Select(dimension => - VisitExpression(context, dimension.getDimension().FromOptional())) - .Where(syntax => syntax != null); - rankSyntaxes.AddRange(expressionSyntaxes!); + var expressionSyntaxes = rankDimensions + .Select(dimension => VisitExpression(context, dimension.getDimension().FromOptional())) + .OfType(); // filter out nulls + rankSyntaxes.AddRange(expressionSyntaxes); } - var rankSpecifier = rankDimensions?.Count > 0 && rankSyntaxes.Count == 0 + var rankSpecifier = rankDimensions?.Count > 0 && rankSyntaxes.Count == 0 ? SyntaxFactory.ArrayRankSpecifier(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.OmittedArraySizeExpression())) : SyntaxFactory.ArrayRankSpecifier(SyntaxFactory.SeparatedList(rankSyntaxes, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), rankSyntaxes.Count - 1))); - if (initializer == null) - return SyntaxFactory.ArrayCreationExpression(SyntaxFactory.ArrayType(SyntaxFactory.ParseTypeName(type))) + if (initializer is null) + { + return SyntaxFactory.ArrayCreationExpression(arrayTypeSyntax) .AddTypeRankSpecifiers(rankSpecifier); + } // todo: support multi-dimensional and jagged arrays - var values = initializer.getValues()?.ToList() ?? new List(); + var values = initializer.getValues()?.ToList() ?? []; + var syntaxes = values.Select(value => VisitExpression(context, value)) - .Where(syntax => syntax != null)! - .ToList(); + .OfType() // filter out nulls + .ToList(); + var initSyntax = - syntaxes.Any() + syntaxes.Count != 0 ? SyntaxFactory.InitializerExpression(SyntaxKind.ArrayInitializerExpression, SyntaxFactory.SeparatedList(syntaxes, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), syntaxes.Count - 1))) : SyntaxFactory.InitializerExpression(SyntaxKind.ArrayInitializerExpression); - return SyntaxFactory.ArrayCreationExpression(SyntaxFactory.ArrayType(SyntaxFactory.ParseTypeName(type)), initSyntax) + return SyntaxFactory.ArrayCreationExpression(arrayTypeSyntax, initSyntax) .AddTypeRankSpecifiers(rankSpecifier); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/ArrayInitializerExpressionVisitor.cs b/JavaToCSharp/Expressions/ArrayInitializerExpressionVisitor.cs index 55d47108..31e575b7 100644 --- a/JavaToCSharp/Expressions/ArrayInitializerExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ArrayInitializerExpressionVisitor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.expr; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,12 +6,12 @@ namespace JavaToCSharp.Expressions; public class ArrayInitializerExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, ArrayInitializerExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, ArrayInitializerExpr expr) { - var expressions = expr.getValues()?.ToList() ?? new List(); + var expressions = expr.getValues()?.ToList() ?? []; var syntaxes = expressions.Select(valueExpression => VisitExpression(context, valueExpression)) - .Where(syntax => syntax != null)! - .ToList(); + .OfType() // filter out nulls + .ToList(); return SyntaxFactory.ImplicitArrayCreationExpression( SyntaxFactory.InitializerExpression( diff --git a/JavaToCSharp/Expressions/AssignmentExpressionVisitor.cs b/JavaToCSharp/Expressions/AssignmentExpressionVisitor.cs index 0506b4fe..c21db49a 100644 --- a/JavaToCSharp/Expressions/AssignmentExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/AssignmentExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class AssignmentExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, AssignExpr assignExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, AssignExpr assignExpr) { var left = assignExpr.getTarget(); var leftSyntax = VisitExpression(context, left); @@ -49,7 +49,7 @@ public class AssignmentExpressionVisitor : ExpressionVisitor kind = SyntaxKind.MultiplyAssignmentExpression; else if (op == AssignExpr.Operator.XOR) kind = SyntaxKind.ExclusiveOrAssignmentExpression; - + return SyntaxFactory.AssignmentExpression(kind, leftSyntax, rightSyntax); } } diff --git a/JavaToCSharp/Expressions/BinaryExpressionVisitor.cs b/JavaToCSharp/Expressions/BinaryExpressionVisitor.cs index ccfe7053..86aa7656 100644 --- a/JavaToCSharp/Expressions/BinaryExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/BinaryExpressionVisitor.cs @@ -10,14 +10,14 @@ namespace JavaToCSharp.Expressions; /// public class BinaryExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, BinaryExpr binaryExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, BinaryExpr binaryExpr) { var leftExpr = binaryExpr.getLeft(); if (leftExpr is null) { return null; } - + var leftSyntax = VisitExpression(context, leftExpr); if (leftSyntax is null) { @@ -29,7 +29,7 @@ public class BinaryExpressionVisitor : ExpressionVisitor { return null; } - + var rightSyntax = VisitExpression(context, rightExpr); if (rightSyntax is null) { diff --git a/JavaToCSharp/Expressions/BooleanLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/BooleanLiteralExpressionVisitor.cs index cd2b286a..2d69af81 100644 --- a/JavaToCSharp/Expressions/BooleanLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/BooleanLiteralExpressionVisitor.cs @@ -6,8 +6,8 @@ namespace JavaToCSharp.Expressions; public class BooleanLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, BooleanLiteralExpr expr) => - SyntaxFactory.LiteralExpression(expr.getValue() - ? SyntaxKind.TrueLiteralExpression + protected override ExpressionSyntax Visit(ConversionContext context, BooleanLiteralExpr expr) => + SyntaxFactory.LiteralExpression(expr.getValue() + ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression); } diff --git a/JavaToCSharp/Expressions/CastExpressionVisitor.cs b/JavaToCSharp/Expressions/CastExpressionVisitor.cs index 86632cc6..d052d83d 100644 --- a/JavaToCSharp/Expressions/CastExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/CastExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class CastExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, CastExpr castExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, CastExpr castExpr) { var expr = castExpr.getExpression(); var exprSyntax = VisitExpression(context, expr); @@ -18,4 +18,4 @@ public class CastExpressionVisitor : ExpressionVisitor var type = TypeHelper.ConvertTypeOf(castExpr); return SyntaxFactory.CastExpression(SyntaxFactory.ParseTypeName(type), exprSyntax); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/CharLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/CharLiteralExpressionVisitor.cs index d2ea9803..cc76c3b4 100644 --- a/JavaToCSharp/Expressions/CharLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/CharLiteralExpressionVisitor.cs @@ -7,7 +7,7 @@ namespace JavaToCSharp.Expressions; public class CharLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, CharLiteralExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, CharLiteralExpr expr) { var value = Regex.Unescape(expr.getValue()); return SyntaxFactory.LiteralExpression(SyntaxKind.CharacterLiteralExpression, SyntaxFactory.Literal(value[0])); diff --git a/JavaToCSharp/Expressions/ClassExpressionVisitor.cs b/JavaToCSharp/Expressions/ClassExpressionVisitor.cs index 4d3d40f3..f7e41b0e 100644 --- a/JavaToCSharp/Expressions/ClassExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ClassExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class ClassExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, ClassExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, ClassExpr expr) { var type = TypeHelper.ConvertTypeOf(expr); diff --git a/JavaToCSharp/Expressions/ConditionalExpressionVisitor.cs b/JavaToCSharp/Expressions/ConditionalExpressionVisitor.cs index afcadd73..89c3e572 100644 --- a/JavaToCSharp/Expressions/ConditionalExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ConditionalExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class ConditionalExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, ConditionalExpr conditionalExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, ConditionalExpr conditionalExpr) { var condition = conditionalExpr.getCondition(); var conditionSyntax = VisitExpression(context, condition); diff --git a/JavaToCSharp/Expressions/DoubleLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/DoubleLiteralExpressionVisitor.cs index e3fc5808..cad2a027 100644 --- a/JavaToCSharp/Expressions/DoubleLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/DoubleLiteralExpressionVisitor.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; using com.github.javaparser.ast.expr; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -16,13 +15,13 @@ public DoubleLiteralExpressionVisitor() _cultureInfo.NumberFormat.CurrencyDecimalSeparator = "."; } - public override ExpressionSyntax Visit(ConversionContext context, DoubleLiteralExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, DoubleLiteralExpr expr) { - var value = expr.getValue().Replace("_", String.Empty); - - var literalSyntax = value.EndsWith("f", StringComparison.OrdinalIgnoreCase) - ? SyntaxFactory.Literal(Single.Parse(value.TrimEnd('f', 'F'), NumberStyles.Any, _cultureInfo)) - : SyntaxFactory.Literal(Double.Parse(value.TrimEnd('d', 'D'), NumberStyles.Any, _cultureInfo)); + var value = expr.getValue().Replace("_", string.Empty); + + var literalSyntax = value.EndsWith("f", StringComparison.OrdinalIgnoreCase) + ? SyntaxFactory.Literal(float.Parse(value.TrimEnd('f', 'F'), NumberStyles.Any, _cultureInfo)) + : SyntaxFactory.Literal(double.Parse(value.TrimEnd('d', 'D'), NumberStyles.Any, _cultureInfo)); return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, literalSyntax); } diff --git a/JavaToCSharp/Expressions/EnclosedExpressionVisitor.cs b/JavaToCSharp/Expressions/EnclosedExpressionVisitor.cs index d000e0ce..c30523bf 100644 --- a/JavaToCSharp/Expressions/EnclosedExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/EnclosedExpressionVisitor.cs @@ -6,15 +6,11 @@ namespace JavaToCSharp.Expressions; public class EnclosedExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, EnclosedExpr enclosedExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, EnclosedExpr enclosedExpr) { var expr = enclosedExpr.getInner(); var exprSyntax = VisitExpression(context, expr); - if (exprSyntax is null) - { - return null; - } - return SyntaxFactory.ParenthesizedExpression(exprSyntax); + return exprSyntax is null ? null : SyntaxFactory.ParenthesizedExpression(exprSyntax); } } diff --git a/JavaToCSharp/Expressions/ExpressionVisitor.cs b/JavaToCSharp/Expressions/ExpressionVisitor.cs index e0d3325b..0f26be13 100644 --- a/JavaToCSharp/Expressions/ExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ExpressionVisitor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.expr; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace JavaToCSharp.Expressions; @@ -8,14 +6,15 @@ namespace JavaToCSharp.Expressions; public abstract class ExpressionVisitor : ExpressionVisitor where T : Expression { - public abstract ExpressionSyntax? Visit(ConversionContext context, T expr); + protected abstract ExpressionSyntax? Visit(ConversionContext context, T expr); - protected sealed override ExpressionSyntax? Visit(ConversionContext context, Expression expr) => Visit(context, (T)expr); + protected sealed override ExpressionSyntax? Visit(ConversionContext context, Expression expr) => + Visit(context, (T)expr); } public abstract class ExpressionVisitor { - private static readonly IDictionary _visitors; + private static readonly Dictionary _visitors; static ExpressionVisitor() { @@ -45,9 +44,10 @@ static ExpressionVisitor() { typeof(ThisExpr), new ThisExpressionVisitor() }, { typeof(UnaryExpr), new UnaryExpressionVisitor() }, { typeof(LongLiteralExpr), new LongLiteralExpressionVisitor() }, - { typeof(LambdaExpr), new LambdaExpressionVisitor() }, - { typeof(MethodReferenceExpr), new MethodReferenceExpressionVisitor() }, - { typeof(TypeExpr), new TypeExpressionVisitor() } + { typeof(LambdaExpr), new LambdaExpressionVisitor() }, + { typeof(MethodReferenceExpr), new MethodReferenceExpressionVisitor() }, + { typeof(TypeExpr), new TypeExpressionVisitor() }, + { typeof(SwitchExpr), new SwitchExpressionVisitor() }, }; } @@ -55,21 +55,26 @@ static ExpressionVisitor() public static ExpressionSyntax? VisitExpression(ConversionContext context, Expression? expr) { - if (expr == null) + if (expr is null) + { return null; + } ExpressionVisitor? visitor = null; var t = expr.GetType(); - while (t != null && !_visitors.TryGetValue(t, out visitor)) + while (t is not null && !_visitors.TryGetValue(t, out visitor)) { t = t.BaseType; } - if (visitor != null) + if (visitor is not null) + { return visitor.Visit(context, expr); + } var message = $"Expression visitor not implemented for expression type `{expr.GetType()}`{Environment.NewLine}{expr}"; + throw new NotImplementedException(message); } } diff --git a/JavaToCSharp/Expressions/FieldAccessExpressionVisitor.cs b/JavaToCSharp/Expressions/FieldAccessExpressionVisitor.cs index b151165a..69ef55ec 100644 --- a/JavaToCSharp/Expressions/FieldAccessExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/FieldAccessExpressionVisitor.cs @@ -6,19 +6,19 @@ namespace JavaToCSharp.Expressions; public class FieldAccessExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, FieldAccessExpr fieldAccessExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, FieldAccessExpr fieldAccessExpr) { var scope = fieldAccessExpr.getScope(); ExpressionSyntax? scopeSyntax = null; - if (scope != null) + if (scope is not null) { scopeSyntax = VisitExpression(context, scope); - + // TODO.PI: This should probably live in TypeHelper somehow if (context.Options.ConvertSystemOutToConsole) { - if (scopeSyntax is IdentifierNameSyntax { Identifier: { Text: "System" } } + if (scopeSyntax is IdentifierNameSyntax { Identifier.Text: "System" } && fieldAccessExpr.getNameAsString() == "out") { return SyntaxFactory.IdentifierName("Console"); @@ -32,6 +32,9 @@ public class FieldAccessExpressionVisitor : ExpressionVisitor } var field = TypeHelper.EscapeIdentifier(fieldAccessExpr.getNameAsString()); + // array length accessor should be capitalized + field = field == "length" ? "Length" : field; + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, scopeSyntax, SyntaxFactory.IdentifierName(field)); } } diff --git a/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs b/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs index 85c6c59c..044f8d00 100644 --- a/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs @@ -6,10 +6,11 @@ namespace JavaToCSharp.Expressions; public class InstanceOfExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, InstanceOfExpr expr) + protected override ExpressionSyntax? Visit(ConversionContext context, InstanceOfExpr expr) { var innerExpr = expr.getExpression(); var exprSyntax = VisitExpression(context, innerExpr); + if (exprSyntax is null) { return null; diff --git a/JavaToCSharp/Expressions/IntegerLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/IntegerLiteralExpressionVisitor.cs index fa99d9a1..ecf86f61 100644 --- a/JavaToCSharp/Expressions/IntegerLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/IntegerLiteralExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class IntegerLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, IntegerLiteralExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, IntegerLiteralExpr expr) { string value = expr.getValue().Replace("_", string.Empty); int int32Value; @@ -15,7 +15,11 @@ public override ExpressionSyntax Visit(ConversionContext context, IntegerLiteral { int32Value = Convert.ToInt32(value, 16); } - else if (value.StartsWith("0") && value.Length > 1) + else if (value.StartsWith("0b", StringComparison.OrdinalIgnoreCase)) + { + int32Value = Convert.ToInt32(value[2..], 2); + } + else if (value.StartsWith('0') && value.Length > 1) { int32Value = Convert.ToInt32(value, 8); value = int32Value.ToString(); @@ -24,7 +28,7 @@ public override ExpressionSyntax Visit(ConversionContext context, IntegerLiteral { int32Value = Convert.ToInt32(value); } - + return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value, int32Value)); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/LambdaExpressionVisitor.cs b/JavaToCSharp/Expressions/LambdaExpressionVisitor.cs index 91a4b3cf..e1fa9056 100644 --- a/JavaToCSharp/Expressions/LambdaExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/LambdaExpressionVisitor.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using JavaToCSharp.Statements; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -10,11 +8,11 @@ namespace JavaToCSharp.Expressions; public class LambdaExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, LambdaExpr expr) + protected override ExpressionSyntax? Visit(ConversionContext context, LambdaExpr expr) { var bodyStatement = expr.getBody(); var bodyStatementSyntax = StatementVisitor.VisitStatement(context, bodyStatement); - + ParenthesizedLambdaExpressionSyntax? lambdaExpressionSyntax = null; if (bodyStatementSyntax is ExpressionStatementSyntax ess) @@ -35,11 +33,14 @@ public class LambdaExpressionVisitor : ExpressionVisitor foreach (var param in parameters) { - string typeName = TypeHelper.ConvertTypeOf(param); + var type = param.getType(); + int arrayLevel = type.getArrayLevel(); string identifier = TypeHelper.EscapeIdentifier(param.getNameAsString()); - if ((param.getType().getArrayLevel() > 0 && !typeName.EndsWith("[]")) || param.isVarArgs()) - typeName += "[]"; + if (param.isVarArgs() && arrayLevel == 0) + { + arrayLevel = 1; + } var modifiers = SyntaxFactory.TokenList(); @@ -47,9 +48,9 @@ public class LambdaExpressionVisitor : ExpressionVisitor modifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ParamsKeyword)); var paramSyntax = SyntaxFactory.Parameter( - attributeLists: new SyntaxList(), + attributeLists: [], modifiers: modifiers, - type: SyntaxFactory.ParseTypeName(typeName), + type: TypeHelper.ConvertTypeSyntax(type, arrayLevel), identifier: SyntaxFactory.ParseToken(identifier), @default: null); @@ -57,6 +58,7 @@ public class LambdaExpressionVisitor : ExpressionVisitor } lambdaExpressionSyntax = lambdaExpressionSyntax?.AddParameterListParameters(paramSyntaxes.ToArray()); + return lambdaExpressionSyntax; } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/LongLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/LongLiteralExpressionVisitor.cs index bc793936..438de6ba 100644 --- a/JavaToCSharp/Expressions/LongLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/LongLiteralExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class LongLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, LiteralStringValueExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, LiteralStringValueExpr expr) { string value = expr is LongLiteralExpr longLiteralExpr ? longLiteralExpr.getValue() : expr.toString(); value = value.Trim('\"') @@ -20,7 +20,11 @@ public override ExpressionSyntax Visit(ConversionContext context, LiteralStringV { int64Value = Convert.ToInt64(value, 16); } - else if (value.StartsWith("0") && value.Length > 1) + else if (value.StartsWith("0b", StringComparison.OrdinalIgnoreCase)) + { + int64Value = Convert.ToInt64(value[2..], 2); + } + else if (value.StartsWith('0') && value.Length > 1) { int64Value = Convert.ToInt64(value, 8); value = int64Value.ToString(); @@ -29,7 +33,7 @@ public override ExpressionSyntax Visit(ConversionContext context, LiteralStringV { int64Value = Convert.ToInt64(value); } - + return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value, int64Value)); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/MethodCallExpressionVisitor.cs b/JavaToCSharp/Expressions/MethodCallExpressionVisitor.cs index 19d46623..2bc27069 100644 --- a/JavaToCSharp/Expressions/MethodCallExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/MethodCallExpressionVisitor.cs @@ -6,7 +6,7 @@ namespace JavaToCSharp.Expressions; public class MethodCallExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, MethodCallExpr methodCallExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, MethodCallExpr methodCallExpr) { if (TypeHelper.TryTransformMethodCall(context, methodCallExpr, out var transformedSyntax)) { @@ -15,11 +15,11 @@ public class MethodCallExpressionVisitor : ExpressionVisitor var scope = methodCallExpr.getScope().FromOptional(); ExpressionSyntax? scopeSyntax = null; - + var methodName = TypeHelper.Capitalize(methodCallExpr.getNameAsString()); methodName = TypeHelper.ReplaceCommonMethodNames(methodName); - if (scope != null) + if (scope is not null) { scopeSyntax = VisitExpression(context, scope); @@ -41,9 +41,15 @@ public class MethodCallExpressionVisitor : ExpressionVisitor } } + // Override methodName if a mapping is found + if (TryGetMappedMethodName(methodCallExpr.getNameAsString(), scope, context, out var mappedMethodName)) + { + methodName = mappedMethodName; + } + ExpressionSyntax methodExpression; - if (scopeSyntax == null) + if (scopeSyntax is null) { methodExpression = SyntaxFactory.IdentifierName(methodName); } @@ -53,9 +59,29 @@ public class MethodCallExpressionVisitor : ExpressionVisitor } var args = methodCallExpr.getArguments(); - if (args == null || args.size() == 0) + + if (args is null || args.size() == 0) + { return SyntaxFactory.InvocationExpression(methodExpression); + } return SyntaxFactory.InvocationExpression(methodExpression, TypeHelper.GetSyntaxFromArguments(context, args)); } + + private static bool TryGetMappedMethodName(string methodName, Expression? scope, ConversionContext context, out string mappedMethodName) + { + var mappings = context.Options.SyntaxMappings; + if (scope is null && mappings.VoidMethodMappings.TryGetValue(methodName, out var voidMapping)) + { + mappedMethodName = voidMapping; + return true; + } + else if (scope is not null && mappings.NonVoidMethodMappings.TryGetValue(methodName, out var nonVoidMapping)) + { + mappedMethodName = nonVoidMapping; + return true; + } + mappedMethodName = methodName; + return false; + } } diff --git a/JavaToCSharp/Expressions/MethodReferenceExpressionVisitor.cs b/JavaToCSharp/Expressions/MethodReferenceExpressionVisitor.cs index d0f66242..27af830a 100644 --- a/JavaToCSharp/Expressions/MethodReferenceExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/MethodReferenceExpressionVisitor.cs @@ -7,12 +7,12 @@ namespace JavaToCSharp.Expressions; public class MethodReferenceExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, MethodReferenceExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, MethodReferenceExpr expr) { var scope = expr.getScope(); ExpressionSyntax? scopeSyntax = null; - if (scope != null) + if (scope is not null) { scopeSyntax = VisitExpression(context, scope); } @@ -22,7 +22,7 @@ public override ExpressionSyntax Visit(ConversionContext context, MethodReferenc ExpressionSyntax methodExpression; - if (scopeSyntax == null) + if (scopeSyntax is null) { methodExpression = SyntaxFactory.IdentifierName(methodName); } @@ -32,8 +32,11 @@ public override ExpressionSyntax Visit(ConversionContext context, MethodReferenc } var args = expr.getTypeArguments().FromOptional(); - if (args == null || args.size() == 0) + + if (args is null || args.size() == 0) + { return SyntaxFactory.InvocationExpression(methodExpression); + } return SyntaxFactory.InvocationExpression(methodExpression, TypeHelper.GetSyntaxFromArguments(context, args)); } diff --git a/JavaToCSharp/Expressions/NameExpressionVisitor.cs b/JavaToCSharp/Expressions/NameExpressionVisitor.cs index af8ff83e..24dbbeb0 100644 --- a/JavaToCSharp/Expressions/NameExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/NameExpressionVisitor.cs @@ -6,6 +6,6 @@ namespace JavaToCSharp.Expressions; public class NameExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, NameExpr nameExpr) => + protected override ExpressionSyntax Visit(ConversionContext context, NameExpr nameExpr) => SyntaxFactory.IdentifierName(TypeHelper.EscapeIdentifier(nameExpr.getNameAsString())); -} \ No newline at end of file +} diff --git a/JavaToCSharp/Expressions/NullLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/NullLiteralExpressionVisitor.cs index 6da7f356..5883f849 100644 --- a/JavaToCSharp/Expressions/NullLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/NullLiteralExpressionVisitor.cs @@ -6,6 +6,6 @@ namespace JavaToCSharp.Expressions; public class NullLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, NullLiteralExpr expr) => + protected override ExpressionSyntax Visit(ConversionContext context, NullLiteralExpr expr) => SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); } diff --git a/JavaToCSharp/Expressions/ObjectCreationExpressionVisitor.cs b/JavaToCSharp/Expressions/ObjectCreationExpressionVisitor.cs index 5e51693c..c972d063 100644 --- a/JavaToCSharp/Expressions/ObjectCreationExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ObjectCreationExpressionVisitor.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using JavaToCSharp.Declarations; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Linq; using com.github.javaparser.ast; using com.github.javaparser.ast.type; @@ -13,11 +10,11 @@ namespace JavaToCSharp.Expressions; public class ObjectCreationExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, ObjectCreationExpr newExpr) + protected override ExpressionSyntax Visit(ConversionContext context, ObjectCreationExpr newExpr) { var anonBody = newExpr.getAnonymousClassBody().FromOptional().ToList(); - if (anonBody is {Count: > 0}) + if (anonBody is { Count: > 0 }) { return VisitAnonymousClassCreationExpression(context, newExpr, anonBody); } @@ -26,7 +23,7 @@ public override ExpressionSyntax Visit(ConversionContext context, ObjectCreation //var scope = newExpr.getScope(); //ExpressionSyntax scopeSyntax = null; - //if (scope != null) { + //if (scope is not null) { // scopeSyntax = ExpressionVisitor.VisitExpression(context, scope); //} @@ -35,13 +32,16 @@ public override ExpressionSyntax Visit(ConversionContext context, ObjectCreation var typeSyntax = TypeHelper.GetSyntaxFromType(type); var args = newExpr.getArguments(); - if (args == null || args.size() == 0) + + if (args is null || args.size() == 0) + { return SyntaxFactory.ObjectCreationExpression(typeSyntax).WithArgumentList(SyntaxFactory.ArgumentList()); + } return SyntaxFactory.ObjectCreationExpression(typeSyntax, TypeHelper.GetSyntaxFromArguments(context, args), null); } - private static ExpressionSyntax VisitAnonymousClassCreationExpression(ConversionContext context, ObjectCreationExpr newExpr, List anonBody) + private static ObjectCreationExpressionSyntax VisitAnonymousClassCreationExpression(ConversionContext context, ObjectCreationExpr newExpr, List anonBody) { string baseTypeName = TypeHelper.ConvertType(newExpr.getType().getNameAsString()); string anonTypeName = string.Empty; @@ -49,13 +49,14 @@ private static ExpressionSyntax VisitAnonymousClassCreationExpression(Conversion for (int i = 0; i <= 100; i++) { if (i == 100) + { throw new InvalidOperationException("Too many anonymous types"); + } - anonTypeName = $"Anonymous{baseTypeName}{(i == 0 ? String.Empty : i.ToString())}"; + anonTypeName = $"Anonymous{baseTypeName}{(i == 0 ? string.Empty : i.ToString())}"; - if (!context.UsedAnonymousTypeNames.Contains(anonTypeName)) + if (context.UsedAnonymousTypeNames.Add(anonTypeName)) { - context.UsedAnonymousTypeNames.Add(anonTypeName); break; // go with this one } } @@ -70,6 +71,7 @@ private static ExpressionSyntax VisitAnonymousClassCreationExpression(Conversion }))); string? contextLastTypeName = context.LastTypeName; + if (contextLastTypeName is not null) { var parentField = SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName(contextLastTypeName)) @@ -87,17 +89,20 @@ private static ExpressionSyntax VisitAnonymousClassCreationExpression(Conversion // TODO.PI: do we need to pass extends/implements here? foreach (var memberSyntax in anonBody .Select(member => BodyDeclarationVisitor.VisitBodyDeclarationForClass(context, classSyntax, member, new List(), new List())) - .Where(memberSyntax => memberSyntax != null)) + .OfType()) { - classSyntax = classSyntax.AddMembers(memberSyntax!); + classSyntax = classSyntax.AddMembers(memberSyntax); } context.PendingAnonymousTypes.Enqueue(classSyntax); var args = newExpr.getArguments(); - if (args == null || args.size() == 0) + + if (args is null || args.size() == 0) + { return SyntaxFactory.ObjectCreationExpression(SyntaxFactory.ParseTypeName(anonTypeName)) .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.ThisExpression())); + } return SyntaxFactory.ObjectCreationExpression(SyntaxFactory.ParseTypeName(anonTypeName), TypeHelper.GetSyntaxFromArguments(context, args), diff --git a/JavaToCSharp/Expressions/StringLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/StringLiteralExpressionVisitor.cs index a086814b..7eef8c8f 100644 --- a/JavaToCSharp/Expressions/StringLiteralExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/StringLiteralExpressionVisitor.cs @@ -7,7 +7,7 @@ namespace JavaToCSharp.Expressions; public class StringLiteralExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, StringLiteralExpr expr) + protected override ExpressionSyntax Visit(ConversionContext context, StringLiteralExpr expr) { var value = Regex.Unescape(expr.getValue()); return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)); diff --git a/JavaToCSharp/Expressions/SuperExpressionVisitor.cs b/JavaToCSharp/Expressions/SuperExpressionVisitor.cs index 1105ef2f..be8acc3e 100644 --- a/JavaToCSharp/Expressions/SuperExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/SuperExpressionVisitor.cs @@ -6,5 +6,5 @@ namespace JavaToCSharp.Expressions; public class SuperExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, SuperExpr expr) => SyntaxFactory.BaseExpression(); + protected override ExpressionSyntax Visit(ConversionContext context, SuperExpr expr) => SyntaxFactory.BaseExpression(); } diff --git a/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs new file mode 100644 index 00000000..5087195f --- /dev/null +++ b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs @@ -0,0 +1,90 @@ +using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.stmt; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace JavaToCSharp.Expressions; + +public class SwitchExpressionVisitor : ExpressionVisitor +{ + protected override ExpressionSyntax Visit(ConversionContext context, SwitchExpr expr) + { + var entries = expr.getEntries().ToList() ?? []; + + var governingExpr = VisitExpression(context, expr.getSelector()) + ?? throw new InvalidOperationException("Switch expression selector cannot be null"); + + return SwitchExpression( + governingExpr, + SeparatedList(entries.Select(e => Visit(context, e))) + ); + } + + private SwitchExpressionArmSyntax Visit(ConversionContext context, SwitchEntry entry) + { + var pattern = GetArmPatternSyntax(context, entry); + var expr = GetArmExpressionSyntax(context, entry); + + return SwitchExpressionArm( + pattern, + expr + ); + } + + private static PatternSyntax GetArmPatternSyntax(ConversionContext context, SwitchEntry entry) + { + var labels = entry.getLabels().ToList() ?? []; + + if (labels.Count == 0) + { + return DiscardPattern(); + } + + var patterns = new List(); + + foreach (var label in labels) + { + if (VisitExpression(context, label) is not ExpressionSyntax labelExpr) + { + throw new InvalidOperationException("Switch expression label must contain an expression"); + } + + patterns.Add(ConstantPattern(labelExpr)); + } + + if (patterns.Count == 1) + { + return patterns[0]; + } + + var orPattern = BinaryPattern(SyntaxKind.OrPattern, patterns[0], patterns[1]); + + for (var i = 2; i < patterns.Count; i++) + { + orPattern = BinaryPattern(SyntaxKind.OrPattern, orPattern, patterns[i]); + } + + return orPattern ?? throw new InvalidOperationException("Switch expression label must contain an expression"); + } + + private static ExpressionSyntax GetArmExpressionSyntax(ConversionContext context, SwitchEntry entry) + { + var statements = entry.getStatements().ToList() ?? []; + + if (statements.Count > 1) + { + throw new InvalidOperationException("Switch expressions with multiple statements are not supported"); + } + + var armExpr = statements[0] switch + { + ThrowStmt throwStmt => throwStmt.getExpression(), + ExpressionStmt exprStmt => exprStmt.getExpression(), + _ => throw new InvalidOperationException("Only throw and expression statements are supported in switch expressions") + }; + + return VisitExpression(context, armExpr) + ?? throw new InvalidOperationException("Switch expression entry must contain a single expression statement"); + } +} diff --git a/JavaToCSharp/Expressions/ThisExpressionVisitor.cs b/JavaToCSharp/Expressions/ThisExpressionVisitor.cs index 51c6bb79..d1548cff 100644 --- a/JavaToCSharp/Expressions/ThisExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ThisExpressionVisitor.cs @@ -6,5 +6,5 @@ namespace JavaToCSharp.Expressions; public class ThisExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, ThisExpr expr) => SyntaxFactory.ThisExpression(); + protected override ExpressionSyntax Visit(ConversionContext context, ThisExpr expr) => SyntaxFactory.ThisExpression(); } diff --git a/JavaToCSharp/Expressions/TypeExpressionVisitor.cs b/JavaToCSharp/Expressions/TypeExpressionVisitor.cs index 6887f328..f559b484 100644 --- a/JavaToCSharp/Expressions/TypeExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/TypeExpressionVisitor.cs @@ -6,5 +6,5 @@ namespace JavaToCSharp.Expressions; public class TypeExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax Visit(ConversionContext context, TypeExpr expr) => SyntaxFactory.ParseTypeName(TypeHelper.ConvertTypeOf(expr)); -} \ No newline at end of file + protected override ExpressionSyntax Visit(ConversionContext context, TypeExpr expr) => SyntaxFactory.ParseTypeName(TypeHelper.ConvertTypeOf(expr)); +} diff --git a/JavaToCSharp/Expressions/UnaryExpressionVisitor.cs b/JavaToCSharp/Expressions/UnaryExpressionVisitor.cs index 834c0634..62cfb43e 100644 --- a/JavaToCSharp/Expressions/UnaryExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/UnaryExpressionVisitor.cs @@ -6,10 +6,11 @@ namespace JavaToCSharp.Expressions; public class UnaryExpressionVisitor : ExpressionVisitor { - public override ExpressionSyntax? Visit(ConversionContext context, UnaryExpr unaryExpr) + protected override ExpressionSyntax? Visit(ConversionContext context, UnaryExpr unaryExpr) { var expr = unaryExpr.getExpression(); var exprSyntax = VisitExpression(context, expr); + if (exprSyntax is null) { return null; diff --git a/JavaToCSharp/Extensions.cs b/JavaToCSharp/Extensions.cs index 342ded21..dcaa31dc 100644 --- a/JavaToCSharp/Extensions.cs +++ b/JavaToCSharp/Extensions.cs @@ -1,68 +1,105 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using java.util; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using JavaAst = com.github.javaparser.ast; namespace JavaToCSharp; public static class Extensions { - /// - /// Converts a Java Iterable to a .NET IEnumerable<T> and filters the elements of type T. - /// - /// Type of items to be returned. /// The java Iterable to be enumerated. - /// A filtered enumeration of items of type T - public static IEnumerable OfType(this java.lang.Iterable iterable) + extension(java.lang.Iterable iterable) { - var iterator = iterable.iterator(); - while (iterator.hasNext()) + /// + /// Converts a Java Iterable to a .NET IEnumerable<T> and filters the elements of type T. + /// + /// Type of items to be returned. + /// A filtered enumeration of items of type T + public IEnumerable OfType() { - if (iterator.next() is T item) + var iterator = iterable.iterator(); + while (iterator.hasNext()) { - yield return item; + if (iterator.next() is T item) + { + yield return item; + } } } } - public static List? ToList(this java.util.List? list) + extension(java.util.List? list) { - if (list == null) - return null; + public List? ToList() + { + if (list is null) + { + return null; + } - var newList = new List(); + var newList = new List(); - for (int i = 0; i < list.size(); i++) - { - newList.Add((T)list.get(i)); + for (int i = 0; i < list.size(); i++) + { + newList.Add((T)list.get(i)); + } + + return newList; } + } + + extension(java.util.EnumSet values) + { + public bool HasFlag(T flag) => values.contains(flag); + } - return newList; + extension(CompilationUnitSyntax syntax) + { + public CompilationUnitSyntax WithPackageFileComments(ConversionContext context, + JavaAst.CompilationUnit compilationUnit, + JavaAst.PackageDeclaration? packageDeclaration) + => context.Options.IncludeComments + ? CommentsHelper.AddPackageComments(syntax, compilationUnit, packageDeclaration) + : syntax; } - - public static bool HasFlag(this java.util.EnumSet values, T flag) => values.contains(flag); - public static TSyntax? WithJavaComments(this TSyntax? syntax, ConversionContext context, JavaAst.Node? node, string? singleLineCommentEnd = null) - where TSyntax : SyntaxNode => - context.Options.IncludeComments ? CommentsHelper.AddCommentsTrivias(syntax, node, singleLineCommentEnd) : syntax; + extension(Optional optional) + { + public T? FromOptional() + where T : class + => optional.isPresent() + ? optional.get() as T ?? throw new InvalidOperationException($"Optional did not convert to {typeof(T)}") + : null; - public static T? FromOptional(this Optional optional) - where T : class - => optional.isPresent() - ? optional.get() as T ?? - throw new InvalidOperationException($"Optional did not convert to {typeof(T)}") - : null; + public T FromRequiredOptional() + where T : class + => optional.isPresent() + ? optional.get() as T ?? throw new InvalidOperationException($"Optional did not convert to {typeof(T)}") + : throw new InvalidOperationException("Required optional did not have a value"); + } - public static T FromRequiredOptional(this Optional optional) - where T : class - => optional.isPresent() - ? optional.get() as T ?? - throw new InvalidOperationException($"Optional did not convert to {typeof(T)}") - : throw new InvalidOperationException("Required optional did not have a value"); + extension(JavaAst.NodeList nodeList) + { + public ISet ToModifierKeywordSet() + => nodeList.ToList()?.Select(i => i.getKeyword()).ToHashSet() ?? []; + } - public static ISet ToModifierKeywordSet(this JavaAst.NodeList nodeList) - => nodeList.ToList()?.Select(i => i.getKeyword()).ToHashSet() - ?? new HashSet(); + extension(TSyntax syntax) where TSyntax : SyntaxNode + { + public TSyntax WithLeadingNewLines(int count = 1) + => syntax.WithLeadingTrivia(Enumerable.Repeat(Whitespace.NewLine, count)); + + public TSyntax WithTrailingNewLines(int count = 1) + => syntax.WithTrailingTrivia(Enumerable.Repeat(Whitespace.NewLine, count)); + } + + extension(TSyntax? syntax) where TSyntax : SyntaxNode + { + [return: NotNullIfNotNull(nameof(node))] + public TSyntax? WithJavaComments(ConversionContext context, JavaAst.Node? node) + => context.Options.IncludeComments + ? CommentsHelper.AddCommentsTrivias(syntax, node) + : syntax; + } } diff --git a/JavaToCSharp/JavaConversionOptions.cs b/JavaToCSharp/JavaConversionOptions.cs index cf626e41..85795cdf 100644 --- a/JavaToCSharp/JavaConversionOptions.cs +++ b/JavaToCSharp/JavaConversionOptions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace JavaToCSharp; @@ -12,32 +10,33 @@ public class JavaConversionOptions public IList PackageReplacements { get; } = new List(); - public IList Usings { get; } = new List - { - "System", - "System.Collections.Generic", - "System.Collections.ObjectModel", - "System.Linq", - "System.Text" - }; + public IList Usings { get; } = new List(); + + public IList StaticUsingEnumNames { get; } = new List(); + + public bool IncludeSubdirectories { get; set; } = true; public bool IncludeUsings { get; set; } = true; public bool IncludeNamespace { get; set; } = true; public bool UseDebugAssertForAsserts { get; set; } - + public bool StartInterfaceNamesWithI { get; set; } /// /// Unrecognized code is translated into comments /// public bool UseUnrecognizedCodeToComment { get; set; } = true; - + public bool ConvertSystemOutToConsole { get; set; } public bool IncludeComments { get; set; } = true; + public bool UseFileScopedNamespaces { get; set; } + + public SyntaxMapping SyntaxMappings { get; set; } = new SyntaxMapping(); + public ConversionState ConversionState { get; set; } public JavaConversionOptions AddPackageReplacement(string pattern, string replacement, RegexOptions options = RegexOptions.None) @@ -61,6 +60,18 @@ public JavaConversionOptions AddUsing(string ns) return this; } + public JavaConversionOptions SetUsings(IEnumerable usings) + { + Usings.Clear(); + + foreach (string u in usings) + { + Usings.Add(u); + } + + return this; + } + internal void Warning(string message, int javaLineNumber) => WarningEncountered?.Invoke(this, new ConversionWarningEventArgs(message, javaLineNumber)); internal void ConversionStateChanged(ConversionState newState) @@ -69,4 +80,4 @@ internal void ConversionStateChanged(ConversionState newState) StateChanged?.Invoke(this, new ConversionStateChangedEventArgs(newState)); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/JavaToCSharp.csproj b/JavaToCSharp/JavaToCSharp.csproj index 346eae3b..2cad3e2c 100644 --- a/JavaToCSharp/JavaToCSharp.csproj +++ b/JavaToCSharp/JavaToCSharp.csproj @@ -1,26 +1,28 @@  - - 3.0.0 - net6.0 - enable - enable - nullable - Paul Irwin - A Java to C# converter. - Copyright 2021-2023, Paul Irwin - MIT - https://github.com/paulirwin/JavaToCSharp - latest - - - - - - - - - - - - + + 4.0.0 + net10.0 + enable + enable + true + Paul Irwin + A Java to C# converter. + Copyright 2021-2025, Paul Irwin + MIT + https://github.com/paulirwin/JavaToCSharp + latest + + + + + + + + + + + + + + diff --git a/JavaToCSharp/JavaToCSharpConverter.cs b/JavaToCSharp/JavaToCSharpConverter.cs index f52834d0..676dbacf 100644 --- a/JavaToCSharp/JavaToCSharpConverter.cs +++ b/JavaToCSharp/JavaToCSharpConverter.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using System.Text; using com.github.javaparser; using com.github.javaparser.ast; using com.github.javaparser.ast.body; @@ -24,7 +20,7 @@ public static class JavaToCSharpConverter var context = new ConversionContext(options); - var textBytes = Encoding.UTF8.GetBytes(javaText ?? System.String.Empty); + var textBytes = Encoding.UTF8.GetBytes(javaText ?? string.Empty); using var memoryStream = new MemoryStream(textBytes); using var wrapper = new InputStreamWrapper(memoryStream); @@ -32,7 +28,8 @@ public static class JavaToCSharpConverter options.ConversionStateChanged(ConversionState.ParsingJavaAst); var parser = new JavaParser(); - + parser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); + var parsed = parser.parse(wrapper); if (!parsed.isSuccessful()) @@ -52,12 +49,12 @@ public static class JavaToCSharpConverter options.ConversionStateChanged(ConversionState.BuildingCSharpAst); - var types = result.getTypes().ToList() ?? new List(); - var imports = result.getImports()?.ToList() ?? new List(); + var types = result.getTypes().ToList() ?? []; + var imports = result.getImports()?.ToList() ?? []; var package = result.getPackageDeclaration().FromOptional(); var rootMembers = new List(); - NamespaceDeclarationSyntax? namespaceSyntax = null; + NameSyntax? namespaceNameSyntax = null; if (options.IncludeNamespace) { @@ -65,18 +62,17 @@ public static class JavaToCSharpConverter foreach (var packageReplacement in options.PackageReplacements) { - if (System.String.IsNullOrWhiteSpace(packageName)) + if (string.IsNullOrWhiteSpace(packageName)) { continue; } - - packageName = packageReplacement.Replace(packageName)!; + + packageName = packageReplacement.Replace(packageName); } packageName = TypeHelper.Capitalize(packageName); - namespaceSyntax = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(packageName)) - .WithJavaComments(context, package); + namespaceNameSyntax = SyntaxFactory.ParseName(packageName); } foreach (var type in types) @@ -86,52 +82,61 @@ public static class JavaToCSharpConverter if (classOrIntType.isInterface()) { var interfaceSyntax = ClassOrInterfaceDeclarationVisitor.VisitInterfaceDeclaration(context, classOrIntType); - - if (namespaceSyntax != null && interfaceSyntax != null) - namespaceSyntax = namespaceSyntax.AddMembers(interfaceSyntax); - else if(interfaceSyntax != null) - rootMembers.Add(interfaceSyntax); + rootMembers.Add(interfaceSyntax.NormalizeWhitespace().WithTrailingNewLines()); } else { var classSyntax = ClassOrInterfaceDeclarationVisitor.VisitClassDeclaration(context, classOrIntType); - - if (namespaceSyntax != null && classSyntax != null) - namespaceSyntax = namespaceSyntax.AddMembers(classSyntax); - else if(classSyntax != null) - rootMembers.Add(classSyntax); + rootMembers.Add(classSyntax.NormalizeWhitespace().WithTrailingNewLines()); } } else if (type is EnumDeclaration enumType) { - var classSyntax = EnumDeclarationVisitor.VisitEnumDeclaration(context, enumType); + var enumSyntax = EnumDeclarationVisitor.VisitEnumDeclaration(context, enumType); + rootMembers.Add(enumSyntax.NormalizeWhitespace().WithTrailingNewLines()); + } + } - if (namespaceSyntax != null && classSyntax != null) - namespaceSyntax = namespaceSyntax.AddMembers(classSyntax); - else if(classSyntax != null) - rootMembers.Add(classSyntax); + if (rootMembers.Count > 1) + { + for (int i = 1; i < rootMembers.Count; i++) + { + rootMembers[i] = rootMembers[i].WithLeadingNewLines(); } } - if (namespaceSyntax != null) - rootMembers.Add(namespaceSyntax); + if (namespaceNameSyntax is not null) + { + if (options.UseFileScopedNamespaces && rootMembers.Count > 0) + { + rootMembers[0] = rootMembers[0].WithLeadingNewLines(); + } + + MemberDeclarationSyntax namespaceSyntax = + options.UseFileScopedNamespaces + ? SyntaxFactory.FileScopedNamespaceDeclaration(namespaceNameSyntax) + .NormalizeWhitespace() + .WithTrailingNewLines() + .WithMembers(SyntaxFactory.List(rootMembers)) + : SyntaxFactory.NamespaceDeclaration(namespaceNameSyntax) + .WithMembers(SyntaxFactory.List(rootMembers)) + .NormalizeWhitespace(); + + rootMembers = [namespaceSyntax]; + } var root = SyntaxFactory.CompilationUnit( - externs: new SyntaxList(), - usings: SyntaxFactory.List(UsingsHelper.GetUsings(imports, options)), - attributeLists: new SyntaxList(), + externs: [], + usings: SyntaxFactory.List(UsingsHelper.GetUsings(context, imports, options, namespaceNameSyntax)), + attributeLists: [], members: SyntaxFactory.List(rootMembers) - ) - .NormalizeWhitespace(); + ); - root = root.WithJavaComments(context, result, Environment.NewLine); - if (root is null) - { - return null; - } + root = root.WithPackageFileComments(context, result, package); var postConversionSanitizer = new SanitizingSyntaxRewriter(); var sanitizedRoot = postConversionSanitizer.VisitCompilationUnit(root); + if (sanitizedRoot is null) { return null; diff --git a/JavaToCSharp/Replacement.cs b/JavaToCSharp/Replacement.cs index 18937ed8..00213b9e 100644 --- a/JavaToCSharp/Replacement.cs +++ b/JavaToCSharp/Replacement.cs @@ -2,24 +2,19 @@ namespace JavaToCSharp; -public class Replacement +public class Replacement(string pattern, string replacement, RegexOptions options = RegexOptions.None) { - public Replacement(string pattern, string replacement, RegexOptions options = RegexOptions.None) - { - Regex = new Regex(pattern, options); - ReplacementValue = replacement; - } + public Regex Regex { get; } = new(pattern, options); - public Regex? Regex { get; } + public string? ReplacementValue { get; } = replacement; - public string? ReplacementValue { get; } - - public string? Replace(string input) => System.String.IsNullOrWhiteSpace(ReplacementValue) ? null : Regex?.Replace(input, ReplacementValue); + public string Replace(string input) + => string.IsNullOrWhiteSpace(ReplacementValue) + ? string.Empty + : Regex.Replace(input, ReplacementValue); protected bool Equals(Replacement other) - { - return Equals(Regex, other.Regex) && ReplacementValue == other.ReplacementValue; - } + => Equals(Regex, other.Regex) && ReplacementValue == other.ReplacementValue; public override bool Equals(object? obj) { @@ -30,10 +25,5 @@ public override bool Equals(object? obj) } public override int GetHashCode() - { - unchecked - { - return ((Regex != null ? Regex.GetHashCode() : 0) * 397) ^ (ReplacementValue != null ? ReplacementValue.GetHashCode() : 0); - } - } + => HashCode.Combine(Regex, ReplacementValue); } diff --git a/JavaToCSharp/SanitizingSyntaxRewriter.cs b/JavaToCSharp/SanitizingSyntaxRewriter.cs index 37a1689f..d6b910f9 100644 --- a/JavaToCSharp/SanitizingSyntaxRewriter.cs +++ b/JavaToCSharp/SanitizingSyntaxRewriter.cs @@ -11,7 +11,7 @@ internal class SanitizingSyntaxRewriter : CSharpSyntaxRewriter { public override SyntaxNode? Visit(SyntaxNode? node) { - if (node != null) + if (node is not null) { // We must do this after whitespace normalization! node = CommentsHelper.FixCommentsWhitespaces(node); diff --git a/JavaToCSharp/Statements/AssertStatementVisitor.cs b/JavaToCSharp/Statements/AssertStatementVisitor.cs index b7c9b2cf..50cac058 100644 --- a/JavaToCSharp/Statements/AssertStatementVisitor.cs +++ b/JavaToCSharp/Statements/AssertStatementVisitor.cs @@ -11,7 +11,9 @@ public class AssertStatementVisitor : StatementVisitor public override StatementSyntax? Visit(ConversionContext context, AssertStmt assertStmt) { if (!context.Options.UseDebugAssertForAsserts) + { return null; + } var check = assertStmt.getCheck(); var checkSyntax = ExpressionVisitor.VisitExpression(context, check); @@ -22,14 +24,15 @@ public class AssertStatementVisitor : StatementVisitor var message = assertStmt.getMessage().FromOptional(); - if (message == null) + if (message is null) + { return SyntaxFactory.ExpressionStatement( SyntaxFactory.InvocationExpression( SyntaxFactory.IdentifierName("Debug.Assert"), - SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] - { + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList([ SyntaxFactory.Argument(checkSyntax) - })))); + ])))); + } var messageSyntax = ExpressionVisitor.VisitExpression(context, message); if (messageSyntax is null) @@ -40,6 +43,7 @@ public class AssertStatementVisitor : StatementVisitor return SyntaxFactory.ExpressionStatement( SyntaxFactory.InvocationExpression( SyntaxFactory.IdentifierName("Debug.Assert"), - SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] { SyntaxFactory.Argument(checkSyntax), SyntaxFactory.Argument(messageSyntax) }, new[] { SyntaxFactory.Token(SyntaxKind.CommaToken) })))); + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList([SyntaxFactory.Argument(checkSyntax), SyntaxFactory.Argument(messageSyntax) + ], [SyntaxFactory.Token(SyntaxKind.CommaToken)])))); } } diff --git a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs index 6aea4649..262cdbc7 100644 --- a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs +++ b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using JavaToCSharp.Expressions; @@ -17,40 +15,49 @@ public class ExpressionStatementVisitor : StatementVisitor // handle special case where AST is different if (expression is VariableDeclarationExpr expr) + { return VisitVariableDeclarationStatement(context, expr); + } var expressionSyntax = ExpressionVisitor.VisitExpression(context, expression); - if (expressionSyntax is null) - { - return null; - } - return SyntaxFactory.ExpressionStatement(expressionSyntax); + return expressionSyntax is null ? null : SyntaxFactory.ExpressionStatement(expressionSyntax); } private static StatementSyntax VisitVariableDeclarationStatement(ConversionContext context, VariableDeclarationExpr varExpr) { - var type = TypeHelper.ConvertType(varExpr.getCommonType()); + var commonType = varExpr.getCommonType(); + int? arrayRank = null; var variables = new List(); - var variableDeclarators = varExpr.getVariables()?.ToList() ?? 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 (id.getArrayLevel() > 0) + if (type.getArrayLevel() > 0) { - if (!type.EndsWith("[]")) - type += "[]"; - if (name.EndsWith("[]")) - name = name.Substring(0, name.Length - 2); + while (name.EndsWith("[]")) + { + name = name[..^2]; + } } var initExpr = item.getInitializer().FromOptional(); - if (initExpr != null) + if (initExpr is not null) { var initSyntax = ExpressionVisitor.VisitExpression(context, initExpr); if (initSyntax is not null) @@ -60,10 +67,14 @@ private static StatementSyntax VisitVariableDeclarationStatement(ConversionConte } } else + { variables.Add(SyntaxFactory.VariableDeclarator(name)); + } } + var typeSyntax = TypeHelper.ConvertTypeSyntax(commonType, arrayRank ?? 0); + return SyntaxFactory.LocalDeclarationStatement( - SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName(type), SyntaxFactory.SeparatedList(variables, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), variables.Count - 1)))); + SyntaxFactory.VariableDeclaration(typeSyntax, SyntaxFactory.SeparatedList(variables, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), variables.Count - 1)))); } } diff --git a/JavaToCSharp/Statements/ForEachStatementVisitor.cs b/JavaToCSharp/Statements/ForEachStatementVisitor.cs index f2b54250..19a43bec 100644 --- a/JavaToCSharp/Statements/ForEachStatementVisitor.cs +++ b/JavaToCSharp/Statements/ForEachStatementVisitor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.stmt; using JavaToCSharp.Expressions; using Microsoft.CodeAnalysis.CSharp; @@ -23,7 +21,7 @@ public class ForEachStatementVisitor : StatementVisitor var varType = varExpr.getCommonType(); var type = TypeHelper.ConvertType(varType); - var variableDeclarators = varExpr.getVariables()?.ToList()?? new List(); + var variableDeclarators = varExpr.getVariables()?.ToList()?? []; var vars = variableDeclarators .Select(i => SyntaxFactory.VariableDeclarator(i.toString())) .ToArray(); @@ -31,6 +29,6 @@ public class ForEachStatementVisitor : StatementVisitor var body = foreachStmt.getBody(); var bodySyntax = VisitStatement(context, body); - return bodySyntax == null ? null : SyntaxFactory.ForEachStatement(SyntaxFactory.ParseTypeName(type), vars[0].Identifier.ValueText, iterableSyntax, bodySyntax); + return bodySyntax is null ? null : SyntaxFactory.ForEachStatement(SyntaxFactory.ParseTypeName(type), vars[0].Identifier.ValueText, iterableSyntax, bodySyntax); } } diff --git a/JavaToCSharp/Statements/ForStatementVisitor.cs b/JavaToCSharp/Statements/ForStatementVisitor.cs index 33ca65c8..1f20c533 100644 --- a/JavaToCSharp/Statements/ForStatementVisitor.cs +++ b/JavaToCSharp/Statements/ForStatementVisitor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.body; +using com.github.javaparser.ast.body; using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using JavaToCSharp.Expressions; @@ -18,7 +16,7 @@ public class ForStatementVisitor : StatementVisitor var initSyntaxes = new List(); VariableDeclarationSyntax? varSyntax = null; - if (inits != null) + if (inits is not null) { foreach (var init in inits) { @@ -26,7 +24,7 @@ public class ForStatementVisitor : StatementVisitor { var type = TypeHelper.ConvertType(varExpr.getCommonType()); - var variableDeclarators = varExpr.getVariables()?.ToList() ?? new List(); + var variableDeclarators = varExpr.getVariables()?.ToList() ?? []; var vars = variableDeclarators .Select(i => SyntaxFactory.VariableDeclarator(i.toString())) .ToArray(); @@ -50,17 +48,19 @@ public class ForStatementVisitor : StatementVisitor var increments = forStmt.getUpdate().ToList(); var incrementSyntaxes = new List(); - if (increments != null) + if (increments is not null) { var expressionSyntaxes = increments.Select(increment => ExpressionVisitor.VisitExpression(context, increment)); - incrementSyntaxes.AddRange(expressionSyntaxes.Where(expressionSyntax => expressionSyntax != null)!); + incrementSyntaxes.AddRange(expressionSyntaxes.OfType()); } var body = forStmt.getBody(); var bodySyntax = VisitStatement(context, body); - if (bodySyntax == null) + if (bodySyntax is null) + { return null; + } return SyntaxFactory.ForStatement(bodySyntax) .WithDeclaration(varSyntax) diff --git a/JavaToCSharp/Statements/IfStatementVisitor.cs b/JavaToCSharp/Statements/IfStatementVisitor.cs index f50f210c..1f5f8f67 100644 --- a/JavaToCSharp/Statements/IfStatementVisitor.cs +++ b/JavaToCSharp/Statements/IfStatementVisitor.cs @@ -11,6 +11,7 @@ public class IfStatementVisitor : StatementVisitor { var condition = ifStmt.getCondition(); var conditionSyntax = ExpressionVisitor.VisitExpression(context, condition); + if (conditionSyntax is null) { return null; @@ -19,20 +20,25 @@ public class IfStatementVisitor : StatementVisitor var thenStmt = ifStmt.getThenStmt(); var thenSyntax = VisitStatement(context, thenStmt); - if (thenSyntax == null) + if (thenSyntax is null) + { return null; + } var elseStmt = ifStmt.getElseStmt().FromOptional(); - if (elseStmt == null) + if (elseStmt is null) + { return SyntaxFactory.IfStatement(conditionSyntax, thenSyntax); + } var elseStatementSyntax = VisitStatement(context, elseStmt); + if (elseStatementSyntax is null) { return null; } - + var elseSyntax = SyntaxFactory.ElseClause(elseStatementSyntax); return SyntaxFactory.IfStatement(conditionSyntax, thenSyntax, elseSyntax); } diff --git a/JavaToCSharp/Statements/LabeledStatementVisitor.cs b/JavaToCSharp/Statements/LabeledStatementVisitor.cs index 7a5958c5..dbba5333 100644 --- a/JavaToCSharp/Statements/LabeledStatementVisitor.cs +++ b/JavaToCSharp/Statements/LabeledStatementVisitor.cs @@ -11,6 +11,6 @@ public class LabeledStatementVisitor : StatementVisitor var statement = labeledStmt.getStatement(); var syntax = VisitStatement(context, statement); - return syntax == null ? null : SyntaxFactory.LabeledStatement(labeledStmt.getLabel().asString(), syntax); + return syntax is null ? null : SyntaxFactory.LabeledStatement(labeledStmt.getLabel().asString(), syntax); } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Statements/ReturnStatementVisitor.cs b/JavaToCSharp/Statements/ReturnStatementVisitor.cs index d3542523..3b92de16 100644 --- a/JavaToCSharp/Statements/ReturnStatementVisitor.cs +++ b/JavaToCSharp/Statements/ReturnStatementVisitor.cs @@ -12,8 +12,10 @@ public override StatementSyntax Visit(ConversionContext context, ReturnStmt retu { var expr = returnStmt.getExpression().FromOptional(); - if (expr == null) + if (expr is null) + { return SyntaxFactory.ReturnStatement(); // i.e. "return" in a void method + } var exprSyntax = ExpressionVisitor.VisitExpression(context, expr); diff --git a/JavaToCSharp/Statements/StatementVisitor.cs b/JavaToCSharp/Statements/StatementVisitor.cs index 21ff885b..196cb9c9 100644 --- a/JavaToCSharp/Statements/StatementVisitor.cs +++ b/JavaToCSharp/Statements/StatementVisitor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.stmt; +using com.github.javaparser.ast.stmt; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace JavaToCSharp.Statements; @@ -48,11 +45,12 @@ static StatementVisitor() protected abstract StatementSyntax? Visit(ConversionContext context, Statement statement); - public static List VisitStatements(ConversionContext context, IEnumerable? statements) => - statements == null - ? new List() + public static List VisitStatements(ConversionContext context, IEnumerable? statements) + => statements is null + ? [] : statements.Select(statement => VisitStatement(context, statement)) - .Where(syntax => syntax != null)!.ToList(); + .OfType() // filter out nulls + .ToList(); public static StatementSyntax? VisitStatement(ConversionContext context, Statement statement) { diff --git a/JavaToCSharp/Statements/SwitchStatementVisitor.cs b/JavaToCSharp/Statements/SwitchStatementVisitor.cs index 42e2db25..21612f24 100644 --- a/JavaToCSharp/Statements/SwitchStatementVisitor.cs +++ b/JavaToCSharp/Statements/SwitchStatementVisitor.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using JavaToCSharp.Expressions; using Microsoft.CodeAnalysis.CSharp; @@ -14,6 +12,7 @@ public class SwitchStatementVisitor : StatementVisitor { var selector = switchStmt.getSelector(); var selectorSyntax = ExpressionVisitor.VisitExpression(context, selector); + if (selectorSyntax is null) { return null; @@ -21,8 +20,10 @@ public class SwitchStatementVisitor : StatementVisitor var cases = switchStmt.getEntries().ToList(); - if (cases == null) + if (cases is null) + { return SyntaxFactory.SwitchStatement(selectorSyntax, SyntaxFactory.List()); + } var caseSyntaxes = new List(); @@ -58,9 +59,9 @@ public class SwitchStatementVisitor : StatementVisitor { var labelSyntaxes = labels .Select(i => ExpressionVisitor.VisitExpression(context, i)) - .Where(i => i != null) - .Select(i => (SwitchLabelSyntax)SyntaxFactory.CaseSwitchLabel(i!)); - + .OfType() + .Select(SwitchLabelSyntax (i) => SyntaxFactory.CaseSwitchLabel(i)); + var caseSyntax = SyntaxFactory.SwitchSection( SyntaxFactory.List(labelSyntaxes.ToList()), SyntaxFactory.List(syntaxes.AsEnumerable())); diff --git a/JavaToCSharp/Statements/TryStatementVisitor.cs b/JavaToCSharp/Statements/TryStatementVisitor.cs index 1650d408..e842134b 100644 --- a/JavaToCSharp/Statements/TryStatementVisitor.cs +++ b/JavaToCSharp/Statements/TryStatementVisitor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser; +using com.github.javaparser; using com.github.javaparser.ast.expr; using com.github.javaparser.ast.stmt; using com.github.javaparser.ast.type; @@ -15,7 +12,7 @@ public class TryStatementVisitor : StatementVisitor { public override StatementSyntax Visit(ConversionContext context, TryStmt tryStmt) { - var resources = tryStmt.getResources().ToList() ?? new List(); + var resources = tryStmt.getResources().ToList() ?? []; var tryBlock = tryStmt.getTryBlock(); var tryStatements = tryBlock.getStatements().ToList(); @@ -57,19 +54,18 @@ public override StatementSyntax Visit(ConversionContext context, TryStmt tryStmt var variable = varDecl.getVariable(0); var variableInit = variable.getInitializer().FromRequiredOptional(); - + var initSyntax = ExpressionVisitor.VisitExpression(context, variableInit) ?? throw new InvalidOperationException("Unable to parse try-with-resources variable initializer"); - + result = SyntaxFactory.UsingStatement(result) .WithDeclaration( SyntaxFactory.VariableDeclaration( SyntaxFactory.ParseTypeName(TypeHelper.ConvertType(variable.getType())), - SyntaxFactory.SeparatedList(new[] - { + SyntaxFactory.SeparatedList([ SyntaxFactory.VariableDeclarator(variable.getNameAsString()) .WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)) - }) + ]) )); } else @@ -81,13 +77,13 @@ public override StatementSyntax Visit(ConversionContext context, TryStmt tryStmt if (tryStmt.getFinallyBlock().isPresent() || tryStmt.getCatchClauses().ToList()?.Count > 0) { - result = TransformTryBlock(context, tryStmt, new[] { result }); + result = TransformTryBlock(context, tryStmt, [result]); } return result; } - private static StatementSyntax TransformTryBlock(ConversionContext context, TryStmt tryStmt, + private static TryStatementSyntax TransformTryBlock(ConversionContext context, TryStmt tryStmt, IEnumerable tryBlockStatements) { var catches = tryStmt.getCatchClauses().ToList(); @@ -95,14 +91,14 @@ private static StatementSyntax TransformTryBlock(ConversionContext context, TryS var trySyn = SyntaxFactory.TryStatement() .AddBlockStatements(tryBlockStatements.ToArray()); - if (catches != null) + if (catches is not null) { foreach (var catchClause in catches) { var paramType = catchClause.getParameter().getType(); if (paramType is UnionType) { - var nodes = paramType.getChildNodes()?.ToList() ?? new List(); + var nodes = paramType.getChildNodes()?.ToList() ?? []; foreach (var node in nodes) { var referenceTypeName = node.getElementType().ToString(); @@ -119,8 +115,10 @@ private static StatementSyntax TransformTryBlock(ConversionContext context, TryS var finallyBlock = tryStmt.getFinallyBlock().FromOptional(); - if (finallyBlock == null) + if (finallyBlock is null) + { return trySyn; + } var finallyStatements = finallyBlock.getStatements().ToList(); var finallyConverted = VisitStatements(context, finallyStatements); @@ -154,4 +152,4 @@ private static TryStatementSyntax AddCatches(ConversionContext context, CatchCla return trySyn; } -} \ No newline at end of file +} diff --git a/JavaToCSharp/Statements/TypeDeclarationStatementVisitor.cs b/JavaToCSharp/Statements/TypeDeclarationStatementVisitor.cs index 4fd737d6..666460a3 100644 --- a/JavaToCSharp/Statements/TypeDeclarationStatementVisitor.cs +++ b/JavaToCSharp/Statements/TypeDeclarationStatementVisitor.cs @@ -13,11 +13,12 @@ public class TypeDeclarationStatementVisitor : StatementVisitor { var expr = whileStmt.getCondition(); var syntax = ExpressionVisitor.VisitExpression(context, expr); + if (syntax is null) { return null; diff --git a/JavaToCSharp/SyntaxMapping.cs b/JavaToCSharp/SyntaxMapping.cs new file mode 100644 index 00000000..08d8f5dc --- /dev/null +++ b/JavaToCSharp/SyntaxMapping.cs @@ -0,0 +1,43 @@ +namespace JavaToCSharp; + +public class SyntaxMapping +{ + public Dictionary ImportMappings { get; set; } = new(); + public Dictionary VoidMethodMappings { get; set; } = new(); + public Dictionary NonVoidMethodMappings { get; set; } = new(); + public Dictionary AnnotationMappings { get; set; } = new(); + + public static SyntaxMapping Deserialize(string yaml) + { + var deserializer = new YamlDotNet.Serialization.Deserializer(); + SyntaxMapping mapping = deserializer.Deserialize(yaml); + mapping.Validate(); + return mapping; + } + + private void Validate() + { + // Throw exception if any of the requirements are not meet + ValidateMethodMapping(VoidMethodMappings); + ValidateMethodMapping(NonVoidMethodMappings); + } + + private static void ValidateMethodMapping(Dictionary mapping) + { + // Throw exception if any of the requirements are not meet + foreach (string key in mapping.Keys) + { + if (key.Contains('.')) + { + throw new YamlDotNet.Core.SemanticErrorException("Mappings from fully qualified java methods are not supported"); + } + } + foreach (string value in mapping.Values) + { + if (string.IsNullOrEmpty(value)) + { + throw new YamlDotNet.Core.SemanticErrorException("Mappings from java methods can not have an empty value"); + } + } + } +} diff --git a/JavaToCSharp/TypeHelper.cs b/JavaToCSharp/TypeHelper.cs index d532418a..6bb5f6bb 100644 --- a/JavaToCSharp/TypeHelper.cs +++ b/JavaToCSharp/TypeHelper.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.expr; +using com.github.javaparser.ast.type; using JavaToCSharp.Expressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using ast = com.github.javaparser.ast; +using Ast = com.github.javaparser.ast; +using Type = com.github.javaparser.ast.type.Type; namespace JavaToCSharp; @@ -51,33 +51,60 @@ public static void AddOrUpdateTypeNameConversions(string key, string value) _typeNameConversions[key] = value; } - public static string ConvertTypeOf(ast.nodeTypes.NodeWithType typedNode) + public static string ConvertTypeOf(Ast.nodeTypes.NodeWithType typedNode) { return ConvertType(typedNode.getType().toString()); } - - public static string ConvertType(ast.type.Type type) + + public static string ConvertType(Type type) { return ConvertType(type.toString()); } public static string ConvertType(string typeName) + => TypeNameParser.ParseTypeName(typeName, s => _typeNameConversions.TryGetValue(s, out string? converted) ? converted : s); + + public static TypeSyntax ConvertTypeSyntax(Type type, int arrayRank) { - return TypeNameParser.ParseTypeName(typeName, s => + if (type is ArrayType arrayType) { - if (_typeNameConversions.TryGetValue(s, out string? converted)) + if (arrayRank > 0 && arrayType.getArrayLevel() != arrayRank) { - return converted; + throw new ArgumentException("Given array rank does not match the array level of the type", nameof(arrayRank)); } - return s; - }); + + arrayRank = arrayType.getArrayLevel(); + Type elementType; + + while ((elementType = arrayType.getElementType()) is ArrayType nestedArrayType) + { + arrayType = nestedArrayType; + } + + return ConvertTypeSyntax(elementType, arrayRank); + } + + return arrayRank == 0 + ? SyntaxFactory.ParseTypeName(ConvertType(type)) + : ConvertArrayTypeSyntax(type, arrayRank); + } + + public static ArrayTypeSyntax ConvertArrayTypeSyntax(Type type, int arrayRank) + { + var rankSpecifiers = SyntaxFactory.ArrayRankSpecifier( + SyntaxFactory.SeparatedList( + Enumerable.Repeat(SyntaxFactory.OmittedArraySizeExpression(), arrayRank) + )); + + return SyntaxFactory.ArrayType(ConvertTypeSyntax(type, 0)) + .WithRankSpecifiers(SyntaxFactory.SingletonList(rankSpecifiers)); } public static string Capitalize(string name) { var parts = name.Split('.'); - var joined = System.String.Join(".", parts.Select(i => + var joined = string.Join(".", parts.Select(i => { if (i.Length == 1) return i.ToUpper(); @@ -92,8 +119,8 @@ public static string EscapeIdentifier(string name) { // @ (C# Reference): https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/verbatim return name switch { - "string" or "ref" or "object" or "int" or "short" or "float" or "long" or "double" or "decimal" or "in" or - "out" or "byte" or "class" or "delegate" or "params" or "is" or "as" or "base" or "namespace" or "event" or + "string" or "ref" or "object" or "int" or "short" or "float" or "long" or "double" or "decimal" or "in" or + "out" or "byte" or "class" or "delegate" or "params" or "is" or "as" or "base" or "namespace" or "event" or "lock" or "operator" or "override" => "@" + name, _ => name, }; @@ -109,10 +136,10 @@ public static string ReplaceCommonMethodNames(string name) }; } - public static TypeSyntax GetSyntaxFromType(ast.type.ClassOrInterfaceType type) + public static TypeSyntax GetSyntaxFromType(ClassOrInterfaceType type) { string typeName = ConvertType(type.getNameAsString()); - var typeArgs = type.getTypeArguments().FromOptional()?.ToList(); + var typeArgs = type.getTypeArguments().FromOptional()?.ToList(); TypeSyntax typeSyntax; @@ -150,7 +177,7 @@ private static SeparatedSyntaxList GetSeparatedListFromArguments { continue; } - + argSyntaxes.Add(SyntaxFactory.Argument(argSyntax)); } @@ -158,6 +185,36 @@ private static SeparatedSyntaxList GetSeparatedListFromArguments return SyntaxFactory.SeparatedList(argSyntaxes, separators); } + /// + /// Returns the list of C# type parameter constraints that must be added to a class syntax + /// to convert the java bounded type parameters of a class declaration. + /// e.g. to convert > ]]> into where T : Clazz ]]> + /// + public static IEnumerable GetTypeParameterListConstraints(List typeParams) + { + var typeParameterConstraints = new List(); + + foreach (TypeParameter typeParam in typeParams) + { + if (typeParam.getTypeBound().size() > 0) + { + var typeConstraintsSyntax = new SeparatedSyntaxList(); + + foreach (ClassOrInterfaceType bound in typeParam.getTypeBound()) + { + typeConstraintsSyntax = typeConstraintsSyntax.Add(SyntaxFactory.TypeConstraint(SyntaxFactory.ParseTypeName(bound.asString()))); + } + + var typeIdentifier = SyntaxFactory.IdentifierName(typeParam.getName().asString()); + var parameterConstraintClauseSyntax = SyntaxFactory.TypeParameterConstraintClause(typeIdentifier, typeConstraintsSyntax); + + typeParameterConstraints.Add(parameterConstraintClauseSyntax); + } + } + + return typeParameterConstraints; + } + /// /// Transforms method calls into property and indexer accesses where appropriate. /// @@ -175,51 +232,63 @@ public static bool TryTransformMethodCall(ConversionContext context, MethodCallE switch (methodName.getIdentifier()) { + case "length" when args.size() == 0: + { + var scopeSyntaxLength = ExpressionVisitor.VisitExpression(context, scope); + transformedSyntax = ReplaceMethodByProperty(scopeSyntaxLength, "Length"); + return true; + } + case "size" when args.size() == 0: + { var scopeSyntaxSize = ExpressionVisitor.VisitExpression(context, scope); - transformedSyntax = ReplaceSizeByCount(scopeSyntaxSize); + transformedSyntax = ReplaceMethodByProperty(scopeSyntaxSize, "Count"); return true; + } case "get" when args.size() == 1: + { var scopeSyntaxGet = ExpressionVisitor.VisitExpression(context, scope); - if (scopeSyntaxGet is null) + if (scopeSyntaxGet is null) { transformedSyntax = null; return false; } - + transformedSyntax = ReplaceGetByIndexAccess(context, scopeSyntaxGet, args); return true; + } case "set" when args.size() == 2: + { var scopeSyntaxSet = ExpressionVisitor.VisitExpression(context, scope); - if (scopeSyntaxSet is null) + if (scopeSyntaxSet is null) { transformedSyntax = null; return false; } - + transformedSyntax = ReplaceSetByIndexAccess(context, scopeSyntaxSet, args); return true; + } } } transformedSyntax = null; return false; - - static MemberAccessExpressionSyntax? ReplaceSizeByCount(ExpressionSyntax? scopeSyntax) + static MemberAccessExpressionSyntax? ReplaceMethodByProperty(ExpressionSyntax? scopeSyntax, string identifier) { if (scopeSyntax is null) { return null; } - + // Replace expr.Size() by expr.Count return SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, scopeSyntax, - SyntaxFactory.IdentifierName(SyntaxFactory.Identifier("Count"))); + SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(identifier))); } static ExpressionSyntax ReplaceGetByIndexAccess(ConversionContext context, ExpressionSyntax scopeSyntax, @@ -233,12 +302,12 @@ static ExpressionSyntax ReplaceGetByIndexAccess(ConversionContext context, Expre } static ExpressionSyntax? ReplaceSetByIndexAccess( - ConversionContext context, + ConversionContext context, ExpressionSyntax scopeSyntax, java.util.List args) { // Replace expr.Set(i,v) by expr[i] = v - var argsList = args.ToList() ?? new List(); + var argsList = args.ToList() ?? []; var left = SyntaxFactory.ElementAccessExpression( scopeSyntax, SyntaxFactory.BracketedArgumentList(GetSeparatedListFromArguments(context, argsList.Take(1))) @@ -248,7 +317,7 @@ static ExpressionSyntax ReplaceGetByIndexAccess(ConversionContext context, Expre { return null; } - + return SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, left, right); } } diff --git a/JavaToCSharp/TypeNameParser.cs b/JavaToCSharp/TypeNameParser.cs index ada68811..353bab20 100644 --- a/JavaToCSharp/TypeNameParser.cs +++ b/JavaToCSharp/TypeNameParser.cs @@ -1,10 +1,9 @@ -using System; -using System.Text; +using System.Text; using System.Text.RegularExpressions; namespace JavaToCSharp; -public static class TypeNameParser +public static partial class TypeNameParser { private enum TokenType { @@ -20,7 +19,8 @@ private enum TokenType QuestionMark } - private static readonly Regex _tokenizePattern = new(@"\w+|\[|\]|<|>|,|\?", RegexOptions.Compiled); + [GeneratedRegex(@"\w+|\[|\]|<|>|,|\?", RegexOptions.Compiled)] + private static partial Regex TokenizePattern { get; } private static (string, TokenType)[]? _tokens; private static (string text, TokenType type) _token; @@ -53,7 +53,7 @@ internal static string ParseTypeName(string typename, Func trans private static (string, TokenType)[] Tokenize(string typeName) { - var matches = _tokenizePattern.Matches(typeName); + var matches = TokenizePattern.Matches(typeName); var tokens = new (string, TokenType)[matches.Count]; for (int i = 0; i < matches.Count; i++) { diff --git a/JavaToCSharp/UsingsHelper.cs b/JavaToCSharp/UsingsHelper.cs index 30e1db47..a4630ff6 100644 --- a/JavaToCSharp/UsingsHelper.cs +++ b/JavaToCSharp/UsingsHelper.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using com.github.javaparser.ast; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,37 +7,76 @@ namespace JavaToCSharp; public static class UsingsHelper { - public static IList GetUsings(List imports, JavaConversionOptions? options) + public static IEnumerable GetUsings(ConversionContext context, + IEnumerable imports, + JavaConversionOptions? options, + NameSyntax? namespaceNameSyntax) { var usings = new List(); foreach (var import in imports) { - // The import directive in Java will import a specific class. + // The import directive in Java will import a specific class. string importName = import.getNameAsString(); - var lastPartStartIndex = importName.LastIndexOf(".", StringComparison.Ordinal); - var importNameWithoutClassName = lastPartStartIndex == -1 ? - importName : + var lastPartStartIndex = importName.LastIndexOf('.'); + var importNameWithoutClassName = lastPartStartIndex == -1 ? + importName : importName[..lastPartStartIndex]; 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 (string.IsNullOrEmpty(mappedNamespace)) + { + continue; + } + nameSpace = mappedNamespace; + } + var usingSyntax = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(nameSpace)); - usings.Add(usingSyntax); + + if (context.Options.IncludeComments) + { + usingSyntax = CommentsHelper.AddUsingComments(usingSyntax, import); + } + + usings.Add(usingSyntax.NormalizeWhitespace().WithTrailingNewLines()); } if (options?.IncludeUsings == true) { - foreach (string ns in options.Usings.Where(x => !String.IsNullOrWhiteSpace(x))) + usings.AddRange(options.Usings + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(ns => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns)).NormalizeWhitespace().WithTrailingNewLines())); + } + + if (namespaceNameSyntax is not null) + { + foreach (var staticUsing in options?.StaticUsingEnumNames ?? []) { - var usingSyntax = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns)); + var usingSyntax = SyntaxFactory + .UsingDirective(SyntaxFactory.ParseName($"{namespaceNameSyntax}.{staticUsing}")) + .WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword)) + .NormalizeWhitespace() + .WithTrailingNewLines(); + usings.Add(usingSyntax); } } - return usings.Distinct(new UsingDirectiveSyntaxComparer()).ToList(); + usings = usings.Distinct(new UsingDirectiveSyntaxComparer()).ToList(); + + if (usings.Count > 0) + { + usings[^1] = usings[^1].WithTrailingNewLines(2); + } + + return usings; } } -public class UsingDirectiveSyntaxComparer : IEqualityComparer +public class UsingDirectiveSyntaxComparer : IEqualityComparer { public bool Equals(UsingDirectiveSyntax? x, UsingDirectiveSyntax? y) { @@ -47,14 +84,11 @@ public bool Equals(UsingDirectiveSyntax? x, UsingDirectiveSyntax? y) if (x is null) return false; if (y is null) return false; if (x.GetType() != y.GetType()) return false; - - return Equals(x.Alias?.ToString(), y.Alias?.ToString()) && + + return Equals(x.Alias?.ToString(), y.Alias?.ToString()) && Equals(x.Name?.ToString(), y.Name?.ToString()); } public int GetHashCode(UsingDirectiveSyntax obj) - { - return HashCode.Combine(obj.Alias?.ToString() ?? "", obj.Name?.ToString() ?? ""); - } + => HashCode.Combine(obj.Alias?.ToString() ?? "", obj.Name?.ToString() ?? ""); } - diff --git a/JavaToCSharp/Whitespace.cs b/JavaToCSharp/Whitespace.cs new file mode 100644 index 00000000..2c3e48e8 --- /dev/null +++ b/JavaToCSharp/Whitespace.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace JavaToCSharp; + +public static class Whitespace +{ + public static SyntaxTrivia NewLine => Environment.NewLine == "\r\n" + ? SyntaxFactory.CarriageReturnLineFeed + : SyntaxFactory.LineFeed; +} diff --git a/JavaToCSharpCli/JavaToCSharpCli.csproj b/JavaToCSharpCli/JavaToCSharpCli.csproj index 9bd6ecd6..564e41a9 100644 --- a/JavaToCSharpCli/JavaToCSharpCli.csproj +++ b/JavaToCSharpCli/JavaToCSharpCli.csproj @@ -1,18 +1,18 @@  - 3.0.0 + 4.0.0 Exe - net6.0 + net10.0 latest enable enable - nullable + true - - + + - \ No newline at end of file + diff --git a/JavaToCSharpCli/Program.cs b/JavaToCSharpCli/Program.cs index f8752b6c..d74a0fc3 100644 --- a/JavaToCSharpCli/Program.cs +++ b/JavaToCSharpCli/Program.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using System.CommandLine.Invocation; using JavaToCSharp; using Microsoft.Extensions.Logging; @@ -13,51 +12,75 @@ public class Program private static readonly ILoggerFactory _loggerFactory; private static readonly ILogger _logger; - private static readonly Option _includeUsingsOption = new( - name: "--include-usings", - description: "Include using directives in output", - getDefaultValue: () => true); - - private static readonly Option _includeNamespaceOption = new( - name: "--include-namespace", - description: "Include namespace in output", - getDefaultValue: () => true); - - private static readonly Option _includeCommentsOption = new( - name: "--include-comments", - description: "Include comments in output", - getDefaultValue: () => true); - - private static readonly Option _useDebugAssertOption = new( - name: "--use-debug-assert", - description: "Use Debug.Assert for asserts", - getDefaultValue: () => false); - - private static readonly Option _startInterfaceNamesWithIOption = new( - name: "--start-interface-names-with-i", - description: "Prefix interface names with the letter I", - getDefaultValue: () => true); - - private static readonly Option _commentUnrecognizedCodeOption = new( - name: "--comment-unrecognized-code", - description: "Include unrecognized code in output as commented-out code", - getDefaultValue: () => true); - - private static readonly Option _systemOutToConsoleOption = new( - name: "--system-out-to-console", - description: "Convert System.out calls to Console", - getDefaultValue: () => false); - - private static readonly Option _clearDefaultUsingsOption = new( - name: "--clear-usings", - description: "Remove all default usings provided by this app", - getDefaultValue: () => false); - - private static readonly Option> _addUsingsOption = new( - name: "--add-using", - description: "Adds a using directive to the collection of usings") + private static readonly Option _includeSubdirectoriesOption = new("--include-subdirectories") { - ArgumentHelpName = "namespace" + Description = "When the command is dir, converts files in all subdirectories", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _includeUsingsOption = new("--include-usings") + { + Description = "Include using directives in output", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _includeNamespaceOption = new("--include-namespace") + { + Description = "Include namespace in output", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _includeCommentsOption = new("--include-comments") + { + Description = "Include comments in output", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _useDebugAssertOption = new("--use-debug-assert") + { + Description = "Use Debug.Assert for asserts", + DefaultValueFactory = _ => false, + }; + + private static readonly Option _startInterfaceNamesWithIOption = new("--start-interface-names-with-i") + { + Description = "Prefix interface names with the letter I", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _commentUnrecognizedCodeOption = new("--comment-unrecognized-code") + { + Description = "Include unrecognized code in output as commented-out code", + DefaultValueFactory = _ => true, + }; + + private static readonly Option _systemOutToConsoleOption = new("--system-out-to-console") + { + Description = "Convert System.out calls to Console", + DefaultValueFactory = _ => false, + }; + + private static readonly Option _fileScopedNamespacesOption = new("--file-scoped-namespaces") + { + Description = "Use file-scoped namespaces in C# output", + DefaultValueFactory = _ => false, + }; + + private static readonly Option _clearDefaultUsingsOption = new("--clear-usings") + { + Description = "Remove all default usings provided by this app", + DefaultValueFactory = _ => false, + }; + + private static readonly Option> _addUsingsOption = new("--add-using") + { + Description = "Adds a using directive to the collection of usings", + HelpName = "namespace", + }; + + private static readonly Option _mappingsFileNameOption = new("--mappings-file") + { + Description = "A yaml file with syntax mappings from imports, methods and annotations", }; static Program() @@ -71,23 +94,26 @@ public static async Task Main(string[] args) { var rootCommand = new RootCommand("Java to C# Converter") { - Description = "A syntactic transformer of source code from Java to C#." + Description = "A syntactic transformer of source code from Java to C#.", }; - rootCommand.AddCommand(CreateFileCommand()); - rootCommand.AddCommand(CreateDirectoryCommand()); - - rootCommand.AddGlobalOption(_includeUsingsOption); - rootCommand.AddGlobalOption(_includeNamespaceOption); - rootCommand.AddGlobalOption(_includeCommentsOption); - rootCommand.AddGlobalOption(_useDebugAssertOption); - rootCommand.AddGlobalOption(_startInterfaceNamesWithIOption); - rootCommand.AddGlobalOption(_commentUnrecognizedCodeOption); - rootCommand.AddGlobalOption(_systemOutToConsoleOption); - rootCommand.AddGlobalOption(_clearDefaultUsingsOption); - rootCommand.AddGlobalOption(_addUsingsOption); - - await rootCommand.InvokeAsync(args); + rootCommand.Subcommands.Add(CreateFileCommand()); + rootCommand.Subcommands.Add(CreateDirectoryCommand()); + + rootCommand.Options.Add(_includeSubdirectoriesOption); + rootCommand.Options.Add(_includeUsingsOption); + rootCommand.Options.Add(_includeNamespaceOption); + rootCommand.Options.Add(_includeCommentsOption); + rootCommand.Options.Add(_useDebugAssertOption); + rootCommand.Options.Add(_startInterfaceNamesWithIOption); + rootCommand.Options.Add(_commentUnrecognizedCodeOption); + rootCommand.Options.Add(_systemOutToConsoleOption); + rootCommand.Options.Add(_fileScopedNamespacesOption); + rootCommand.Options.Add(_clearDefaultUsingsOption); + rootCommand.Options.Add(_addUsingsOption); + rootCommand.Options.Add(_mappingsFileNameOption); + + await rootCommand.Parse(args).InvokeAsync(); // flush logs _loggerFactory.Dispose(); @@ -95,23 +121,27 @@ public static async Task Main(string[] args) private static Command CreateFileCommand() { - var inputArgument = new Argument( - name: "input", - description: "A Java source code file to convert"); + var inputArgument = new Argument("input") + { + Description = "A Java source code file to convert", + }; - var outputArgument = new Argument( - name: "output", - description: "Path to place the C# output file, or stdout if omitted", - getDefaultValue: () => null); + var outputArgument = new Argument("output") + { + Description = "Path to place the C# output file, or stdout if omitted", + DefaultValueFactory = _ => null, + }; - var fileCommand = new Command("file", "Convert a Java file to C#"); - fileCommand.AddArgument(inputArgument); - fileCommand.AddArgument(outputArgument); + var fileCommand = new Command("file", "Convert a Java file to C#") + { + inputArgument, + outputArgument, + }; - fileCommand.SetHandler(context => + fileCommand.SetAction(context => { - var input = context.ParseResult.GetValueForArgument(inputArgument); - var output = context.ParseResult.GetValueForArgument(outputArgument); + var input = context.GetRequiredValue(inputArgument); + var output = context.GetValue(outputArgument); var options = GetJavaConversionOptions(context); @@ -121,50 +151,77 @@ private static Command CreateFileCommand() return fileCommand; } - private static JavaConversionOptions GetJavaConversionOptions(InvocationContext context) + private static JavaConversionOptions GetJavaConversionOptions(ParseResult context) { var options = new JavaConversionOptions { - IncludeUsings = context.ParseResult.GetValueForOption(_includeUsingsOption), - IncludeComments = context.ParseResult.GetValueForOption(_includeCommentsOption), - IncludeNamespace = context.ParseResult.GetValueForOption(_includeNamespaceOption), - ConvertSystemOutToConsole = context.ParseResult.GetValueForOption(_systemOutToConsoleOption), - StartInterfaceNamesWithI = context.ParseResult.GetValueForOption(_startInterfaceNamesWithIOption), - UseDebugAssertForAsserts = context.ParseResult.GetValueForOption(_useDebugAssertOption), - UseUnrecognizedCodeToComment = context.ParseResult.GetValueForOption(_commentUnrecognizedCodeOption) + IncludeSubdirectories = context.GetValue(_includeSubdirectoriesOption), + IncludeUsings = context.GetValue(_includeUsingsOption), + IncludeComments = context.GetValue(_includeCommentsOption), + IncludeNamespace = context.GetValue(_includeNamespaceOption), + ConvertSystemOutToConsole = context.GetValue(_systemOutToConsoleOption), + StartInterfaceNamesWithI = context.GetValue(_startInterfaceNamesWithIOption), + UseDebugAssertForAsserts = context.GetValue(_useDebugAssertOption), + UseUnrecognizedCodeToComment = context.GetValue(_commentUnrecognizedCodeOption), + UseFileScopedNamespaces = context.GetValue(_fileScopedNamespacesOption), }; - if (context.ParseResult.GetValueForOption(_clearDefaultUsingsOption)) + if (context.GetValue(_clearDefaultUsingsOption)) { options.ClearUsings(); } + else + { + options.AddUsing("System"); + options.AddUsing("System.Collections.Generic"); + options.AddUsing("System.Collections.ObjectModel"); + options.AddUsing("System.Linq"); + options.AddUsing("System.Text"); + } - foreach (string ns in context.ParseResult.GetValueForOption(_addUsingsOption) ?? new List()) + foreach (string ns in context.GetValue(_addUsingsOption) ?? []) { options.AddUsing(ns); } - + + var mappingsFile = context.GetValue(_mappingsFileNameOption); + if (!string.IsNullOrEmpty(mappingsFile)) + { + options.SyntaxMappings = ReadMappingsFile(mappingsFile); + } + return options; } + private static SyntaxMapping ReadMappingsFile(string mappingsFile) + { + // Let fail if cannot be read or deserialized to display the exception message in the CLI + var mappingsStr = File.ReadAllText(mappingsFile); + return SyntaxMapping.Deserialize(mappingsStr); + } + private static Command CreateDirectoryCommand() { - var inputArgument = new Argument( - name: "input", - description: "A directory containing Java source code files to convert"); + var inputArgument = new Argument("input") + { + Description = "A directory containing Java source code files to convert", + }; - var outputArgument = new Argument( - name: "output", - description: "Path to place the C# output files"); + var outputArgument = new Argument("output") + { + Description = "Path to place the C# output files", + }; - var dirCommand = new Command("dir", "Convert a directory containing Java files to C#"); - dirCommand.AddArgument(inputArgument); - dirCommand.AddArgument(outputArgument); + var dirCommand = new Command("dir", "Convert a directory containing Java files to C#") + { + inputArgument, + outputArgument, + }; - dirCommand.SetHandler(context => + dirCommand.SetAction(context => { - var input = context.ParseResult.GetValueForArgument(inputArgument); - var output = context.ParseResult.GetValueForArgument(outputArgument); + var input = context.GetRequiredValue(inputArgument); + var output = context.GetRequiredValue(outputArgument); var options = GetJavaConversionOptions(context); @@ -178,7 +235,8 @@ private static void ConvertToCSharpDir(DirectoryInfo inputDirectory, DirectoryIn { if (inputDirectory.Exists) { - foreach (var f in inputDirectory.GetFiles("*.java", SearchOption.AllDirectories)) + var searchOption = options.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + foreach (var f in inputDirectory.GetFiles("*.java", searchOption)) { string? directoryName = f.DirectoryName; if (string.IsNullOrWhiteSpace(directoryName)) @@ -198,13 +256,17 @@ private static void ConvertToCSharpDir(DirectoryInfo inputDirectory, DirectoryIn } } else + { _logger.LogError("Java input folder {path} doesn't exist!", inputDirectory); + } } private static void ConvertToCSharpFile(FileSystemInfo inputFile, FileSystemInfo? outputFile, JavaConversionOptions options, bool overwrite = true) { if (!overwrite && outputFile is { Exists: true }) + { _logger.LogInformation("{outputFilePath} exists, skip to next.", outputFile); + } else if (inputFile.Exists) { try @@ -242,7 +304,9 @@ private static void ConvertToCSharpFile(FileSystemInfo inputFile, FileSystemInfo } } else + { _logger.LogError("Java input file {filePath} doesn't exist!", inputFile.FullName); + } } private static void OutputFileOrPrint(string? fileName, string contents) @@ -256,4 +320,4 @@ private static void OutputFileOrPrint(string? fileName, string contents) File.WriteAllText(fileName, contents); } } -} \ No newline at end of file +} diff --git a/JavaToCSharpGui/App.axaml b/JavaToCSharpGui/App.axaml index 00e2485b..29a53652 100644 --- a/JavaToCSharpGui/App.axaml +++ b/JavaToCSharpGui/App.axaml @@ -1,11 +1,16 @@  - + + 14 + Cascadia Code,SF Mono,DejaVu Sans Mono,Menlo,Consolas + - + + + - \ No newline at end of file + diff --git a/JavaToCSharpGui/App.axaml.cs b/JavaToCSharpGui/App.axaml.cs index fc990f8e..5ab33082 100644 --- a/JavaToCSharpGui/App.axaml.cs +++ b/JavaToCSharpGui/App.axaml.cs @@ -15,13 +15,13 @@ public override void Initialize() public override void OnFrameworkInitializationCompleted() { - if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { // Line below is needed to remove Avalonia data validation. // Without this line you will get duplicate validations from both Avalonia and CT BindingPlugins.DataValidators.RemoveAt(0); - desktop.MainWindow = new MainWindow(); + desktop.MainWindow = new MainWindow(); base.OnFrameworkInitializationCompleted(); } } -} \ No newline at end of file +} diff --git a/JavaToCSharpGui/App.config b/JavaToCSharpGui/App.config index 97587b2d..070571fe 100644 --- a/JavaToCSharpGui/App.config +++ b/JavaToCSharpGui/App.config @@ -25,6 +25,12 @@ True + + System;System.Collections.Generic;System.Collections.ObjectModel;System.Linq;System.Text + + + False + \ No newline at end of file diff --git a/JavaToCSharpGui/CurrentOptions.cs b/JavaToCSharpGui/CurrentOptions.cs new file mode 100644 index 00000000..4c421995 --- /dev/null +++ b/JavaToCSharpGui/CurrentOptions.cs @@ -0,0 +1,36 @@ +using JavaToCSharp; +using JavaToCSharpGui.Properties; + +namespace JavaToCSharpGui; + +public static class CurrentOptions +{ + static CurrentOptions() + { + Options.IncludeUsings = Settings.Default.UseUsingsPreference; + Options.IncludeNamespace = Settings.Default.UseNamespacePreference; + Options.IncludeComments = Settings.Default.IncludeComments; + Options.UseDebugAssertForAsserts = Settings.Default.UseDebugAssertPreference; + Options.UseUnrecognizedCodeToComment = Settings.Default.UseUnrecognizedCodeToComment; + Options.ConvertSystemOutToConsole = Settings.Default.ConvertSystemOutToConsole; + Options.UseFileScopedNamespaces = Settings.Default.UseFileScopedNamespaces; + + Options.SetUsings(Settings.Default.Usings.Split(';')); + } + + public static JavaConversionOptions Options { get; } = new JavaConversionOptions(); + + public static void Persist() + { + Settings.Default.UseUsingsPreference = Options.IncludeUsings; + Settings.Default.UseNamespacePreference = Options.IncludeNamespace; + Settings.Default.IncludeComments = Options.IncludeComments; + Settings.Default.UseDebugAssertPreference = Options.UseDebugAssertForAsserts; + Settings.Default.UseUnrecognizedCodeToComment = Options.UseUnrecognizedCodeToComment; + Settings.Default.ConvertSystemOutToConsole = Options.ConvertSystemOutToConsole; + Settings.Default.UseFileScopedNamespaces = Options.UseFileScopedNamespaces; + Settings.Default.Usings = string.Join(";", Options.Usings); + + Settings.Default.Save(); + } +} diff --git a/JavaToCSharpGui/Infrastructure/HostStorageProvider.cs b/JavaToCSharpGui/Infrastructure/HostStorageProvider.cs index f70a1fba..3c8f31c4 100644 --- a/JavaToCSharpGui/Infrastructure/HostStorageProvider.cs +++ b/JavaToCSharpGui/Infrastructure/HostStorageProvider.cs @@ -3,33 +3,44 @@ namespace JavaToCSharpGui.Infrastructure; /// -public class HostStorageProvider : IHostStorageProvider +public class HostStorageProvider(IStorageProvider storageProvider) : IHostStorageProvider { - private readonly IStorageProvider _storageProvider; - - public HostStorageProvider(IStorageProvider storageProvider) => _storageProvider = storageProvider; + /// + public bool CanPickFolder => storageProvider.CanPickFolder; /// - public bool CanPickFolder => _storageProvider.CanPickFolder; + public bool CanOpen => storageProvider.CanOpen; /// - public bool CanOpen => _storageProvider.CanOpen; + public bool CanSave => storageProvider.CanSave; /// public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - return await _storageProvider.OpenFilePickerAsync(options); + return await storageProvider.OpenFilePickerAsync(options); } /// public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - return await _storageProvider.OpenFolderPickerAsync(options); + return await storageProvider.OpenFolderPickerAsync(options); } - + /// public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { - return await _storageProvider.TryGetWellKnownFolderAsync(wellKnownFolder); + return await storageProvider.TryGetWellKnownFolderAsync(wellKnownFolder); + } + + /// + public async Task OpenSaveFileDialogAsync(FilePickerSaveOptions options) + { + return await storageProvider.SaveFilePickerAsync(options); + } + + /// + public async Task TryGetFolderFromPathAsync(string path) + { + return await storageProvider.TryGetFolderFromPathAsync(path); } } diff --git a/JavaToCSharpGui/Infrastructure/IHostStorageProvider.cs b/JavaToCSharpGui/Infrastructure/IHostStorageProvider.cs index 6bb0d994..2773e2d8 100644 --- a/JavaToCSharpGui/Infrastructure/IHostStorageProvider.cs +++ b/JavaToCSharpGui/Infrastructure/IHostStorageProvider.cs @@ -17,6 +17,11 @@ public interface IHostStorageProvider /// bool CanOpen { get; } + /// + /// Can the save file picker be opened on the current platform. + /// + bool CanSave { get; } + /// /// Gets the path to a well known folder. /// @@ -37,4 +42,18 @@ public interface IHostStorageProvider /// The file picker configuration. /// A list of selected files. Task> OpenFilePickerAsync(FilePickerOpenOptions options); + + /// + /// Opens the save file picker dialog. + /// + /// The file picker configuration. + /// The selected file. + Task OpenSaveFileDialogAsync(FilePickerSaveOptions options); + + /// + /// Tries to get a folder from a path. + /// + /// The path to the folder. + /// The folder, or null if not found. + Task TryGetFolderFromPathAsync(string path); } diff --git a/JavaToCSharpGui/Infrastructure/ITextClipboard.cs b/JavaToCSharpGui/Infrastructure/ITextClipboard.cs index 7057f18e..9a5cb9d6 100644 --- a/JavaToCSharpGui/Infrastructure/ITextClipboard.cs +++ b/JavaToCSharpGui/Infrastructure/ITextClipboard.cs @@ -5,6 +5,12 @@ /// public interface ITextClipboard { + /// + /// Gets the clipboard's text. + /// + /// A Task representing the async operation. The result is the clipboard text, or null if unavailable. + Task GetTextAsync(); + /// /// Sets the clipboard's text. /// diff --git a/JavaToCSharpGui/Infrastructure/IUIDispatcher.cs b/JavaToCSharpGui/Infrastructure/IUIDispatcher.cs index 44e8d0bf..33b31d20 100644 --- a/JavaToCSharpGui/Infrastructure/IUIDispatcher.cs +++ b/JavaToCSharpGui/Infrastructure/IUIDispatcher.cs @@ -3,7 +3,7 @@ namespace JavaToCSharpGui.Infrastructure; /// -/// Provides acces to the UI thread. +/// Provides access to the UI thread. /// public interface IUIDispatcher { diff --git a/JavaToCSharpGui/Infrastructure/TextClipboard.cs b/JavaToCSharpGui/Infrastructure/TextClipboard.cs index 043357ed..3f5302d2 100644 --- a/JavaToCSharpGui/Infrastructure/TextClipboard.cs +++ b/JavaToCSharpGui/Infrastructure/TextClipboard.cs @@ -3,19 +3,27 @@ namespace JavaToCSharpGui.Infrastructure; /// -internal class TextClipboard : ITextClipboard +internal class TextClipboard(IClipboard? clipboard) : ITextClipboard { - private readonly IClipboard? _clipboard; + /// + public async Task GetTextAsync() + { + if (clipboard is null) + { + return null; + } - public TextClipboard(IClipboard? clipboard) => _clipboard = clipboard; + return await clipboard.TryGetTextAsync(); + } /// public async Task SetTextAsync(string? text) { - if(_clipboard is null) + if (clipboard is null) { return; } - await _clipboard.SetTextAsync(text); + + await clipboard.SetTextAsync(text); } } diff --git a/JavaToCSharpGui/Infrastructure/UIDispatcher.cs b/JavaToCSharpGui/Infrastructure/UIDispatcher.cs index efc1bbd4..08da38e1 100644 --- a/JavaToCSharpGui/Infrastructure/UIDispatcher.cs +++ b/JavaToCSharpGui/Infrastructure/UIDispatcher.cs @@ -5,19 +5,12 @@ using Avalonia.Threading; /// -public class UIDispatcher : IUIDispatcher +public class UIDispatcher(IDispatcher dispatcher) : IUIDispatcher { - private readonly IDispatcher _dispatcher; - - public UIDispatcher(IDispatcher dispatcher) - { - _dispatcher = dispatcher; - } - /// public async Task InvokeAsync(Action callback, DispatcherPriority priority) { - if (_dispatcher is Dispatcher avaloniaDispatcher) + if (dispatcher is Dispatcher avaloniaDispatcher) { await avaloniaDispatcher.InvokeAsync(callback, priority); } diff --git a/JavaToCSharpGui/JavaToCSharpGui.csproj b/JavaToCSharpGui/JavaToCSharpGui.csproj index 3148ed76..334d7f4c 100644 --- a/JavaToCSharpGui/JavaToCSharpGui.csproj +++ b/JavaToCSharpGui/JavaToCSharpGui.csproj @@ -1,34 +1,36 @@  - 3.0.0 + 4.0.0 true app.manifest WinExe - net6.0 + net10.0 latest enable enable - Nullable + true true + 11.3.8 - - - - - - - - - + + + + - - + + + + + + + + - \ No newline at end of file + diff --git a/JavaToCSharpGui/Program.cs b/JavaToCSharpGui/Program.cs index b4e7b55b..c3856476 100644 --- a/JavaToCSharpGui/Program.cs +++ b/JavaToCSharpGui/Program.cs @@ -1,4 +1,6 @@ using Avalonia; +using Projektanker.Icons.Avalonia; +using Projektanker.Icons.Avalonia.FontAwesome; namespace JavaToCSharpGui; @@ -13,8 +15,13 @@ public static void Main(string[] args) => BuildAvaloniaApp() // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() + { + IconProvider.Current + .Register(); + + return AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .LogToTrace(); + } } diff --git a/JavaToCSharpGui/Properties/Settings.Designer.cs b/JavaToCSharpGui/Properties/Settings.Designer.cs index 6d9d0458..85bf611e 100644 --- a/JavaToCSharpGui/Properties/Settings.Designer.cs +++ b/JavaToCSharpGui/Properties/Settings.Designer.cs @@ -93,5 +93,30 @@ public bool IncludeComments { this["IncludeComments"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("System;System.Collections.Generic;System.Collections.ObjectModel;System.Linq;Syst" + + "em.Text")] + public string Usings { + get { + return ((string)(this["Usings"])); + } + set { + this["Usings"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UseFileScopedNamespaces { + get { + return ((bool)(this["UseFileScopedNamespaces"])); + } + set { + this["UseFileScopedNamespaces"] = value; + } + } } } diff --git a/JavaToCSharpGui/Properties/Settings.settings b/JavaToCSharpGui/Properties/Settings.settings index 5535f883..1f26626c 100644 --- a/JavaToCSharpGui/Properties/Settings.settings +++ b/JavaToCSharpGui/Properties/Settings.settings @@ -20,5 +20,11 @@ True + + System;System.Collections.Generic;System.Collections.ObjectModel;System.Linq;System.Text + + + False + - \ No newline at end of file + diff --git a/JavaToCSharpGui/ViewModels/MainWindowViewModel.cs b/JavaToCSharpGui/ViewModels/MainWindowViewModel.cs index 896e9c54..ac058707 100644 --- a/JavaToCSharpGui/ViewModels/MainWindowViewModel.cs +++ b/JavaToCSharpGui/ViewModels/MainWindowViewModel.cs @@ -1,40 +1,38 @@ using System.Diagnostics; using Avalonia; -using CommunityToolkit.Mvvm.Input; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; -using CommunityToolkit.Mvvm.ComponentModel; using Avalonia.Platform.Storage; using Avalonia.Threading; +using AvaloniaEdit.Document; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using JavaToCSharp; using JavaToCSharpGui.Infrastructure; +using JavaToCSharpGui.Views; namespace JavaToCSharpGui.ViewModels; public partial class MainWindowViewModel : ViewModelBase { - private const string CopyToClipboardDefaultText = "Copy to Clipboard"; - - private bool _includeUsings = true; - private bool _includeNamespace = true; - private bool _includeComments = true; - private bool _useDebugAssertForAsserts; - private bool _useUnrecognizedCodeToComment; - private bool _convertSystemOutToConsole; - private readonly IHostStorageProvider? _storageProvider; private readonly IUIDispatcher _dispatcher; private readonly ITextClipboard? _clipboard; + private bool _usingFolderConvert; + /// /// Constructor for the Avalonia Designer view inside the IDE. /// public MainWindowViewModel() { _dispatcher = new UIDispatcher(Dispatcher.UIThread); - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: not null } desktop) + + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime + { + MainWindow: not null + } desktop) { _storageProvider = new HostStorageProvider(desktop.MainWindow.StorageProvider); _clipboard = new TextClipboard(desktop.MainWindow.Clipboard); @@ -53,138 +51,29 @@ public MainWindowViewModel(IHostStorageProvider storageProvider, IUIDispatcher d _dispatcher = dispatcher; _clipboard = clipboard; DisplayName = "Java to C# Converter"; - - _isConvertEnabled = true; - _useFolderConvert = false; - - _includeUsings = Properties.Settings.Default.UseUsingsPreference; - _includeNamespace = Properties.Settings.Default.UseNamespacePreference; - _includeComments = Properties.Settings.Default.IncludeComments; - _useDebugAssertForAsserts = Properties.Settings.Default.UseDebugAssertPreference; - _useUnrecognizedCodeToComment = Properties.Settings.Default.UseUnrecognizedCodeToComment; - _convertSystemOutToConsole = Properties.Settings.Default.ConvertSystemOutToConsole; } - [ObservableProperty] private string _addUsingInput = ""; + [ObservableProperty] private AvaloniaList _folderInputFiles = []; - private IList _javaFiles = new List(); - private string _currentJavaFile = ""; + [ObservableProperty] private AvaloniaList _folderOutputFiles = []; - [ObservableProperty] private AvaloniaList _usings = new(); + private string _currentJavaFile = ""; - [ObservableProperty] private string _javaText = ""; + [ObservableProperty] private TextDocument _javaText = new(); - [ObservableProperty] private string _cSharpText = ""; + [ObservableProperty] private TextDocument _cSharpText = new(); [ObservableProperty] private string _openPath = ""; - [ObservableProperty] private string _copyToClipboardText = CopyToClipboardDefaultText; + [ObservableProperty] private string _openFolderPath = ""; [ObservableProperty] private string _conversionStateLabel = ""; - [ObservableProperty] private string? _selectedUsing; - - [RelayCommand] - private void RemoveSelectedUsing() - { - if (SelectedUsing is not null && Usings.Contains(SelectedUsing)) - { - Usings.Remove(SelectedUsing); - } - } - - public bool IncludeUsings - { - get => _includeUsings; - set - { - SetProperty(ref _includeUsings, value); - Properties.Settings.Default.UseUsingsPreference = value; - Properties.Settings.Default.Save(); - } - } - - public bool IncludeNamespace - { - get => _includeNamespace; - set - { - SetProperty(ref _includeNamespace, value); - Properties.Settings.Default.UseNamespacePreference = value; - Properties.Settings.Default.Save(); - } - } - - public bool IncludeComments - { - get => _includeComments; - set - { - SetProperty(ref _includeComments, value); - Properties.Settings.Default.IncludeComments = value; - Properties.Settings.Default.Save(); - } - } - - public bool UseDebugAssertForAsserts - { - get => _useDebugAssertForAsserts; - set - { - SetProperty(ref _useDebugAssertForAsserts, value); - Properties.Settings.Default.UseDebugAssertPreference = value; - Properties.Settings.Default.Save(); - - if (value && !Usings.Contains("System.Diagnostics")) - { - AddUsingInput = "System.Diagnostics"; - AddUsing(); - } - } - } - - public bool UseUnrecognizedCodeToComment - { - get => _useUnrecognizedCodeToComment; - set - { - _useUnrecognizedCodeToComment = value; - SetProperty(ref _useUnrecognizedCodeToComment, value); - Properties.Settings.Default.UseUnrecognizedCodeToComment = value; - Properties.Settings.Default.Save(); - } - } - - public bool ConvertSystemOutToConsole - { - get => _convertSystemOutToConsole; - set - { - _convertSystemOutToConsole = value; - SetProperty(ref _convertSystemOutToConsole, value); - Properties.Settings.Default.ConvertSystemOutToConsole = value; - Properties.Settings.Default.Save(); - } - } - - public FontFamily MonospaceFontFamily { get; } = FontFamily.Parse("Cascadia Code,SF Mono,DejaVu Sans Mono,Menlo,Consolas"); + public FontFamily MonospaceFontFamily { get; } = + FontFamily.Parse("Cascadia Code,SF Mono,DejaVu Sans Mono,Menlo,Consolas"); [ObservableProperty] private bool _isConvertEnabled = true; - [ObservableProperty] private bool _useFolderConvert; - - [RelayCommand] - private void AddUsing() - { - Usings.Add(AddUsingInput); - AddUsingInput = string.Empty; - } - - public void RemoveUsing(string value) - { - Usings.Remove(value); - } - [ObservableProperty] private string _message = ""; [ObservableProperty] private string _messageTitle = ""; @@ -192,56 +81,39 @@ public void RemoveUsing(string value) [RelayCommand] private async Task Convert() { - var options = new JavaConversionOptions(); - options.ClearUsings(); - - foreach (string ns in Usings) - { - options.AddUsing(ns); - } + CurrentOptions.Options.WarningEncountered += Options_WarningEncountered; + CurrentOptions.Options.StateChanged += Options_StateChanged; - options.IncludeUsings = IncludeUsings; - options.IncludeNamespace = IncludeNamespace; - options.IncludeComments = IncludeComments; - options.UseDebugAssertForAsserts = UseDebugAssertForAsserts; - options.UseUnrecognizedCodeToComment = UseUnrecognizedCodeToComment; - options.ConvertSystemOutToConsole = ConvertSystemOutToConsole; + IsConvertEnabled = false; + _usingFolderConvert = false; - options.WarningEncountered += Options_WarningEncountered; - options.StateChanged += Options_StateChanged; + var text = JavaText.Text; - IsConvertEnabled = false; await Task.Run(async () => { try { - if (UseFolderConvert) - { - await FolderConvert(options); - } - else - { - string? csharp = JavaToCSharpConverter.ConvertText(JavaText, options); - await DispatcherInvoke(() => CSharpText = csharp ?? ""); - } + string? csharp = JavaToCSharpConverter.ConvertText(text, CurrentOptions.Options); + await DispatcherInvoke(() => CSharpText.Text = csharp ?? ""); } catch (Exception ex) { await DispatcherInvoke(() => - ShowMessage($"There was an error converting the text to C#: {ex.GetBaseException().Message}", "Conversion Error")); + ShowMessage($"There was an error converting the text to C#: {ex.GetBaseException().Message}", + "Conversion Error")); ConversionStateLabel = ""; } finally { await DispatcherInvoke(() => IsConvertEnabled = true); + CurrentOptions.Options.WarningEncountered -= Options_WarningEncountered; + CurrentOptions.Options.StateChanged -= Options_StateChanged; } }); } - /// - /// Folder Browser OpenFolderDialog - /// + [RelayCommand] private async Task OpenFolderDialog() { if (_storageProvider?.CanPickFolder is true) @@ -252,9 +124,9 @@ private async Task OpenFolderDialog() AllowMultiple = false, SuggestedStartLocation = await _storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents) }; - + var result = await _storageProvider.OpenFolderPickerAsync(options); - + if (!result.Any()) { return; @@ -262,92 +134,111 @@ private async Task OpenFolderDialog() string path = result[0].Path.LocalPath; var dir = new DirectoryInfo(path); - + if (!dir.Exists) { - OpenPath = string.Empty; - JavaText = string.Empty; - _javaFiles = Array.Empty(); + OpenFolderPath = string.Empty; + FolderInputFiles.Clear(); return; } - OpenPath = path; + OpenFolderPath = path; - await Task.Run(async () => + await Task.Run(() => { var files = dir.GetFiles("*.java", SearchOption.AllDirectories); - _javaFiles = files; - - int subStartIndex = path.Length; - string javaText = string.Join(Environment.NewLine, files.Select(x => x.FullName[subStartIndex..])); - - await DispatcherInvoke(() => JavaText = javaText); + FolderInputFiles.Clear(); + FolderInputFiles.AddRange(files); + return Task.CompletedTask; }); } } - /// - /// Folder Code Convert - /// - /// The user options used for the conversion. - private async Task FolderConvert(JavaConversionOptions? options) + [RelayCommand] + private async Task FolderConvert() { - if (_javaFiles.Count == 0 || options == null) + if (FolderInputFiles.Count == 0) { ShowMessage("No Java files found in the specified folder!"); return; } - var dir = new DirectoryInfo(OpenPath); - var pDir = dir.Parent ?? throw new FileNotFoundException($"dir {OpenPath} parent"); - string dirName = dir.Name; - string outDirName = $"{dirName}_net_{DateTime.Now.Millisecond}"; - var outDir = pDir.CreateSubdirectory(outDirName); - - if (outDir is not { Exists: true }) - throw new FileNotFoundException($"outDir {outDirName}"); - - string outDirFullName = outDir.FullName; - int subStartIndex = dir.FullName.Length; - - foreach (var jFile in _javaFiles.Where(static x => x.Directory is not null)) - { - string jPath = jFile.Directory!.FullName; - string jOutPath = $"{outDirFullName}{jPath[subStartIndex..]}"; - string jOutFileName = Path.GetFileNameWithoutExtension(jFile.Name) + ".cs"; - string jOutFileFullName = Path.Combine(jOutPath, jOutFileName); - - _currentJavaFile = jFile.FullName; - - if (!Directory.Exists(jOutPath)) - Directory.CreateDirectory(jOutPath); + CurrentOptions.Options.WarningEncountered += Options_WarningEncountered; + CurrentOptions.Options.StateChanged += Options_StateChanged; - string jText = await File.ReadAllTextAsync(_currentJavaFile); - - if (string.IsNullOrEmpty(jText)) - continue; + IsConvertEnabled = false; + _usingFolderConvert = true; + FolderOutputFiles.Clear(); + await Task.Run(async () => + { try { - string? csText = JavaToCSharpConverter.ConvertText(jText, options); - await File.WriteAllTextAsync(jOutFileFullName, csText); + var dir = new DirectoryInfo(OpenFolderPath); + string dirName = dir.Name; + string outDirName = $"{dirName}_csharp_output"; + var outDir = new DirectoryInfo(Path.Combine(OpenFolderPath, outDirName)); - await DispatcherInvoke(() => + if (!outDir.Exists) + outDir.Create(); + + string outDirFullName = outDir.FullName; + int subStartIndex = dir.FullName.Length; + + foreach (var jFile in FolderInputFiles.Where(static x => x.Directory is not null)) { - CSharpText = - $"{CSharpText} {Environment.NewLine}=================={Environment.NewLine}out.path: {jOutPath},{Environment.NewLine}\t\tfile: {jOutFileName}"; - }); + // ! null checked above + string jPath = jFile.Directory!.FullName; + string jOutPath = $"{outDirFullName}{jPath[subStartIndex..]}"; + string jOutFileName = Path.GetFileNameWithoutExtension(jFile.Name) + ".cs"; + string jOutFileFullName = Path.Combine(jOutPath, jOutFileName); + + _currentJavaFile = jFile.FullName; + + if (!Directory.Exists(jOutPath)) + Directory.CreateDirectory(jOutPath); + + string jText = await File.ReadAllTextAsync(_currentJavaFile); + + if (string.IsNullOrEmpty(jText)) + continue; + + try + { + string? csText = JavaToCSharpConverter.ConvertText(jText, CurrentOptions.Options); + await File.WriteAllTextAsync(jOutFileFullName, csText); + + await DispatcherInvoke(() => + { + FolderOutputFiles.Add(jOutFileFullName); + }); + } + catch (Exception ex) + { + await DispatcherInvoke(() => + { + ShowMessage($"There was an error converting {jFile.FullName} to C#: {ex.GetBaseException().Message}", + "Conversion Error"); + }); + } + } } catch (Exception ex) { await DispatcherInvoke(() => - { - CSharpText = - $"{CSharpText} {Environment.NewLine}=================={Environment.NewLine}[ERROR]out.path: {jOutPath},{Environment.NewLine}ex: {ex} {Environment.NewLine}"; - }); + ShowMessage($"There was an error converting the text to C#: {ex.GetBaseException().Message}", + "Conversion Error")); + + ConversionStateLabel = ""; } - } + finally + { + await DispatcherInvoke(() => IsConvertEnabled = true); + CurrentOptions.Options.WarningEncountered -= Options_WarningEncountered; + CurrentOptions.Options.StateChanged -= Options_StateChanged; + } + }); } [RelayCommand] @@ -381,12 +272,14 @@ private void Options_StateChanged(object? sender, ConversionStateChangedEventArg private async void Options_WarningEncountered(object? sender, ConversionWarningEventArgs e) { - if (UseFolderConvert) + if (_usingFolderConvert) { await DispatcherInvoke(() => { - CSharpText = - $"{CSharpText} {Environment.NewLine}=================={Environment.NewLine}[WARN]out.path: {_currentJavaFile},{Environment.NewLine}\t\tConversionWarning-JavaLine:[{e.JavaLineNumber}]-Message:[{e.Message}]{Environment.NewLine}"; + CSharpText.Text = $"{CSharpText.Text}{Environment.NewLine}" + + $"=================={Environment.NewLine}" + + $"[WARN] {_currentJavaFile}{e.JavaLineNumber}{Environment.NewLine}" + + $"\t\tMessage: {e.Message}{Environment.NewLine}"; }); } else @@ -398,11 +291,7 @@ await DispatcherInvoke(() => [RelayCommand] private async Task OpenFileDialog() { - if (UseFolderConvert) - { - await OpenFolderDialog(); - } - else if (_storageProvider?.CanOpen is true) + if (_storageProvider?.CanOpen is true) { var filePickerOpenOptions = new FilePickerOpenOptions { @@ -416,15 +305,95 @@ private async Task OpenFileDialog() }; var result = await _storageProvider.OpenFilePickerAsync(filePickerOpenOptions); - + if (result.Any()) { OpenPath = result[0].Path.LocalPath; - JavaText = await File.ReadAllTextAsync(result[0].Path.LocalPath); + JavaText.Text = await File.ReadAllTextAsync(result[0].Path.LocalPath); } } } + [RelayCommand] + private async Task PasteJavaCode() + { + if (_clipboard is null) + { + return; + } + + string? clipboardText = await _clipboard.GetTextAsync(); + if (clipboardText is null) + { + ShowMessage("Clipboard is empty or unavailable.", "Paste Error"); + return; + } + + JavaText.Text = clipboardText; + ConversionStateLabel = "Pasted Java code from clipboard!"; + + await Task.Delay(2000); + + await _dispatcher.InvokeAsync(() => { ConversionStateLabel = ""; }, DispatcherPriority.Background); + } + + [RelayCommand] + private async Task ClipboardConvert() + { + if (_clipboard is null) + { + return; + } + + // Get clipboard text + string? clipboardText = await _clipboard.GetTextAsync(); + if (clipboardText is null) + { + ShowMessage("Clipboard is empty or unavailable.", "Clipboard Convert Error"); + return; + } + + // Set Java text + JavaText.Text = clipboardText; + + // Perform conversion + CurrentOptions.Options.WarningEncountered += Options_WarningEncountered; + CurrentOptions.Options.StateChanged += Options_StateChanged; + + IsConvertEnabled = false; + _usingFolderConvert = false; + + await Task.Run(async () => + { + try + { + string? csharp = JavaToCSharpConverter.ConvertText(clipboardText, CurrentOptions.Options); + await DispatcherInvoke(() => CSharpText.Text = csharp ?? ""); + + // Copy result to clipboard + await _clipboard.SetTextAsync(csharp ?? ""); + ConversionStateLabel = "Conversion complete! C# code copied to clipboard."; + + await Task.Delay(2000); + await _dispatcher.InvokeAsync(() => { ConversionStateLabel = ""; }, DispatcherPriority.Background); + } + catch (Exception ex) + { + await DispatcherInvoke(() => + ShowMessage($"There was an error converting the text to C#: {ex.GetBaseException().Message}", + "Conversion Error")); + + ConversionStateLabel = ""; + } + finally + { + await DispatcherInvoke(() => IsConvertEnabled = true); + CurrentOptions.Options.WarningEncountered -= Options_WarningEncountered; + CurrentOptions.Options.StateChanged -= Options_StateChanged; + } + }); + } + [RelayCommand] private async Task CopyOutput() { @@ -433,24 +402,93 @@ private async Task CopyOutput() return; } - await _clipboard.SetTextAsync(CSharpText); - CopyToClipboardText = "Copied!"; - - await Task.Delay(1000); - - await _dispatcher.InvokeAsync(() => + await _clipboard.SetTextAsync(CSharpText.Text); + ConversionStateLabel = "Copied C# code to clipboard!"; + + await Task.Delay(2000); + + await _dispatcher.InvokeAsync(() => { ConversionStateLabel = ""; }, DispatcherPriority.Background); + } + + [RelayCommand] + private async Task SaveOutput() + { + if (_storageProvider?.CanSave is true) { - CopyToClipboardText = CopyToClipboardDefaultText; - }, DispatcherPriority.Background); + IStorageFolder? startLocation = null; + + if (Path.GetDirectoryName(OpenPath) is { } dir) + { + startLocation = await _storageProvider.TryGetFolderFromPathAsync(dir); + } + + startLocation ??= await _storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents); + + var filePickerSaveOptions = new FilePickerSaveOptions + { + SuggestedFileName = Path.GetFileNameWithoutExtension(OpenPath) + ".cs", + SuggestedStartLocation = startLocation, + Title = "Save C# File" + }; + + var result = await _storageProvider.OpenSaveFileDialogAsync(filePickerSaveOptions); + + if (result is not null) + { + await File.WriteAllTextAsync(result.Path.LocalPath, CSharpText.Text); + + ConversionStateLabel = "Saved C# code to file!"; + + await Task.Delay(2000); + + await _dispatcher.InvokeAsync(() => { ConversionStateLabel = ""; }, DispatcherPriority.Background); + } + } } [RelayCommand] private static void ForkMeOnGitHub() => Process.Start(new ProcessStartInfo { - FileName = "https://www.github.com/paulirwin/javatocsharp", + FileName = "https://github.com/paulirwin/javatocsharp", UseShellExecute = true }); + [RelayCommand] + private static void OpenSettings() + { + var parent = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop + ? desktop.MainWindow + : null; + var settings = new SettingsWindow(); + + if (parent is not null) + { + settings.ShowDialog(parent); + } + else + { + settings.Show(); + } + } + + [RelayCommand] + private static void OpenAbout() + { + var parent = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop + ? desktop.MainWindow + : null; + var about = new AboutWindow(); + + if (parent is not null) + { + about.ShowDialog(parent); + } + else + { + about.Show(); + } + } + private async Task DispatcherInvoke(Action callback) => await _dispatcher.InvokeAsync(callback, DispatcherPriority.Normal); -} \ No newline at end of file +} diff --git a/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs b/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs new file mode 100644 index 00000000..5dd364fe --- /dev/null +++ b/JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs @@ -0,0 +1,70 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace JavaToCSharpGui.ViewModels; + +public partial class SettingsWindowViewModel : ViewModelBase +{ + [ObservableProperty] private string _addUsingInput = ""; + + [ObservableProperty] private AvaloniaList _usings = new(CurrentOptions.Options.Usings); + + [ObservableProperty] private string? _selectedUsing; + + [ObservableProperty] private bool _includeUsings = CurrentOptions.Options.IncludeUsings; + + [ObservableProperty] private bool _includeNamespace = CurrentOptions.Options.IncludeNamespace; + + [ObservableProperty] private bool _includeComments = CurrentOptions.Options.IncludeComments; + + [ObservableProperty] private bool _useDebugAssertForAsserts = CurrentOptions.Options.UseDebugAssertForAsserts; + + [ObservableProperty] private bool _unrecognizedCodeToComment = CurrentOptions.Options.UseUnrecognizedCodeToComment; + + [ObservableProperty] private bool _convertSystemOutToConsole = CurrentOptions.Options.ConvertSystemOutToConsole; + + [ObservableProperty] private bool _useFileScopedNamespaces = CurrentOptions.Options.UseFileScopedNamespaces; + + public event EventHandler? CloseRequested; + + [RelayCommand] + private void RemoveSelectedUsing() + { + if (SelectedUsing is not null && Usings.Contains(SelectedUsing)) + { + Usings.Remove(SelectedUsing); + } + } + + [RelayCommand] + private void AddUsing() + { + Usings.Add(AddUsingInput); + AddUsingInput = string.Empty; + } + + [RelayCommand] + private void Save() + { + CurrentOptions.Options.IncludeUsings = IncludeUsings; + CurrentOptions.Options.IncludeNamespace = IncludeNamespace; + CurrentOptions.Options.IncludeComments = IncludeComments; + CurrentOptions.Options.UseDebugAssertForAsserts = UseDebugAssertForAsserts; + CurrentOptions.Options.UseUnrecognizedCodeToComment = UnrecognizedCodeToComment; + CurrentOptions.Options.ConvertSystemOutToConsole = ConvertSystemOutToConsole; + CurrentOptions.Options.UseFileScopedNamespaces = UseFileScopedNamespaces; + + CurrentOptions.Options.SetUsings(Usings); + + CurrentOptions.Persist(); + + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + [RelayCommand] + private void Cancel() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } +} diff --git a/JavaToCSharpGui/Views/AboutWindow.axaml b/JavaToCSharpGui/Views/AboutWindow.axaml new file mode 100644 index 00000000..ce0a4172 --- /dev/null +++ b/JavaToCSharpGui/Views/AboutWindow.axaml @@ -0,0 +1,30 @@ + + + Java to C# + + + NOTE: This tool does a + syntactic conversion only from Java + to C#. It does not resolve symbols or namespaces, so the resulting C# code likely will not compile without + modification. You must verify the results of the conversion manually. + + + https://github.com/paulirwin/javatocsharp + + + diff --git a/JavaToCSharpGui/Views/AboutWindow.axaml.cs b/JavaToCSharpGui/Views/AboutWindow.axaml.cs new file mode 100644 index 00000000..ec5a2e93 --- /dev/null +++ b/JavaToCSharpGui/Views/AboutWindow.axaml.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Input; + +namespace JavaToCSharpGui.Views; + +public partial class AboutWindow : Window +{ + public AboutWindow() + { + var assembly = Assembly.GetExecutingAssembly(); + VersionString = assembly.GetCustomAttribute()?.InformationalVersion + ?? assembly.GetName().Version?.ToString() + ?? "Unknown"; + + InitializeComponent(); + DataContext = this; + } + + public string VersionString => $"Version {field}"; + + private void GitHubLinkTapped(object? sender, TappedEventArgs e) => Process.Start(new ProcessStartInfo + { + FileName = "https://github.com/paulirwin/javatocsharp", + UseShellExecute = true + }); +} diff --git a/JavaToCSharpGui/Views/MainWindow.axaml b/JavaToCSharpGui/Views/MainWindow.axaml index fa40cdb1..ab9e6a54 100644 --- a/JavaToCSharpGui/Views/MainWindow.axaml +++ b/JavaToCSharpGui/Views/MainWindow.axaml @@ -3,6 +3,8 @@ xmlns:vm="using:JavaToCSharpGui.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="https://github.com/projektanker/icons.avalonia" + xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="JavaToCSharpGui.Views.MainWindow" x:DataType="vm:MainWindowViewModel" @@ -15,124 +17,177 @@ - + - - NOTE: This tool does a - syntactic conversion only from Java - to C#. It does not resolve symbols or namespaces, so the resulting C# code likely will not compile without - modification. You must verify the results of the conversion manually. - - Java to C# + - + File: + + + + + + + + - - - - - Include Usings in Output - Include Namespace in Output - Include Comments in Output - - Use Debug.Assert() for asserts - - - Use Unrecognized Code To Comment - - Use Folder Convert - - Convert System.out to Console - - - - - - - Java Source Code Input: - - File: - - + + C# Output: + + + + + + - - - - - - C# Output: - - - + + + + + + C# Output Paths: + + + + - - - + + @@ -155,4 +210,4 @@ - \ No newline at end of file + diff --git a/JavaToCSharpGui/Views/MainWindow.axaml.cs b/JavaToCSharpGui/Views/MainWindow.axaml.cs index 4b047213..c19dd313 100644 --- a/JavaToCSharpGui/Views/MainWindow.axaml.cs +++ b/JavaToCSharpGui/Views/MainWindow.axaml.cs @@ -2,8 +2,11 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Styling; +using AvaloniaEdit; +using AvaloniaEdit.TextMate; using JavaToCSharpGui.Infrastructure; using JavaToCSharpGui.ViewModels; +using TextMateSharp.Grammars; namespace JavaToCSharpGui.Views; @@ -23,9 +26,36 @@ protected override void OnOpened(EventArgs e) var vm = new MainWindowViewModel(storageProvider, dispatcher, clipboard); DataContext = vm; - this.Usings.DoubleTapped += (_, _) => vm.RemoveSelectedUsingCommand.Execute(null); + + ConfigureEditors(); + InstallTextMate(); + } + + private void ConfigureEditors() + { + ConfigureEditor(JavaTextEditor); + ConfigureEditor(CSharpTextEditor); + } + + private static void ConfigureEditor(ITextEditorComponent editor) + { + // TODO.PI: make these options in settings for people with odd preferences + editor.Options.ConvertTabsToSpaces = true; + editor.Options.IndentationSize = 4; } - + + private void InstallTextMate() + { + var appTheme = Application.Current?.ActualThemeVariant ?? ThemeVariant.Dark; + var registryOptions = new RegistryOptions(appTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus); + + var javaTextMate = JavaTextEditor.InstallTextMate(registryOptions); + javaTextMate.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".java").Id)); + + var csharpTextMate = CSharpTextEditor.InstallTextMate(registryOptions); + csharpTextMate.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".cs").Id)); + } + private void ToggleButton_OnIsCheckedChanged(object sender, RoutedEventArgs e) { var app = Application.Current; @@ -33,6 +63,7 @@ private void ToggleButton_OnIsCheckedChanged(object sender, RoutedEventArgs e) { var theme = app.ActualThemeVariant; app.RequestedThemeVariant = theme == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark; + InstallTextMate(); } } -} \ No newline at end of file +} diff --git a/JavaToCSharpGui/Views/SettingsWindow.axaml b/JavaToCSharpGui/Views/SettingsWindow.axaml new file mode 100644 index 00000000..1b53d4a6 --- /dev/null +++ b/JavaToCSharpGui/Views/SettingsWindow.axaml @@ -0,0 +1,69 @@ + + + + + + Add Usings: + + + + + + + + + + + + Include usings in output + + + Include namespace in output + + + Use file-scoped namespaces + + + Include comments in output + + + Use Debug.Assert() for asserts + + + Comment out unrecognized code + + + Convert System.out to Console + + + + + + + + + + diff --git a/JavaToCSharpGui/Views/SettingsWindow.axaml.cs b/JavaToCSharpGui/Views/SettingsWindow.axaml.cs new file mode 100644 index 00000000..c8b29851 --- /dev/null +++ b/JavaToCSharpGui/Views/SettingsWindow.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using JavaToCSharpGui.ViewModels; + +namespace JavaToCSharpGui.Views; + +public partial class SettingsWindow : Window +{ + public SettingsWindow() + { + InitializeComponent(); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + + var vm = new SettingsWindowViewModel(); + DataContext = vm; + + vm.CloseRequested += OnCloseRequested; + + Usings.DoubleTapped += (_, _) => vm.RemoveSelectedUsingCommand.Execute(null); + } + + private void OnCloseRequested(object? o, EventArgs eventArgs) => Close(); + + protected override void OnUnloaded(RoutedEventArgs e) + { + if (DataContext is SettingsWindowViewModel vm) + { + vm.CloseRequested -= OnCloseRequested; + } + + base.OnUnloaded(e); + } +} diff --git a/README.md b/README.md index 724beaad..b1a3a686 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,79 @@ from the command line. The core library is installable via NuGet at https://www.nuget.org/packages/JavaToCSharp/ +## Syntax Mappings + +By default, JavaToCSharp translates some usual Java classes and methods into their C# counterparts +(e.g. Java maps are converted into dictionaries). +You can specify additional mappings to fine tune the translation of the syntactic elements. + +The mappings are specified in a yaml file with root keys that represent the kind of mapping, +each having a set of key-value pairs that specify the java to C# mappings: + +- `ImportMappings`: Mappings from Java package names to the C# namespaces. + If a key-value pair has an empty value, the package will be removed from the resulting C#. +- `VoidMethodMappings`: Mappings from unqualified Java void methods to C#. +- `NonVoidMethodMappings`: Same as before, but for non-void java methods. +- `AnnotationMappings`: Mappings from Java method annotations to C#. + +For example, to convert *JUnit* tests into *xUnit* you can create this mapping file: +```yaml +ImportMappings: + org.junit.Test : Xunit + org.junit.Assert.assertEquals : "" + org.junit.Assert.assertTrue : "" +VoidMethodMappings: + assertEquals : Assert.Equal + assertTrue : Assert.True +AnnotationMappings: + Test : Fact +``` + +If you specify this file in the `--mappings-file` CLI argument, the conversion of this JUnit test: +```Java +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +public class MappingsTest { + @Test + public void testAsserts() { + assertEquals("a", "a"); + assertTrue(true); + } +} +``` + +will produce this xUnit test: +```csharp +using Xunit; + +public class MappingsTest +{ + [Fact] + public virtual void TestAsserts() + { + Assert.Equal("a", "a"); + Assert.True(true); + } +} +``` + +## .NET Support + +Trunk will generally always target the latest LTS version of .NET for the core library and the CLI/GUI apps. +If you require running the app on a prior version of .NET, please see the historical releases. +Where STS BCL or preview (as of latest LTS SDK) C# language features are used in the converter, they will be options that are disabled by default. +These options may become enabled by default when the next LTS ships, as a major version bump. + +.NET Framework is no longer supported and will not be supported going forward. +This includes not just for the core library (i.e. .NET Standard will not be supported) and running the CLI/GUI apps, +but also for the C# code generation where applicable. + +Bug fixes and features may be backported to the latest major version that targets an actively-supported, non-latest .NET LTS, +but only based on community interest. +Please submit an issue if you want a bugfix or feature backported; better yet, please submit a PR. +Only clean cherry-picks or minor merges will be supported for backports; significant/messy merges or PRs will likely not be supported or merged. + ## License for JavaParser Licensed under the Apache License available at https://github.com/javaparser/javaparser/blob/master/LICENSE.APACHE diff --git a/SECURITY.md b/SECURITY.md index 33dc4248..928f7104 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,8 +7,8 @@ The following versions are supported for security updates: | Version | Supported | | ------- | ------------------ | | trunk | :white_check_mark: | -| 2.1.x | :white_check_mark: | -| 2.0.x | :x: | +| 3.0.x | :white_check_mark: | +| 2.x.x | :x: | | 1.x.x | :x: | ## Reporting a Vulnerability