SwiftLint/Source/SwiftLintFramework/Models/Configuration.swift

293 lines
12 KiB
Swift

import Foundation
import SourceKittenFramework
/// The configuration struct for SwiftLint. User-defined in the `.swiftlint.yml` file, drives the behavior of SwiftLint.
public struct Configuration: Hashable {
/// Represents how a Configuration object can be configured with regards to rules.
public enum RulesMode {
/// The default rules mode, which will enable all rules that aren't defined as being opt-in
/// (conforming to the `OptInRule` protocol), minus the rules listed in `disabled`, plus the rules lised in
/// `optIn`.
case `default`(disabled: [String], optIn: [String])
/// Only enable the rules explicitly listed.
case whitelisted([String])
/// Enable all available rules.
case allEnabled
}
// MARK: Properties
/// The standard file name to look for user-defined configurations.
public static let fileName = ".swiftlint.yml"
/// The style to use when indenting Swift source code.
public let indentation: IndentationStyle
/// Included paths to lint.
public let included: [String]
/// Excluded paths to not lint.
public let excluded: [String]
/// The identifier for the `Reporter` to use to report style violations.
public let reporter: String
/// The threshold for the number of warnings to tolerate before treating the lint as having failed.
public let warningThreshold: Int?
/// The root directory to search for nested configurations.
public private(set) var rootPath: String?
/// The absolute path from where this configuration was loaded from, if any.
public private(set) var configurationPath: String?
/// The location of the persisted cache to use whith this configuration.
public let cachePath: String?
public func hash(into hasher: inout Hasher) {
if let configurationPath = configurationPath {
hasher.combine(configurationPath)
} else if let rootPath = rootPath {
hasher.combine(rootPath)
} else if let cachePath = cachePath {
hasher.combine(cachePath)
} else {
hasher.combine(included)
hasher.combine(excluded)
hasher.combine(reporter)
}
}
internal var computedCacheDescription: String?
internal var customRuleIdentifiers: [String] {
let customRule = rules.first(where: { $0 is CustomRules }) as? CustomRules
return customRule?.configuration.customRuleConfigurations.map { $0.identifier } ?? []
}
// MARK: Rules Properties
/// All rules enabled in this configuration, derived from disabled, opt-in and whitelist rules
public let rules: [Rule]
internal let rulesMode: RulesMode
// MARK: Initializers
/// Creates a `Configuration` by specifying its properties directly.
public init?(rulesMode: RulesMode = .default(disabled: [], optIn: []),
included: [String] = [],
excluded: [String] = [],
warningThreshold: Int? = nil,
reporter: String = XcodeReporter.identifier,
ruleList: RuleList = masterRuleList,
configuredRules: [Rule]? = nil,
swiftlintVersion: String? = nil,
cachePath: String? = nil,
indentation: IndentationStyle = .default,
customRulesIdentifiers: [String] = []) {
if let pinnedVersion = swiftlintVersion, pinnedVersion != Version.current.value {
queuedPrintError("Currently running SwiftLint \(Version.current.value) but " +
"configuration specified version \(pinnedVersion).")
exit(2)
}
let configuredRules = configuredRules
?? (try? ruleList.configuredRules(with: [:]))
?? []
let handleAliasWithRuleList: (String) -> String = { ruleList.identifier(for: $0) ?? $0 }
guard let rules = enabledRules(from: configuredRules,
with: rulesMode,
aliasResolver: handleAliasWithRuleList,
customRulesIdentifiers: customRulesIdentifiers) else {
return nil
}
self.init(rulesMode: rulesMode,
included: included,
excluded: excluded,
warningThreshold: warningThreshold,
reporter: reporter,
rules: rules,
cachePath: cachePath,
indentation: indentation)
}
internal init(rulesMode: RulesMode,
included: [String],
excluded: [String],
warningThreshold: Int?,
reporter: String,
rules: [Rule],
cachePath: String?,
rootPath: String? = nil,
indentation: IndentationStyle) {
self.rulesMode = rulesMode
self.included = included
self.excluded = excluded
self.reporter = reporter
self.cachePath = cachePath
self.rules = rules.sorted { type(of: $0).description.identifier < type(of: $1).description.identifier }
self.rootPath = rootPath
self.indentation = indentation
// set the config threshold to the threshold provided in the config file
self.warningThreshold = warningThreshold
}
private init(_ configuration: Configuration) {
rulesMode = configuration.rulesMode
included = configuration.included
excluded = configuration.excluded
warningThreshold = configuration.warningThreshold
reporter = configuration.reporter
rules = configuration.rules
cachePath = configuration.cachePath
rootPath = configuration.rootPath
indentation = configuration.indentation
}
/// Creates a `Configuration` with convenience parameters.
public init(path: String = Configuration.fileName, rootPath: String? = nil,
optional: Bool = true, quiet: Bool = false, enableAllRules: Bool = false,
cachePath: String? = nil, customRulesIdentifiers: [String] = []) {
let fullPath: String
if let rootPath = rootPath, rootPath.isDirectory() {
fullPath = path.bridge().absolutePathRepresentation(rootDirectory: rootPath)
} else {
fullPath = path.bridge().absolutePathRepresentation()
}
if let cachedConfig = Configuration.getCached(atPath: fullPath) {
self.init(cachedConfig)
configurationPath = fullPath
return
}
let fail = { (msg: String) in
queuedPrintError("\(fullPath):\(msg)")
queuedFatalError("Could not read configuration file at path '\(fullPath)'")
}
let rulesMode: RulesMode = enableAllRules ? .allEnabled : .default(disabled: [], optIn: [])
if path.isEmpty || !FileManager.default.fileExists(atPath: fullPath) {
if !optional { fail("File not found.") }
self.init(rulesMode: rulesMode, cachePath: cachePath, customRulesIdentifiers: customRulesIdentifiers)!
self.rootPath = rootPath
return
}
do {
let yamlContents = try String(contentsOfFile: fullPath, encoding: .utf8)
let dict = try YamlParser.parse(yamlContents)
if !quiet {
queuedPrintError("Loading configuration from '\(path)'")
}
self.init(dict: dict, enableAllRules: enableAllRules,
cachePath: cachePath, customRulesIdentifiers: customRulesIdentifiers)!
configurationPath = fullPath
self.rootPath = rootPath
setCached(atPath: fullPath)
return
} catch YamlParserError.yamlParsing(let message) {
fail(message)
} catch {
fail("\(error)")
}
self.init(rulesMode: rulesMode, cachePath: cachePath, customRulesIdentifiers: customRulesIdentifiers)!
setCached(atPath: fullPath)
}
// MARK: Equatable
public static func == (lhs: Configuration, rhs: Configuration) -> Bool {
return (lhs.warningThreshold == rhs.warningThreshold) &&
(lhs.reporter == rhs.reporter) &&
(lhs.rootPath == rhs.rootPath) &&
(lhs.configurationPath == rhs.configurationPath) &&
(lhs.cachePath == rhs.cachePath) &&
(lhs.included == rhs.included) &&
(lhs.excluded == rhs.excluded) &&
(lhs.rules == rhs.rules) &&
(lhs.indentation == rhs.indentation)
}
}
// MARK: Identifier Validation
private func validateRuleIdentifiers(ruleIdentifiers: [String], validRuleIdentifiers: [String]) -> [String] {
// Validate that all rule identifiers map to a defined rule
let invalidRuleIdentifiers = ruleIdentifiers.filter { !validRuleIdentifiers.contains($0) }
if !invalidRuleIdentifiers.isEmpty {
for invalidRuleIdentifier in invalidRuleIdentifiers {
queuedPrintError("configuration error: '\(invalidRuleIdentifier)' is not a valid rule identifier")
}
let listOfValidRuleIdentifiers = validRuleIdentifiers.sorted().joined(separator: "\n")
queuedPrintError("Valid rule identifiers:\n\(listOfValidRuleIdentifiers)")
}
return ruleIdentifiers.filter(validRuleIdentifiers.contains)
}
private func containsDuplicateIdentifiers(_ identifiers: [String]) -> Bool {
// Validate that rule identifiers aren't listed multiple times
guard Set(identifiers).count != identifiers.count else {
return false
}
let duplicateRules = identifiers.reduce(into: [String: Int]()) { $0[$1, default: 0] += 1 }
.filter { $0.1 > 1 }
queuedPrintError(duplicateRules.map { rule in
"configuration error: '\(rule.0)' is listed \(rule.1) times"
}.joined(separator: "\n"))
return true
}
private func enabledRules(from configuredRules: [Rule],
with mode: Configuration.RulesMode,
aliasResolver: (String) -> String,
customRulesIdentifiers: [String]) -> [Rule]? {
let regularRuleIdentifiers = configuredRules.map { type(of: $0).description.identifier }
let configurationCustomRulesIdentifiers = (configuredRules.first(where: { $0 is CustomRules }) as? CustomRules)?
.configuration.customRuleConfigurations.map { $0.identifier } ?? []
let validRuleIdentifiers = regularRuleIdentifiers + configurationCustomRulesIdentifiers + customRulesIdentifiers
switch mode {
case .allEnabled:
return configuredRules
case .whitelisted(let whitelistedRuleIdentifiers):
let validWhitelistedRuleIdentifiers = validateRuleIdentifiers(
ruleIdentifiers: whitelistedRuleIdentifiers.map(aliasResolver),
validRuleIdentifiers: validRuleIdentifiers)
// Validate that rule identifiers aren't listed multiple times
if containsDuplicateIdentifiers(validWhitelistedRuleIdentifiers) {
return nil
}
return configuredRules.filter { rule in
return validWhitelistedRuleIdentifiers.contains(type(of: rule).description.identifier)
}
case let .default(disabledRuleIdentifiers, optInRuleIdentifiers):
let validDisabledRuleIdentifiers = validateRuleIdentifiers(
ruleIdentifiers: disabledRuleIdentifiers.map(aliasResolver),
validRuleIdentifiers: validRuleIdentifiers)
let validOptInRuleIdentifiers = validateRuleIdentifiers(
ruleIdentifiers: optInRuleIdentifiers.map(aliasResolver),
validRuleIdentifiers: validRuleIdentifiers)
// Same here
if containsDuplicateIdentifiers(validDisabledRuleIdentifiers)
|| containsDuplicateIdentifiers(validOptInRuleIdentifiers) {
return nil
}
return configuredRules.filter { rule in
let id = type(of: rule).description.identifier
if validDisabledRuleIdentifiers.contains(id) { return false }
return validOptInRuleIdentifiers.contains(id) || !(rule is OptInRule)
}
}
}
private extension String {
func isDirectory() -> Bool {
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: self, isDirectory: &isDir) {
return isDir.boolValue
}
return false
}
}