293 lines
12 KiB
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
|
|
}
|
|
}
|