Skip to content

Commit c548f4f

Browse files
atamez31allevato
authored andcommitted
Implement ValidateDocumentationComments and parseDocComments (swiftlang#96)
1 parent fb416b7 commit c548f4f

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

tools/swift-format/Sources/Rules/DeclSyntax+Comments.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,74 @@ extension DeclSyntax {
6767
let docLineComments = comment.reversed().map { $0.dropFirst(3) }
6868
return comment.isEmpty ? nil : docLineComments.joined(separator: "\n")
6969
}
70+
71+
var docCommentInfo: ParseComment? {
72+
guard let docComment = self.docComment else { return nil }
73+
let comments = docComment.components(separatedBy: .newlines)
74+
var params = [ParseComment.Parameter]()
75+
var commentParagraphs = [String]()
76+
var hasFoundParameterList = false
77+
var hasFoundReturn = false
78+
var returnsDescription: String?
79+
// Takes the first sentence of the comment, and counts the number of lines it uses.
80+
let oneSenteceSummary = docComment.components(separatedBy: ".").first
81+
let numOfOneSentenceLines = oneSenteceSummary!.components(separatedBy: .newlines).count
82+
83+
// Iterates to all the comments after the one sentence summary to find the parameter(s)
84+
// return tags and get their description.
85+
for line in comments.dropFirst(numOfOneSentenceLines) {
86+
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
87+
88+
if trimmedLine.starts(with: "- Parameters") {
89+
hasFoundParameterList = true
90+
}
91+
else if trimmedLine.starts(with: "- Parameter") {
92+
// If it's only a parameter it's information is inline eith the parameter
93+
// tag, just after the ':'.
94+
guard let index = trimmedLine.firstIndex(of: ":") else { continue }
95+
let name = trimmedLine.dropFirst("- Parameter".count)[..<index]
96+
.trimmingCharacters(in: .init(charactersIn: " -:"))
97+
let summary = trimmedLine[index...].trimmingCharacters(in: .init(charactersIn: " -:"))
98+
params.append(ParseComment.Parameter(name: name, summary: summary))
99+
}
100+
else if trimmedLine.starts(with: "- Returns:") {
101+
hasFoundParameterList = false
102+
hasFoundReturn = true
103+
returnsDescription = String(trimmedLine.dropFirst("- Returns:".count))
104+
}
105+
else if hasFoundParameterList {
106+
// After the paramters tag is found the following lines should be the parameters
107+
// description.
108+
guard let index = trimmedLine.firstIndex(of: ":") else { continue }
109+
let name = trimmedLine[..<index].trimmingCharacters(in: .init(charactersIn: " -:"))
110+
let summary = trimmedLine[index...].trimmingCharacters(in: .init(charactersIn: " -:"))
111+
params.append(ParseComment.Parameter(name: name, summary: summary))
112+
}
113+
else if hasFoundReturn {
114+
// Appends the return description that is not inline with the return tag.
115+
returnsDescription!.append(trimmedLine)
116+
}
117+
else if trimmedLine != ""{
118+
commentParagraphs.append(" " + trimmedLine)
119+
}
120+
}
121+
return ParseComment(
122+
oneSentenceSummary: oneSenteceSummary,
123+
commentParagraphs: commentParagraphs,
124+
parameters: params,
125+
returnsDescription: returnsDescription
126+
)
127+
}
128+
}
129+
130+
struct ParseComment {
131+
struct Parameter {
132+
var name: String
133+
var summary: String
134+
}
135+
136+
var oneSentenceSummary: String?
137+
var commentParagraphs: [String]?
138+
var parameters: [Parameter]?
139+
var returnsDescription: String?
70140
}

