@@ -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