Skip to content

Commit bf77214

Browse files
atamez31allevato
authored andcommitted
Implement use triple slash for documentation comments (swiftlang#71)
1 parent d514b47 commit bf77214

File tree

3 files changed

+205
-12
lines changed

3 files changed

+205
-12
lines changed

Diff for: Sources/Rules/DeclSyntax+Comments.swift

+32-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ extension DeclSyntax {
55
public var docComment: String? {
66
guard let tok = firstToken else { return nil }
77
var comment = [String]()
8-
8+
99
// We need to skip trivia until we see the first comment. This trivia will include all the
1010
// spaces and newlines before the doc comment.
1111
var hasSeenFirstLineComment = false
12-
12+
1313
// Look through for discontiguous doc comments, separated by more than 1 newline.
1414
gatherComments: for piece in tok.leadingTrivia.reversed() {
1515
switch piece {
@@ -19,7 +19,32 @@ extension DeclSyntax {
1919
if hasSeenFirstLineComment {
2020
break gatherComments
2121
}
22-
return text
22+
let blockComment = text.components(separatedBy: "\n")
23+
// Removes the marks of the block comment.
24+
var isTheFirstLine = true
25+
let blockCommentWithoutMarks = blockComment.map { (line: String) -> String in
26+
// Only the first line of the block comment start with '/**'
27+
let markToRemove = isTheFirstLine ? "/**" : "* "
28+
let lineTrim = line.trimmingCharacters(in: .whitespaces)
29+
if lineTrim.starts(with: markToRemove) {
30+
let numCharsToRemove = isTheFirstLine ? markToRemove.count : markToRemove.count - 1
31+
isTheFirstLine = false
32+
return lineTrim.hasSuffix("*/") ?
33+
String(lineTrim.dropFirst(numCharsToRemove).dropLast(3)) :
34+
String(lineTrim.dropFirst(numCharsToRemove))
35+
}
36+
else if lineTrim == "*" {
37+
return ""
38+
39+
}
40+
else if lineTrim.hasSuffix("*/") {
41+
return String(line.dropLast(3))
42+
}
43+
isTheFirstLine = false
44+
return line
45+
}
46+
47+
return blockCommentWithoutMarks.joined(separator: "\n").trimmingCharacters(in: .newlines)
2348
case .docLineComment(let text):
2449
// Mark that we've started grabbing sequential line comments and append it to the
2550
// comment buffer.
@@ -38,6 +63,9 @@ extension DeclSyntax {
3863
}
3964
}
4065
}
41-
return comment.isEmpty ? nil : comment.joined(separator: "\n")
66+
67+
/// Removes the "///" from every line of comment
68+
let docLineComments = comment.reversed().map { $0.dropFirst(3) }
69+
return comment.isEmpty ? nil : docLineComments.joined(separator: "\n")
4270
}
4371
}

Diff for: Sources/Rules/UseTripleSlashForDocumentationComments.swift

+106-8
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,115 @@ import SwiftSyntax
44

55
/// Documentation comments must use the `///` form.
66
///
7-
/// Flag comments (e.g. `// TODO(username):`) are exempted from this rule.
7+
/// This is similar to `NoBlockComments` but is meant to prevent documentation block comments.
88
///
9-
/// This is similar to `NoBlockComments` but is meant to prevent multi-line comments that use `//`.
9+
/// Lint: If a doc block comment appears, a lint error is raised.
1010
///
11-
/// Lint: If a declaration has a multi-line comment preceding it and that comment is not in `///`
12-
/// form, a lint error is raised.
13-
///
14-
/// Format: If a declaration has a multi-line comment preceding it and that comment is not in `///`
15-
/// form, it is converted to the `///` form.
11+
/// Format: If a doc block comment appears on its own on a line, or if a doc block comment spans multiple
12+
/// lines without appearing on the same line as code, it will be replaced with multiple
13+
/// doc line comments.
1614
///
1715
/// - SeeAlso: https://google.github.io/swift#general-format
18-
public final class UseTripleSlashForDocumentationComments {
16+
public final class UseTripleSlashForDocumentationComments: SyntaxFormatRule {
17+
public override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
18+
return convertDocBlockCommentToDocLineComment(node)
19+
}
20+
21+
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
22+
return convertDocBlockCommentToDocLineComment(node)
23+
}
24+
25+
public override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax {
26+
return convertDocBlockCommentToDocLineComment(node)
27+
}
28+
29+
public override func visit(_ node: DeinitializerDeclSyntax) -> DeclSyntax {
30+
return convertDocBlockCommentToDocLineComment(node)
31+
}
32+
33+
public override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax {
34+
return convertDocBlockCommentToDocLineComment(node)
35+
}
36+
37+
public override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
38+
return convertDocBlockCommentToDocLineComment(node)
39+
}
40+
41+
public override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
42+
return convertDocBlockCommentToDocLineComment(node)
43+
}
44+
45+
public override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
46+
return convertDocBlockCommentToDocLineComment(node)
47+
}
48+
49+
public override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax {
50+
return convertDocBlockCommentToDocLineComment(node)
51+
}
1952

