diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index cb2cbc247..b7ee200f2 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,23 @@ 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 used to capture all messages normally printed to stdout and return them back to the caller. + /// + /// > Warning: Shall only be used in tests to verify console output. + /// + /// - parameter runner: The code to run. Messages printed during the execution are collected. + /// + /// - returns: The collected messages produced while running the code in the runner. + static func captureConsole(runner: () throws -> Void) rethrows -> String { + var console = "" + messageConsumer = { console += $0 } + defer { messageConsumer = nil } + try runner() + return console + } + + private 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 +122,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 be0cc48e1..022800c0f 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 b0287c482..85b540149 100644 --- a/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/IndentationWidthRuleTests.swift @@ -1,4 +1,5 @@ @testable import SwiftLintBuiltInRules +@testable import SwiftLintCore import SwiftLintTestHelpers import XCTest @@ -6,9 +7,12 @@ class IndentationWidthRuleTests: SwiftLintTestCase { func testInvalidIndentation() throws { var testee = IndentationWidthConfiguration() let defaultValue = testee.indentationWidth - for indentation in [0, -1, -5] { - try testee.apply(configuration: ["indentation_width": indentation]) + for indentation in [0, -1, -5] { + XCTAssertEqual( + try Issue.captureConsole { try testee.apply(configuration: ["indentation_width": indentation]) }, + "warning: Invalid configuration for 'indentation_width' rule. Falling back to default." + ) // Value remains the default. XCTAssertEqual(testee.indentationWidth, defaultValue) } diff --git a/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift b/Tests/SwiftLintFrameworkTests/RuleConfigurationDescriptionTests.swift index c8719cc7f..eee9cce12 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,15 @@ class RuleConfigurationDescriptionTests: XCTestCase { XCTAssertEqual(configuration.nestedSeverityLevels, SeverityLevelsConfiguration(warning: 6, error: 7)) } + func testDeprecationWarning() throws { + var configuration = TestConfiguration() + + XCTAssertEqual( + try Issue.captureConsole { try configuration.apply(configuration: ["set": [6, 7]]) }, + "warning: Configuration option 'set' in 'my_rule' rule is deprecated. Use the option 'other_opt' instead." + ) + } + func testInvalidKeys() throws { var configuration = TestConfiguration()