Skip to content

Commit 6b895e1

Browse files
Refactor Trivia.docCommentValue for readability and correctness
- Fix blank line stripping to only remove first/last line, not all blank lines - Add regression test and convert test strings to multi-line format - Move release notes entry from 602 to 604
1 parent aec28c1 commit 6b895e1

4 files changed

Lines changed: 86 additions & 21 deletions

File tree

Release Notes/602.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525
- Pull request: https://github.com/swiftlang/swift-syntax/pull/3030
2626
- Migration stems: None required.
2727

28-
- `Trivia` has a new `docCommentValue` property.
29-
- Description: Extracts sanitized comment text from doc comment trivia pieces, omitting leading comment markers (`///`, `/**`).
30-
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2966
31-
3228
## API Behavior Changes
3329

3430
## Deprecations

Release Notes/604.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Swift Syntax 604 Release Notes
2+
3+
## New APIs
4+
5+
- `Trivia` has a new `docCommentValue` property.
6+
- Description: Extracts sanitized comment text from doc comment trivia pieces, omitting leading comment markers (`///`, `/**`).
7+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2966
8+
9+
10+
## API Behavior Changes
11+
12+
## Deprecations
13+
14+
## API-Incompatible Changes
15+
16+
## Template
17+
18+
- *Affected API or two word description*
19+
- Description: *A 1-2 sentence description of the new/modified API*
20+
- Issue: *If an issue exists for this change, a link to the issue*
21+
- Pull Request: *Link to the pull request(s) that introduces this change*
22+
- Migration steps: Steps that adopters of swift-syntax should take to move to the new API (required for deprecations and API-incompatible changes).
23+
- Notes: *In case of deprecations or API-incompatible changes, the reason why this change was made and the suggested alternative*
24+
25+
*Insert entries in chronological order, with newer entries at the bottom*

