From 97c1dcd0fa0907fc33b7948f9e2459fbd97bddbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Copin?= Date: Mon, 17 Jul 2017 23:53:02 +0100 Subject: [PATCH] Add support for nested configuration Original commit here: https://github.com/realm/SwiftLint/commit/9eef125245e43a8ff333d45abd13f9f0177d7258 Significant modifications made by @jpsim --- .../Extensions/Configuration+Merging.swift | 64 +++++++++++++++++-- .../Models/Configuration.swift | 49 +++++++++++--- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift b/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift index 12493ec2b..2bcc61ff8 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift @@ -43,10 +43,66 @@ extension Configuration { return self } - // Currently merge simply overrides the current configuration with the new configuration. - // This requires that all configuration files be fully specified. In the future this should be - // improved to do a more intelligent merge allowing for partial nested configurations. + private struct HashableRule: Hashable { + let rule: Rule + + static func == (lhs: HashableRule, rhs: HashableRule) -> Bool { + // Don't use `isEqualTo` in case its internal implementation changes from + // using the identifier to something else, which could mess up with the `Set` + return type(of: lhs.rule).description.identifier == type(of: rhs.rule).description.identifier + } + + var hashValue: Int { + return type(of: rule).description.identifier.hashValue + } + } + + private func mergingRules(with configuration: Configuration) -> [Rule] { + guard configuration.whitelistRules.isEmpty else { + // Use an intermediate set to filter out duplicate rules when merging configurations + // (always use the nested rule first if it exists) + return Set(configuration.rules.map(HashableRule.init)) + .union(rules.map(HashableRule.init)) + .map { $0.rule } + .filter { rule in + return configuration.whitelistRules.contains(type(of: rule).description.identifier) + } + } + + // Same here + return Set( + configuration.rules + // Enable rules that are opt-in by the nested configuration + .filter { rule in + return configuration.optInRules.contains(type(of: rule).description.identifier) + } + .map(HashableRule.init) + ) + // And disable rules that are disabled by the nested configuration + .union( + rules.filter { rule in + return !configuration.disabledRules.contains(type(of: rule).description.identifier) + }.map(HashableRule.init) + ) + .map { $0.rule } + } + internal func merge(with configuration: Configuration) -> Configuration { - return configuration + return Configuration( + disabledRules: [], + optInRules: [], + whitelistRules: [], + included: configuration.included, // Always use the nested included directories + excluded: configuration.excluded, // Always use the nested excluded directories + // The minimum warning threshold if both exist, otherwise the nested, + // and if it doesn't exist try to use the parent one + warningThreshold: warningThreshold.map { warningThreshold in + return min(configuration.warningThreshold ?? .max, warningThreshold) + } ?? configuration.warningThreshold, + reporter: reporter, // Always use the parent reporter + rules: mergingRules(with: configuration), + cachePath: cachePath, // Always use the parent cache path + rootPath: configuration.rootPath + ) } } diff --git a/Source/SwiftLintFramework/Models/Configuration.swift b/Source/SwiftLintFramework/Models/Configuration.swift index 5f2aaa1a1..54e781bf4 100644 --- a/Source/SwiftLintFramework/Models/Configuration.swift +++ b/Source/SwiftLintFramework/Models/Configuration.swift @@ -23,9 +23,15 @@ public struct Configuration: Equatable { public var configurationPath: String? // if successfully loaded from a path public let cachePath: String? + // MARK: Rules Properties + // All rules enabled in this configuration, derived from disabled, opt-in and whitelist rules public let rules: [Rule] + internal let disabledRules: [String] + internal let optInRules: [String] + internal let whitelistRules: [String] + // MARK: Initializers public init?(disabledRules: [String] = [], @@ -46,11 +52,6 @@ public struct Configuration: Equatable { "configuration specified version \(pinnedVersion).") } - self.included = included - self.excluded = excluded - self.reporter = reporter - self.cachePath = cachePath - let configuredRules = configuredRules ?? (try? ruleList.configuredRules(with: [:])) ?? [] @@ -73,10 +74,8 @@ public struct Configuration: Equatable { return nil } - // set the config threshold to the threshold provided in the config file - self.warningThreshold = warningThreshold - // Precedence is enableAllRules > whitelistRules > everything else + let rules: [Rule] if enableAllRules { rules = configuredRules } else if !whitelistRules.isEmpty { @@ -97,6 +96,40 @@ public struct Configuration: Equatable { return optInRules.contains(id) || !(rule is OptInRule) } } + self.init(disabledRules: disabledRules, + optInRules: optInRules, + whitelistRules: whitelistRules, + included: included, + excluded: excluded, + warningThreshold: warningThreshold, + reporter: reporter, + rules: rules, + cachePath: cachePath) + } + + internal init(disabledRules: [String], + optInRules: [String], + whitelistRules: [String], + included: [String], + excluded: [String], + warningThreshold: Int?, + reporter: String, + rules: [Rule], + cachePath: String?, + rootPath: String? = nil) { + + self.disabledRules = disabledRules + self.optInRules = optInRules + self.whitelistRules = whitelistRules + self.included = included + self.excluded = excluded + self.reporter = reporter + self.cachePath = cachePath + self.rules = rules + self.rootPath = rootPath + + // set the config threshold to the threshold provided in the config file + self.warningThreshold = warningThreshold } private init(_ configuration: Configuration) {