From ee0f8bc831b4051eec8d6ff361b89d288168a7d5 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Fri, 14 Aug 2026 13:56:50 -0600 Subject: [PATCH] Fix crash on Javadoc comments before package/import (#143) SyntaxKind.XmlComment is not valid for SyntaxFactory.SyntaxTrivia, and XML doc comments cannot attach to a file header or a using directive. Render Javadoc as a normalized block comment in those positions instead. --- JavaToCSharp.Tests/CommentTests.cs | 76 ++++++++++++++++++++++++++++++ JavaToCSharp/CommentsHelper.cs | 53 +++++++++++++++++---- 2 files changed, 120 insertions(+), 9 deletions(-) diff --git a/JavaToCSharp.Tests/CommentTests.cs b/JavaToCSharp.Tests/CommentTests.cs index 3bd5514..4e1c3da 100644 --- a/JavaToCSharp.Tests/CommentTests.cs +++ b/JavaToCSharp.Tests/CommentTests.cs @@ -165,4 +165,80 @@ public virtual void Method1() Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); } + + [Fact] + public void JavadocBeforePackage_ShouldNotCrash() + { + const string javaCode = """ + /** + * Some comments + */ + package foo; + + import java.util.List; + + public class Foo { + } + """; + var options = new JavaConversionOptions(); + options.Usings.Clear(); + + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + testOutputHelper.WriteLine(parsed); + + const string expected = """ + /* + * Some comments + */ + using Java.Util; + + namespace Foo + { + public class Foo + { + } + } + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } + + [Fact] + public void JavadocBeforeImport_ShouldNotCrash() + { + const string javaCode = """ + package foo; + + /** + * Import comment + */ + import java.util.List; + + public class Foo { + } + """; + var options = new JavaConversionOptions(); + options.Usings.Clear(); + + var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + + testOutputHelper.WriteLine(parsed); + + const string expected = """ + /* + * Import comment + */ + using Java.Util; + + namespace Foo + { + public class Foo + { + } + } + """; + + Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings()); + } } diff --git a/JavaToCSharp/CommentsHelper.cs b/JavaToCSharp/CommentsHelper.cs index 06da9c9..31cafc9 100644 --- a/JavaToCSharp/CommentsHelper.cs +++ b/JavaToCSharp/CommentsHelper.cs @@ -42,9 +42,7 @@ public static UsingDirectiveSyntax AddUsingComments(UsingDirectiveSyntax syntax, foreach (var (comment, _) in comments) { - var (kind, pre, post) = GetCommentInfo(comment); - var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post); - leadingTriviaList.Add(commentTrivia); + leadingTriviaList.Add(CreateNonMemberCommentTrivia(comment)); } return syntax.WithUsingKeyword(SyntaxFactory.Token(SyntaxKind.UsingKeyword).WithLeadingTrivia(leadingTriviaList)); @@ -59,9 +57,7 @@ public static CompilationUnitSyntax AddPackageComments(CompilationUnitSyntax syn 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); + leadingTriviaList.Add(CreateNonMemberCommentTrivia(compilationUnitComment, Environment.NewLine)); } if (packageDeclaration is not null) @@ -72,9 +68,7 @@ public static CompilationUnitSyntax AddPackageComments(CompilationUnitSyntax syn { 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.Add(CreateNonMemberCommentTrivia(comment, Environment.NewLine)); } } } @@ -128,6 +122,47 @@ public static CompilationUnitSyntax AddPackageComments(CompilationUnitSyntax syn return syntax; } + /// + /// Builds comment trivia for positions that cannot carry XML doc comments, such as file-level + /// and using directive comments. Javadoc is rendered as a block comment instead. + /// + private static SyntaxTrivia CreateNonMemberCommentTrivia(JavaComments.Comment comment, string suffix = "") + { + var (kind, pre, post) = GetCommentInfo(comment); + + if (kind == SyntaxKind.XmlComment) + { + return SyntaxFactory.SyntaxTrivia(SyntaxKind.MultiLineCommentTrivia, + FormatAsBlockComment(comment.getContent()) + suffix); + } + + return SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post + suffix); + } + + private static string FormatAsBlockComment(string content) + { + var lines = content.ReplaceLineEndings("\n") + .Split('\n') + .Select(line => line.TrimStart().TrimStart('*').Trim()) + .ToList(); + + TrimTrailingEmptyLines(lines); + + while (lines.Count > 0 && lines[0].Length == 0) + { + lines.RemoveAt(0); + } + + var builder = new System.Text.StringBuilder("/*"); + + foreach (var line in lines) + { + builder.Append(Environment.NewLine).Append(line.Length == 0 ? " *" : " * " + line); + } + + return builder.Append(Environment.NewLine).Append(" */").ToString(); + } + private static (SyntaxKind kind, string? pre, string? post) GetCommentInfo( JavaComments.Comment comment) {