Skip to content

Commit f33fb6e

Browse files
authored
Merge pull request #3266 from fsprojects/repo-assist/fix-issue-2499-duplicate-doc-comment-10e89c3f45a0db9c
[Repo Assist] Fix doc comment (///) at end of file being duplicated
2 parents ebeb52b + 2be3413 commit f33fb6e

File tree

3 files changed

+65
-4
lines changed

3 files changed

+65
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Fixed
1010

1111
- Multiline `val` body in signature files was not indented correctly. [#3269](https://github.com/fsprojects/fantomas/pull/3269)
12+
- `///` doc comment without associated declaration (e.g. at end of file) was duplicated when formatting. [#2499](https://github.com/fsprojects/fantomas/issues/2499)
1213

1314
## [8.0.0-alpha-007] - 2026-03-10
1415

src/Fantomas.Core.Tests/CommentTests.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2611,3 +2611,14 @@ let IsMangledInfixOperator mangled = (* where mangled is assumed to be a compile
26112611
|| // INFIX_AT_HAT_OP
26122612
s = "**") // INFIX_STAR_STAR_OP
26132613
"""
2614+
2615+
[<Test>]
2616+
let ``doc comment without associated declaration should not be duplicated, 2499`` () =
2617+
formatSourceString
2618+
"""/// Returns `unit` if validation was successful otherwise will throw an `Exception`.
2619+
"""
2620+
config
2621+
|> should
2622+
equal
2623+
"""/// Returns `unit` if validation was successful otherwise will throw an `Exception`.
2624+
"""

src/Fantomas.FCS/Parse.fs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,54 @@ let QualFileNameOfImpls filename specs =
154154

155155
let collectCodeComments (lexbuf: UnicodeLexing.Lexbuf) =
156156
let tripleSlashComments = XmlDocStore.ReportInvalidXmlDocPositions lexbuf
157+
let comments = CommentStore.GetComments(lexbuf)
157158

158-
[ yield! CommentStore.GetComments(lexbuf)
159-
yield! List.map CommentTrivia.LineComment tripleSlashComments ]
159+
// At EOF the lexer may record a /// comment in both CommentStore and XmlDocStore,
160+
// producing a duplicate. Only pay the dedup cost when orphan /// comments exist.
161+
let uniqueTripleSlash =
162+
if List.isEmpty tripleSlashComments then
163+
[]
164+
else
165+
// Retrieve the source text stored by createLexbuf so we can verify that a
166+
// LineComment from CommentStore at the same position genuinely starts with
167+
// "///". This guards against accidentally suppressing a triple-slash entry
168+
// that happens to share coordinates with an unrelated line comment.
169+
let sourceText =
170+
match lexbuf.BufferLocalStore.TryGetValue("SourceText") with
171+
| true, (:? ISourceText as st) -> Some st
172+
| _ -> None
173+
174+
let isTripleSlashComment (r: range) =
175+
match sourceText with
176+
| None -> true // conservative: assume it may be /// if we can't verify
177+
| Some st ->
178+
let lineIdx = r.StartLine - 1 // StartLine is 1-based; GetLineString is 0-based
179+
let col = r.StartColumn
180+
181+
lineIdx >= 0
182+
&& lineIdx < st.GetLineCount()
183+
&& let line = st.GetLineString(lineIdx) in
184+
185+
line.Length > col + 2
186+
&& line[col] = '/'
187+
&& line[col + 1] = '/'
188+
&& line[col + 2] = '/'
189+
190+
let existingPositions =
191+
comments
192+
|> List.choose (function
193+
| CommentTrivia.LineComment r when isTripleSlashComment r -> Some(r.StartLine, r.StartColumn)
194+
| _ -> None)
195+
|> Set.ofList
196+
197+
tripleSlashComments
198+
|> List.choose (fun r ->
199+
if Set.contains (r.StartLine, r.StartColumn) existingPositions then
200+
None
201+
else
202+
Some(CommentTrivia.LineComment r))
203+
204+
[ yield! comments; yield! uniqueTripleSlash ]
160205
|> List.sortBy (function
161206
| CommentTrivia.LineComment r
162207
| CommentTrivia.BlockComment r -> r.StartLine, r.StartColumn)
@@ -335,8 +380,12 @@ let EmptyParsedInput (filename, isLastCompiland) =
335380
)
336381
)
337382

338-
let createLexbuf langVersion sourceText =
339-
UnicodeLexing.SourceTextAsLexbuf(true, LanguageVersion(langVersion), Some true, sourceText)
383+
let createLexbuf langVersion (sourceText: ISourceText) =
384+
let lexbuf =
385+
UnicodeLexing.SourceTextAsLexbuf(true, LanguageVersion(langVersion), Some true, sourceText)
386+
387+
lexbuf.BufferLocalStore["SourceText"] <- (sourceText :> obj)
388+
lexbuf
340389

341390
let createLexerFunction (defines: string list) lexbuf (errorLogger: CapturingDiagnosticsLogger) =
342391
// Note: we don't really attempt to intern strings across a large scope.

0 commit comments

Comments
 (0)