diff --git a/CHANGELOG.md b/CHANGELOG.md index b87e71e2c9..9148f4f3dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,15 @@ [Martin Redington](https://github.com/mildm8nnered) [#5788](https://github.com/realm/SwiftLint/issues/5788) +* Fixes the `--only-rule` command line option, when a default `.swiftlint.yml` + is absent. Additionally rules specified with `--only-rule` on the command + line can now be disabled in a child configuration, to allow specific + directories to be excluded from the rule (or from being auto-corrected by + the rule), and `--only-rule` can now be specified multiple times + to run multiple rules. + [Martin Redington](https://github.com/mildm8nnered) + [#5711](https://github.com/realm/SwiftLint/issues/5711) + ## 0.57.0: Squeaky Clean Cycle #### Breaking diff --git a/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift b/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift index 4c151e8ab3..464553659e 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+FileGraph.swift @@ -41,7 +41,7 @@ package extension Configuration { // MARK: - Methods internal mutating func resultingConfiguration( enableAllRules: Bool, - onlyRule: String?, + onlyRule: [String], cachePath: String? ) throws -> Configuration { // Build if needed @@ -250,7 +250,7 @@ package extension Configuration { private func merged( configurationData: [(configurationDict: [String: Any], rootDirectory: String)], enableAllRules: Bool, - onlyRule: String?, + onlyRule: [String], cachePath: String? ) throws -> Configuration { // Split into first & remainder; use empty dict for first if the array is empty diff --git a/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift b/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift index 232cf0ae72..1528591840 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Parsing.swift @@ -42,7 +42,7 @@ extension Configuration { dict: [String: Any], ruleList: RuleList = RuleRegistry.shared.list, enableAllRules: Bool = false, - onlyRule: String? = nil, + onlyRule: [String] = [], cachePath: String? = nil ) throws { func defaultStringArray(_ object: Any?) -> [String] { [String].array(of: object) ?? [] } @@ -81,7 +81,7 @@ extension Configuration { analyzerRules: analyzerRules ) - if onlyRule == nil { + if onlyRule.isEmpty { Self.validateConfiguredRulesAreEnabled( parentConfiguration: parentConfiguration, configurationDictionary: dict, @@ -174,12 +174,12 @@ extension Configuration { } switch rulesMode { - case .allEnabled: + case .allCommandLine, .onlyCommandLine: return - case .only(let onlyRules): + case .onlyConfiguration(let onlyRules): let issue = validateConfiguredRuleIsEnabled(onlyRules: onlyRules, ruleType: ruleType) issue?.print() - case let .default(disabled: disabledRules, optIn: optInRules): + case let .defaultConfiguration(disabled: disabledRules, optIn: optInRules): let issue = validateConfiguredRuleIsEnabled( parentConfiguration: parentConfiguration, disabledRules: disabledRules, @@ -201,9 +201,11 @@ extension Configuration { var disabledInParentRules: Set = [] var allEnabledRules: Set = [] - if case .only(let onlyRules) = parentConfiguration?.rulesMode { + if case .onlyConfiguration(let onlyRules) = parentConfiguration?.rulesMode { enabledInParentRules = onlyRules - } else if case .default(let parentDisabledRules, let parentOptInRules) = parentConfiguration?.rulesMode { + } else if case .defaultConfiguration( + let parentDisabledRules, let parentOptInRules + ) = parentConfiguration?.rulesMode { enabledInParentRules = parentOptInRules disabledInParentRules = parentDisabledRules } @@ -243,7 +245,7 @@ extension Configuration { allEnabledRules: Set, ruleType: any Rule.Type ) -> Issue? { - if case .allEnabled = parentConfiguration?.rulesMode { + if case .allCommandLine = parentConfiguration?.rulesMode { if disabledRules.contains(ruleType.identifier) { return Issue.ruleDisabledInDisabledRules(ruleID: ruleType.identifier) } @@ -264,7 +266,7 @@ extension Configuration { if enabledInParentRules.union(optInRules).isDisjoint(with: allIdentifiers) { return Issue.ruleNotEnabledInOptInRules(ruleID: ruleType.identifier) } - } else if case .only(let enabledInParentRules) = parentConfiguration?.rulesMode, + } else if case .onlyConfiguration(let enabledInParentRules) = parentConfiguration?.rulesMode, enabledInParentRules.isDisjoint(with: allIdentifiers) { return Issue.ruleNotEnabledInParentOnlyRules(ruleID: ruleType.identifier) } diff --git a/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift b/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift index e03cadd0a2..897fbfe39e 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+RulesMode.swift @@ -21,17 +21,21 @@ public extension Configuration { /// The default rules mode, which will enable all rules that aren't defined as being opt-in /// (conforming to the `OptInRule` protocol), minus the rules listed in `disabled`, plus the rules listed in /// `optIn`. - case `default`(disabled: Set, optIn: Set) + case defaultConfiguration(disabled: Set, optIn: Set) - /// Only enable the rules explicitly listed. - case only(Set) + /// Only enable the rules explicitly listed in the configuration files. + case onlyConfiguration(Set) + + /// Only enable the rule(s) explicitly listed on the command line (and their aliases). `--only-rule` can be + /// specified multiple times to enable multiple rules. + case onlyCommandLine(Set) /// Enable all available rules. - case allEnabled + case allCommandLine internal init( enableAllRules: Bool, - onlyRule: String?, + onlyRule: [String], onlyRules: [String], optInRules: [String], disabledRules: [String], @@ -48,9 +52,9 @@ public extension Configuration { } if enableAllRules { - self = .allEnabled - } else if let onlyRule { - self = .only(Set([onlyRule])) + self = .allCommandLine + } else if onlyRule.isNotEmpty { + self = .onlyCommandLine(Set(onlyRule)) } else if onlyRules.isNotEmpty { if disabledRules.isNotEmpty || optInRules.isNotEmpty { throw Issue.genericWarning( @@ -61,7 +65,7 @@ public extension Configuration { } warnAboutDuplicates(in: onlyRules + analyzerRules) - self = .only(Set(onlyRules + analyzerRules)) + self = .onlyConfiguration(Set(onlyRules + analyzerRules)) } else { warnAboutDuplicates(in: disabledRules) @@ -86,23 +90,28 @@ public extension Configuration { } warnAboutDuplicates(in: effectiveOptInRules + effectiveAnalyzerRules) - self = .default(disabled: Set(disabledRules), optIn: Set(effectiveOptInRules + effectiveAnalyzerRules)) + self = .defaultConfiguration( + disabled: Set(disabledRules), optIn: Set(effectiveOptInRules + effectiveAnalyzerRules) + ) } } internal func applied(aliasResolver: (String) -> String) -> Self { switch self { - case let .default(disabled, optIn): - return .default( + case let .defaultConfiguration(disabled, optIn): + return .defaultConfiguration( disabled: Set(disabled.map(aliasResolver)), optIn: Set(optIn.map(aliasResolver)) ) - case let .only(onlyRules): - return .only(Set(onlyRules.map(aliasResolver))) + case let .onlyConfiguration(onlyRules): + return .onlyConfiguration(Set(onlyRules.map(aliasResolver))) + + case let .onlyCommandLine(onlyRules): + return .onlyCommandLine(Set(onlyRules.map(aliasResolver))) - case .allEnabled: - return .allEnabled + case .allCommandLine: + return .allCommandLine } } @@ -110,9 +119,11 @@ public extension Configuration { // In the only mode, if the custom rules rule is enabled, all custom rules are also enabled implicitly // This method makes the implicitly explicit switch self { - case let .only(onlyRules) where onlyRules.contains { $0 == CustomRules.description.identifier }: + case let .onlyConfiguration(onlyRules) where onlyRules.contains { + $0 == CustomRules.identifier + }: let customRulesRule = (allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules - return .only(onlyRules.union(Set(customRulesRule?.customRuleIdentifiers ?? []))) + return .onlyConfiguration(onlyRules.union(Set(customRulesRule?.customRuleIdentifiers ?? []))) default: return self diff --git a/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift b/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift index e06a692a6f..2a947148a1 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+RulesWrapper.swift @@ -34,18 +34,18 @@ internal extension Configuration { let customRulesFilter: (RegexConfiguration) -> (Bool) var resultingRules = [any Rule]() switch mode { - case .allEnabled: + case .allCommandLine: customRulesFilter = { _ in true } resultingRules = allRulesWrapped.map(\.rule) - case var .only(onlyRulesRuleIdentifiers): + case let .onlyConfiguration(onlyRulesRuleIdentifiers), let .onlyCommandLine(onlyRulesRuleIdentifiers): customRulesFilter = { onlyRulesRuleIdentifiers.contains($0.identifier) } - onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers) + let onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers) resultingRules = allRulesWrapped.filter { tuple in onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).description.identifier) }.map(\.rule) - case var .default(disabledRuleIdentifiers, optInRuleIdentifiers): + case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) } disabledRuleIdentifiers = validate(ruleIds: disabledRuleIdentifiers, valid: validRuleIdentifiers) optInRuleIdentifiers = validate(optInRuleIds: optInRuleIdentifiers, valid: validRuleIdentifiers) @@ -75,11 +75,11 @@ internal extension Configuration { lazy var disabledRuleIdentifiers: [String] = { switch mode { - case let .default(disabled, _): + case let .defaultConfiguration(disabled, _): return validate(ruleIds: disabled, valid: validRuleIdentifiers, silent: true) .sorted(by: <) - case let .only(onlyRules): + case let .onlyConfiguration(onlyRules), let .onlyCommandLine(onlyRules): return validate( ruleIds: Set(allRulesWrapped .map { type(of: $0.rule).description.identifier } @@ -88,7 +88,7 @@ internal extension Configuration { silent: true ).sorted(by: <) - case .allEnabled: + case .allCommandLine: return [] } }() @@ -147,7 +147,7 @@ internal extension Configuration { let validRuleIdentifiers = self.validRuleIdentifiers.union(child.validRuleIdentifiers) let newMode: RulesMode switch child.mode { - case let .default(childDisabled, childOptIn): + case let .defaultConfiguration(childDisabled, childOptIn): newMode = mergeDefaultMode( newAllRulesWrapped: newAllRulesWrapped, child: child, @@ -156,13 +156,21 @@ internal extension Configuration { validRuleIdentifiers: validRuleIdentifiers ) - case let .only(childOnlyRules): - // Always use the child only rules - newMode = .only(childOnlyRules) + case let .onlyConfiguration(childOnlyRules): + // Use the child only rules, unless the parent is onlyRule + switch mode { + case let .onlyCommandLine(onlyRules): + newMode = .onlyCommandLine(onlyRules) + default: + newMode = .onlyConfiguration(childOnlyRules) + } + case let .onlyCommandLine(onlyRules): + // Always use the only rule + newMode = .onlyCommandLine(onlyRules) - case .allEnabled: + case .allCommandLine: // Always use .allEnabled mode - newMode = .allEnabled + newMode = .allCommandLine } // Assemble & return merged rules @@ -225,12 +233,12 @@ internal extension Configuration { let childOptIn = child.validate(optInRuleIds: childOptIn, valid: validRuleIdentifiers) switch mode { // Switch parent's mode. Child is in default mode. - case var .default(disabled, optIn): + case var .defaultConfiguration(disabled, optIn): disabled = validate(ruleIds: disabled, valid: validRuleIdentifiers) optIn = child.validate(optInRuleIds: optIn, valid: validRuleIdentifiers) // Only use parent disabled / optIn if child config doesn't tell the opposite - return .default( + return .defaultConfiguration( disabled: Set(childDisabled).union(Set(disabled.filter { !childOptIn.contains($0) })), optIn: Set(childOptIn).union(Set(optIn.filter { !childDisabled.contains($0) })) .filter { @@ -238,7 +246,7 @@ internal extension Configuration { } ) - case var .only(onlyRules): + case var .onlyConfiguration(onlyRules): // Also add identifiers of child custom rules iff the custom_rules rule is enabled // (parent custom rules are already added) if (onlyRules.contains { $0 == CustomRules.description.identifier }) { @@ -256,13 +264,17 @@ internal extension Configuration { // Allow parent only rules that weren't disabled via the child config // & opt-ins from the child config - return .only(Set( + return .onlyConfiguration(Set( childOptIn.union(onlyRules).filter { !childDisabled.contains($0) } )) - case .allEnabled: + case let .onlyCommandLine(onlyRules): + // Like .allEnabled, rules can be disabled in a child config + return .onlyCommandLine(onlyRules.filter { !childDisabled.contains($0) }) + + case .allCommandLine: // Opt-in to every rule that isn't disabled via child config - return .default( + return .defaultConfiguration( disabled: childDisabled .filter { !isOptInRule($0, allRulesWrapped: newAllRulesWrapped) diff --git a/Source/SwiftLintCore/Models/Configuration.swift b/Source/SwiftLintCore/Models/Configuration.swift index 47401ed79e..944bbb6792 100644 --- a/Source/SwiftLintCore/Models/Configuration.swift +++ b/Source/SwiftLintCore/Models/Configuration.swift @@ -147,7 +147,7 @@ public struct Configuration { /// - parameter writeBaseline: The path to write a baseline to. /// - parameter checkForUpdates: Check for updates to SwiftLint. package init( - rulesMode: RulesMode = .default(disabled: [], optIn: []), + rulesMode: RulesMode = .defaultConfiguration(disabled: [], optIn: []), allRulesWrapped: [ConfigurationRuleWrapper]? = nil, ruleList: RuleList = RuleRegistry.shared.list, fileGraph: FileGraph? = nil, @@ -210,7 +210,7 @@ public struct Configuration { public init( configurationFiles: [String], // No default value here to avoid ambiguous Configuration() initializer enableAllRules: Bool = false, - onlyRule: String? = nil, + onlyRule: [String] = [], cachePath: String? = nil, ignoreParentAndChildConfigs: Bool = false, mockedNetworkResults: [String: String] = [:], @@ -230,7 +230,13 @@ public struct Configuration { defer { basedOnCustomConfigurationFiles = hasCustomConfigurationFiles } let currentWorkingDirectory = FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized() - let rulesMode: RulesMode = enableAllRules ? .allEnabled : .default(disabled: [], optIn: []) + let rulesMode: RulesMode = if enableAllRules { + .allCommandLine + } else if onlyRule.isNotEmpty { + .onlyCommandLine(Set(onlyRule)) + } else { + .defaultConfiguration(disabled: [], optIn: []) + } // Try obtaining cached config let cacheIdentifier = "\(currentWorkingDirectory) - \(configurationFiles)" diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index 7afeef8abf..f9e24151b4 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -50,7 +50,7 @@ package struct LintOrAnalyzeOptions { let cachePath: String? let ignoreCache: Bool let enableAllRules: Bool - let onlyRule: String? + let onlyRule: [String] let autocorrect: Bool let format: Bool let compilerLogPath: String? @@ -78,7 +78,7 @@ package struct LintOrAnalyzeOptions { cachePath: String?, ignoreCache: Bool, enableAllRules: Bool, - onlyRule: String?, + onlyRule: [String], autocorrect: Bool, format: Bool, compilerLogPath: String?, diff --git a/Source/swiftlint/Commands/Analyze.swift b/Source/swiftlint/Commands/Analyze.swift index 7ff786edec..10471b1dda 100644 --- a/Source/swiftlint/Commands/Analyze.swift +++ b/Source/swiftlint/Commands/Analyze.swift @@ -13,8 +13,14 @@ extension SwiftLint { var compilerLogPath: String? @Option(help: "The path of a compilation database to use when running AnalyzerRules.") var compileCommands: String? - @Option(help: "Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`.") - var onlyRule: String? + @Option( + parsing: .singleValue, + help: """ + Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`. + Can be specified repeatedly to run multiple rules. + """ + ) + var onlyRule: [String] = [] @Argument(help: pathsArgumentDescription(for: .analyze)) var paths = [String]() diff --git a/Source/swiftlint/Commands/Lint.swift b/Source/swiftlint/Commands/Lint.swift index eadc1ab416..c1edb053b7 100644 --- a/Source/swiftlint/Commands/Lint.swift +++ b/Source/swiftlint/Commands/Lint.swift @@ -19,8 +19,14 @@ extension SwiftLint { var noCache = false @Flag(help: "Run all rules, even opt-in and disabled ones, ignoring `only_rules`.") var enableAllRules = false - @Option(help: "Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`.") - var onlyRule: String? + @Option( + parsing: .singleValue, + help: """ + Run only the specified rule, ignoring `only_rules`, `opt_in_rules` and `disabled_rules`. + Can be specified repeatedly to run multiple rules. + """ + ) + var onlyRule: [String] = [] @Argument(help: pathsArgumentDescription(for: .lint)) var paths = [String]() diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index fb6945ed47..c4830a3ed1 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -54,7 +54,7 @@ final class IntegrationTests: SwiftLintTestCase { } func testDefaultConfigurations() { - let defaultConfig = Configuration(rulesMode: .allEnabled).rules + let defaultConfig = Configuration(rulesMode: .allCommandLine).rules .map { type(of: $0) } .filter { $0.identifier != "custom_rules" } .map { diff --git a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift index 8d4aa43cb5..dc5cea9e4f 100644 --- a/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/CollectingRuleTests.swift @@ -147,7 +147,7 @@ extension MockCollectingRule { RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint) } static var configuration: Configuration? { - Configuration(rulesMode: .only([description.identifier]), ruleList: RuleList(rules: self)) + Configuration(rulesMode: .onlyConfiguration([identifier]), ruleList: RuleList(rules: self)) } init(configuration _: Any) throws { self.init() } diff --git a/Tests/SwiftLintFrameworkTests/CommandTests.swift b/Tests/SwiftLintFrameworkTests/CommandTests.swift index 5532fc0fa2..6ede31b63f 100644 --- a/Tests/SwiftLintFrameworkTests/CommandTests.swift +++ b/Tests/SwiftLintFrameworkTests/CommandTests.swift @@ -393,7 +393,9 @@ final class CommandTests: SwiftLintTestCase { } func testSuperfluousDisableCommandsDisabledOnConfiguration() { - let rulesMode = Configuration.RulesMode.default(disabled: ["superfluous_disable_command"], optIn: []) + let rulesMode = Configuration.RulesMode.defaultConfiguration( + disabled: ["superfluous_disable_command"], optIn: [] + ) let configuration = Configuration(rulesMode: rulesMode) XCTAssertEqual( @@ -456,7 +458,7 @@ final class CommandTests: SwiftLintTestCase { func testSuperfluousDisableCommandsEnabledForAnalyzer() { let configuration = Configuration( - rulesMode: .default(disabled: [], optIn: [UnusedDeclarationRule.description.identifier]) + rulesMode: .defaultConfiguration(disabled: [], optIn: [UnusedDeclarationRule.identifier]) ) let violations = violations( Example(""" diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift b/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift index a5c1328741..2c3c742e66 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationTests+MultipleConfigs.swift @@ -52,7 +52,7 @@ extension ConfigurationTests { func testOnlyRulesMerging() { let baseConfiguration = Configuration( - rulesMode: .default( + rulesMode: .defaultConfiguration( disabled: [], optIn: [ ForceTryRule.description.identifier, @@ -60,7 +60,7 @@ extension ConfigurationTests { ] ) ) - let onlyConfiguration = Configuration(rulesMode: .only([TodoRule.description.identifier])) + let onlyConfiguration = Configuration(rulesMode: .onlyConfiguration([TodoRule.identifier])) XCTAssertTrue(baseConfiguration.contains(rule: TodoRule.self)) XCTAssertEqual(onlyConfiguration.rules.count, 1) XCTAssertTrue(onlyConfiguration.rules[0] is TodoRule) @@ -77,6 +77,25 @@ extension ConfigurationTests { XCTAssertTrue(mergedConfiguration2.contains(rule: ForceTryRule.self)) } + func testOnlyRuleMerging() { + let ruleIdentifier = TodoRule.description.identifier + let onlyRuleConfiguration = Configuration.onlyRuleConfiguration(ruleIdentifier) + + let emptyDefaultConfiguration = Configuration.emptyDefaultConfiguration() + let mergedConfiguration1 = onlyRuleConfiguration.merged(withChild: emptyDefaultConfiguration) + XCTAssertEqual(mergedConfiguration1.rules.count, 1) + XCTAssertTrue(mergedConfiguration1.rules[0] is TodoRule) + + let disabledDefaultConfiguration = Configuration.disabledDefaultConfiguration(ruleIdentifier) + let mergedConfiguration2 = onlyRuleConfiguration.merged(withChild: disabledDefaultConfiguration) + XCTAssertTrue(mergedConfiguration2.rules.isEmpty) + + let enabledOnlyConfiguration = Configuration.enabledOnlyConfiguration(ForceTryRule.description.identifier) + let mergedConfiguration3 = onlyRuleConfiguration.merged(withChild: enabledOnlyConfiguration) + XCTAssertEqual(mergedConfiguration3.rules.count, 1) + XCTAssertTrue(mergedConfiguration3.rules[0] is TodoRule) + } + func testCustomRulesMerging() { let mergedConfiguration = Mock.Config._0CustomRules.merged( withChild: Mock.Config._2CustomRules, @@ -345,11 +364,11 @@ extension ConfigurationTests { XCTAssertTrue((ruleType as Any) is any OptInRule.Type) let ruleIdentifier = ruleType.description.identifier for testCase in testCases { - let parentConfiguration = Configuration(rulesMode: .default( + let parentConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: testCase.optedInInParent ? [ruleIdentifier] : [] )) - let childConfiguration = Configuration(rulesMode: .default( + let childConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: testCase.optedInInChild ? [ruleIdentifier] : [] )) @@ -380,10 +399,10 @@ extension ConfigurationTests { let ruleIdentifier = ruleType.description.identifier for testCase in testCases { let parentConfiguration = Configuration( - rulesMode: .default(disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: []) + rulesMode: .defaultConfiguration(disabled: testCase.disabledInParent ? [ruleIdentifier] : [], optIn: []) ) let childConfiguration = Configuration( - rulesMode: .default(disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: []) + rulesMode: .defaultConfiguration(disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: []) ) let mergedConfiguration = parentConfiguration.merged(withChild: childConfiguration) let isEnabled = mergedConfiguration.contains(rule: ruleType) @@ -410,9 +429,9 @@ extension ConfigurationTests { let ruleType = ImplicitReturnRule.self XCTAssertTrue((ruleType as Any) is any OptInRule.Type) let ruleIdentifier = ruleType.description.identifier - let parentConfiguration = Configuration(rulesMode: .only([ruleIdentifier])) + let parentConfiguration = Configuration(rulesMode: .onlyConfiguration([ruleIdentifier])) for testCase in testCases { - let childConfiguration = Configuration(rulesMode: .default( + let childConfiguration = Configuration(rulesMode: .defaultConfiguration( disabled: testCase.disabledInChild ? [ruleIdentifier] : [], optIn: testCase.optedInInChild ? [ruleIdentifier] : [] )) @@ -448,10 +467,10 @@ extension ConfigurationTests { ] let configurations = [ - Configuration(rulesMode: .default(disabled: [], optIn: [])), - Configuration(rulesMode: .default(disabled: [], optIn: [ruleIdentifier])), - Configuration(rulesMode: .default(disabled: [ruleIdentifier], optIn: [ruleIdentifier])), - Configuration(rulesMode: .default(disabled: [ruleIdentifier], optIn: [])), + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [])), + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier])), + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier])), + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [])), ] for parentConfiguration in parentConfigurations { @@ -466,7 +485,7 @@ extension ConfigurationTests { configuration: Configuration, ruleType: any Rule.Type ) { - guard case .default(let disabledRules, let optInRules) = configuration.rulesMode else { + guard case .defaultConfiguration(let disabledRules, let optInRules) = configuration.rulesMode else { XCTFail("Configuration rulesMode was not the default") return } @@ -637,20 +656,23 @@ extension ConfigurationTests { private extension Configuration { static func emptyDefaultConfiguration() -> Self { - Configuration(rulesMode: .default(disabled: [], optIn: [])) + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [])) } static func optInDefaultConfiguration(_ ruleIdentifier: String) -> Self { - Configuration(rulesMode: .default(disabled: [], optIn: [ruleIdentifier])) + Configuration(rulesMode: .defaultConfiguration(disabled: [], optIn: [ruleIdentifier])) } static func optInDisabledDefaultConfiguration(_ ruleIdentifier: String) -> Self { - Configuration(rulesMode: .default(disabled: [ruleIdentifier], optIn: [ruleIdentifier])) + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [ruleIdentifier])) } static func disabledDefaultConfiguration(_ ruleIdentifier: String) -> Self { - Configuration(rulesMode: .default(disabled: [ruleIdentifier], optIn: [])) + Configuration(rulesMode: .defaultConfiguration(disabled: [ruleIdentifier], optIn: [])) } - static func emptyOnlyConfiguration() -> Self { Configuration(rulesMode: .only([])) } + static func emptyOnlyConfiguration() -> Self { Configuration(rulesMode: .onlyConfiguration([])) } static func enabledOnlyConfiguration(_ ruleIdentifier: String) -> Self { - Configuration(rulesMode: .only([ruleIdentifier])) + Configuration(rulesMode: .onlyConfiguration([ruleIdentifier])) + } + static func allEnabledConfiguration() -> Self { Configuration(rulesMode: .allCommandLine)} + static func onlyRuleConfiguration(_ ruleIdentifier: String) -> Self { + Configuration(rulesMode: .onlyCommandLine([ruleIdentifier])) } - static func allEnabledConfiguration() -> Self { Configuration(rulesMode: .allEnabled)} } diff --git a/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift index f0ea288ba0..f718aed784 100644 --- a/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift +++ b/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift @@ -90,13 +90,27 @@ final class ConfigurationTests: SwiftLintTestCase { func testOnlyRule() throws { let configuration = try Configuration( dict: [:], - onlyRule: "nesting", + onlyRule: ["nesting"], cachePath: nil ) XCTAssertEqual(configuration.rules.count, 1) } + func testOnlyRuleMultiple() throws { + let onlyRuleIdentifiers = ["nesting", "todo"].sorted() + let configuration = try Configuration( + dict: ["only_rules": "line_length"], + onlyRule: onlyRuleIdentifiers, + cachePath: nil + ) + XCTAssertEqual(onlyRuleIdentifiers, configuration.enabledRuleIdentifiers) + + let childConfiguration = try Configuration(dict: ["disabled_rules": onlyRuleIdentifiers.last ?? ""]) + let mergedConfiguration = configuration.merged(withChild: childConfiguration) + XCTAssertEqual(onlyRuleIdentifiers.dropLast(), mergedConfiguration.enabledRuleIdentifiers) + } + func testOnlyRules() throws { let only = ["nesting", "todo"] @@ -180,10 +194,7 @@ final class ConfigurationTests: SwiftLintTestCase { "initializing Configuration with valid rules in YAML string should succeed") let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys .filter({ !([validRule] + optInRules).contains($0) })) - let configuredIdentifiers = Set(configuration.rules.map { - type(of: $0).description.identifier - }) - XCTAssertEqual(expectedIdentifiers, configuredIdentifiers) + XCTAssertEqual(expectedIdentifiers, Set(configuration.enabledRuleIdentifiers)) } func testDuplicatedRules() { @@ -592,3 +603,11 @@ private extension Sequence where Element == String { map { $0.absolutePathStandardized() } } } + +private extension Configuration { + var enabledRuleIdentifiers: [String] { + rules.map { + type(of: $0).identifier + }.sorted() + } +} diff --git a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift index dab162bfaf..4b5b8d3e22 100644 --- a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift +++ b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift @@ -52,7 +52,7 @@ final class SourceKitCrashTests: SwiftLintTestCase { file.assertHandler = { XCTFail("If this called, rule's SourceKitFreeRule is not properly configured") } - let configuration = Configuration(rulesMode: .only(allRuleIdentifiers)) + let configuration = Configuration(rulesMode: .onlyConfiguration(allRuleIdentifiers)) let storage = RuleStorage() _ = Linter(file: file, configuration: configuration).collect(into: storage).styleViolations(using: storage) file.sourcekitdFailed = false diff --git a/Tests/SwiftLintTestHelpers/TestHelpers.swift b/Tests/SwiftLintTestHelpers/TestHelpers.swift index 6a3293133f..c28f43e055 100644 --- a/Tests/SwiftLintTestHelpers/TestHelpers.swift +++ b/Tests/SwiftLintTestHelpers/TestHelpers.swift @@ -52,7 +52,7 @@ public let allRuleIdentifiers = Set(RuleRegistry.shared.list.list.keys) public extension Configuration { func applyingConfiguration(from example: Example) -> Configuration { guard let exampleConfiguration = example.configuration, - case let .only(onlyRules) = self.rulesMode, + case let .onlyConfiguration(onlyRules) = self.rulesMode, let firstRule = (onlyRules.first { $0 != "superfluous_disable_command" }), case let configDict: [_: any Sendable] = ["only_rules": onlyRules, firstRule: exampleConfiguration], let typedConfiguration = try? Configuration(dict: configDict) else { return self } @@ -281,12 +281,12 @@ public func makeConfig(_ ruleConfiguration: Any?, return (try? ruleType.init(configuration: ruleConfiguration)).flatMap { configuredRule in let rules = skipDisableCommandTests ? [configuredRule] : [configuredRule, SuperfluousDisableCommandRule()] return Configuration( - rulesMode: .only(identifiers), + rulesMode: .onlyConfiguration(identifiers), allRulesWrapped: rules.map { ($0, false) } ) } } - return Configuration(rulesMode: .only(identifiers)) + return Configuration(rulesMode: .onlyConfiguration(identifiers)) } private func testCorrection(_ correction: (Example, Example), @@ -299,7 +299,7 @@ private func testCorrection(_ correction: (Example, Example), #endif var config = configuration if let correctionConfiguration = correction.0.configuration, - case let .only(onlyRules) = configuration.rulesMode, + case let .onlyConfiguration(onlyRules) = configuration.rulesMode, let ruleToConfigure = (onlyRules.first { $0 != SuperfluousDisableCommandRule.description.identifier }), case let configDict: [_: any Sendable] = ["only_rules": onlyRules, ruleToConfigure: correctionConfiguration], let typedConfiguration = try? Configuration(dict: configDict) {