From c7cf980b632eeb3072bacba58ebb503c584a5ff3 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Sun, 20 Apr 2025 22:55:45 +0100 Subject: [PATCH 01/19] First cut --- .../Configuration/Configuration+RulesWrapper.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index 2d883ee532..9a1b36e114 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.first { $0.rule is CustomRules }?.rule as? 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 }) { + if customRulesIdentifiers.intersection(onlyRulesRuleIdentifiers).isNotEmpty { + resultingRules.append((allRulesWrapped.first { $0.rule is CustomRules }?.rule)!) + } + } case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) } From febb2dfbd73bb94ee918e4bfc6601685b03f44c5 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Sun, 20 Apr 2025 23:07:58 +0100 Subject: [PATCH 02/19] Tweaks --- .../Configuration+RulesWrapper.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index 9a1b36e114..dc26178e65 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -11,13 +11,16 @@ internal extension Configuration { private let aliasResolver: (String) -> String private var invalidRuleIdsWarnedAbout: Set = [] - private var customRulesIdentifiers: Set { - Set((allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules)?.customRuleIdentifiers ?? []) - } - private var validRuleIdentifiers: Set { + private lazy var customRules: CustomRules? = { + allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules + }() + private lazy var customRulesIdentifiers: Set = { + Set(customRules?.customRuleIdentifiers ?? []) + }() + private lazy var validRuleIdentifiers: Set = { let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } return Set(regularRuleIdentifiers + customRulesIdentifiers) - } + }() private var cachedResultingRules: [any Rule]? private let resultingRulesLock = NSLock() @@ -47,8 +50,8 @@ internal extension Configuration { onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) }.map(\.rule) if !resultingRules.contains(where: { $0 is CustomRules }) { - if customRulesIdentifiers.intersection(onlyRulesRuleIdentifiers).isNotEmpty { - resultingRules.append((allRulesWrapped.first { $0.rule is CustomRules }?.rule)!) + if customRulesIdentifiers.intersection(onlyRulesRuleIdentifiers).isNotEmpty, let customRules { + resultingRules.append(customRules) } } From 890261f5c285b7e069c4c7feb5d5534d0a2553a9 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Sun, 20 Apr 2025 23:40:21 +0100 Subject: [PATCH 03/19] Added a test for configuration support --- Tests/FrameworkTests/CustomRulesTests.swift | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index cf98a15860..eec66ac459 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -875,6 +875,33 @@ final class CustomRulesTests: SwiftLintTestCase { XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/Tests/file.swift")) } + // MARK: - only_rules support + + func testOnlyRulesWithCustomRules() throws { + let ruleIdentifierToEnable = "aaa" + let customRules: [String: Any] = [ + ruleIdentifierToEnable: [ + "regex": "aaa" + ], + "bbb": [ + "regex": "bbb" + ], + ] + let example = Example(""" + let a = "aaa" + let b = "bbb" + """) + + let configDict: [String: Any] = [ + "only_rules": [ruleIdentifierToEnable], + "custom_rules": customRules, + ] + let configuration = try SwiftLintFramework.Configuration(dict: configDict) + let violations = TestHelpers.violations(example.skipWrappingInCommentTest(), config: configuration) + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, ruleIdentifierToEnable) + } + // MARK: - Private private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { From d138724b4aeab35b97323fbc51d8db574d29231d Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 03:02:44 +0100 Subject: [PATCH 04/19] Suppress the configuration warning if a custom rule is explicitly enabled --- .../Configuration/Configuration+Parsing.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index 33ae5e8bb9..30f760f738 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, + allRulesWrapped: allRulesWrapped ) } @@ -167,7 +168,8 @@ extension Configuration { parentConfiguration: Configuration?, configurationDictionary dict: [String: Any], ruleList: RuleList, - rulesMode: RulesMode + rulesMode: RulesMode, + allRulesWrapped: [ConfigurationRuleWrapper] ) { 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, + allRulesWrapped: allRulesWrapped + ) issue?.print() case let .defaultConfiguration(disabled: disabledRules, optIn: optInRules): let issue = validateConfiguredRuleIsEnabled( @@ -229,9 +235,15 @@ extension Configuration { static func validateConfiguredRuleIsEnabled( onlyRules: Set, - ruleType: any Rule.Type + ruleType: any Rule.Type, + allRulesWrapped: [ConfigurationRuleWrapper] ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { + if ruleType is CustomRules.Type, + let customRules = (allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules, + !Set(customRules.customRuleIdentifiers).intersection(onlyRules).isEmpty { + return nil + } return Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier) } return nil From c969cd7e824a9e6d8a70bca71d11e6ec94f28e6c Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 03:09:21 +0100 Subject: [PATCH 05/19] Refactor --- .../Configuration/Configuration+Parsing.swift | 2 +- .../Configuration/Configuration+RulesMode.swift | 2 +- .../Configuration+RulesWrapper.swift | 17 ++++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index 30f760f738..b048911c69 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -240,7 +240,7 @@ extension Configuration { ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { if ruleType is CustomRules.Type, - let customRules = (allRulesWrapped.first { $0.rule is CustomRules })?.rule as? CustomRules, + let customRules = allRulesWrapped.customRules, !Set(customRules.customRuleIdentifiers).intersection(onlyRules).isEmpty { 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 dc26178e65..b1e040a68c 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -12,7 +12,7 @@ internal extension Configuration { private var invalidRuleIdsWarnedAbout: Set = [] private lazy var customRules: CustomRules? = { - allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules + allRulesWrapped.customRules }() private lazy var customRulesIdentifiers: Set = { Set(customRules?.customRuleIdentifiers ?? []) @@ -209,10 +209,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 = customRules, + let childCustomRulesRule = child.allRulesWrapped.customRules else { // Merging is only needed if both parent & child have a custom rules rule return newAllRulesWrapped @@ -260,8 +258,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 @@ -319,3 +316,9 @@ internal extension Configuration { } } } + +extension [ConfigurationRuleWrapper] { + var customRules: CustomRules? { + first { $0.rule is CustomRules }?.rule as? CustomRules + } +} From 5e94e679beecb7de1d97f13ae75fa256788c1243 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 12:24:44 +0100 Subject: [PATCH 06/19] Test fixes --- .../Configuration/Configuration+Parsing.swift | 2 +- Tests/FrameworkTests/CustomRulesTests.swift | 57 ++++++++++++------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index b048911c69..35c64d0541 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -236,7 +236,7 @@ extension Configuration { static func validateConfiguredRuleIsEnabled( onlyRules: Set, ruleType: any Rule.Type, - allRulesWrapped: [ConfigurationRuleWrapper] + allRulesWrapped: [ConfigurationRuleWrapper] = [] ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { if ruleType is CustomRules.Type, diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index eec66ac459..4db2ea9537 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -879,29 +879,27 @@ final class CustomRulesTests: SwiftLintTestCase { func testOnlyRulesWithCustomRules() throws { let ruleIdentifierToEnable = "aaa" - let customRules: [String: Any] = [ - ruleIdentifierToEnable: [ - "regex": "aaa" - ], - "bbb": [ - "regex": "bbb" - ], - ] - let example = Example(""" - let a = "aaa" - let b = "bbb" - """) - - let configDict: [String: Any] = [ - "only_rules": [ruleIdentifierToEnable], - "custom_rules": customRules, - ] - let configuration = try SwiftLintFramework.Configuration(dict: configDict) - let violations = TestHelpers.violations(example.skipWrappingInCommentTest(), config: configuration) + 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( + // Order of custom rule violations is not deterministic :-( + violationsWithIndividualRuleIdentifiers.map { $0.ruleIdentifier }.sorted(), + customRuleIdentifiers + ) + let violationsWithCustomRulesIdentifier = try testOnlyRulesWithCustomRules(["custom_rules"]) + XCTAssertEqual( + violationsWithCustomRulesIdentifier.map { $0.ruleIdentifier }.sorted(), + customRuleIdentifiers + ) + } + // MARK: - Private private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { @@ -1100,6 +1098,27 @@ final class CustomRulesTests: SwiftLintTestCase { // Should produce no violations since there are no built-in attributes XCTAssertEqual(violations.count, 0) } + + 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 { From 953cf75d749e33bcb2b0b77e804fb15697874c09 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 12:35:56 +0100 Subject: [PATCH 07/19] Use isDisjoint --- .../Configuration/Configuration+Parsing.swift | 2 +- .../Configuration/Configuration+RulesWrapper.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index 35c64d0541..96f2fd88d1 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -241,7 +241,7 @@ extension Configuration { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { if ruleType is CustomRules.Type, let customRules = allRulesWrapped.customRules, - !Set(customRules.customRuleIdentifiers).intersection(onlyRules).isEmpty { + !Set(customRules.customRuleIdentifiers).isDisjoint(with: onlyRules) { return nil } return Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index b1e040a68c..9307fe857c 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -50,7 +50,7 @@ internal extension Configuration { onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) }.map(\.rule) if !resultingRules.contains(where: { $0 is CustomRules }) { - if customRulesIdentifiers.intersection(onlyRulesRuleIdentifiers).isNotEmpty, let customRules { + if !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), let customRules { resultingRules.append(customRules) } } From 18e9d065df57c553c7abc3c84a263c336c491a4f Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 13:46:19 +0100 Subject: [PATCH 08/19] Simplified logic --- .../Configuration/Configuration+RulesWrapper.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index 9307fe857c..f4182e2ebb 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -49,10 +49,10 @@ internal extension Configuration { resultingRules = allRulesWrapped.filter { tuple in onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).identifier) }.map(\.rule) - if !resultingRules.contains(where: { $0 is CustomRules }) { - if !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), let customRules { - resultingRules.append(customRules) - } + if !resultingRules.contains(where: { $0 is CustomRules }), + !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), + let customRules { + resultingRules.append(customRules) } case var .defaultConfiguration(disabledRuleIdentifiers, optInRuleIdentifiers): From f813a2a73914ffbd4814e86a184cf1ede8464d28 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 18:06:22 +0100 Subject: [PATCH 09/19] Added a test for command line only --- Tests/FrameworkTests/ConfigurationTests.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Tests/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index f88852c94c..d43a1157c6 100644 --- a/Tests/FrameworkTests/ConfigurationTests.swift +++ b/Tests/FrameworkTests/ConfigurationTests.swift @@ -142,6 +142,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) From 6f532d04f03d31c9eb518f27b8ef66c0dd3a6258 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Mon, 21 Apr 2025 18:19:50 +0100 Subject: [PATCH 10/19] Refactored --- .../Configuration/Configuration+Parsing.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index 96f2fd88d1..50e4f3a373 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -88,7 +88,7 @@ extension Configuration { configurationDictionary: dict, ruleList: ruleList, rulesMode: rulesMode, - allRulesWrapped: allRulesWrapped + customRuleIdentifiers: Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) ) } @@ -169,7 +169,7 @@ extension Configuration { configurationDictionary dict: [String: Any], ruleList: RuleList, rulesMode: RulesMode, - allRulesWrapped: [ConfigurationRuleWrapper] + customRuleIdentifiers: Set ) { for key in dict.keys where !validGlobalKeys.contains(key) { guard let identifier = ruleList.identifier(for: key), @@ -184,7 +184,7 @@ extension Configuration { let issue = validateConfiguredRuleIsEnabled( onlyRules: onlyRules, ruleType: ruleType, - allRulesWrapped: allRulesWrapped + customRuleIdentifiers: customRuleIdentifiers ) issue?.print() case let .defaultConfiguration(disabled: disabledRules, optIn: optInRules): @@ -236,12 +236,10 @@ extension Configuration { static func validateConfiguredRuleIsEnabled( onlyRules: Set, ruleType: any Rule.Type, - allRulesWrapped: [ConfigurationRuleWrapper] = [] + customRuleIdentifiers: Set ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { - if ruleType is CustomRules.Type, - let customRules = allRulesWrapped.customRules, - !Set(customRules.customRuleIdentifiers).isDisjoint(with: onlyRules) { + if ruleType is CustomRules.Type, !customRuleIdentifiers.isDisjoint(with: onlyRules) { return nil } return Issue.ruleNotPresentInOnlyRules(ruleID: ruleType.identifier) From 8c796f3535e325bb6372cc05ac7553f1c797bf0b Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 00:04:09 +0100 Subject: [PATCH 11/19] Fixed unit tests --- .../Configuration/Configuration+Parsing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index 50e4f3a373..bf327accdb 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -236,7 +236,7 @@ extension Configuration { static func validateConfiguredRuleIsEnabled( onlyRules: Set, ruleType: any Rule.Type, - customRuleIdentifiers: Set + customRuleIdentifiers: Set = [] ) -> Issue? { if onlyRules.isDisjoint(with: ruleType.description.allIdentifiers) { if ruleType is CustomRules.Type, !customRuleIdentifiers.isDisjoint(with: onlyRules) { From 54f8b175a06cc7c27baa8b2da6ef34ac24f43a4e Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 00:22:00 +0100 Subject: [PATCH 12/19] Tweaks --- Tests/FrameworkTests/ConfigurationTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index d43a1157c6..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 @@ -155,7 +156,7 @@ final class ConfigurationTests: SwiftLintTestCase { let config = try Configuration( dict: [ "only_rules": only, - "custom_rules": customRules + "custom_rules": customRules, ], onlyRule: [customRuleIdentifier] ) From 06f1d8834662b4e7725af008495c1a1abb671194 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 03:53:09 +0100 Subject: [PATCH 13/19] Removed comment about sort order --- Tests/FrameworkTests/CustomRulesTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 4db2ea9537..41a73f564e 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -889,7 +889,6 @@ final class CustomRulesTests: SwiftLintTestCase { let violationsWithIndividualRuleIdentifiers = try testOnlyRulesWithCustomRules(customRuleIdentifiers) XCTAssertEqual(violationsWithIndividualRuleIdentifiers.count, 2) XCTAssertEqual( - // Order of custom rule violations is not deterministic :-( violationsWithIndividualRuleIdentifiers.map { $0.ruleIdentifier }.sorted(), customRuleIdentifiers ) From 0fd0bfedc12849dcfecbbd6ee46efaa575cbcf18 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 11:28:04 +0100 Subject: [PATCH 14/19] Custom rule violations are now reported in a deterministic order --- Source/SwiftLintFramework/Rules/CustomRules.swift | 3 +++ Tests/FrameworkTests/CustomRulesTests.swift | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index d54c57488e..9275036214 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -45,6 +45,9 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { customRuleConfigurations.append(ruleConfiguration) } + customRuleConfigurations.sort { first, second in + first.identifier < second.identifier + } } } diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 41a73f564e..2b4c858faa 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -889,14 +889,11 @@ final class CustomRulesTests: SwiftLintTestCase { let violationsWithIndividualRuleIdentifiers = try testOnlyRulesWithCustomRules(customRuleIdentifiers) XCTAssertEqual(violationsWithIndividualRuleIdentifiers.count, 2) XCTAssertEqual( - violationsWithIndividualRuleIdentifiers.map { $0.ruleIdentifier }.sorted(), + violationsWithIndividualRuleIdentifiers.map { $0.ruleIdentifier }, customRuleIdentifiers ) let violationsWithCustomRulesIdentifier = try testOnlyRulesWithCustomRules(["custom_rules"]) - XCTAssertEqual( - violationsWithCustomRulesIdentifier.map { $0.ruleIdentifier }.sorted(), - customRuleIdentifiers - ) + XCTAssertEqual(violationsWithIndividualRuleIdentifiers, violationsWithCustomRulesIdentifier) } // MARK: - Private From ef098ea55281f772115086faf5d8c04af1356f01 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 11:44:58 +0100 Subject: [PATCH 15/19] Tidying --- .../Configuration/Configuration+RulesWrapper.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index f4182e2ebb..d0e1e2a2d1 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -11,11 +11,8 @@ internal extension Configuration { private let aliasResolver: (String) -> String private var invalidRuleIdsWarnedAbout: Set = [] - private lazy var customRules: CustomRules? = { - allRulesWrapped.customRules - }() private lazy var customRulesIdentifiers: Set = { - Set(customRules?.customRuleIdentifiers ?? []) + Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) }() private lazy var validRuleIdentifiers: Set = { let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } @@ -51,7 +48,7 @@ internal extension Configuration { }.map(\.rule) if !resultingRules.contains(where: { $0 is CustomRules }), !customRulesIdentifiers.isDisjoint(with: onlyRulesRuleIdentifiers), - let customRules { + let customRules = allRulesWrapped.customRules { resultingRules.append(customRules) } @@ -209,7 +206,7 @@ internal extension Configuration { newAllRulesWrapped: [ConfigurationRuleWrapper], with child: RulesWrapper ) -> [ConfigurationRuleWrapper] { guard - let parentCustomRulesRule = customRules, + let parentCustomRulesRule = allRulesWrapped.customRules, let childCustomRulesRule = child.allRulesWrapped.customRules else { // Merging is only needed if both parent & child have a custom rules rule From 9d4554de2e0e749f5bcfd3405ba1899e2f8430bc Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 11:48:51 +0100 Subject: [PATCH 16/19] Anonymous arguments --- Source/SwiftLintFramework/Rules/CustomRules.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index 9275036214..3da8c68f61 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -45,9 +45,7 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { customRuleConfigurations.append(ruleConfiguration) } - customRuleConfigurations.sort { first, second in - first.identifier < second.identifier - } + customRuleConfigurations.sort { $0.identifier < $1.identifier } } } From e650e174eae0f5a0a0c05c780ca9116ef0e939e8 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 13:10:24 +0100 Subject: [PATCH 17/19] added entry --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694500781c..e5e931b111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,13 @@ Ensure especially that the `error` level is not set to `nil` when the `warning` level isn't set either. [SimplyDanny](https://github.com/SimplyDanny) +* 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 From 6edca892123b1e4f645ce8ddd8eadd23fa546cc8 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Tue, 22 Apr 2025 16:12:30 +0100 Subject: [PATCH 18/19] Removed lazy --- .../Configuration/Configuration+RulesWrapper.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift index d0e1e2a2d1..36b2e9bb1b 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesWrapper.swift @@ -11,13 +11,13 @@ internal extension Configuration { private let aliasResolver: (String) -> String private var invalidRuleIdsWarnedAbout: Set = [] - private lazy var customRulesIdentifiers: Set = { + private var customRulesIdentifiers: Set { Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) - }() - private lazy var validRuleIdentifiers: Set = { + } + private var validRuleIdentifiers: Set { let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).identifier } return Set(regularRuleIdentifiers + customRulesIdentifiers) - }() + } private var cachedResultingRules: [any Rule]? private let resultingRulesLock = NSLock() From 16aafdea1a764be5d784b1a1db93abbf2414c7a3 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Sat, 24 May 2025 11:47:54 +0100 Subject: [PATCH 19/19] fixed formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e931b111..4636c80ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ Ensure especially that the `error` level is not set to `nil` when the `warning` level isn't set either. [SimplyDanny](https://github.com/SimplyDanny) + * 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