Skip to content

Commit 8337523

Browse files
authored
Merge pull request #581 from allevato/comment-tweaks
Don't alter doc line comments unnecessarily.
2 parents b7260f0 + 9cc4d3c commit 8337523

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)