Skip to content

Commit 64d9811

Browse files
authored
Support validating documentation for unnamed parameters (#1134) (#1138)
* Support validating documentation for unnamed parameters rdar://138771690 * Fix typos in code comments and rename local variables for clarity
1 parent 9171bdc commit 64d9811

File tree

2 files changed

+308
-36
lines changed

2 files changed

+308
-36
lines changed

Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift

Lines changed: 155 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -130,52 +130,109 @@ struct ParametersAndReturnValidator {
130130
}
131131

132132
var variants = DocumentationDataVariants<ParametersSection>()
133-
var allKnownParameterNames: Set<String> = []
133+
var allKnownFunctionParameterNames: Set<String> = []
134134
var parameterNamesByExternalName: [String: Set<String>] = [:]
135-
// Group the parameters by name to be able to diagnose parameters documented more than once.
136-
let parametersByName = [String: [Parameter]](grouping: parameters, by: \.name)
135+
136+
// Wrap the documented parameters in classes so that `isMatchedInAnySignature` can be modified and tracked across different signatures.
137+
// This is used later in this method body to raise warnings about documented parameters that wasn't found in any symbol representation's function signature.
138+
class ParameterReference {
139+
let wrapped: Parameter
140+
init(_ parameter: Parameter) {
141+
self.wrapped = parameter
142+
}
143+
144+
var isMatchedInAnySignature: Bool = false
145+
var name: String { wrapped.name }
146+
}
147+
let parameterReferences = parameters.map { ParameterReference($0) }
148+
// Accumulate which unnamed parameters in the function signatures are missing documentation.
149+
var undocumentedUnnamedParameters = Set<Int>()
137150

138151
for (trait, signature) in signatures {
139-
// Collect all language representations's parameter information from the function signatures.
140-
let knownParameterNames = Set(signature.parameters.map(\.name))
141-
allKnownParameterNames.formUnion(knownParameterNames)
152+
// Insert parameter documentation in the order of the signature.
153+
var orderedParameters = [ParameterReference?](repeating: nil, count: signature.parameters.count)
154+
var remainingDocumentedParameters = parameterReferences
142155

143-
// Remove documented parameters that don't apply to this language representation's function signature.
144-
var languageApplicableParametersByName = parametersByName.filter { knownParameterNames.contains($0.key) }
156+
// Some languages support both names and unnamed parameters, and allow mixing them in the same declaration.
157+
// It's an uncommon style to mix both in the same declaration but if the developer's code does this we try to minimize warnings
158+
// and display as much written documentation as possible on the rendered page by matching _named_ parameters first.
159+
// This only matters if the developer _both_ mixes named and unnamed parameters in the same declaration and documents those parameters out-of-order.
160+
//
161+
// For example, consider this C function:
162+
//
163+
// /// - Parameters:
164+
// /// - first: Some documentation
165+
// /// - last: Some documentation
166+
// /// - anything: Some documentation
167+
// void functionName (int first, int /*unnamed*/, int last);
168+
//
169+
// Because there is a parameter named "last", the 2nd parameter documentation matches the 3rd (named) function parameter instead of the 2nd (unnamed).
170+
// This means that the above doesn't raise any warnings and that the displayed order on the page is "first", "anything", "last".
171+
//
172+
// If we'd matched unnamed parameters first, the above would have matched the 2nd ("last") parameter documentation with the 2nd unnamed function parameter,
173+
// which would have resulted in two confusing warnings:
174+
// - One that the "last" parameter is missing documentation
175+
// - One that the "anything" parameter doesn't exist in the function signature.
176+
//
177+
// Again, unless the developer documents the mixed named and unnamed parameters out-of-order, both approaches behave the same.
145178

146-
// Add a missing error parameter documentation if needed.
147-
if trait == .objectiveC, Self.shouldAddObjectiveCErrorParameter(signatures, parameters) {
148-
languageApplicableParametersByName["error"] = [Parameter(name: "error", contents: Self.objcErrorDescription)] // This parameter is synthesized and doesn't have a source range.
179+
// As described above, match the named function parameters first.
180+
for (index, functionParameter) in signature.parameters.enumerated() where !functionParameter.isUnnamed {
181+
// While we're looping over the parameters, gather information about the function parameters to use in diagnostics later in this method body.
182+
if let externalName = functionParameter.externalName {
183+
parameterNamesByExternalName[externalName, default: []].insert(functionParameter.name)
184+
}
185+
allKnownFunctionParameterNames.insert(functionParameter.name)
186+
187+
// Match this function parameter with a named documented parameter.
188+
guard let parameterIndex = remainingDocumentedParameters.firstIndex(where: { $0.name == functionParameter.name }) else {
189+
continue
190+
}
191+
let parameter = remainingDocumentedParameters.remove(at: parameterIndex)
192+
parameter.isMatchedInAnySignature = true
193+
orderedParameters[index] = parameter
149194
}
150195

151-
// Ensure that the parameters are displayed in the order that they need to be passed.
152-
var sortedParameters: [Parameter] = []
153-
sortedParameters.reserveCapacity(languageApplicableParametersByName.count)
154-
for parameter in signature.parameters {
155-
if let foundParameter = languageApplicableParametersByName[parameter.name]?.first {
156-
sortedParameters.append(foundParameter)
157-
}
158-
// While we're looping over the parameters, gather the parameters with external names so that this information can be
159-
// used to diagnose authored documentation that uses the "external name" instead of the "name" to refer to the parameter.
160-
if let externalName = parameter.externalName {
161-
parameterNamesByExternalName[externalName, default: []].insert(parameter.name)
196+
// As describe above, match the unnamed parameters last.
197+
var unnamedParameterNumber = 0 // Track how many unnamed parameters we've encountered.
198+
for (index, functionParameter) in signature.parameters.enumerated() where functionParameter.isUnnamed {
199+
defer { unnamedParameterNumber += 1 }
200+
guard !remainingDocumentedParameters.isEmpty else {
201+
// If the function signature has more unnamed parameters than there are documented parameters, the remaining function parameters are missing documentation.
202+
undocumentedUnnamedParameters.insert(unnamedParameterNumber)
203+
continue
162204
}
205+
// Match this function parameter with a named documented parameter.
206+
let parameter = remainingDocumentedParameters.removeFirst()
207+
parameter.isMatchedInAnySignature = true
208+
orderedParameters[index] = parameter
163209
}
164210

165-
variants[trait] = ParametersSection(parameters: sortedParameters)
211+
// Add a missing error parameter documentation if needed.
212+
if trait == .objectiveC, Self.shouldAddObjectiveCErrorParameter(signatures, parameters) {
213+
orderedParameters.append(
214+
ParameterReference(
215+
Parameter(name: "error", contents: Self.objcErrorDescription) // This parameter is synthesized and doesn't have a source range.
216+
)
217+
)
218+
}
219+
220+
variants[trait] = ParametersSection(parameters: orderedParameters.compactMap { $0?.wrapped })
166221
}
167222

168-
// Diagnose documented parameters that's not found in any language representation's function signature.
169-
for parameter in parameters where !allKnownParameterNames.contains(parameter.name) {
223+
// Diagnose documented parameters that aren't found in any language representation's function signature.
224+
for parameterReference in parameterReferences where !parameterReference.isMatchedInAnySignature && !allKnownFunctionParameterNames.contains(parameterReference.name) {
225+
let parameter = parameterReference.wrapped
170226
if let matchingParameterNames = parameterNamesByExternalName[parameter.name] {
171227
diagnosticEngine.emit(makeExternalParameterNameProblem(parameter, knownParameterNamesWithSameExternalName: matchingParameterNames.sorted()))
172228
} else {
173-
diagnosticEngine.emit(makeExtraParameterProblem(parameter, knownParameterNames: allKnownParameterNames, symbolKind: symbolKind))
229+
diagnosticEngine.emit(makeExtraParameterProblem(parameter, knownParameterNames: allKnownFunctionParameterNames, symbolKind: symbolKind))
174230
}
175231
}
176232

177233
// Diagnose parameters that are documented more than once.
178-
for (name, parameters) in parametersByName where allKnownParameterNames.contains(name) && parameters.count > 1 {
234+
let documentedParametersByName = [String: [Parameter]](grouping: parameters, by: \.name)
235+
for (name, parameters) in documentedParametersByName where allKnownFunctionParameterNames.contains(name) && parameters.count > 1 {
179236
let first = parameters.first! // Each group is guaranteed to be non-empty.
180237
for parameter in parameters.dropFirst() {
181238
diagnosticEngine.emit(makeDuplicateParameterProblem(parameter, previous: first))
@@ -191,23 +248,28 @@ struct ParametersAndReturnValidator {
191248
if let parameterNameOrder = signatures.values.map(\.parameters).max(by: { $0.count < $1.count })?.map(\.name) {
192249
let parametersEndLocation = parameters.last?.range?.upperBound
193250

194-
var missingParameterNames = allKnownParameterNames.subtracting(["error"]).filter { parametersByName[$0] == nil }
251+
var missingParameterNames = allKnownFunctionParameterNames.subtracting(["error"]).filter { documentedParametersByName[$0] == nil }
195252
for parameter in parameters {
196253
// Parameters that are documented using their external name already has raised a more specific diagnostic.
197254
if let documentedByExternalName = parameterNamesByExternalName[parameter.name] {
198255
missingParameterNames.subtract(documentedByExternalName)
199256
}
200257
}
201258

202-
for parameterName in missingParameterNames{
259+
for parameterName in missingParameterNames {
203260
// Look for the parameter that should come after the missing parameter to insert the placeholder documentation in the right location.
204261
let parameterAfter = parameterNameOrder.drop(while: { $0 != parameterName }).dropFirst()
205-
.mapFirst(where: { parametersByName[$0]?.first! /* Each group is guaranteed to be non-empty */ })
262+
.mapFirst(where: { documentedParametersByName[$0]?.first! /* Each group is guaranteed to be non-empty */ })
206263

207264
// Match the placeholder formatting with the other parameters; either as a standalone parameter or as an item in a parameters outline.
208-
let standalone = parameterAfter?.isStandalone ?? parametersByName.first?.value.first?.isStandalone ?? false
265+
let standalone = parameterAfter?.isStandalone ?? parameters.first?.isStandalone ?? false
209266
diagnosticEngine.emit(makeMissingParameterProblem(name: parameterName, before: parameterAfter, standalone: standalone, lastParameterEndLocation: parametersEndLocation))
210267
}
268+
269+
for unnamedParameterNumber in undocumentedUnnamedParameters.sorted() {
270+
let standalone = parameters.first?.isStandalone ?? false
271+
diagnosticEngine.emit(makeMissingUnnamedParameterProblem(unnamedParameterNumber: unnamedParameterNumber, standalone: standalone, lastParameterEndLocation: parametersEndLocation))
272+
}
211273
}
212274

213275
return variants
@@ -584,7 +646,7 @@ struct ParametersAndReturnValidator {
584646
)
585647
}
586648

587-
/// Creates a new problem about a parameter that's missing documentation.
649+
/// Creates a new problem about a named parameter that's missing documentation.
588650
///
589651
/// ## Example
590652
///
@@ -603,6 +665,51 @@ struct ParametersAndReturnValidator {
603665
/// - location: The end of the last parameter. Used as the diagnostic location when the undocumented parameter is the last parameter of the symbol.
604666
/// - Returns: A new problem that suggests that the developer adds documentation for the parameter.
605667
private func makeMissingParameterProblem(name: String, before nextParameter: Parameter?, standalone: Bool, lastParameterEndLocation: SourceLocation?) -> Problem {
668+
_makeMissingParameterProblem(
669+
diagnosticSummary: "Parameter \(name.singleQuoted) is missing documentation",
670+
solutionText: "Document \(name.singleQuoted) parameter",
671+
replacementText: Self.newParameterDescription(name: name, standalone: standalone),
672+
before: nextParameter,
673+
standalone: standalone,
674+
lastParameterEndLocation: lastParameterEndLocation
675+
)
676+
}
677+
/// Creates a new problem about an unnamed parameter that's missing documentation.
678+
///
679+
/// ## Example
680+
///
681+
/// ```c
682+
/// /// - Parameters:
683+
/// /// - firstValue: Description of the first parameter
684+
/// /// ^
685+
/// /// Unnamed parameter #1 is missing documentation
686+
/// void doSomething(int firstValue, int /*unnamed*/);
687+
/// ```
688+
///
689+
/// - Parameters:
690+
/// - unnamedParameterNumber: A number indicating which unnamed parameter this diagnostic refers to.
691+
/// - standalone: `true` if the existing documented parameters use the standalone syntax `- Parameter someValue:` or `false` if the existing documented parameters use the `- someValue` syntax within a `- Parameters:` list.
692+
/// - location: The end of the last parameter. Used as the diagnostic location when the undocumented parameter is the last parameter of the symbol.
693+
/// - Returns: A new problem that suggests that the developer adds documentation for the parameter.
694+
private func makeMissingUnnamedParameterProblem(unnamedParameterNumber: Int, standalone: Bool, lastParameterEndLocation: SourceLocation?) -> Problem {
695+
_makeMissingParameterProblem(
696+
diagnosticSummary: "Unnamed parameter #\(unnamedParameterNumber + 1) is missing documentation",
697+
solutionText: "Document unnamed parameter #\(unnamedParameterNumber + 1)",
698+
replacementText: Self.newUnnamedParameterDescription(standalone: standalone),
699+
before: nil,
700+
standalone: standalone,
701+
lastParameterEndLocation: lastParameterEndLocation
702+
)
703+
}
704+
705+
private func _makeMissingParameterProblem(
706+
diagnosticSummary: String,
707+
solutionText: String,
708+
replacementText: String,
709+
before nextParameter: Parameter?,
710+
standalone: Bool,
711+
lastParameterEndLocation: SourceLocation?
712+
) -> Problem {
606713
let solutions: [Solution]
607714
if let insertLocation = nextParameter?.range?.lowerBound ?? lastParameterEndLocation {
608715
let extraWhitespace = "\n///" + String(repeating: " ", count: (nextParameter?.range?.lowerBound.column ?? 1 + (standalone ? 0 : 2) /* indent items in a parameter outline by 2 spaces */) - 1)
@@ -612,17 +719,17 @@ struct ParametersAndReturnValidator {
612719
// /// - nextParameter: Description
613720
// ^inserting "- parameterName: placeholder\n/// "
614721
// ^^^^ add newline after to insert before the other parameter
615-
replacement = Self.newParameterDescription(name: name, standalone: standalone) + extraWhitespace
722+
replacement = replacementText + extraWhitespace
616723
} else {
617724
// /// - Parameters:
618725
// /// - otherParameter: Description
619726
// ^inserting "\n/// - parameterName: placeholder"
620727
// ^^^^ add newline before to insert after the last parameter
621-
replacement = extraWhitespace + Self.newParameterDescription(name: name, standalone: standalone)
728+
replacement = extraWhitespace + replacementText
622729
}
623730
solutions = [
624731
Solution(
625-
summary: "Document \(name.singleQuoted) parameter",
732+
summary: solutionText,
626733
replacements: [
627734
Replacement(range: adjusted(insertLocation ..< insertLocation), replacement: replacement)
628735
]
@@ -638,7 +745,7 @@ struct ParametersAndReturnValidator {
638745
severity: .warning,
639746
range: adjusted(lastParameterEndLocation.map { $0 ..< $0 }),
640747
identifier: "org.swift.docc.MissingParameterDocumentation",
641-
summary: "Parameter \(name.singleQuoted) is missing documentation"
748+
summary: diagnosticSummary
642749
),
643750
possibleSolutions: solutions
644751
)
@@ -663,6 +770,10 @@ struct ParametersAndReturnValidator {
663770
private static func newParameterDescription(name: String, standalone: Bool) -> String {
664771
"- \(standalone ? "Parameter " : "")\(name): <#parameter description#>"
665772
}
773+
774+
private static func newUnnamedParameterDescription(standalone: Bool) -> String {
775+
"- \(standalone ? "Parameter " : "")<#uniqueParameterName#>: <#parameter description#>"
776+
}
666777
}
667778

668779
// MARK: Helper extensions
@@ -715,3 +826,11 @@ private extension SymbolGraph.Symbol.FunctionSignature {
715826
}
716827
}
717828
}
829+
830+
private extension SymbolGraph.Symbol.FunctionSignature.FunctionParameter {
831+
/// A Boolean value indicating whether this function parameter is "unnamed".
832+
var isUnnamed: Bool {
833+
// C and C++ use "" to indicate an unnamed function parameter whereas Swift use "_" to indicate an unnamed function parameter.
834+
name.isEmpty || name == "_"
835+
}
836+
}

0 commit comments

Comments
 (0)