Skip to content

Commit ec22810

Browse files
paulirwinclaude
andauthored
Fix multi-line Javadoc dropped when file and host line endings differ (#97) (#176)
Fixes #97. ## The bug `CommentsHelper.ConvertDocComment` split Javadoc content on `Environment.NewLine`: ```csharp comment.getContent().Split([Environment.NewLine], StringSplitOptions.None) ``` JavaParser's `getContent()` preserves the line endings of the **parsed file**, which are independent of the **host OS**. I confirmed this with a throwaway probe: CRLF source yields content with 3 CRs, LF-only source yields 0. So when the two disagree — most commonly a Unix-line-ending file converted on Windows, which is exactly what the reporter narrowed it down to — the content stays one unsplit line, the per-line regex never matches, and the Javadoc is dropped from the output entirely. Single-line `/** ... */` comments were unaffected because they contain no newline to split on. That's why the bug appeared specific to multi-line Javadoc. ## Verification Rather than assume, I reproduced it: on unmodified `master`, patching the split to `"\r\n"` (simulating a Windows host) makes the new tests fail with the Javadoc **completely absent** — no `<summary>`, no `<param>`. With the fix, all cases pass. ## The change - Added `SplitLines()`, which normalizes line endings before splitting, and applied it to the three places that split text originating from the Java source. `FormatAsBlockComment` already did this correctly by hand and now shares the helper. - Added `NormalizeLineEndings()` on the raw comment-trivia paths so comment content adopts the generated file's line endings instead of leaking the Java file's, consistent with the existing `Whitespace.NewLine` convention. - The remaining `Environment.NewLine` uses are all on the **output** side — including `AdjustBlockCommentIndentation`, which splits trivia we generate ourselves — and are correct as-is. Only the input-side splits were platform-sensitive. ## Tests Two new theories in `CommentTests.cs`, each run against both `\n` and `\r\n` input. Worth flagging: there was previously **no test anywhere** asserting Javadoc → `<summary>`/`<param>`/`<returns>`/`<exception>` output — every other suite sets `IncludeComments = false`, so that path was entirely uncovered. These tests close that gap as well. Full suite: 382/382 passing, 0 build warnings. ## Out of scope Two pre-existing issues I noticed and left alone, each confirmed to reproduce on `master` independently of this change: - Generated output has mixed line endings because Roslyn's `NormalizeWhitespace()` emits `\r\n` regardless of platform — this happens even for a file containing no comments. - The CLI's `file` mode ignored the package declaration and dropped the type for one of my test inputs. Happy to look into either separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 539f413 commit ec22810

2 files changed

Lines changed: 100 additions & 6 deletions

File tree

‎JavaToCSharp.Tests/CommentTests.cs‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,86 @@ public class Foo
241241

242242
Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings());
243243
}
244+
245+
[Theory]
246+
[InlineData("\n")]
247+
[InlineData("\r\n")]
248+
public void MultiLineJavadoc_ShouldConvertToXmlDoc_RegardlessOfLineEndings(string newLine)
249+
{
250+
// Issue #97: the Javadoc content was split on Environment.NewLine, so a Java file whose
251+
// line endings differ from the host OS collapsed into a single unparsed line.
252+
string javaCode = """
253+
package foo;
254+
255+
public class Foo {
256+
/**
257+
* Really cool field that contains something.
258+
* Keep in mind that this field is cooler than awesomeField.
259+
*/
260+
public int reallyAwesomeField;
261+
}
262+
""".ReplaceLineEndings(newLine);
263+
264+
var options = new JavaConversionOptions();
265+
options.Usings.Clear();
266+
267+
var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";
268+
269+
testOutputHelper.WriteLine(parsed);
270+
271+
const string expected = """
272+
namespace Foo
273+
{
274+
public class Foo
275+
{
276+
/// <summary>
277+
/// Really cool field that contains something.
278+
/// Keep in mind that this field is cooler than awesomeField.
279+
/// </summary>
280+
public int reallyAwesomeField;
281+
}
282+
}
283+
""";
284+
285+
Assert.Equal(expected.ReplaceLineEndings(), parsed.ReplaceLineEndings());
286+
}
287+
288+
[Theory]
289+
[InlineData("\n")]
290+
[InlineData("\r\n")]
291+
public void MultiLineJavadocWithTags_ShouldConvertToXmlDoc_RegardlessOfLineEndings(string newLine)
292+
{
293+
string javaCode = """
294+
package foo;
295+
296+
public class Foo {
297+
/**
298+
* Does something useful.
299+
* Second line of the summary.
300+
*
301+
* @param name the name to use
302+
* @return the computed value
303+
* @throws IllegalStateException if it breaks
304+
*/
305+
public int doIt(String name) {
306+
return 1;
307+
}
308+
}
309+
""".ReplaceLineEndings(newLine);
310+
311+
var options = new JavaConversionOptions();
312+
options.Usings.Clear();
313+
314+
var parsed = JavaToCSharpConverter.ConvertText(javaCode, options) ?? "";
315+
316+
testOutputHelper.WriteLine(parsed);
317+
318+
Assert.Contains("/// <summary>", parsed);
319+
Assert.Contains("/// Does something useful.", parsed);
320+
Assert.Contains("/// Second line of the summary.", parsed);
321+
Assert.Contains("/// </summary>", parsed);
322+
Assert.Contains("""/// <param name="name">the name to use</param>""", parsed);
323+
Assert.Contains("/// <returns>the computed value</returns>", parsed);
324+
Assert.Contains("""/// <exception cref="IllegalStateException">if it breaks</exception>""", parsed);
325+
}
244326
}

