From bddee4ce1867ee70cb6cac6512c5255c0e5c21a8 Mon Sep 17 00:00:00 2001 From: Alexander Kochupalov Date: Mon, 25 May 2026 16:35:30 +0400 Subject: [PATCH] Fix `--only-rule` to run custom rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `activateCustomRuleIdentifiers` only expanded custom rules for the config's `only_rules:` (`.onlyConfiguration`) when `custom_rules` itself was the listed identifier — never for the `--only-rule` command-line flag (`.onlyCommandLine`), and never for individual custom rules listed in `only_rules:`. As a result `--only-rule custom_rules`, `--only-rule ` and `only_rules: []` silently produced no violations. Handle `.onlyCommandLine` the same way as `.onlyConfiguration`, and in both modes additionally enable the parent `custom_rules` rule when an individual custom rule is requested so it can actually run. Add `OnlyRuleCustomRulesTests` covering the three previously broken shapes plus a regression test that `--only-rule ` does not accidentally pull in the `custom_rules` parent. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 10 +++ .../Configuration+RulesMode.swift | 34 +++++++-- .../OnlyRuleCustomRulesTests.swift | 74 +++++++++++++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 Tests/FrameworkTests/OnlyRuleCustomRulesTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3510e2b45ee..f5b08423749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,16 @@ ### Bug Fixes +* Make `--only-rule` and `only_rules:` actually run custom rules. + `activateCustomRuleIdentifiers` previously only expanded custom rules for + the `only_rules:` config key when `custom_rules` itself was listed, + never for the `--only-rule` command-line flag or for an individual + custom rule identifier. As a result `--only-rule custom_rules`, + `--only-rule ` and `only_rules: []` + silently produced no violations. Now any of these enables the parent + `custom_rules` rule together with the requested custom rules. + [Chupik](https://github.com/Chupik) + * Detect and autocorrect missing whitespace before `else` in `guard` statements for the `statement_position` rule. [theamodhshetty](https://github.com/theamodhshetty) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift b/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift index cfc59022618..ff712226474 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+RulesMode.swift @@ -116,18 +116,38 @@ public extension Configuration { } internal func activateCustomRuleIdentifiers(allRulesWrapped: [ConfigurationRuleWrapper]) -> Self { - // In the only mode, if the custom rules rule is enabled, all custom rules are also enabled implicitly - // This method makes the implicitly explicit + // In the "only" modes the parent `custom_rules` rule and the individual custom rules are + // entangled: enabling `custom_rules` implies enabling every custom rule, and enabling any + // individual custom rule requires the parent `custom_rules` rule to be enabled too. + // This method makes those implicit relations explicit so that both `only_rules` (config) + // and `--only-rule` (command line) behave consistently — previously `--only-rule` left + // custom rules silently disabled, and `only_rules: []` did too. switch self { - case let .onlyConfiguration(onlyRules) where onlyRules.contains { - $0 == CustomRules.identifier - }: - let customRulesRule = allRulesWrapped.customRules - return .onlyConfiguration(onlyRules.union(Set(customRulesRule?.customRuleIdentifiers ?? []))) + case let .onlyConfiguration(onlyRules): + return .onlyConfiguration(Self.withCustomRulesActivated(onlyRules, allRulesWrapped: allRulesWrapped)) + + case let .onlyCommandLine(onlyRules): + return .onlyCommandLine(Self.withCustomRulesActivated(onlyRules, allRulesWrapped: allRulesWrapped)) default: return self } } + + private static func withCustomRulesActivated( + _ onlyRules: Set, + allRulesWrapped: [ConfigurationRuleWrapper] + ) -> Set { + let customRuleIdentifiers = Set(allRulesWrapped.customRules?.customRuleIdentifiers ?? []) + var onlyRules = onlyRules + if onlyRules.contains(CustomRules.identifier) { + // `custom_rules` is enabled → enable all of its custom rules. + onlyRules.formUnion(customRuleIdentifiers) + } else if !onlyRules.isDisjoint(with: customRuleIdentifiers) { + // At least one individual custom rule is enabled → enable the parent `custom_rules` rule. + onlyRules.insert(CustomRules.identifier) + } + return onlyRules + } } } diff --git a/Tests/FrameworkTests/OnlyRuleCustomRulesTests.swift b/Tests/FrameworkTests/OnlyRuleCustomRulesTests.swift new file mode 100644 index 00000000000..0bba4771614 --- /dev/null +++ b/Tests/FrameworkTests/OnlyRuleCustomRulesTests.swift @@ -0,0 +1,74 @@ +@testable import SwiftLintCore +@testable import SwiftLintFramework +import TestHelpers +import XCTest + +/// Tests that custom (regex) rules can be selected via `--only-rule` from the command line and via +/// `only_rules:` from a configuration file. +/// +/// Before the fix, `activateCustomRuleIdentifiers` only expanded custom rules for the +/// `.onlyConfiguration` mode (and even there, only when `custom_rules` itself was the listed +/// identifier), so: +/// +/// * `--only-rule custom_rules` ran the parent rule but had every individual custom rule filtered +/// out, producing zero violations. +/// * `--only-rule my_custom_rule` left the parent `custom_rules` rule out of the resulting rule set +/// entirely, so the custom rule never executed. +/// * `only_rules: [my_custom_rule]` had the same problem as the second case. +final class OnlyRuleCustomRulesTests: SwiftLintTestCase { + /// Instance-scoped so that a non-`Sendable` `[String: Any]` literal is not held as shared + /// mutable global state. A `static let` here is rejected by the Swift 6 language mode that + /// Bazel CI compiles with. + private var customRulesDict: [String: Any] { + [ + "custom_rules": [ + "rule_a": ["name": "RuleA", "regex": "a", "message": "msg"], + "rule_b": ["name": "RuleB", "regex": "b", "message": "msg"], + ], + ] + } + + // MARK: - `--only-rule` from the command line + + func testOnlyRuleFromCommandLineForIndividualCustomRule() throws { + let config = try Configuration(dict: customRulesDict, onlyRule: ["rule_a"]) + let activeCustomRules = activeCustomRuleIdentifiers(in: config) + XCTAssertEqual(activeCustomRules, ["rule_a"]) + } + + func testOnlyRuleFromCommandLineForCustomRulesParentEnablesAll() throws { + let config = try Configuration(dict: customRulesDict, onlyRule: ["custom_rules"]) + let activeCustomRules = activeCustomRuleIdentifiers(in: config) + XCTAssertEqual(activeCustomRules, ["rule_a", "rule_b"]) + } + + func testOnlyRuleFromCommandLineForUnrelatedRuleDoesNotEnableCustomRules() throws { + let config = try Configuration(dict: customRulesDict, onlyRule: ["line_length"]) + XCTAssertNil( + config.rules.first(where: { $0 is CustomRules }), + "`--only-rule` for an unrelated built-in rule must not pull in the custom_rules parent rule" + ) + } + + // MARK: - `only_rules:` in configuration + + func testOnlyRulesInConfigForIndividualCustomRule() throws { + var dict = customRulesDict + dict["only_rules"] = ["rule_a"] + let config = try Configuration(dict: dict) + let activeCustomRules = activeCustomRuleIdentifiers(in: config) + XCTAssertEqual(activeCustomRules, ["rule_a"]) + } + + // MARK: - Helpers + + /// Returns the identifiers of the custom rule configurations that remain active in `config`'s + /// `resultingRules`, sorted for deterministic comparison. Empty if the parent `custom_rules` + /// rule was filtered out entirely. + private func activeCustomRuleIdentifiers(in config: Configuration) -> [String] { + guard let customRules = config.rules.first(where: { $0 is CustomRules }) as? CustomRules else { + return [] + } + return customRules.configuration.customRuleConfigurations.map(\.identifier).sorted() + } +}