diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index cb2cbc24717..1405717bb5e 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -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) @@ -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)? + /// Wraps any `Error` into a `SwiftLintError.genericWarning` if it is not already a `SwiftLintError`. /// /// - parameter error: Any `Error`. @@ -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): diff --git a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift index be0cc48e18a..022800c0fc2 100644 --- a/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift +++ b/Source/SwiftLintCore/Models/RuleConfigurationDescription.swift @@ -415,9 +415,18 @@ public protocol InlinableOptionType: AcceptableByConfigurationElement {} /// @propertyWrapper public struct ConfigurationElement: 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) } @@ -437,6 +446,7 @@ public struct ConfigurationElement Void /// Default constructor. @@ -444,11 +454,20 @@ public struct ConfigurationElement 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) @@ -488,10 +507,12 @@ public struct ConfigurationElement Void = { _ in }) { self.wrappedValue = wrappedValue self.key = key self.inline = inline + self.deprecationNotice = deprecationNotice self.postprocessor = postprocessor } diff --git a/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift b/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift index b0287c482a5..3b41143bba4 100644 --- a/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift @@ -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." + ) } } diff --git a/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift b/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift index c8719cc7f16..5954f6590e4 100644 --- a/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift +++ b/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift @@ -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 = [1, 2, 3] @ConfigurationElement(inline: true) var severityConfig = SeverityConfiguration(.error) @@ -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()