Skip to content

Commit

Permalink
Allow to set configuration elements as deprecated
Browse files Browse the repository at this point in the history
Automatically print an appropriate warning to the console.
  • Loading branch information
SimplyDanny committed Apr 23, 2024
1 parent 1b7fbc4 commit 2eaab32
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 3 deletions.
19 changes: 18 additions & 1 deletion Source/SwiftLintCore/Models/Issue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ public enum Issue: LocalizedError, Equatable {
/// The configuration didn't match internal expectations.
case invalidConfiguration(ruleID: String)

/// Issued when an option is deprecated. Suggests an alternative optionally.
case deprecatedConfigurationOption(ruleID: String, key: String, alternative: String? = nil)

/// Used in configuration parsing when no changes have been applied. Use only internally!
case nothingApplied(ruleID: String)

Expand Down Expand Up @@ -71,6 +74,10 @@ public enum Issue: LocalizedError, Equatable {
/// Flag to enable warnings for deprecations being printed to the console. Printing is enabled by default.
public static var printDeprecationWarnings = true

/// Hook to redirect all messages to a consumer other than stdout. Shall only be used in tests to verify
/// console output.
public static var messageConsumer: ((String) -> Void)? = nil

/// Wraps any `Error` into a `SwiftLintError.genericWarning` if it is not already a `SwiftLintError`.
///
/// - parameter error: Any `Error`.
Expand Down Expand Up @@ -102,13 +109,23 @@ public enum Issue: LocalizedError, Equatable {
if case .ruleDeprecated = self, !Self.printDeprecationWarnings {
return
}
queuedPrintError(errorDescription)
if let consumer = Self.messageConsumer {
consumer(errorDescription)
} else {
queuedPrintError(errorDescription)
}
}

private var message: String {
switch self {
case let .invalidConfiguration(id):
return "Invalid configuration for '\(id)' rule. Falling back to default."
case let .deprecatedConfigurationOption(id, key, alternative):
let baseMessage = "Configuration option '\(key)' in '\(id)' rule is deprecated."
if let alternative {
return baseMessage + " Use the option '\(alternative)' instead."
}
return baseMessage
case let .nothingApplied(ruleID: id):
return Self.invalidConfiguration(ruleID: id).message
case let .listedMultipleTime(id, times):
Expand Down
23 changes: 22 additions & 1 deletion Source/SwiftLintCore/Models/RuleConfigurationDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,18 @@ public protocol InlinableOptionType: AcceptableByConfigurationElement {}
///
@propertyWrapper
public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatable>: Equatable {
/// A deprecation notice.
public enum DeprecationNotice {
/// Warning suggesting an alternative option.
case suggestAlternative(ruleID: String, name: String)
}

/// Wrapped option value.
public var wrappedValue: T {
didSet {
if case let .suggestAlternative(id, name) = deprecationNotice {
Issue.deprecatedConfigurationOption(ruleID: id, key: key, alternative: name).print()
}
if wrappedValue != oldValue {
postprocessor(&wrappedValue)
}
Expand All @@ -437,18 +446,28 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
/// Whether this configuration element will be inlined into its description.
public let inline: Bool

private let deprecationNotice: DeprecationNotice?
private let postprocessor: (inout T) -> Void

/// Default constructor.
///
/// - Parameters:
/// - value: Value to be wrapped.
/// - key: Optional name of the option. If not specified, it will be inferred from the attributed property.
/// - deprecationNotice: An optional deprecation notice in case an option is outdated and/or has been replaced by
/// an alternative.
/// - postprocessor: Function to be applied to the wrapped value after parsing to validate and modify it.
public init(wrappedValue value: T,
key: String,
deprecationNotice: DeprecationNotice? = nil,
postprocessor: @escaping (inout T) -> Void = { _ in }) {
self.init(wrappedValue: value, key: key, inline: false, postprocessor: postprocessor)
self.init(
wrappedValue: value,
key: key,
inline: false,
deprecationNotice: deprecationNotice,
postprocessor: postprocessor
)

// Modify the set value immediately.
postprocessor(&wrappedValue)
Expand Down Expand Up @@ -488,10 +507,12 @@ public struct ConfigurationElement<T: AcceptableByConfigurationElement & Equatab
private init(wrappedValue: T,
key: String,
inline: Bool,
deprecationNotice: DeprecationNotice? = nil,
postprocessor: @escaping (inout T) -> Void = { _ in }) {
self.wrappedValue = wrappedValue
self.key = key
self.inline = inline
self.deprecationNotice = deprecationNotice
self.postprocessor = postprocessor
}

Expand Down
9 changes: 9 additions & 0 deletions Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ class IndentationWidthRuleTests: SwiftLintTestCase {
func testInvalidIndentation() throws {
var testee = IndentationWidthConfiguration()
let defaultValue = testee.indentationWidth

var messages = ""
Issue.messageConsumer = { messages = $0 }
defer { Issue.messageConsumer = nil }

for indentation in [0, -1, -5] {
try testee.apply(configuration: ["indentation_width": indentation])

// Value remains the default.
XCTAssertEqual(testee.indentationWidth, defaultValue)
XCTAssertEqual(
messages,
"warning: Invalid configuration for 'indentation_width' rule. Falling back to default."
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class RuleConfigurationDescriptionTests: XCTestCase {
postprocessor: { list in list = list.map { $0.uppercased() } }
)
var list = ["string1", "string2"]
@ConfigurationElement(key: "set")
@ConfigurationElement(key: "set", deprecationNotice: .suggestAlternative(ruleID: "my_rule", name: "other_opt"))
var set: Set<Int> = [1, 2, 3]
@ConfigurationElement(inline: true)
var severityConfig = SeverityConfiguration<Parent>(.error)
Expand Down Expand Up @@ -490,6 +490,20 @@ class RuleConfigurationDescriptionTests: XCTestCase {
XCTAssertEqual(configuration.nestedSeverityLevels, SeverityLevelsConfiguration(warning: 6, error: 7))
}

func testDeprecationWarning() throws {
var messages = ""
Issue.messageConsumer = { messages += $0 }
defer { Issue.messageConsumer = nil }

var configuration = TestConfiguration()
try configuration.apply(configuration: ["set": [6, 7]])

XCTAssertEqual(
messages,
"warning: Configuration option 'set' in 'my_rule' rule is deprecated. Use the option 'other_opt' instead."
)
}

func testInvalidKeys() throws {
var configuration = TestConfiguration()

Expand Down

0 comments on commit 2eaab32

Please sign in to comment.