Sources/SwiftSyntax/Trivia+commentValue.swift

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ extension Trivia {
1414
/// The contents of the last doc comment piece with any comment markers removed and indentation whitespace stripped.
1515
public var docCommentValue: String? {
1616
var comments: [Substring] = []
17-
var currentLineComments: [Substring] = []
17+
var currentLineComments: [String] = [] // Reset line comments when encountering a block comment
1818
var isInsideDocLineCommentSection = false
1919
var consecutiveNewlines = 0
2020

@@ -28,22 +28,19 @@ extension Trivia {
2828
consecutiveNewlines = 0
2929
case .docLineComment(let text):
3030
if isInsideDocLineCommentSection {
31-
currentLineComments.append(text[...])
31+
currentLineComments.append(text)
3232
} else {
33-
currentLineComments = [text[...]]
33+
currentLineComments = [text]
3434
isInsideDocLineCommentSection = true
3535
}
3636
consecutiveNewlines = 0
3737

3838
case .newlines(let n), .carriageReturns(let n), .carriageReturnLineFeeds(let n):
39-
if n == 1 {
40-
consecutiveNewlines += 1
41-
} else {
42-
consecutiveNewlines = 0
43-
}
39+
consecutiveNewlines += n
4440

4541
if consecutiveNewlines != 1 {
4642
processSectionBreak()
43+
consecutiveNewlines = 0
4744
}
4845
default:
4946
processSectionBreak()
@@ -56,45 +53,60 @@ extension Trivia {
5653
var lines = text.dropPrefix("/**").dropSuffix("*/")
5754
.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)
5855

56+
// If the comment content starts on the same line as the `/**` marker or ends on the same line as the `*/` marker,
57+
// it is common to separate the marker and the actual comment using spaces. Strip those spaces if they exists.
58+
// If there are non no-space characters on the first / last line, then the comment doesn't start / end on the line
59+
// with the marker, so don't do the stripping.
5960
if let firstLine = lines.first, firstLine.contains(where: { $0 != " " }) {
6061
lines[0] = firstLine.drop { $0 == " " }
6162
}
6263
if let lastLine = lines.last, lastLine.contains(where: { $0 != " " }) {
6364
lines[lines.count - 1] = lastLine.dropLast { $0 == " " }
6465
}
6566

67+
// Find the lowest indentation that is common among all lines in the block comment. Do not consider the first line
68+
// because it won't have any indentation since it starts with /**
6669
let indentation = lines.dropFirst()
6770
.map { $0.prefix(while: { $0 == " " || $0 == "\t" }) }
6871
.reduce(nil as Substring?) { (acc: Substring?, element: Substring.SubSequence) in
6972
acc.map { commonPrefix($0, element) } ?? element
7073
}
7174

7275
guard let firstLine = lines.first else {
76+
// We did not have any lines. This should never happen in practice because `split` never returns an empty array
77+
// but be safe and return `nil` here anyway.
7378
return nil
7479
}
7580

7681
var unindentedLines = [firstLine] + lines.dropFirst().map { $0.dropPrefix(indentation ?? "") }
7782

78-
while unindentedLines.first?.allSatisfy({ $0 == " " }) == true {
83+
// If the first line only contained the comment marker, don't include it. We don't want to start the comment value
84+
// with a newline if `/**` is on its own line. Same for the end marker.
85+
if unindentedLines.first?.allSatisfy({ $0 == " " }) ?? false {
7986
unindentedLines.removeFirst()
8087
}
81-
while unindentedLines.last?.allSatisfy({ $0 == " " }) == true {
88+
if unindentedLines.last?.allSatisfy({ $0 == " " }) ?? false {
8289
unindentedLines.removeLast()
8390
}
8491

92+
// We canonicalize the line endings to `\n` here. This matches how we concatenate the different line comment
93+
// pieces using \n as well.
8594
return Substring(unindentedLines.joined(separator: "\n"))
8695
}
8796

97+
/// Processes a section break, which is defined as a sequence of newlines or other trivia pieces that are not comments.
8898
func processSectionBreak() {
99+
// If we have a section break, we reset the current line comments.
89100
if !currentLineComments.isEmpty {
90-
comments = currentLineComments
101+
comments = currentLineComments.map { $0[...] }
91102
currentLineComments = []
92103
}
93104
isInsideDocLineCommentSection = false
94105
}
95106

107+
// If there are remaining line comments, use them as the last doc comment block.
96108
if !currentLineComments.isEmpty {
97-
comments = currentLineComments
109+
comments = currentLineComments.map { $0[...] }
98110
}
99111

100112
if comments.isEmpty { return nil }
@@ -125,6 +137,6 @@ fileprivate extension StringProtocol where SubSequence == Substring {
125137
}
126138
}
127139

128-
fileprivate func commonPrefix(_ lhs: Substring, _ rhs: Substring) -> Substring {
140+
private func commonPrefix(_ lhs: Substring, _ rhs: Substring) -> Substring {
129141
return lhs[..<lhs.index(lhs.startIndex, offsetBy: zip(lhs, rhs).prefix { $0 == $1 }.count)]
130142
}

Tests/SwiftSyntaxTest/TriviaCommentValueTests.swift

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ class TriviaCommentValueTests: XCTestCase {
2424
/// Some doc line comment
2525
/// Another
2626
""",
27-
docCommentValue: "Some doc line comment\nAnother"
27+
docCommentValue: """
28+
Some doc line comment
29+
Another
30+
"""
2831
)
2932
assertCommentValue(
3033
"""
@@ -87,7 +90,10 @@ class TriviaCommentValueTests: XCTestCase {
8790
/// included
8891
/// also included
8992
""",
90-
docCommentValue: "included\nalso included"
93+
docCommentValue: """
94+
included
95+
also included
96+
"""
9197
)
9298
assertCommentValue(
9399
"""
@@ -96,7 +102,10 @@ class TriviaCommentValueTests: XCTestCase {
96102
/// included
97103
/// also included
98104
""",
99-
docCommentValue: "included\nalso included"
105+
docCommentValue: """
106+
included
107+
also included
108+
"""
100109
)
101110
}
102111

@@ -123,7 +132,10 @@ class TriviaCommentValueTests: XCTestCase {
123132
* spread on many lines
124133
*/
125134
""",
126-
docCommentValue: "Some doc block comment\n* spread on many lines"
135+
docCommentValue: """
136+
Some doc block comment
137+
* spread on many lines
138+
"""
127139
)
128140
assertCommentValue(
129141
"""
@@ -222,6 +234,24 @@ class TriviaCommentValueTests: XCTestCase {
222234
""",
223235
docCommentValue: "abc"
224236
)
237+
assertCommentValue(
238+
"""
239+
/**
240+
241+
First paragraph.
242+
243+
Second paragraph.
244+
245+
*/
246+
""",
247+
docCommentValue: """
248+
249+
First paragraph.
250+
251+
Second paragraph.
252+
253+
"""
254+
)
225255
}
226256

227257
func testMixedStyleCommentValues() {
@@ -265,9 +295,11 @@ private func assertCommentValue(
265295
}
266296

267297
private func parseTrivia(from input: String) -> Trivia {
298+
// Wrap the input in valid Swift code so the parser can recognize it
268299
let wrappedSource = "let _ = 0\n\(input)\nlet _ = 1"
269300
let sourceFile = Parser.parse(source: wrappedSource)
270301

302+
// Find the token where the comment would appear (before `let _ = 1`)
271303
guard
272304
let commentToken = sourceFile.tokens(viewMode: .sourceAccurate).first(where: {
273305
$0.leadingTrivia.contains(where: { $0.isComment })

0 commit comments

Comments
 (0)