Skip to content

Commit c84cac0

Browse files
atamez31allevato
authored andcommitted
Implementation of BeginDocumentationCommentWithOneLineSummary (swiftlang#86)
1 parent bf77214 commit c84cac0

File tree

3 files changed

+203
-11
lines changed

3 files changed

+203
-11
lines changed

Sources/Rules/BeginDocumentationCommentWithOneLineSummary.swift

+98-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,103 @@ import SwiftSyntax
77
/// Lint: If a comment does not begin with a single-line summary, a lint error is raised.
88
///
99
/// - SeeAlso: https://google.github.io/swift#single-sentence-summary
10-
public final class BeginDocumentationCommentWithOneLineSummary: SyntaxLintRule {
10+
public final class BeginDocumentationCommentWithOneLineSummary: SyntaxLintRule {
11+
override public func visit(_ node: FunctionDeclSyntax) {
12+
diagnoseDocComments(node)
13+
}
1114

15+
override public func visit(_ node: EnumDeclSyntax) {
16+
diagnoseDocComments(node)
17+
}
18+
19+
override public func visit(_ node: InitializerDeclSyntax) {
20+
diagnoseDocComments(node)
21+
}
22+
23+
override public func visit(_ node: DeinitializerDeclSyntax) {
24+
diagnoseDocComments(node)
25+
}
26+
27+
override public func visit(_ node: SubscriptDeclSyntax) {
28+
diagnoseDocComments(node)
29+
}
30+
31+
override public func visit(_ node: ClassDeclSyntax) {
32+
diagnoseDocComments(node)
33+
}
34+
35+
override public func visit(_ node: VariableDeclSyntax) {
36+
diagnoseDocComments(node)
37+
}
38+
39+
override public func visit(_ node: StructDeclSyntax) {
40+
diagnoseDocComments(node)
41+
}
42+
43+
override public func visit(_ node: ProtocolDeclSyntax) {
44+
diagnoseDocComments(node)
45+
}
46+
47+
override public func visit(_ node: TypealiasDeclSyntax) {
48+
diagnoseDocComments(node)
49+
}
50+
51+
/// Diagnose documentation comments that don't start
52+
/// with one sentence summary.
53+
func diagnoseDocComments(_ decl: DeclSyntax) {
54+
guard let commentText = decl.docComment else { return }
55+
let docComments = commentText.components(separatedBy: "\n")
56+
guard let firstPart = firstParagraph(docComments) else { return }
57+
58+
let commentSentences = sentences(in: firstPart)
59+
if commentSentences.count > 1 {
60+
diagnose(.docCommentRequiresOneSentenceSummary(commentSentences.first!), on: decl)
61+
}
62+
}
63+
64+
/// Returns the text of the first part of the comment,
65+
func firstParagraph(_ comments: [String]) -> String? {
66+
var text = [String]()
67+
var index = 0
68+
while index < comments.count &&
69+
comments[index] != "*" &&
70+
comments[index] != "" {
71+
text.append(comments[index])
72+
index = index + 1
73+
}
74+
return comments.isEmpty ? nil : text.joined(separator:" ")
75+
}
76+
77+
/// Returns all the sentences in the given text.
78+
func sentences(in text: String) -> [String] {
79+
var sentences = [String]()
80+
if #available(OSX 10.13, *) { /// add linux condition
81+
let tagger = NSLinguisticTagger(tagSchemes: [.tokenType], options: 0)
82+
tagger.string = text
83+
let range = NSRange(location: 0, length: text.utf16.count)
84+
let options: NSLinguisticTagger.Options = [.omitWhitespace, .omitOther]
85+
tagger.enumerateTags(
86+
in: range,
87+
unit: .sentence,
88+
scheme: .tokenType,
89+
options: options
90+
) {_, tokenRange, _ in
91+
let sentence = (text as NSString).substring(with: tokenRange)
92+
sentences.append(sentence)
93+
}
94+
} else {
95+
return text.components(separatedBy: ". ")
96+
}
97+
return sentences
98+
}
99+
}
100+
101+
extension Diagnostic.Message {
102+
static func docCommentRequiresOneSentenceSummary(_ firstSentence: String) -> Diagnostic.Message {
103+
let sentenceWithoutExtraSpaces = firstSentence.trimmingCharacters(in: .whitespacesAndNewlines)
104+
return .init(
105+
.warning,
106+
"add a blank comment line after this sentence: \"\(sentenceWithoutExtraSpaces)\""
107+
)
108+
}
12109
}

Sources/Rules/DeclSyntax+Comments.swift

+9-10
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,24 @@ extension DeclSyntax {
2525
let blockCommentWithoutMarks = blockComment.map { (line: String) -> String in
2626
// Only the first line of the block comment start with '/**'
2727
let markToRemove = isTheFirstLine ? "/**" : "* "
28-
let lineTrim = line.trimmingCharacters(in: .whitespaces)
29-
if lineTrim.starts(with: markToRemove) {
28+
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
29+
if trimmedLine.starts(with: markToRemove) {
3030
let numCharsToRemove = isTheFirstLine ? markToRemove.count : markToRemove.count - 1
3131
isTheFirstLine = false
32-
return lineTrim.hasSuffix("*/") ?
33-
String(lineTrim.dropFirst(numCharsToRemove).dropLast(3)) :
34-
String(lineTrim.dropFirst(numCharsToRemove))
32+
return trimmedLine.hasSuffix("*/") ?
33+
String(trimmedLine.dropFirst(numCharsToRemove).dropLast(3)) :
34+
String(trimmedLine.dropFirst(numCharsToRemove))
3535
}
36-
else if lineTrim == "*" {
36+
else if trimmedLine == "*" {
3737
return ""
38-
3938
}
40-
else if lineTrim.hasSuffix("*/") {
39+
else if trimmedLine.hasSuffix("*/") {
4140
return String(line.dropLast(3))
4241
}
4342
isTheFirstLine = false
4443
return line
4544
}
46-
45+
4746
return blockCommentWithoutMarks.joined(separator: "\n").trimmingCharacters(in: .newlines)
4847
case .docLineComment(let text):
4948
// Mark that we've started grabbing sequential line comments and append it to the
@@ -63,7 +62,7 @@ extension DeclSyntax {
6362
}
6463
}
6564
}
66-
65+
6766
/// Removes the "///" from every line of comment
6867
let docLineComments = comment.reversed().map { $0.dropFirst(3) }
6968
return comment.isEmpty ? nil : docLineComments.joined(separator: "\n")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import Foundation
2+
import SwiftSyntax
3+
import XCTest
4+
5+
@testable import Rules
6+
7+
public class BeginDocumentationCommentWithOneLineSummaryTests: DiagnosingTestCase {
8+
public func testDocLineCommentsWithoutOneSentenceSummary() {
9+
let input =
10+
"""
11+
/// Returns a bottle of Dr. Pepper from the vending machine.
12+
public func drPepper(from vendingMachine: VendingMachine) -> Soda {}
13+
14+
/// Contains a comment as description that needs a sentece
15+
/// of two lines of code.
16+
public var twoLinesForOneSentence = "test"
17+
18+
/// The background color of the view.
19+
var backgroundColor: UIColor
20+
21+
/// Returns the sum of the numbers.
22+
///
23+
/// - Parameter numbers: The numbers to sum.
24+
/// - Returns: The sum of the numbers.
25+
func sum(_ numbers: [Int]) -> Int {
26+
// ...
27+
}
28+
29+
/// This docline should not succeed.
30+
/// There are two sentences without a blank line between them.
31+
struct Test {}
32+
33+
/// This docline should not succeed. There are two sentences.
34+
public enum Token { case comma, semicolon, identifier }
35+
"""
36+
performLint(BeginDocumentationCommentWithOneLineSummary.self, input: input)
37+
XCTAssertDiagnosed(.docCommentRequiresOneSentenceSummary("This docline should not succeed."))
38+
XCTAssertDiagnosed(.docCommentRequiresOneSentenceSummary("This docline should not succeed."))
39+
40+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary(
41+
"Returns a bottle of Dr. Pepper from the vending machine."))
42+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary(
43+
"Contains a comment as description that needs a sentece of two lines of code."))
44+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary("The background color of the view."))
45+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary("Returns the sum of the numbers."))
46+
}
47+
48+
public func testBlockLineCommentsWithoutOneSentenceSummary() {
49+
let input =
50+
"""
51+
/**
52+
* Returns the numeric value.
53+
*
54+
* - Parameters:
55+
* - digit: The Unicode scalar whose numeric value should be returned.
56+
* - radix: The radix, between 2 and 36, used to compute the numeric value.
57+
* - Returns: The numeric value of the scalar.*/
58+
func numericValue(of digit: UnicodeScalar, radix: Int = 10) -> Int {}
59+
60+
/**
61+
* This block comment contains a sentence summary
62+
* of two lines of code.
63+
*/
64+
public var twoLinesForOneSentence = "test"
65+
66+
/**
67+
* This block comment should not succeed, struct.
68+
* There are two sentences without a blank line between them.
69+
*/
70+
struct TestStruct {}
71+
72+
/**
73+
This block comment should not succeed, class.
74+
Add a blank comment after the first line.
75+
*/
76+
public class TestClass {}
77+
/** This block comment should not succeed, enum. There are two sentences. */
78+
public enum testEnum {}
79+
"""
80+
performLint(BeginDocumentationCommentWithOneLineSummary.self, input: input)
81+
XCTAssertDiagnosed(.docCommentRequiresOneSentenceSummary("This block comment should not succeed, struct."))
82+
XCTAssertDiagnosed(.docCommentRequiresOneSentenceSummary("This block comment should not succeed, class."))
83+
XCTAssertDiagnosed(.docCommentRequiresOneSentenceSummary("This block comment should not succeed, enum."))
84+
85+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary("Returns the numeric value."))
86+
XCTAssertNotDiagnosed(.docCommentRequiresOneSentenceSummary(
87+
"This block comment contains a sentence summary of two lines of code."))
88+
}
89+
90+
#if !os(macOS)
91+
static let allTests = [
92+
BeginDocumentationCommentWithOneLineSummaryTests.testDocLineCommentsWithoutOneSentenceSummary,
93+
BeginDocumentationCommentWithOneLineSummaryTests.testBlockLineCommentsWithoutOneSentenceSummary
94+
]
95+
#endif
96+
}

0 commit comments

Comments
 (0)