53+
public override func visit(_ node: TypealiasDeclSyntax) -> DeclSyntax {
54+
return convertDocBlockCommentToDocLineComment(node)
55+
}
56+
57+
public override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax {
58+
return convertDocBlockCommentToDocLineComment(node)
59+
}
60+
61+
/// In the case the given declaration has a docBlockComment as it's documentation
62+
/// comment. Returns the declaration with the docBlockComment converted to
63+
/// a docLineComment.
64+
func convertDocBlockCommentToDocLineComment(_ decl: DeclSyntax) -> DeclSyntax {
65+
guard let commentText = decl.docComment else { return decl }
66+
guard let declLeadinTrivia = decl.leadingTrivia else { return decl }
67+
let docComments = commentText.components(separatedBy: "\n")
68+
var pieces = [TriviaPiece]()
69+
70+
// Ensures the documentation comment is a docLineComment.
71+
var hasFoundDocComment = false
72+
for piece in declLeadinTrivia.reversed() {
73+
if case .docBlockComment(_) = piece, !hasFoundDocComment {
74+
hasFoundDocComment = true
75+
diagnose(.avoidDocBlockComment, on: decl)
76+
pieces.append(contentsOf: separateDocBlockIntoPieces(docComments).reversed())
77+
}
78+
else {
79+
pieces.append(piece)
80+
}
81+
}
82+
83+
return !hasFoundDocComment ? decl :
84+
replaceTrivia(
85+
on: decl,
86+
token: decl.firstToken,
87+
leadingTrivia: Trivia(pieces: pieces.reversed())
88+
) as! DeclSyntax
89+
}
90+
91+
/// Breaks down the docBlock comment into the correct trivia pieces
92+
/// for a docLineComment.
93+
func separateDocBlockIntoPieces(_ docComments: [String]) -> [TriviaPiece]
94+
{
95+
var pieces = [TriviaPiece]()
96+
for lineText in docComments.dropLast() {
97+
// Adds an space as indentation for the lines that needed it.
98+
let docLineMark = lineText.first == " " ||
99+
lineText.trimmingCharacters(in: .whitespaces) == "" ? "///" : "/// "
100+
pieces.append(.docLineComment(docLineMark + lineText))
101+
pieces.append(.newlines(1))
102+
}
103+
104+
// The last piece doesn't need a newline after it.
105+
if docComments.last!.trimmingCharacters(in: .whitespaces) != "" {
106+
let docLineMark = docComments.last!.first == " " ||
107+
docComments.last!.trimmingCharacters(in: .whitespaces) == "" ? "///" : "/// "
108+
pieces.append(.docLineComment(docLineMark + docComments.last!))
109+
}
110+
return pieces
111+
}
112+
}
113+
114+
extension Diagnostic.Message {
115+
static let avoidDocBlockComment =
116+
Diagnostic.Message(.warning, "Documentation block comments are not allowed")
20117
}
118+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import SwiftSyntax
2+
import XCTest
3+
4+
@testable import Rules
5+
6+
public class UseTripleSlashForDocumentationCommentsTests: DiagnosingTestCase {
7+
public func testRemoveDocBlockComments() {
8+
XCTAssertFormatting(
9+
UseTripleSlashForDocumentationComments.self,
10+
input: """
11+
/**
12+
* This comment should not be converted.
13+
*/
14+
15+
/**
16+
* Returns a docLineComment.
17+
*
18+
* - Parameters:
19+
* - withOutStar: Indicates if the comment start with a star.
20+
* - Returns: docLineComment.
21+
*/
22+
func foo(withOutStar: Bool) {}
23+
""",
24+
expected: """
25+
/**
26+
* This comment should not be converted.
27+
*/
28+
29+
/// Returns a docLineComment.
30+
///
31+
/// - Parameters:
32+
/// - withOutStar: Indicates if the comment start with a star.
33+
/// - Returns: docLineComment.
34+
func foo(withOutStar: Bool) {}
35+
""")
36+
}
37+
38+
public func testRemoveDocBlockCommentsWithoutStars() {
39+
XCTAssertFormatting(
40+
UseTripleSlashForDocumentationComments.self,
41+
input: """
42+
/**
43+
Returns a docLineComment.
44+
45+
- Parameters:
46+
- withStar: Indicates if the comment start with a star.
47+
- Returns: docLineComment.
48+
*/
49+
public var test = 1
50+
""",
51+
expected: """
52+
/// Returns a docLineComment.
53+
///
54+
/// - Parameters:
55+
/// - withStar: Indicates if the comment start with a star.
56+
/// - Returns: docLineComment.
57+
public var test = 1
58+
""")
59+
}
60+
61+
#if !os(macOS)
62+
static let allTests = [
63+
UseTripleSlashForDocumentationCommentsTests.testRemoveDocBlockComments,
64+
UseTripleSlashForDocumentationCommentsTeststestRemoveDocBlockCommentsWithoutStars,
65+
]
66+
#endif
67+
}

0 commit comments

Comments
 (0)