diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index ec753c8..8d121e1 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -67,6 +67,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/Java9PrivateInterfaceMethods.java")] [InlineData("Resources/Java10TypeInference.java")] [InlineData("Resources/Java14SwitchExpressions.java")] + [InlineData("Resources/Java15TextBlocks.java")] [InlineData("Resources/NewArrayLiteralBug.java")] [InlineData("Resources/OctalLiteralBug.java")] [InlineData("Resources/DeprecatedAnnotation.java")] diff --git a/JavaToCSharp.Tests/Resources/Java15TextBlocks.java b/JavaToCSharp.Tests/Resources/Java15TextBlocks.java new file mode 100644 index 0000000..1273cc4 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/Java15TextBlocks.java @@ -0,0 +1,33 @@ +/// Expect: +/// - output: "\n hi\n\n|he said \"\"quoted\"\" ok\n|a b\tc\n" +package example; + +// https://docs.oracle.com/en/java/javase/15/language/text-blocks.html + +public class Program { + public static void main(String[] args) { + String html = """ + + hi + + """; + + // Two adjacent quotes are legal inside a text block, and require a longer + // delimiter when converted to a C# raw string literal. + String quotes = """ + he said ""quoted"" ok + """; + + // \s keeps a trailing space, a trailing backslash joins lines, and \t is a tab. + String escapes = """ + a\s\ + b\tc + """; + + System.out.print(html); + System.out.print("|"); + System.out.print(quotes); + System.out.print("|"); + System.out.print(escapes); + } +} diff --git a/JavaToCSharp.Tests/VisitLiteralExpressionTests.cs b/JavaToCSharp.Tests/VisitLiteralExpressionTests.cs index cf5aaa6..6785419 100644 --- a/JavaToCSharp.Tests/VisitLiteralExpressionTests.cs +++ b/JavaToCSharp.Tests/VisitLiteralExpressionTests.cs @@ -19,6 +19,23 @@ public void VisitLiteralExpression_String() Assert.Equal("\\r", expr?.GetFirstToken().ValueText); } + [Theory] + // The value is written as it appears in Java source, indented under the opening """. + [InlineData(" a\n b\n ", "a\nb\n")] + [InlineData(" \n hi\n \n ", "\n hi\n\n")] + // Escapes are resolved once: \t stays a tab and \\ collapses to a single backslash. + [InlineData(" tab\there \\\\ backslash\n ", "tab\there \\ backslash\n")] + // Two adjacent quotes need a four-quote delimiter in the generated C#. + [InlineData(" he said \"\"quoted\"\" ok\n ", "he said \"\"quoted\"\" ok\n")] + public void VisitLiteralExpression_TextBlock(string javaValue, string expected) + { + var expr = ExpressionVisitor.VisitExpression( + new ConversionContext(new JavaConversionOptions()), + new TextBlockLiteralExpr(javaValue)); + + Assert.Equal(expected, expr?.GetFirstToken().ValueText); + } + [Theory] [InlineData("0b10", 2)] [InlineData("0b100", 4)] diff --git a/JavaToCSharp/Expressions/ExpressionVisitor.cs b/JavaToCSharp/Expressions/ExpressionVisitor.cs index 0f26be1..9b5ab0e 100644 --- a/JavaToCSharp/Expressions/ExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/ExpressionVisitor.cs @@ -48,6 +48,7 @@ static ExpressionVisitor() { typeof(MethodReferenceExpr), new MethodReferenceExpressionVisitor() }, { typeof(TypeExpr), new TypeExpressionVisitor() }, { typeof(SwitchExpr), new SwitchExpressionVisitor() }, + { typeof(TextBlockLiteralExpr), new TextBlockLiteralExpressionVisitor() }, }; } diff --git a/JavaToCSharp/Expressions/TextBlockLiteralExpressionVisitor.cs b/JavaToCSharp/Expressions/TextBlockLiteralExpressionVisitor.cs new file mode 100644 index 0000000..8e2c0fd --- /dev/null +++ b/JavaToCSharp/Expressions/TextBlockLiteralExpressionVisitor.cs @@ -0,0 +1,57 @@ +using com.github.javaparser.ast.expr; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace JavaToCSharp.Expressions; + +public class TextBlockLiteralExpressionVisitor : ExpressionVisitor +{ + protected override ExpressionSyntax Visit(ConversionContext context, TextBlockLiteralExpr expr) + { + // asString() applies the Java text block rules for us: incidental whitespace is + // stripped and escape sequences (including \s and line continuations) are resolved, + // so the result is the final string value and must not be unescaped again. + var value = expr.asString(); + + return SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + CreateRawStringLiteral(value)); + } + + private static SyntaxToken CreateRawStringLiteral(string value) + { + // C# requires the delimiter to be longer than the longest run of quotes in the + // content, so text containing "" needs at least four quotes to fence it. + var fence = new string('"', Math.Max(3, LongestQuoteRun(value) + 1)); + + // The opening fence is followed by a newline, and the closing fence sits on its own + // line. That final newline belongs to the delimiter rather than the value, so a Java + // text block ending in a newline needs the value written out verbatim before it. + var text = $"{fence}\n{value}\n{fence}"; + + return SyntaxFactory.Token( + SyntaxTriviaList.Empty, + SyntaxKind.MultiLineRawStringLiteralToken, + text, + value, + SyntaxTriviaList.Empty); + } + + private static int LongestQuoteRun(string value) + { + int longest = 0, run = 0; + + foreach (var c in value) + { + run = c == '"' ? run + 1 : 0; + + if (run > longest) + { + longest = run; + } + } + + return longest; + } +}