mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
Over the years, SwiftLintFramework had become a fairly massive monolith, containing over 400 source files with both core infrastructure and rules. Architecturally, the rules should rely on the core infrastructure but not the other way around. There are two exceptions to this: `custom_rules` and `superfluous_disable_command` which need special integration with the linter infrastructure. Now the time has come to formalize this architecture and one way to do that is to move the core SwiftLint functionality out of SwiftLintFramework and into a new SwiftLintCore module that the rules can depend on. Beyond enforcing architectural patterns, this also has the advantage of speeding up incremental compilation by skipping rebuilding the core functionality when iterating on rules. Because the core functionality is always useful when building rules, I'm opting to import SwiftLintCore in SwiftLintFramework as `@_exported` so that it's implicitly available to all files in SwiftLintFramework without needing to import it directly. In a follow-up I'll also split the built-in rules and the extra rules into their own modules. More modularization is possible from there, but not planned. The bulk of this PR just moves files from `Source/SwiftLintFramework/*` to `Source/SwiftLintCore/*`. There are some other changes that can't be split up into their own PRs: * Change jazzy to document the SwiftLintCore module instead of SwiftLintFramework. * Change imports in unit tests to reflect where code was moved to. * Update `sourcery` make rule to reflect where code was moved to. * Create a new `coreRules` array and register those rules with the registry. This allows the `custom_rules` and `superfluous_disable_command` rule implementations to remain internal to the SwiftLintCore module, preventing more implementation details from leaking across architectural layers. * Move `RuleRegistry.registerAllRulesOnce()` out of the type declaration and up one level so it can access rules defined downstream from SwiftLintCore.
300 lines
14 KiB
Swift
300 lines
14 KiB
Swift
import Foundation
|
|
|
|
internal extension Configuration {
|
|
class RulesWrapper {
|
|
// MARK: - Properties
|
|
private static var isOptInRuleCache: [String: Bool] = [:]
|
|
|
|
let allRulesWrapped: [ConfigurationRuleWrapper]
|
|
internal let mode: RulesMode
|
|
private let aliasResolver: (String) -> String
|
|
|
|
private var invalidRuleIdsWarnedAbout: Set<String> = []
|
|
private var validRuleIdentifiers: Set<String> {
|
|
let regularRuleIdentifiers = allRulesWrapped.map { type(of: $0.rule).description.identifier }
|
|
let configurationCustomRulesIdentifiers =
|
|
(allRulesWrapped.first { $0.rule is CustomRules }?.rule as? CustomRules)?
|
|
.configuration.customRuleConfigurations.map { $0.identifier } ?? []
|
|
return Set(regularRuleIdentifiers + configurationCustomRulesIdentifiers)
|
|
}
|
|
|
|
private var cachedResultingRules: [Rule]?
|
|
private let resultingRulesLock = NSLock()
|
|
|
|
/// All rules enabled in this configuration,
|
|
/// derived from rule mode (only / optIn - disabled) & existing rules
|
|
var resultingRules: [Rule] {
|
|
// Lock for thread-safety (that's also why this is not a lazy var)
|
|
resultingRulesLock.lock()
|
|
defer { resultingRulesLock.unlock() }
|
|
|
|
// Return existing value if it's available
|
|
if let cachedResultingRules { return cachedResultingRules }
|
|
|
|
// Calculate value
|
|
let customRulesFilter: (RegexConfiguration) -> (Bool)
|
|
var resultingRules = [Rule]()
|
|
switch mode {
|
|
case .allEnabled:
|
|
customRulesFilter = { _ in true }
|
|
resultingRules = allRulesWrapped.map { $0.rule }
|
|
|
|
case var .only(onlyRulesRuleIdentifiers):
|
|
customRulesFilter = { onlyRulesRuleIdentifiers.contains($0.identifier) }
|
|
onlyRulesRuleIdentifiers = validate(ruleIds: onlyRulesRuleIdentifiers, valid: validRuleIdentifiers)
|
|
resultingRules = allRulesWrapped.filter { tuple in
|
|
onlyRulesRuleIdentifiers.contains(type(of: tuple.rule).description.identifier)
|
|
}.map { $0.rule }
|
|
|
|
case var .default(disabledRuleIdentifiers, optInRuleIdentifiers):
|
|
customRulesFilter = { !disabledRuleIdentifiers.contains($0.identifier) }
|
|
disabledRuleIdentifiers = validate(ruleIds: disabledRuleIdentifiers, valid: validRuleIdentifiers)
|
|
optInRuleIdentifiers = validate(optInRuleIds: optInRuleIdentifiers, valid: validRuleIdentifiers)
|
|
resultingRules = allRulesWrapped.filter { tuple in
|
|
let id = type(of: tuple.rule).description.identifier
|
|
return !disabledRuleIdentifiers.contains(id)
|
|
&& (!(tuple.rule is OptInRule) || optInRuleIdentifiers.contains(id))
|
|
}.map { $0.rule }
|
|
}
|
|
|
|
// Filter custom rules
|
|
if var customRulesRule = (resultingRules.first { $0 is CustomRules }) as? CustomRules {
|
|
customRulesRule.configuration.customRuleConfigurations =
|
|
customRulesRule.configuration.customRuleConfigurations.filter(customRulesFilter)
|
|
resultingRules = resultingRules.filter { !($0 is CustomRules) } + [customRulesRule]
|
|
}
|
|
|
|
// Sort by name
|
|
resultingRules = resultingRules.sorted {
|
|
type(of: $0).description.identifier < type(of: $1).description.identifier
|
|
}
|
|
|
|
// Store & return
|
|
cachedResultingRules = resultingRules
|
|
return resultingRules
|
|
}
|
|
|
|
lazy var disabledRuleIdentifiers: [String] = {
|
|
switch mode {
|
|
case let .default(disabled, _):
|
|
return validate(ruleIds: disabled, valid: validRuleIdentifiers, silent: true)
|
|
.sorted(by: <)
|
|
|
|
case let .only(onlyRules):
|
|
return validate(
|
|
ruleIds: Set(allRulesWrapped
|
|
.map { type(of: $0.rule).description.identifier }
|
|
.filter { !onlyRules.contains($0) }),
|
|
valid: validRuleIdentifiers,
|
|
silent: true
|
|
).sorted(by: <)
|
|
|
|
case .allEnabled:
|
|
return []
|
|
}
|
|
}()
|
|
|
|
// MARK: - Initializers
|
|
init(
|
|
mode: RulesMode,
|
|
allRulesWrapped: [ConfigurationRuleWrapper],
|
|
aliasResolver: @escaping (String) -> String,
|
|
originatesFromMergingProcess: Bool = false
|
|
) {
|
|
self.allRulesWrapped = allRulesWrapped
|
|
self.aliasResolver = aliasResolver
|
|
let mode = mode.applied(aliasResolver: aliasResolver)
|
|
|
|
// If this instance originates from a merging process, some custom rules may be treated as not activated
|
|
// Otherwise, custom rules should be treated as implicitly activated
|
|
self.mode = originatesFromMergingProcess
|
|
? mode
|
|
: mode.activateCustomRuleIdentifiers(allRulesWrapped: allRulesWrapped)
|
|
}
|
|
|
|
// MARK: - Methods: Validation
|
|
private func validate(optInRuleIds: Set<String>, valid: Set<String>) -> Set<String> {
|
|
validate(ruleIds: optInRuleIds, valid: valid.union([RuleIdentifier.all.stringRepresentation]))
|
|
}
|
|
|
|
private func validate(ruleIds: Set<String>, valid: Set<String>, silent: Bool = false) -> Set<String> {
|
|
// Process invalid rule identifiers
|
|
if !silent {
|
|
let invalidRuleIdentifiers = ruleIds.subtracting(valid)
|
|
if !invalidRuleIdentifiers.isEmpty {
|
|
for invalidRuleIdentifier in invalidRuleIdentifiers.subtracting(invalidRuleIdsWarnedAbout) {
|
|
invalidRuleIdsWarnedAbout.insert(invalidRuleIdentifier)
|
|
queuedPrintError(
|
|
"warning: '\(invalidRuleIdentifier)' is not a valid rule identifier"
|
|
)
|
|
}
|
|
|
|
queuedPrintError(
|
|
"Valid rule identifiers:\n\(valid.sorted().joined(separator: "\n"))"
|
|
)
|
|
}
|
|
}
|
|
|
|
// Return valid rule identifiers
|
|
return ruleIds.intersection(valid)
|
|
}
|
|
|
|
// MARK: Merging
|
|
func merged(with child: RulesWrapper) -> RulesWrapper {
|
|
// Merge allRulesWrapped
|
|
let newAllRulesWrapped = mergedAllRulesWrapped(with: child)
|
|
|
|
// Merge mode
|
|
let validRuleIdentifiers = self.validRuleIdentifiers.union(child.validRuleIdentifiers)
|
|
let newMode: RulesMode
|
|
switch child.mode {
|
|
case let .default(childDisabled, childOptIn):
|
|
newMode = mergeDefaultMode(
|
|
newAllRulesWrapped: newAllRulesWrapped,
|
|
child: child,
|
|
childDisabled: childDisabled,
|
|
childOptIn: childOptIn,
|
|
validRuleIdentifiers: validRuleIdentifiers
|
|
)
|
|
|
|
case let .only(childOnlyRules):
|
|
// Always use the child only rules
|
|
newMode = .only(childOnlyRules)
|
|
|
|
case .allEnabled:
|
|
// Always use .allEnabled mode
|
|
newMode = .allEnabled
|
|
}
|
|
|
|
// Assemble & return merged rules
|
|
return RulesWrapper(
|
|
mode: newMode,
|
|
allRulesWrapped: mergedCustomRules(newAllRulesWrapped: newAllRulesWrapped, with: child),
|
|
aliasResolver: { child.aliasResolver(self.aliasResolver($0)) },
|
|
originatesFromMergingProcess: true
|
|
)
|
|
}
|
|
|
|
private func mergedAllRulesWrapped(with child: RulesWrapper) -> [ConfigurationRuleWrapper] {
|
|
let mainConfigSet = Set(allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init))
|
|
let childConfigSet = Set(child.allRulesWrapped.map(HashableConfigurationRuleWrapperWrapper.init))
|
|
let childConfigRulesWithConfig = childConfigSet.filter {
|
|
$0.configurationRuleWrapper.initializedWithNonEmptyConfiguration
|
|
}
|
|
|
|
let rulesUniqueToChildConfig = childConfigSet.subtracting(mainConfigSet)
|
|
return childConfigRulesWithConfig // Include, if rule is configured in child
|
|
.union(rulesUniqueToChildConfig) // Include, if rule is in child config only
|
|
.union(mainConfigSet) // Use configurations from parent for remaining rules
|
|
.map { $0.configurationRuleWrapper }
|
|
}
|
|
|
|
private func mergedCustomRules(
|
|
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
|
|
else {
|
|
// Merging is only needed if both parent & child have a custom rules rule
|
|
return newAllRulesWrapped
|
|
}
|
|
|
|
// Create new custom rules rule, prioritizing child custom rules
|
|
var configuration = CustomRulesConfiguration()
|
|
configuration.customRuleConfigurations = childCustomRulesRule.configuration.customRuleConfigurations
|
|
+ parentCustomRulesRule.configuration.customRuleConfigurations.filter { parentConfig in
|
|
!childCustomRulesRule.configuration.customRuleConfigurations.contains { childConfig in
|
|
childConfig.identifier == parentConfig.identifier
|
|
}
|
|
}
|
|
var newCustomRulesRule = CustomRules()
|
|
newCustomRulesRule.configuration = configuration
|
|
|
|
return newAllRulesWrapped.filter { !($0.rule is CustomRules) } + [(newCustomRulesRule, true)]
|
|
}
|
|
|
|
private func mergeDefaultMode(
|
|
newAllRulesWrapped: [ConfigurationRuleWrapper],
|
|
child: RulesWrapper,
|
|
childDisabled: Set<String>,
|
|
childOptIn: Set<String>,
|
|
validRuleIdentifiers: Set<String>
|
|
) -> RulesMode {
|
|
let childDisabled = child.validate(ruleIds: childDisabled, valid: validRuleIdentifiers)
|
|
let childOptIn = child.validate(optInRuleIds: childOptIn, valid: validRuleIdentifiers)
|
|
|
|
switch mode { // Switch parent's mode. Child is in default mode.
|
|
case var .default(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(
|
|
disabled: Set(childDisabled).union(Set(disabled.filter { !childOptIn.contains($0) }))
|
|
.filter {
|
|
!isOptInRule($0, allRulesWrapped: newAllRulesWrapped)
|
|
},
|
|
optIn: Set(childOptIn).union(Set(optIn.filter { !childDisabled.contains($0) }))
|
|
.filter {
|
|
isOptInRule($0, allRulesWrapped: newAllRulesWrapped)
|
|
}
|
|
)
|
|
|
|
case var .only(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 }) {
|
|
if let childCustomRulesRule = (child.allRulesWrapped.first { $0.rule is CustomRules })?.rule
|
|
as? CustomRules {
|
|
onlyRules = onlyRules.union(
|
|
Set(
|
|
childCustomRulesRule.configuration.customRuleConfigurations.map { $0.identifier }
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
onlyRules = validate(ruleIds: onlyRules, valid: validRuleIdentifiers)
|
|
|
|
// Allow parent only rules that weren't disabled via the child config
|
|
// & opt-ins from the child config
|
|
return .only(Set(
|
|
childOptIn + onlyRules.filter { !childDisabled.contains($0) }
|
|
))
|
|
|
|
case .allEnabled:
|
|
// Opt-in to every rule that isn't disabled via child config
|
|
return .default(
|
|
disabled: childDisabled
|
|
.filter {
|
|
!isOptInRule($0, allRulesWrapped: newAllRulesWrapped)
|
|
},
|
|
optIn: Set(newAllRulesWrapped.map { type(of: $0.rule).description.identifier }
|
|
.filter {
|
|
!childDisabled.contains($0)
|
|
&& isOptInRule($0, allRulesWrapped: newAllRulesWrapped)
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Helpers
|
|
private func isOptInRule(
|
|
_ identifier: String, allRulesWrapped: [ConfigurationRuleWrapper]
|
|
) -> Bool {
|
|
if let cachedIsOptInRule = Self.isOptInRuleCache[identifier] {
|
|
return cachedIsOptInRule
|
|
}
|
|
|
|
let isOptInRule = allRulesWrapped
|
|
.first { type(of: $0.rule).description.identifier == identifier }?.rule is OptInRule
|
|
Self.isOptInRuleCache[identifier] = isOptInRule
|
|
return isOptInRule
|
|
}
|
|
}
|
|
}
|