Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import SwiftLintCore
struct UnusedParameterConfiguration: SeverityBasedRuleConfiguration {
@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.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
}
115 changes: 87 additions & 28 deletions Source/SwiftLintCore/Models/RuleConfigurationDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,19 @@ extension RuleConfigurationDescription: Documentable {
guard hasContent else {
return ""
}
let includesDocumentationColumn = options.contains(where: \.hasDocumentation)
let header = includesDocumentationColumn
? "<tr><th>Key</th><th>Value</th><th>Description</th></tr>"
: "<tr><th>Key</th><th>Value</th></tr>"
return """
<table>
<thead>
<tr><th>Key</th><th>Value</th></tr>
\(header)
</thead>
<tbody>
\(options.map { $0.markdown() }.joined(separator: "\n"))
\(options
.map { $0.markdown(includeDocumentationColumn: includesDocumentationColumn) }
.joined(separator: "\n"))
</tbody>
</table>
"""
Expand All @@ -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: "<nothing>", value: .empty)
public static let noOptions = Self(key: "<nothing>", 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 {
Expand All @@ -136,16 +147,35 @@ extension RuleConfigurationOption: Documentable {
}

public func markdown() -> String {
"""
<tr>
<td>
\(key)
</td>
<td>
\(value.markdown())
</td>
</tr>
"""
markdown(includeDocumentationColumn: hasDocumentation)
}

fileprivate func markdown(includeDocumentationColumn: Bool) -> String {
if includeDocumentationColumn {
return """
<tr>
<td>
\(key)
</td>
<td>
\(value.markdown())
</td>
<td>
\(documentation ?? "-")
</td>
</tr>
"""
}
return """
<tr>
<td>
\(key)
</td>
<td>
\(value.markdown())
</td>
</tr>
"""
}

public func oneLiner() -> String {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand All @@ -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) {
Expand Down Expand Up @@ -452,6 +498,9 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
/// Whether this configuration element will be inlined into its description.
public let inline: Bool

/// Optional documentation describing this configuration entry in rendered docs.
public let documentation: String?

private let deprecationNotice: DeprecationNotice?
private let documentPostprocessedValue: Bool
private let postprocessor: @Sendable (inout T) -> Void
Expand All @@ -473,6 +522,7 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
@preconcurrency
public init(wrappedValue value: T,
key: String,
documentation: String? = nil,
deprecationNotice: DeprecationNotice? = nil,
documentPostprocessedValue: Bool = true,
postprocessor: @escaping @Sendable (inout T) -> Void = { _ in }) {
Expand All @@ -481,6 +531,7 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
wrappedValue: value,
key: key,
inline: false,
documentation: documentation,
deprecationNotice: deprecationNotice,
documentPostprocessedValue: documentPostprocessedValue,
postprocessor: postprocessor
Expand All @@ -496,8 +547,9 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
///
/// - Parameters:
/// - key: Optional name of the option. If not specified, it will be inferred from the attributed property.
public init<Wrapped>(key: String) where T == Wrapped? {
self.init(wrappedValue: nil, key: key, inline: false)
/// - documentation: Optional documentation describing the option in rendered docs.
public init<Wrapped>(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.
Expand All @@ -507,23 +559,26 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
/// - inline: If `true`, the option will be handled as it would be part of its parent. All of its options
/// will be inlined. Otherwise, it will be treated as a normal nested configuration with its name
/// inferred from the name of the attributed property.
public init(wrappedValue value: T, inline: Bool) where T: InlinableOptionType {
/// - documentation: Optional documentation describing the option in rendered docs.
public init(wrappedValue value: T, inline: Bool, documentation: String? = nil) where T: InlinableOptionType {
assert(inline, "Only 'inline: true' is allowed at the moment.")
self.init(wrappedValue: value, key: "", inline: inline)
self.init(wrappedValue: value, key: "", inline: inline, documentation: documentation)
}

/// Constructor for an ``InlinableOptionType`` with a name. The configuration will explicitly not be inlined.
///
/// - Parameters:
/// - value: Value to be wrapped.
/// - key: Name of the option.
public init(wrappedValue value: T, key: String) where T: InlinableOptionType {
self.init(wrappedValue: value, key: key, inline: false)
/// - documentation: Optional documentation describing the option in rendered docs.
public init(wrappedValue value: T, key: String, documentation: String? = nil) where T: InlinableOptionType {
self.init(wrappedValue: value, key: key, inline: false, documentation: documentation)
}

private init(wrappedValue: T,
key: String,
inline: Bool,
documentation: String? = nil,
deprecationNotice: DeprecationNotice? = nil,
documentPostprocessedValue: Bool = true,
postprocessor: @escaping @Sendable (inout T) -> Void = { _ in }) {
Expand All @@ -532,6 +587,7 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
self.wrappedValue = wrappedValue
self.key = key
self.inline = inline
self.documentation = documentation
self.deprecationNotice = deprecationNotice
self.documentPostprocessedValue = documentPostprocessedValue
self.postprocessor = postprocessor
Expand All @@ -544,7 +600,10 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab

extension ConfigurationElement: AnyConfigurationElement {
fileprivate var description: RuleConfigurationDescription {
(documentPostprocessedValue ? wrappedValue : rawWrappedValue).asDescription(with: key)
(documentPostprocessedValue ? wrappedValue : rawWrappedValue).asDescription(
with: key,
documentation: documentation
)
}
}

Expand Down Expand Up @@ -673,11 +732,11 @@ public extension AcceptableByConfigurationElement where Self: RuleConfiguration
.nested(.from(configuration: self))
}

func asDescription(with key: String) -> 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) {
Expand All @@ -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
Expand All @@ -704,6 +763,6 @@ public extension SeverityConfiguration {
"""
)
}
return RuleConfigurationDescription(options: [key => option])
return asDescriptionImpl(key: key, option: option, documentation: documentation)
}
}
45 changes: 45 additions & 0 deletions Tests/CoreTests/RuleConfigurationDescriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() == """
<table>
<thead>
<tr><th>Key</th><th>Value</th><th>Description</th></tr>
</thead>
<tbody>
<tr>
<td>
documented
</td>
<td>
true
</td>
<td>
Shown in docs
</td>
</tr>
<tr>
<td>
plain
</td>
<td>
2
</td>
<td>
-
</td>
</tr>
</tbody>
</table>
""")
}

@Test
func emptyDescription() {
let description = description { RuleConfigurationOption.noOptions }
Expand Down