-
Notifications
You must be signed in to change notification settings - Fork 246
/
Copy pathValidateDocumentationComments.swift
198 lines (176 loc) · 7.37 KB
/
ValidateDocumentationComments.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
import Markdown
import SwiftSyntax
/// Documentation comments must be complete and valid.
///
/// "Command + Option + /" in Xcode produces a minimal valid documentation comment.
///
/// Lint: Documentation comments that are incomplete (e.g. missing parameter documentation) or
/// invalid (uses `Parameters` when there is only one parameter) will yield a lint error.
@_spi(Rules)
public final class ValidateDocumentationComments: SyntaxLintRule {
/// Identifies this rule as being opt-in. Accurate and complete documentation comments are
/// important, but this rule isn't able to handle situations where portions of documentation are
/// redundant. For example when the returns clause is redundant for a simple declaration.
public override class var isOptIn: Bool { return true }
public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
return checkFunctionLikeDocumentation(
DeclSyntax(node), name: "init", signature: node.signature)
}
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
return checkFunctionLikeDocumentation(
DeclSyntax(node), name: node.name.text, signature: node.signature,
returnClause: node.signature.returnClause)
}
private func checkFunctionLikeDocumentation(
_ node: DeclSyntax,
name: String,
signature: FunctionSignatureSyntax,
returnClause: ReturnClauseSyntax? = nil
) -> SyntaxVisitorContinueKind {
guard
let docComment = DocumentationComment(extractedFrom: node),
!docComment.parameters.isEmpty
else {
return .skipChildren
}
// If a single sentence summary is the only documentation, parameter(s) and
// returns tags may be omitted.
if docComment.briefSummary != nil
&& docComment.bodyNodes.isEmpty
&& docComment.parameters.isEmpty
&& docComment.returns == nil
{
return .skipChildren
}
validateThrows(
signature.effectSpecifiers?.throwsClause?.throwsSpecifier,
name: name,
throwsDescription: docComment.throws,
node: node)
validateReturn(
returnClause,
name: name,
returnsDescription: docComment.returns,
node: node)
let funcParameters = funcParametersIdentifiers(in: signature.parameterClause.parameters)
// If the documentation of the parameters is wrong 'docCommentInfo' won't
// parse the parameters correctly. First the documentation has to be fix
// in order to validate the other conditions.
if docComment.parameterLayout != .separated && funcParameters.count == 1 {
diagnose(.useSingularParameter, on: node)
return .skipChildren
} else if docComment.parameterLayout != .outline && funcParameters.count > 1 {
diagnose(.usePluralParameters, on: node)
return .skipChildren
}
// Ensures that the parameters of the documentation and the function signature
// are the same.
if (docComment.parameters.count != funcParameters.count)
|| !parametersAreEqual(params: docComment.parameters, funcParam: funcParameters)
{
diagnose(.parametersDontMatch(funcName: name), on: node)
}
return .skipChildren
}
/// Ensures the function has a return documentation if it actually returns
/// a value.
private func validateReturn(
_ returnClause: ReturnClauseSyntax?,
name: String,
returnsDescription: Paragraph?,
node: DeclSyntax
) {
if returnClause == nil && returnsDescription != nil {
diagnose(.removeReturnComment(funcName: name), on: node)
} else if let returnClause = returnClause, returnsDescription == nil {
if let returnTypeIdentifier = returnClause.type.as(IdentifierTypeSyntax.self),
returnTypeIdentifier.name.text == "Never"
{
return
}
diagnose(.documentReturnValue(funcName: name), on: returnClause)
}
}
/// Ensures the function has throws documentation if it may actually throw
/// an error.
private func validateThrows(
_ throwsOrRethrowsKeyword: TokenSyntax?,
name: String,
throwsDescription: Paragraph?,
node: DeclSyntax
) {
// If a function is marked as `rethrows`, it doesn't have any errors of its
// own that should be documented. So only require documentation for
// functions marked `throws`.
let needsThrowsDesc = throwsOrRethrowsKeyword?.tokenKind == .keyword(.throws)
if !needsThrowsDesc && throwsDescription != nil {
diagnose(
.removeThrowsComment(funcName: name),
on: throwsOrRethrowsKeyword ?? node.firstToken(viewMode: .sourceAccurate))
} else if needsThrowsDesc && throwsDescription == nil {
diagnose(.documentErrorsThrown(funcName: name), on: throwsOrRethrowsKeyword)
}
}
}
/// Iterates through every parameter of paramList and returns a list of the
/// parameters identifiers.
fileprivate func funcParametersIdentifiers(in paramList: FunctionParameterListSyntax) -> [String] {
var funcParameters = [String]()
for parameter in paramList {
// If there is a label and an identifier, then the identifier (`secondName`) is the name that
// should be documented. Otherwise, the label and identifier are the same, occupying
// `firstName`.
let parameterIdentifier = parameter.secondName ?? parameter.firstName
funcParameters.append(parameterIdentifier.text)
}
return funcParameters
}
/// Indicates if the parameters name from the documentation and the parameters
/// from the declaration are the same.
fileprivate func parametersAreEqual(
params: [DocumentationComment.Parameter],
funcParam: [String]
) -> Bool {
for index in 0..<params.count {
if params[index].name != funcParam[index] {
return false
}
}
return true
}
extension Finding.Message {
fileprivate static func documentReturnValue(funcName: String) -> Finding.Message {
"add a 'Returns:' section to document the return value of '\(funcName)'"
}
fileprivate static func removeReturnComment(funcName: String) -> Finding.Message {
"remove the 'Returns:' section of '\(funcName)'; it does not return a value"
}
fileprivate static func parametersDontMatch(funcName: String) -> Finding.Message {
"change the parameters of the documentation of '\(funcName)' to match its parameters"
}
fileprivate static let useSingularParameter: Finding.Message =
"replace the plural 'Parameters:' section with a singular inline 'Parameter' section"
fileprivate static let usePluralParameters: Finding.Message =
"""
replace the singular inline 'Parameter' section with a plural 'Parameters:' section \
that has the parameters nested inside it
"""
fileprivate static func removeThrowsComment(funcName: String) -> Finding.Message {
"remove the 'Throws:' sections of '\(funcName)'; it does not throw any errors"
}
fileprivate static func documentErrorsThrown(funcName: String) -> Finding.Message {
"add a 'Throws:' section to document the errors thrown by '\(funcName)'"
}
}