tools/swift-format/Sources/Rules/ValidateDocumentationComments.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,112 @@ import SwiftSyntax
1111
///
1212
/// - SeeAlso: https://google.github.io/swift#parameter-returns-and-throws-tags
1313
public final class ValidateDocumentationComments: SyntaxLintRule {
14+
public override func visit(_ node: FunctionDeclSyntax) {
15+
guard let declComment = node.docComment else { return }
16+
guard let commentInfo = node.docCommentInfo else { return }
17+
guard let params = commentInfo.parameters else { return }
1418

19+
// If a single sentence summary is the only documentation, parameter(s) and
20+
// returns tags may be ommitted.
21+
if commentInfo.oneSentenceSummary != nil &&
22+
commentInfo.commentParagraphs!.isEmpty &&
23+
params.isEmpty &&
24+
commentInfo.returnsDescription == nil {
25+
return
26+
}
27+
28+
// Indicates if the documentation uses 'Parameters' as description of the
29+
// documented parameters.
30+
let hasPluralDesc = declComment.components(separatedBy: .newlines)
31+
.contains { $0.trimmingCharacters(in: .whitespaces).starts(with: "- Parameters") }
32+
33+
validateReturn(node, returnDesc: commentInfo.returnsDescription)
34+
let funcParameters = funcParametersIdentifiers(in: node.signature.input.parameterList)
35+
36+
// If the documentation of the parameters is wrong 'docCommentInfo' won't
37+
// parse the parameters correctly. First the documentation has to be fix
38+
// in order to validate the other conditions.
39+
if hasPluralDesc && funcParameters.count == 1 {
40+
diagnose(.useSingularParameter, on: node)
41+
return
42+
}
43+
else if !hasPluralDesc && funcParameters.count > 1 {
44+
diagnose(.usePluralParameters, on: node)
45+
return
46+
}
47+
48+
// Ensures that the parameters of the documantation and the function signature
49+
// are the same.
50+
if (params.count != funcParameters.count) ||
51+
!parametersAreEqual(params: params, funcParam: funcParameters) {
52+
diagnose(.parametersDontMatch(funcName: node.identifier.text), on: node)
53+
}
54+
}
55+
56+
/// Ensures the function has a return documentation if it actually returns
57+
/// a value.
58+
func validateReturn(_ node: FunctionDeclSyntax, returnDesc: String?) {
59+
if node.signature.output == nil && returnDesc != nil {
60+
diagnose(.removeReturnComment(funcName: node.identifier.text), on: node)
61+
}
62+
else if node.signature.output != nil && returnDesc == nil {
63+
diagnose(.documentReturnValue(funcName: node.identifier.text), on: node)
64+
}
65+
}
66+
}
67+
68+
/// Iterates through every parameter of paramList and returns a list of the
69+
/// paramters identifiers.
70+
func funcParametersIdentifiers(in paramList: FunctionParameterListSyntax) -> [String] {
71+
var funcParameters = [String]()
72+
for parameter in paramList
73+
{
74+
guard let parameterIdentifier = parameter.firstName else { continue }
75+
funcParameters.append(parameterIdentifier.text)
76+
}
77+
return funcParameters
78+
}
79+
80+
/// Indicates if the parameters name from the documentation and the parameters
81+
/// from the declaration are the same.
82+
func parametersAreEqual(params: [ParseComment.Parameter], funcParam: [String]) -> Bool {
83+
for index in 0..<params.count {
84+
if params[index].name != funcParam[index] {
85+
return false
86+
}
87+
}
88+
return true
89+
}
90+
91+
extension Diagnostic.Message {
92+
static func documentReturnValue(funcName: String) -> Diagnostic.Message {
93+
return Diagnostic.Message(.warning, "document the return value of \(funcName)")
94+
}
95+
96+
static func removeReturnComment(funcName: String) -> Diagnostic.Message {
97+
return Diagnostic.Message(
98+
.warning,
99+
"remove the return comment of \(funcName), it doesn't return a value"
100+
)
101+
}
102+
103+
static func parametersDontMatch(funcName: String) -> Diagnostic.Message {
104+
return Diagnostic.Message(
105+
.warning,
106+
"the parameters of \(funcName) don't match the parameters in its documentation"
107+
)
108+
}
109+
110+
static let useSingularParameter =
111+
Diagnostic.Message(
112+
.warning,
113+
"replace the plural form of 'Parameters' with a singular inline form of the 'Parameter' tag"
114+
)
115+
116+
static let usePluralParameters =
117+
Diagnostic.Message(
118+
.warning,
119+
"replace the singular inline form of 'Parameter' tag with a plural 'Parameters' tag " +
120+
"and group each parameter as a nested list"
121+
)
15122
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class ValidateDocumentationCommentsTests: DiagnosingTestCase {
8+
public func testParameterDocumentation() {
9+
let input =
10+
"""
11+
/// Uses 'Parameters' when it only has one parameter.
12+
///
13+
/// - Parameters singular: singular description.
14+
/// - Returns: A string containing the contents of a
15+
/// description
16+
func testPluralParamDesc(singular: String) -> Bool {}
17+
18+
/// Uses 'Parameter' with a list of parameters.
19+
///
20+
/// - Parameter
21+
/// - command: The command to execute in the shell environment.
22+
/// - stdin: The string to use as standard input.
23+
/// - Returns: A string containing the contents of the invoked process's
24+
/// standard output.
25+
func execute(command: String, stdin: String) -> String {
26+
// ...
27+
}
28+
29+
/// Returns the output generated by executing a command with the given string
30+
/// used as standard input.
31+
///
32+
/// - Parameter command: The command to execute in the shell environment.
33+
/// - Parameter stdin: The string to use as standard input.
34+
/// - Returns: A string containing the contents of the invoked process's
35+
/// standard output.
36+
func testInvalidParameterDesc(command: String, stdin: String) -> String {}
37+
"""
38+
performLint(ValidateDocumentationComments.self, input: input)
39+
XCTAssertDiagnosed(.useSingularParameter)
40+
XCTAssertDiagnosed(.usePluralParameters)
41+
XCTAssertDiagnosed(.usePluralParameters)
42+
}
43+
44+
public func testParametersName() {
45+
let input =
46+
"""
47+
/// Parameters dont match.
48+
///
49+
/// - Parameters:
50+
/// - sum: The sum of all numbers.
51+
/// - avg: The average of all numbers.
52+
/// - Returns: The sum of sum and avg.
53+
func sum(avg: Int, sum: Int) -> Int {}
54+
55+
/// Missing one parameter documentation.
56+
///
57+
/// - Parameters:
58+
/// - p1: Parameter 1.
59+
/// - p2: Parameter 2.
60+
/// - Returns: an integer.
61+
func foo(p1: Int, p2: Int, p3: Int) -> Int {}
62+
"""
63+
performLint(ValidateDocumentationComments.self, input: input)
64+
XCTAssertDiagnosed(.parametersDontMatch(funcName: "sum"))
65+
XCTAssertDiagnosed(.parametersDontMatch(funcName: "foo"))
66+
}
67+
68+
public func testReturnDocumentation() {
69+
let input =
70+
"""
71+
/// One sentence summary.
72+
///
73+
/// - Parameters:
74+
/// - p1: Parameter 1.
75+
/// - p2: Parameter 2.
76+
/// - Returns: an integer.
77+
func noReturn(p1: Int, p2: Int, p3: Int) {}
78+
79+
/// One sentence summary.
80+
///
81+
/// - Parameters:
82+
/// - p1: Parameter 1.
83+
/// - p2: Parameter 2.
84+
func foo(p1: Int, p2: Int, p3: Int) -> Int {}
85+
"""
86+
performLint(ValidateDocumentationComments.self, input: input)
87+
XCTAssertDiagnosed(.removeReturnComment(funcName: "noReturn"))
88+
XCTAssertDiagnosed(.documentReturnValue(funcName: "foo"))
89+
}
90+
91+
public func testValidDocumentation() {
92+
let input =
93+
"""
94+
/// Returns the output generated by executing a command.
95+
///
96+
/// - Parameter command: The command to execute in the shell environment.
97+
/// - Returns: A string containing the contents of the invoked process's
98+
/// standard output.
99+
func singularParam(command: String) -> String {
100+
// ...
101+
}
102+
103+
/// Returns the output generated by executing a command with the given string
104+
/// used as standard input.
105+
///
106+
/// - Parameters:
107+
/// - command: The command to execute in the shell environment.
108+
/// - stdin: The string to use as standard input.
109+
/// - Returns: A string containing the contents of the invoked process's
110+
/// standard output.
111+
func pluralParam(command: String, stdin: String) -> String {
112+
// ...
113+
}
114+
115+
/// Parameter(s) and Returns tags may be omitted only if the single-sentence
116+
/// brief summary fully describes the meaning of those items and including the
117+
/// tags would only repeat what has already been said
118+
func ommitedFunc(p1: Int)
119+
"""
120+
performLint(ValidateDocumentationComments.self, input: input)
121+
XCTAssertNotDiagnosed(.useSingularParameter)
122+
XCTAssertNotDiagnosed(.usePluralParameters)
123+
124+
XCTAssertNotDiagnosed(.documentReturnValue(funcName: "singularParam"))
125+
XCTAssertNotDiagnosed(.removeReturnComment(funcName: "singularParam"))
126+
XCTAssertNotDiagnosed(.parametersDontMatch(funcName: "singularParam"))
127+
128+
XCTAssertNotDiagnosed(.documentReturnValue(funcName: "pluralParam"))
129+
XCTAssertNotDiagnosed(.removeReturnComment(funcName: "pluralParam"))
130+
XCTAssertNotDiagnosed(.parametersDontMatch(funcName: "pluralParam"))
131+
132+
XCTAssertNotDiagnosed(.documentReturnValue(funcName: "ommitedFunc"))
133+
XCTAssertNotDiagnosed(.removeReturnComment(funcName: "ommitedFunc"))
134+
XCTAssertNotDiagnosed(.parametersDontMatch(funcName: "ommitedFunc"))
135+
}
136+
137+
#if !os(macOS)
138+
static let allTests = [
139+
ValidateDocumentationCommentsTests.testParameterDocumentation,
140+
ValidateDocumentationCommentsTests.testParametersName,
141+
ValidateDocumentationCommentsTests.tesReturnDocumentation,
142+
ValidateDocumentationCommentsTests.testValidDocumentation
143+
]
144+
#endif
145+
}

0 commit comments

Comments
 (0)