diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedParameterConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedParameterConfiguration.swift index bbab780bc8..bdc2d06dd8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedParameterConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedParameterConfiguration.swift @@ -4,6 +4,9 @@ import SwiftLintCore struct UnusedParameterConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) - @ConfigurationElement(key: "allow_underscore_prefixed_names") + @ConfigurationElement( + key: "allow_underscore_prefixed_names", + documentation: "Parameters whose names start with an underscore will not be considered unused." + ) private(set) var allowUnderscorePrefixedNames = false } diff --git a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift index f08bc4eed0..600d04f554 100644 --- a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift +++ b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift @@ -104,13 +104,19 @@ extension RuleConfigurationDescription: Documentable { guard hasContent else { return "" } + let includesDocumentationColumn = options.contains(where: \.hasDocumentation) + let header = includesDocumentationColumn + ? "KeyValueDescription" + : "KeyValue" return """ - + \(header) - \(options.map { $0.markdown() }.joined(separator: "\n")) + \(options + .map { $0.markdown(includeDocumentationColumn: includesDocumentationColumn) } + .joined(separator: "\n"))
KeyValue
""" @@ -124,10 +130,15 @@ extension RuleConfigurationDescription: Documentable { /// A single option of a ``RuleConfigurationDescription``. public struct RuleConfigurationOption: Equatable, Sendable { /// An option serving as a marker for an empty configuration description. - public static let noOptions = Self(key: "", value: .empty) + public static let noOptions = Self(key: "", value: .empty, documentation: nil) fileprivate let key: String fileprivate let value: OptionType + fileprivate let documentation: String? + + fileprivate var hasDocumentation: Bool { + documentation?.trimmingCharacters(in: .whitespacesAndNewlines).isNotEmpty == true + } } extension RuleConfigurationOption: Documentable { @@ -136,16 +147,35 @@ extension RuleConfigurationOption: Documentable { } public func markdown() -> String { - """ - - - \(key) - - - \(value.markdown()) - - - """ + markdown(includeDocumentationColumn: hasDocumentation) + } + + fileprivate func markdown(includeDocumentationColumn: Bool) -> String { + if includeDocumentationColumn { + return """ + + + \(key) + + + \(value.markdown()) + + + \(documentation ?? "-") + + + """ + } + return """ + + + \(key) + + + \(value.markdown()) + + + """ } public func oneLiner() -> String { @@ -289,7 +319,7 @@ public extension OptionType { /// /// - Returns: A configuration option built up by the given data. static func => (key: String, value: OptionType) -> RuleConfigurationOption { - RuleConfigurationOption(key: key, value: value) + RuleConfigurationOption(key: key, value: value, documentation: nil) } /// Create an option defined by nested configuration description. @@ -330,7 +360,7 @@ public protocol AcceptableByConfigurationElement { /// - key: Name of the option to be put into the description. /// /// - Returns: Configuration description of this object. - func asDescription(with key: String) -> RuleConfigurationDescription + func asDescription(with key: String, documentation: String?) -> RuleConfigurationDescription /// Update the object. /// @@ -342,8 +372,24 @@ public protocol AcceptableByConfigurationElement { /// Default implementations which are shortcuts applicable for most of the types conforming to the protocol. public extension AcceptableByConfigurationElement { - func asDescription(with key: String) -> RuleConfigurationDescription { - RuleConfigurationDescription(options: [key => asOption()]) + func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription { + asDescriptionImpl(key: key, option: asOption(), documentation: documentation) + } + + fileprivate func asDescriptionImpl(key: String, + option: OptionType? = nil, + documentation: String? = nil) -> RuleConfigurationDescription { + let option = option ?? asOption() + if let documentation, documentation.trimmingCharacters(in: .whitespacesAndNewlines).isNotEmpty { + return RuleConfigurationDescription(options: [ + RuleConfigurationOption( + key: key, + value: option, + documentation: documentation + ), + ]) + } + return RuleConfigurationDescription(options: [key => option]) } mutating func apply(_ value: Any, ruleID: String) throws(Issue) { @@ -452,6 +498,9 @@ public struct ConfigurationElement Void @@ -473,6 +522,7 @@ public struct ConfigurationElement Void = { _ in }) { @@ -481,6 +531,7 @@ public struct ConfigurationElement(key: String) where T == Wrapped? { - self.init(wrappedValue: nil, key: key, inline: false) + /// - documentation: Optional documentation describing the option in rendered docs. + public init(key: String, documentation: String? = nil) where T == Wrapped? { + self.init(wrappedValue: nil, key: key, inline: false, documentation: documentation) } /// Constructor for an ``InlinableOptionType`` without a key. @@ -507,9 +559,10 @@ public struct ConfigurationElement Void = { _ in }) { @@ -532,6 +587,7 @@ public struct ConfigurationElement RuleConfigurationDescription { + func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription { if key.isEmpty { return .from(configuration: self) } - return RuleConfigurationDescription(options: [key => asOption()]) + return asDescriptionImpl(key: key, documentation: documentation) } mutating func apply(_ value: Any, ruleID _: String) throws(Issue) { @@ -692,7 +751,7 @@ public extension AcceptableByConfigurationElement where Self: RuleConfiguration public extension SeverityConfiguration { /// Severity configurations are special in that they shall not be nested when an option name is provided. /// Instead, their only option value must be used together with the option name. - func asDescription(with key: String) -> RuleConfigurationDescription { + func asDescription(with key: String, documentation: String? = nil) -> RuleConfigurationDescription { let description = RuleConfigurationDescription.from(configuration: self) if key.isEmpty { return description @@ -704,6 +763,6 @@ public extension SeverityConfiguration { """ ) } - return RuleConfigurationDescription(options: [key => option]) + return asDescriptionImpl(key: key, option: option, documentation: documentation) } } diff --git a/Tests/CoreTests/RuleConfigurationDescriptionTests.swift b/Tests/CoreTests/RuleConfigurationDescriptionTests.swift index d46c63a61a..6cf3ed6d06 100644 --- a/Tests/CoreTests/RuleConfigurationDescriptionTests.swift +++ b/Tests/CoreTests/RuleConfigurationDescriptionTests.swift @@ -268,6 +268,51 @@ struct RuleConfigurationDescriptionTests { // swiftlint:disable:this type_body_ #expect(description.yaml() == "visible: true") } + @Test + func configurationElementDocumentationIsPropagatedToDescription() { + @AutoConfigParser + struct MockConfiguration: RuleConfiguration { + @ConfigurationElement(key: "documented", documentation: "Shown in docs") + var documented = true + + @ConfigurationElement(key: "plain") + var plain = 2 + } + + #expect( + RuleConfigurationDescription.from(configuration: MockConfiguration()).markdown() == """ + + + + + + + + + + + + + + + + +
KeyValueDescription
+ documented + + true + + Shown in docs +
+ plain + + 2 + + - +
+ """) + } + @Test func emptyDescription() { let description = description { RuleConfigurationOption.noOptions }