Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,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 <a_custom_rule>` and `only_rules: [<a_custom_rule>]`
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)

* Recognize `isolated` as an isolation modifier in `modifier_order`, so it can
be ordered via the `isolation` entry in `preferred_modifier_order`.
[leno23](https://github.com/leno23)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [<individual_custom_rule>]` 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<String>,
allRulesWrapped: [ConfigurationRuleWrapper]
) -> Set<String> {
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
}
}
}
74 changes: 74 additions & 0 deletions Tests/FrameworkTests/OnlyRuleCustomRulesTests.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}