‎JavaToCSharp/CommentsHelper.cs‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public static CompilationUnitSyntax AddPackageComments(CompilationUnitSyntax syn
102102
}
103103
else
104104
{
105-
var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post);
105+
var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + NormalizeLineEndings(comment.getContent()) + post);
106106
if (pos == CommentPosition.Leading)
107107
{
108108
leadingTriviaList.Add(commentTrivia);
@@ -136,13 +136,25 @@ private static SyntaxTrivia CreateNonMemberCommentTrivia(JavaComments.Comment co
136136
FormatAsBlockComment(comment.getContent()) + suffix);
137137
}
138138

139-
return SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post + suffix);
139+
return SyntaxFactory.SyntaxTrivia(kind, pre + NormalizeLineEndings(comment.getContent()) + post + suffix);
140140
}
141141

142+
/// <summary>
143+
/// Splits text that originated from the Java source into lines. The line endings of the parsed file are
144+
/// preserved by JavaParser and are independent of the host OS, so splitting on <see cref="Environment.NewLine"/>
145+
/// would fail whenever the two differ (see issue #97).
146+
/// </summary>
147+
private static string[] SplitLines(string text) => text.ReplaceLineEndings("\n").Split('\n');
148+
149+
/// <summary>
150+
/// Rewrites line endings that came from the Java source to the ones used for the generated C# output, so that
151+
/// multi-line comments do not introduce line endings foreign to the rest of the file.
152+
/// </summary>
153+
private static string NormalizeLineEndings(string text) => text.ReplaceLineEndings(Environment.NewLine);
154+
142155
private static string FormatAsBlockComment(string content)
143156
{
144-
var lines = content.ReplaceLineEndings("\n")
145-
.Split('\n')
157+
var lines = SplitLines(content)
146158
.Select(line => line.TrimStart().TrimStart('*').Trim())
147159
.ToList();
148160

@@ -293,7 +305,7 @@ public static IEnumerable<SyntaxTrivia> ConvertToComment(IEnumerable<JavaAst.Nod
293305
var outputs = new List<string>();
294306
foreach (var code in codes)
295307
{
296-
string[] input = code.ToString().Split([Environment.NewLine], StringSplitOptions.None);
308+
string[] input = SplitLines(code.ToString());
297309
outputs.AddRange(input);
298310
}
299311

@@ -318,7 +330,7 @@ public static IEnumerable<SyntaxTrivia> ConvertToComment(IEnumerable<JavaAst.Nod
318330

319331
private static IEnumerable<SyntaxTrivia> ConvertDocComment(JavaComments.Comment comment, string? post)
320332
{
321-
string[] input = comment.getContent().Split([Environment.NewLine], StringSplitOptions.None);
333+
string[] input = SplitLines(comment.getContent());
322334
var output = new List<string>();
323335
var remarks = new List<string>(); // For Java tags unknown in C#
324336
var currentOutput = output;

0 commit comments

Comments
 (0)