Skip to content

Commit d733332

Browse files
Add new doc_comment_parameter rule
1 parent afc957a commit d733332

15 files changed

Lines changed: 945 additions & 49 deletions

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313
### Enhancements
1414

15-
* None.
15+
* Add `doc_comment_parameter` opt-in rule that validates documentation
16+
comments on functions, initializers, and subscripts match their actual
17+
signatures. Reports extra or missing `- Parameter` entries. Optionally
18+
validates `- Returns:` and `- Throws:` sections (`validate_returns`,
19+
`validate_throws`) and enforces consistent parameter documentation
20+
syntax (`enforce_parameter_syntax`).
21+
[Yury Lapitsky](https://github.com/YuryLapitsky-TomTom)
1622

1723
### Bug Fixes
1824

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public let builtInRules: [any Rule.Type] = [
4444
DiscouragedObjectLiteralRule.self,
4545
DiscouragedOptionalBooleanRule.self,
4646
DiscouragedOptionalCollectionRule.self,
47+
DocCommentParameterRule.self,
4748
DuplicateConditionsRule.self,
4849
DuplicateEnumCasesRule.self,
4950
DuplicateImportsRule.self,

Source/SwiftLintBuiltInRules/Rules/Lint/DocCommentParameterRule.swift

Lines changed: 518 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
struct DocCommentParameterRuleExamples {
2+
static let nonTriggeringExamples = [
3+
// Correctly documented function
4+
Example(
5+
"""
6+
/// A function that does something.
7+
/// - Parameter value: The value to process.
8+
func process(value: Int) {}
9+
"""),
10+
// Multiple parameters correctly documented
11+
Example(
12+
"""
13+
/// Adds two numbers together.
14+
/// - Parameters:
15+
/// - lhs: Left-hand side value.
16+
/// - rhs: Right-hand side value.
17+
/// - Returns: The sum of `lhs` and `rhs`.
18+
func add(lhs: Int, rhs: Int) -> Int { lhs + rhs }
19+
"""),
20+
// Function without parameters doesn't need parameter documentation
21+
Example(
22+
"""
23+
/// Returns a greeting.
24+
func greet() -> String { "Hello" }
25+
"""),
26+
// Function without doc comment is not validated
27+
Example(
28+
"""
29+
func process(value: Int) {}
30+
"""),
31+
// Correctly documented method with external and internal parameter names
32+
Example(
33+
"""
34+
/// Updates the label.
35+
/// - Parameter with: The new text for the label.
36+
func updateLabel(with text: String) {}
37+
"""),
38+
// Initializer with correctly documented parameters
39+
Example(
40+
"""
41+
/// Creates a new instance.
42+
/// - Parameter value: The initial value.
43+
init(value: Int) {}
44+
"""),
45+
// Subscript with correctly documented parameters
46+
Example(
47+
"""
48+
/// Accesses the element at the specified index.
49+
/// - Parameter index: The index of the element.
50+
subscript(index: Int) -> Int { 0 }
51+
"""),
52+
// Closure parameter documented
53+
Example(
54+
"""
55+
/// Performs an operation.
56+
/// - Parameter completion: A closure called when complete.
57+
func perform(completion: () -> Void) {}
58+
"""),
59+
// Underscore parameter name uses internal name
60+
Example(
61+
"""
62+
/// Logs a message.
63+
/// - Parameter message: The message to log.
64+
func log(_ message: String) {}
65+
"""),
66+
// Block doc comment
67+
Example(
68+
"""
69+
/**
70+
* Processes the value.
71+
* - Parameter value: The value.
72+
*/
73+
func process(value: Int) {}
74+
"""),
75+
// Doc comment with only description (no parameter docs) for function without params
76+
Example(
77+
"""
78+
/// This function does nothing.
79+
func doNothing() {}
80+
"""),
81+
// validate_returns: returns doc present and function returns a value
82+
Example(
83+
"""
84+
/// Computes a value.
85+
/// - Returns: The computed value.
86+
func compute() -> Int { 0 }
87+
""", configuration: ["validate_returns": true]),
88+
// validate_returns: no returns doc and function does not return a value
89+
Example(
90+
"""
91+
/// Does something.
92+
func doSomething() {}
93+
""", configuration: ["validate_returns": true]),
94+
// validate_returns: returns doc present, Void return type — no violation
95+
Example(
96+
"""
97+
/// Does something.
98+
func doSomethingVoid() -> Void {}
99+
""", configuration: ["validate_returns": true]),
100+
// validate_returns: returns doc present, Never return type — no violation
101+
Example(
102+
"""
103+
/// Always fails.
104+
func fail() -> Never { fatalError() }
105+
""", configuration: ["validate_returns": true]),
106+
// validate_returns disabled (default) — missing returns doc is not flagged
107+
Example(
108+
"""
109+
/// Computes a value.
110+
func computeNoDoc() -> Int { 0 }
111+
"""),
112+
// validate_throws: throws doc present and function throws
113+
Example(
114+
"""
115+
/// Parses the input.
116+
/// - Throws: `ParseError` if the input is invalid.
117+
func parse() throws {}
118+
""", configuration: ["validate_throws": true]),
119+
// validate_throws: no throws doc and function does not throw
120+
Example(
121+
"""
122+
/// Does something safe.
123+
func safe() {}
124+
""", configuration: ["validate_throws": true]),
125+
// validate_throws: rethrows — throws doc is optional, both forms are fine
126+
Example(
127+
"""
128+
/// Maps elements.
129+
/// - Parameter transform: A closure.
130+
/// - Throws: Rethrows errors from the closure.
131+
func map(transform: () throws -> Int) rethrows -> Int { try transform() }
132+
""", configuration: ["validate_throws": true]),
133+
Example(
134+
"""
135+
/// Maps elements.
136+
/// - Parameter transform: A closure.
137+
func mapNoDoc(transform: () throws -> Int) rethrows -> Int { try transform() }
138+
""", configuration: ["validate_throws": true]),
139+
// validate_throws disabled (default) — missing throws doc is not flagged
140+
Example(
141+
"""
142+
/// Parses the input.
143+
func parseNoDoc() throws {}
144+
"""),
145+
// enforce_parameter_syntax: singular with 1 param — correct
146+
Example(
147+
"""
148+
/// Does something.
149+
/// - Parameter value: The value.
150+
func single(value: Int) {}
151+
""", configuration: ["enforce_parameter_syntax": true]),
152+
// enforce_parameter_syntax: plural block with 2+ params — correct
153+
Example(
154+
"""
155+
/// Does something.
156+
/// - Parameters:
157+
/// - a: First.
158+
/// - b: Second.
159+
func multi(a: Int, b: Int) {}
160+
""", configuration: ["enforce_parameter_syntax": true]),
161+
// enforce_parameter_syntax: no parameters — no violation
162+
Example(
163+
"""
164+
/// Does something.
165+
func noParams() {}
166+
""", configuration: ["enforce_parameter_syntax": true]),
167+
// enforce_parameter_syntax disabled (default) — plural block with 1 param is fine
168+
Example(
169+
"""
170+
/// Does something.
171+
/// - Parameters:
172+
/// - value: The value.
173+
func singleDefault(value: Int) {}
174+
"""),
175+
// Preceding doc comment must not bleed into the next declaration
176+
Example(
177+
"""
178+
/// Logs a message.
179+
/// - Parameter message: The message.
180+
func log(_ message: String) {}
181+
182+
// This is a plain (non-doc) comment.
183+
func unrelated(value: Int) {}
184+
"""),
185+
// Parameters block followed by Returns section — Returns must not be parsed as a parameter
186+
Example(
187+
"""
188+
/// Does something.
189+
/// - Parameters:
190+
/// - value: The value.
191+
/// - Returns: The result.
192+
func process(value: Int) -> Int { value }
193+
"""),
194+
// Parameters block followed by Throws section — Throws must exit the block correctly
195+
Example(
196+
"""
197+
/// Parses input.
198+
/// - Parameters:
199+
/// - input: The raw input.
200+
/// - Throws: `ParseError` on bad input.
201+
func parse(input: String) throws {}
202+
"""),
203+
// Doubly-unnamed parameter (_ _:) has no documentable name — no violation
204+
Example(
205+
"""
206+
/// Performs an action.
207+
func perform(_ _: Int) {}
208+
"""),
209+
// Mix of normal and doubly-unnamed — only named params need docs
210+
Example(
211+
"""
212+
/// Processes a value.
213+
/// - Parameter value: The value.
214+
func process(value: Int, _ _: String) {}
215+
"""),
216+
// @discardableResult: missing Returns doc is not flagged even with validate_returns
217+
Example(
218+
"""
219+
/// Computes a value whose result may be ignored.
220+
@discardableResult
221+
func compute() -> Int { 0 }
222+
""", configuration: ["validate_returns": true]),
223+
// Multi-line parameter description (continuation lines must not confuse the parser)
224+
Example(
225+
"""
226+
/// Processes the input.
227+
/// - Parameters:
228+
/// - value: A value that can be very long
229+
/// and spans multiple lines of description.
230+
/// - flag: A boolean flag.
231+
func process(value: Int, flag: Bool) {}
232+
"""),
233+
// Note inside doc comment must not be mistaken for a parameter
234+
Example(
235+
"""
236+
/// Sorts the collection.
237+
/// - Parameter collection: The collection to sort.
238+
/// - Note: Uses a stable sort algorithm.
239+
func sort(collection: [Int]) {}
240+
"""),
241+
// Block comment with Parameters: block
242+
Example(
243+
"""
244+
/**
245+
* Adds two numbers.
246+
* - Parameters:
247+
* - lhs: Left operand.
248+
* - rhs: Right operand.
249+
* - Returns: Their sum.
250+
*/
251+
func add(lhs: Int, rhs: Int) -> Int { lhs + rhs }
252+
"""),
253+
]
254+
255+
static let triggeringExamples = [
256+
// Documented parameter doesn't exist
257+
Example(
258+
"""
259+
/// Greets someone.
260+
/// - Parameter ↓name: The name to greet.
261+
func greet() {}
262+
"""),
263+
// One parameter missing from documentation
264+
Example(
265+
"""
266+
/// Adds two numbers.
267+
/// - Parameter lhs: Left-hand side.
268+
↓func add(lhs: Int, rhs: Int) -> Int { lhs + rhs }
269+
"""),
270+
// Documented parameter name doesn't match
271+
Example(
272+
"""
273+
/// Processes a value.
274+
/// - Parameter ↓val: The value.
275+
↓func process(value: Int) {}
276+
"""),
277+
// Missing one of multiple parameters
278+
Example(
279+
"""
280+
/// Processes values.
281+
/// - Parameters:
282+
/// - first: The first value.
283+
↓func process(first: Int, second: Int) {}
284+
"""),
285+
// Extra documented parameter that doesn't exist
286+
Example(
287+
"""
288+
/// Does something.
289+
/// - Parameters:
290+
/// - value: A value.
291+
/// - ↓extra: This doesn't exist.
292+
func doSomething(value: Int) {}
293+
"""),
294+
// Internal name used instead of external name
295+
Example(
296+
"""
297+
/// Updates the label.
298+
/// - Parameter ↓text: The new text.
299+
↓func updateLabel(with text: String) {}
300+
"""),
301+
// validate_returns: missing returns doc
302+
Example(
303+
"""
304+
/// Computes a value.
305+
↓func compute() -> Int { 0 }
306+
""", configuration: ["validate_returns": true]),
307+
// validate_returns: unexpected returns doc on void function
308+
Example(
309+
"""
310+
/// Does something.
311+
/// - ↓Returns: Some value.
312+
func doSomething() {}
313+
""", configuration: ["validate_returns": true]),
314+
// validate_throws: missing throws doc
315+
Example(
316+
"""
317+
/// Parses the input.
318+
↓func parse() throws {}
319+
""", configuration: ["validate_throws": true]),
320+
// validate_throws: unexpected throws doc on non-throwing function
321+
Example(
322+
"""
323+
/// Does something safe.
324+
/// - ↓Throws: Some error.
325+
func safe() {}
326+
""", configuration: ["validate_throws": true]),
327+
// enforce_parameter_syntax: plural block with 1 param — should use singular
328+
Example(
329+
"""
330+
/// Does something.
331+
/// - ↓Parameters:
332+
/// - value: The value.
333+
func single(value: Int) {}
334+
""", configuration: ["enforce_parameter_syntax": true]),
335+
// enforce_parameter_syntax: multiple singular lines with 2+ params — should use plural
336+
Example(
337+
"""
338+
/// Does something.
339+
/// - Parameter a: First.
340+
/// - ↓Parameter b: Second.
341+
func multi(a: Int, b: Int) {}
342+
""", configuration: ["enforce_parameter_syntax": true]),
343+
]
344+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import SwiftLintCore
2+
3+
@AutoConfigParser
4+
struct DocCommentParameterConfiguration: SeverityBasedRuleConfiguration {
5+
@ConfigurationElement(key: "severity")
6+
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
7+
@ConfigurationElement(key: "validate_returns")
8+
private(set) var validateReturns = false
9+
@ConfigurationElement(key: "validate_throws")
10+
private(set) var validateThrows = false
11+
@ConfigurationElement(key: "enforce_parameter_syntax")
12+
private(set) var enforceParameterSyntax = false
13+
}

0 commit comments

Comments
 (0)