diff --git a/CHANGELOG.md b/CHANGELOG.md index 8758375069..8c7b06b6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ [Martin Redington](https://github.com/mildm8nnered) [#6052](https://github.com/realm/SwiftLint/issues/6052) +* Individual `custom_rules` can now be specified in the `only_rule` configuration + setting and the `--only-rule` command line option without having to specify + `custom_rules` as well. Additionally, violations of custom rules are now reported + in a deterministic order, sorted by the rule's identifier. + [Martin Redington](https://github.com/mildm8nnered) + [#6029](https://github.com/realm/SwiftLint/issues/6029) + [#6058](https://github.com/realm/SwiftLint/issues/6058) + ## 0.59.1: Crisp Spring Clean ### Breaking diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index c8326c0776..ef39bffe99 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -87,7 +87,8 @@ extension Configuration { parentConfiguration: parentConfiguration, configurationDictionary: dict, ruleList: ruleList, - rulesMode: rulesMode + rulesMode: rulesMode, + customRuleIdentifiers: Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) ) } @@ -167,7 +168,8 @@ extension Configuration { parentConfiguration: Configuration?, configurationDictionary dict: [String: Any], ruleList: RuleList, - rulesMode: RulesMode + rulesMode: RulesMode, + customRuleIdentifiers: Set ) { for key in dict.keys where !validGlobalKeys.contains(key) { guard let identifier = ruleList.identifier(for: key), @@ -179,7 +181,11 @@ extension Configuration { case .allCommandLine, .onlyCommandLine: return case .onlyConfiguration(let onlyRules): - let issue = validateConfiguredRuleIsEnabled(onlyRules: onlyRules, ruleType: ruleType) + let issue = validateConfiguredRuleIsEnabled( + onlyRules: onlyRules, + ruleType: ruleType, + customRuleIdentifiers: customRuleIdentifiers + ) issue?.print() case let .defaultConfiguration(disabled: disabledRules, optIn: optInRules): let issue = validateConfiguredRuleIsEnabled( @@ -229,9 +235,13 @@ extension Configuration { static func validateConfiguredRuleIsEnabled( onlyRules: Set, - ruleType: any Rule.Type + ruleType: any Rule.Type, + customRuleIdentifiers: Set = [] ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { + if ruleType is CustomRules.Type, !customRuleIdentifiers.isDisjoint(with: onlyRules) { + return nil + } return Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier) } return nil diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift index b5540cf3ad..cfc5902261 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift @@ -122,7 +122,7 @@ public extension Configuration { case let .onlyConfiguration(onlyRules) where onlyRules.contains { $0 == CustomRules.identifier }: - let customRulesRule = (allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules + let customRulesRule = allRulesWrapped.customRules return .onlyConfiguration(onlyRules.union(Set(customRulesRule?.customRuleIdentifiers ?? []))) default: diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index 2d883ee532..36b2e9bb1b 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -11,11 +11,12 @@ internal extension Configuration { private let aliasResolver: (String) -> String private var invalidRuleIdsWarnedAbout: Set = [] + private var customRulesIdentifiers: Set { + Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) + } private var validRuleIdentifiers: Set { let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } - let configurationCustomRulesIdentifiers = - (allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules)?.customRuleIdentifiers ?? [] - return Set(regularRuleIdentifiers + configurationCustomRulesIdentifiers) + return Set(regularRuleIdentifiers + customRulesIdentifiers) } private var cachedResultingRules: [any Rule]? @@ -45,6 +46,11 @@ internal extension Configuration { resultingRules = allRulesWrapped.filter { tuple in onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) }.map(\.rule) + if !resultingRules.contains(where: { $0 is CustomRules }), + !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), + let customRules = allRulesWrapped.customRules { + resultingRules.append(customRules) + } case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) } @@ -200,10 +206,8 @@ internal extension Configuration { newAllRulesWrapped: [ConfigurationRuleWrapper], with child: RulesWrapper ) -> [ConfigurationRuleWrapper] { guard - let parentCustomRulesRule = (allRulesWrapped.first { $0.rule is CustomRules })?.rule - as? CustomRules, - let childCustomRulesRule = (child.allRulesWrapped.first { $0.rule is CustomRules })?.rule - as? CustomRules + let parentCustomRulesRule = allRulesWrapped.customRules, + let childCustomRulesRule = child.allRulesWrapped.customRules else { // Merging is only needed if both parent & child have a custom rules rule return newAllRulesWrapped @@ -251,8 +255,7 @@ internal extension Configuration { // 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.identifier }) { - if let childCustomRulesRule = (child.allRulesWrapped.first { $0.rule is CustomRules })?.rule - as? CustomRules { + if let childCustomRulesRule = child.allRulesWrapped.customRules { onlyRules = onlyRules.union( Set( childCustomRulesRule.customRuleIdentifiers @@ -310,3 +313,9 @@ internal extension Configuration { } } } + +extension [ConfigurationRuleWrapper] { + var customRules: CustomRules? { + first { $0.rule is CustomRules }?.rule as? CustomRules + } +} diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index ec6a4f9000..fe4d175802 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -31,6 +31,7 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { customRuleConfigurations.append(ruleConfiguration) } + customRuleConfigurations.sort { $0.identifier < $1.identifier } } } diff --git a/Tests/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index f88852c94c..c4adcce111 100644 --- a/Tests/FrameworkTests/ConfigurationTests.swift +++ b/Tests/FrameworkTests/ConfigurationTests.swift @@ -7,6 +7,7 @@ import XCTest private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0) +// swiftlint:disable:next type_body_length final class ConfigurationTests: SwiftLintTestCase { // MARK: Setup & Teardown private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional @@ -142,6 +143,35 @@ final class ConfigurationTests: SwiftLintTestCase { ) } + func testOnlyRulesWithSpecificCustomRules() throws { + // Individual custom rules can be specified on the command line without specifying `custom_rules` as well. + let customRuleIdentifier = "my_custom_rule" + let customRuleIdentifier2 = "my_custom_rule2" + let only = ["custom_rules"] + let customRules = [ + customRuleIdentifier: ["name": "A custom rule", "regex": "this is illegal"], + customRuleIdentifier2: ["name": "Another custom rule", "regex": "this is also illegal"], + ] + + let config = try Configuration( + dict: [ + "only_rules": only, + "custom_rules": customRules, + ], + onlyRule: [customRuleIdentifier] + ) + guard let resultingCustomRules = config.rules.first(where: { $0 is CustomRules }) as? CustomRules + else { + XCTFail("Custom rules are expected to be present") + return + } + let enabledCustomRuleIdentifiers = + resultingCustomRules.configuration.customRuleConfigurations.map { rule in + rule.identifier + } + XCTAssertEqual(enabledCustomRuleIdentifiers, [customRuleIdentifier]) + } + func testWarningThreshold_value() throws { let config = try Configuration(dict: ["warning_threshold": 5]) XCTAssertEqual(config.warningThreshold, 5) diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 87691f4e79..0e1ca0f695 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -506,6 +506,27 @@ final class CustomRulesTests: SwiftLintTestCase { XCTAssertTrue(violations[3].isSuperfluousDisableCommandViolation(for: "rule2")) } + // MARK: - only_rules support + + func testOnlyRulesWithCustomRules() throws { + let ruleIdentifierToEnable = "aaa" + let violations = try testOnlyRulesWithCustomRules([ruleIdentifierToEnable]) + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, ruleIdentifierToEnable) + } + + func testOnlyRulesWithIndividualIdentifiers() throws { + let customRuleIdentifiers = ["aaa", "bbb"] + let violationsWithIndividualRuleIdentifiers = try testOnlyRulesWithCustomRules(customRuleIdentifiers) + XCTAssertEqual(violationsWithIndividualRuleIdentifiers.count, 2) + XCTAssertEqual( + violationsWithIndividualRuleIdentifiers.map { $0.ruleIdentifier }, + customRuleIdentifiers + ) + let violationsWithCustomRulesIdentifier = try testOnlyRulesWithCustomRules(["custom_rules"]) + XCTAssertEqual(violationsWithIndividualRuleIdentifiers, violationsWithCustomRulesIdentifier) + } + // MARK: - Private private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { @@ -568,6 +589,27 @@ final class CustomRulesTests: SwiftLintTestCase { customRules.configuration = customRuleConfiguration return customRules } + + private func testOnlyRulesWithCustomRules(_ onlyRulesIdentifiers: [String]) throws -> [StyleViolation] { + let customRules: [String: Any] = [ + "aaa": [ + "regex": "aaa" + ], + "bbb": [ + "regex": "bbb" + ], + ] + let example = Example(""" + let a = "aaa" + let b = "bbb" + """) + let configDict: [String: Any] = [ + "only_rules": onlyRulesIdentifiers, + "custom_rules": customRules, + ] + let configuration = try SwiftLintFramework.Configuration(dict: configDict) + return TestHelpers.violations(example.skipWrappingInCommentTest(), config: configuration) + } } private extension StyleViolation {