Skip to content

Commit 9cc4d3c

Browse files
committed
Don't alter doc line comments unnecessarily.
Since we introduced the new logic to parse doc comments, the `UseTripleSlashForDocumentationComments` rule started inadvertently normalizing comments that were already doc line comments. For example, ```swift /// /// Foo /// ``` would have its initial blank line(s) and leading spaces removed, leaving this: ```swift /// Foo /// ``` While we may want to be opinionated at some point about how such comments are formatted, it's not a desired consequence of *this* rule; the name on the tin doesn't say anything about altering comments that already meet the requirement, so now we make sure to leave them alone if they're already doc line comments.
1 parent c6484ec commit 9cc4d3c

File tree

6 files changed

+212
-114
lines changed

6 files changed

+212
-114
lines changed

Sources/SwiftFormatCore/DocumentationComment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public struct DocumentationComment {
8282
///
8383
/// - Parameter node: The syntax node from which the documentation comment should be extracted.
8484
public init?<Node: SyntaxProtocol>(extractedFrom node: Node) {
85-
guard let commentInfo = documentationCommentText(extractedFrom: node.leadingTrivia) else {
85+
guard let commentInfo = DocumentationCommentText(extractedFrom: node.leadingTrivia) else {
8686
return nil
8787
}
8888

Sources/SwiftFormatCore/DocumentationCommentText.swift

Lines changed: 139 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -12,121 +12,160 @@
1212

1313
import SwiftSyntax
1414

15-
/// Extracts and returns the body text of a documentation comment represented as a trivia
16-
/// collection.
15+
/// The text contents of a documentation comment extracted from trivia.
1716
///
18-
/// This function should be used when only the text of the comment is important, not the structural
19-
/// organization. It automatically handles trimming leading indentation from comments as well as
20-
/// "ASCII art" in block comments (i.e., leading asterisks on each line).
21-
///
22-
/// This implementation is based on
23-
/// https://github.com/apple/swift/blob/main/lib/Markup/LineList.cpp.
24-
///
25-
/// - Parameter trivia: The trivia collection from which to extract the comment text.
26-
/// - Returns: If a comment was found, a tuple containing the `String` containing the extracted text
27-
/// and the index into the trivia collection where the comment began is returned. Otherwise, `nil`
28-
/// is returned.
29-
public func documentationCommentText(extractedFrom trivia: Trivia)
30-
-> (text: String, startIndex: Trivia.Index)?
31-
{
32-
/// Represents a line of text and its leading indentation.
33-
struct Line {
34-
var text: Substring
35-
var firstNonspaceDistance: Int
36-
37-
init(_ text: Substring) {
38-
self.text = text
39-
self.firstNonspaceDistance = indentationDistance(of: text)
40-
}
17+
/// This type should be used when only the text of the comment is important, not the Markdown
18+
/// structural organization. It automatically handles trimming leading indentation from comments as
19+
/// well as "ASCII art" in block comments (i.e., leading asterisks on each line).
20+
public struct DocumentationCommentText {
21+
/// Denotes the kind of punctuation used to introduce the comment.
22+
public enum Introducer {
23+
/// The comment was introduced entirely by line-style comments (`///`).
24+
case line
25+
26+
/// The comment was introduced entirely by block-style comments (`/** ... */`).
27+
case block
28+
29+
/// The comment was introduced by a mixture of line-style and block-style comments.
30+
case mixed
4131
}
4232

43-
// Look backwards from the end of the trivia collection to find the logical start of the comment.
44-
// We have to copy it into an array since `Trivia` doesn't support bidirectional indexing.
45-
let triviaArray = Array(trivia)
46-
let commentStartIndex: Array<TriviaPiece>.Index
47-
if
48-
let lastNonDocCommentIndex = triviaArray.lastIndex(where: {
49-
switch $0 {
50-
case .docBlockComment, .docLineComment,
51-
.newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1),
52-
.spaces, .tabs:
53-
return false
54-
default:
55-
return true
33+
/// The comment text extracted from the trivia.
34+
public let text: String
35+
36+
/// The index in the trivia collection passed to the initializer where the comment started.
37+
public let startIndex: Trivia.Index
38+
39+
/// The kind of punctuation used to introduce the comment.
40+
public let introducer: Introducer
41+
42+
/// Extracts and returns the body text of a documentation comment represented as a trivia
43+
/// collection.
44+
///
45+
/// This implementation is based on
46+
/// https://github.com/apple/swift/blob/main/lib/Markup/LineList.cpp.
47+
///
48+
/// - Parameter trivia: The trivia collection from which to extract the comment text.
49+
/// - Returns: If a comment was found, a tuple containing the `String` containing the extracted
50+
/// text and the index into the trivia collection where the comment began is returned.
51+
/// Otherwise, `nil` is returned.
52+
public init?(extractedFrom trivia: Trivia) {
53+
/// Represents a line of text and its leading indentation.
54+
struct Line {
55+
var text: Substring
56+
var firstNonspaceDistance: Int
57+
58+
init(_ text: Substring) {
59+
self.text = text
60+
self.firstNonspaceDistance = indentationDistance(of: text)
5661
}
57-
}),
58-
lastNonDocCommentIndex != trivia.endIndex
59-
{
60-
commentStartIndex = triviaArray.index(after: lastNonDocCommentIndex)
61-
} else {
62-
commentStartIndex = triviaArray.startIndex
63-
}
62+
}
6463

65-
// Determine the indentation level of the first line of the comment. This is used to adjust
66-
// block comments, whose text spans multiple lines.
67-
let leadingWhitespace = contiguousWhitespace(in: triviaArray, before: commentStartIndex)
68-
var lines = [Line]()
69-
70-
// Extract the raw lines of text (which will include their leading comment punctuation, which is
71-
// stripped).
72-
for triviaPiece in trivia[commentStartIndex...] {
73-
switch triviaPiece {
74-
case .docLineComment(let line):
75-
lines.append(Line(line.dropFirst(3)))
76-
77-
case .docBlockComment(let line):
78-
var cleaned = line.dropFirst(3)
79-
if cleaned.hasSuffix("*/") {
80-
cleaned = cleaned.dropLast(2)
81-
}
82-
83-
var hasASCIIArt = false
84-
if cleaned.hasPrefix("\n") {
85-
cleaned = cleaned.dropFirst()
86-
hasASCIIArt = asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace) != 0
64+
// Look backwards from the end of the trivia collection to find the logical start of the
65+
// comment. We have to copy it into an array since `Trivia` doesn't support bidirectional
66+
// indexing.
67+
let triviaArray = Array(trivia)
68+
let commentStartIndex: Array<TriviaPiece>.Index
69+
if
70+
let lastNonDocCommentIndex = triviaArray.lastIndex(where: {
71+
switch $0 {
72+
case .docBlockComment, .docLineComment,
73+
.newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1),
74+
.spaces, .tabs:
75+
return false
76+
default:
77+
return true
78+
}
79+
}),
80+
lastNonDocCommentIndex != trivia.endIndex
81+
{
82+
commentStartIndex = triviaArray.index(after: lastNonDocCommentIndex)
83+
} else {
84+
commentStartIndex = triviaArray.startIndex
85+
}
86+
87+
// Determine the indentation level of the first line of the comment. This is used to adjust
88+
// block comments, whose text spans multiple lines.
89+
let leadingWhitespace = contiguousWhitespace(in: triviaArray, before: commentStartIndex)
90+
var lines = [Line]()
91+
92+
var introducer: Introducer?
93+
func updateIntroducer(_ newIntroducer: Introducer) {
94+
if let knownIntroducer = introducer, knownIntroducer != newIntroducer {
95+
introducer = .mixed
96+
} else {
97+
introducer = newIntroducer
8798
}
88-
89-
while !cleaned.isEmpty {
90-
var index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
91-
if hasASCIIArt {
92-
cleaned = cleaned.dropFirst(asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace))
93-
index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
99+
}
100+
101+
// Extract the raw lines of text (which will include their leading comment punctuation, which is
102+
// stripped).
103+
for triviaPiece in trivia[commentStartIndex...] {
104+
switch triviaPiece {
105+
case .docLineComment(let line):
106+
updateIntroducer(.line)
107+
lines.append(Line(line.dropFirst(3)))
108+
109+
case .docBlockComment(let line):
110+
updateIntroducer(.block)
111+
112+
var cleaned = line.dropFirst(3)
113+
if cleaned.hasSuffix("*/") {
114+
cleaned = cleaned.dropLast(2)
94115
}
95116

96-
// Don't add an unnecessary blank line at the end when `*/` is on its own line.
97-
guard cleaned.firstIndex(where: { !$0.isWhitespace }) != nil else {
98-
break
117+
var hasASCIIArt = false
118+
if cleaned.hasPrefix("\n") {
119+
cleaned = cleaned.dropFirst()
120+
hasASCIIArt = asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace) != 0
99121
}
100122

101-
let line = cleaned.prefix(upTo: index)
102-
lines.append(Line(line))
103-
cleaned = cleaned[index...].dropFirst()
104-
}
123+
while !cleaned.isEmpty {
124+
var index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
125+
if hasASCIIArt {
126+
cleaned =
127+
cleaned.dropFirst(asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace))
128+
index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
129+
}
130+
131+
// Don't add an unnecessary blank line at the end when `*/` is on its own line.
132+
guard cleaned.firstIndex(where: { !$0.isWhitespace }) != nil else {
133+
break
134+
}
135+
136+
let line = cleaned.prefix(upTo: index)
137+
lines.append(Line(line))
138+
cleaned = cleaned[index...].dropFirst()
139+
}
105140

106-
default:
107-
break
141+
default:
142+
break
143+
}
108144
}
109-
}
110145

111-
// Concatenate the lines into a single string, trimming any leading indentation that might be
112-
// present.
113-
guard
114-
!lines.isEmpty,
115-
let firstLineIndex = lines.firstIndex(where: { !$0.text.isEmpty })
116-
else { return nil }
117-
118-
let initialIndentation = indentationDistance(of: lines[firstLineIndex].text)
119-
var result = ""
120-
for line in lines[firstLineIndex...] {
121-
let countToDrop = min(initialIndentation, line.firstNonspaceDistance)
122-
result.append(contentsOf: "\(line.text.dropFirst(countToDrop))\n")
123-
}
146+
// Concatenate the lines into a single string, trimming any leading indentation that might be
147+
// present.
148+
guard
149+
let introducer = introducer,
150+
!lines.isEmpty,
151+
let firstLineIndex = lines.firstIndex(where: { !$0.text.isEmpty })
152+
else { return nil }
153+
154+
let initialIndentation = indentationDistance(of: lines[firstLineIndex].text)
155+
var result = ""
156+
for line in lines[firstLineIndex...] {
157+
let countToDrop = min(initialIndentation, line.firstNonspaceDistance)
158+
result.append(contentsOf: "\(line.text.dropFirst(countToDrop))\n")
159+
}
124160

125-
guard !result.isEmpty else { return nil }
161+
guard !result.isEmpty else { return nil }
126162

127-
let commentStartDistance =
128-
triviaArray.distance(from: triviaArray.startIndex, to: commentStartIndex)
129-
return (text: result, startIndex: trivia.index(trivia.startIndex, offsetBy: commentStartDistance))
163+
let commentStartDistance =
164+
triviaArray.distance(from: triviaArray.startIndex, to: commentStartIndex)
165+
self.text = result
166+
self.startIndex = trivia.index(trivia.startIndex, offsetBy: commentStartDistance)
167+
self.introducer = introducer
168+
}
130169
}
131170

132171
/// Returns the distance from the start of the string to the first non-whitespace character.

Sources/SwiftFormatRules/AllPublicDeclarationsHaveDocumentation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public final class AllPublicDeclarationsHaveDocumentation: SyntaxLintRule {
7676
modifiers: DeclModifierListSyntax?
7777
) {
7878
guard
79-
documentationCommentText(extractedFrom: decl.leadingTrivia) == nil,
79+
DocumentationCommentText(extractedFrom: decl.leadingTrivia) == nil,
8080
let mods = modifiers, mods.has(modifier: "public") && !mods.has(modifier: "override")
8181
else {
8282
return

Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,15 @@ public final class UseTripleSlashForDocumentationComments: SyntaxFormatRule {
6868
return convertDocBlockCommentToDocLineComment(DeclSyntax(node))
6969
}
7070

71-
/// In the case the given declaration has a docBlockComment as it's documentation
72-
/// comment. Returns the declaration with the docBlockComment converted to
73-
/// a docLineComment.
71+
/// If the declaration has a doc block comment, return the declaration with the comment rewritten
72+
/// as a line comment.
73+
///
74+
/// If the declaration had no comment or had only line comments, it is returned unchanged.
7475
private func convertDocBlockCommentToDocLineComment(_ decl: DeclSyntax) -> DeclSyntax {
75-
guard let commentInfo = documentationCommentText(extractedFrom: decl.leadingTrivia) else {
76+
guard
77+
let commentInfo = DocumentationCommentText(extractedFrom: decl.leadingTrivia),
78+
commentInfo.introducer != .line
79+
else {
7680
return decl
7781
}
7882

0 commit comments

Comments
 (0)