mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
d305e03905
Current events have renewed the conversation in our community about the roles of terminology with racist connotations in our software. Many companies and developers are now taking appropriate steps to remove this terminology from their codebases and products. (e.g. [GitHub](https://twitter.com/natfriedman/status/1271253144442253312)) This small rule prevents the use of declarations that contain any of the terms: whitelist, blacklist, master, and slave. It may be appropriate to add more terms to this list now or in the future.
325 lines
15 KiB
Swift
325 lines
15 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 only([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?
|
|
/// Allow or disallow SwiftLint to exit successfully when passed only ignored or unlintable files
|
|
public let allowZeroLintableFiles: Bool
|
|
|
|
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)
|
|
hasher.combine(allowZeroLintableFiles)
|
|
}
|
|
}
|
|
|
|
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 `only_rules` rules.
|
|
public let rules: [Rule]
|
|
|
|
internal let rulesMode: RulesMode
|
|
|
|
// MARK: Initializers
|
|
|
|
/// Creates a `Configuration` by specifying its properties directly.
|
|
///
|
|
/// - parameter rulesMode: The `RulesMode` for this configuration.
|
|
/// - parameter included: Included paths to lint.
|
|
/// - parameter excluded: Excluded paths to not lint.
|
|
/// - parameter warningThreshold: The threshold for the number of warnings to tolerate before treating the
|
|
/// lint as having failed.
|
|
/// - parameter reporter: The identifier for the `Reporter` to use to report style violations.
|
|
/// - parameter ruleList: All rules that should be accessible to this configuration.
|
|
/// - parameter configuredRules: The rules with their own configurations already applied.
|
|
/// - parameter swiftlintVersion: The SwiftLint version defined in this configuration.
|
|
/// - parameter cachePath: The location of the persisted cache to use whith this configuration.
|
|
/// - parameter indentation: The style to use when indenting Swift source code.
|
|
/// - parameter customRulesIdentifiers: All custom rule identifiers defined in the configuration.
|
|
/// - parameter allowZeroLintableFiles: Allow SwiftLint to exit successfully when passed ignored or unlintable files
|
|
public init?(rulesMode: RulesMode = .default(disabled: [], optIn: []),
|
|
included: [String] = [],
|
|
excluded: [String] = [],
|
|
warningThreshold: Int? = nil,
|
|
reporter: String = XcodeReporter.identifier,
|
|
ruleList: RuleList = primaryRuleList,
|
|
configuredRules: [Rule]? = nil,
|
|
swiftlintVersion: String? = nil,
|
|
cachePath: String? = nil,
|
|
indentation: IndentationStyle = .default,
|
|
customRulesIdentifiers: [String] = [],
|
|
allowZeroLintableFiles: Bool = false) {
|
|
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,
|
|
allowZeroLintableFiles: allowZeroLintableFiles)
|
|
}
|
|
|
|
internal init(rulesMode: RulesMode,
|
|
included: [String],
|
|
excluded: [String],
|
|
warningThreshold: Int?,
|
|
reporter: String,
|
|
rules: [Rule],
|
|
cachePath: String?,
|
|
rootPath: String? = nil,
|
|
indentation: IndentationStyle,
|
|
allowZeroLintableFiles: Bool) {
|
|
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
|
|
self.allowZeroLintableFiles = allowZeroLintableFiles
|
|
}
|
|
|
|
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
|
|
allowZeroLintableFiles = configuration.allowZeroLintableFiles
|
|
}
|
|
|
|
/// Creates a `Configuration` with convenience parameters.
|
|
///
|
|
/// - parameter path: The path on disk to the configuration file.
|
|
/// - parameter rootPath: The root directory to search for nested configurations.
|
|
/// - parameter optional: If false, the initializer will trap if the file isn't found.
|
|
/// - parameter quiet: If false, a message will be logged to stderr when the configuration file is
|
|
/// loaded.
|
|
/// - parameter enableAllRules: Enable all available rules.
|
|
/// - parameter cachePath: The location of the persisted cache to use whith this configuration.
|
|
/// - parameter customRulesIdentifiers: All custom rule identifiers defined in the configuration.
|
|
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) &&
|
|
(lhs.allowZeroLintableFiles == rhs.allowZeroLintableFiles)
|
|
}
|
|
}
|
|
|
|
// 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.isNotEmpty {
|
|
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 .only(let onlyRuleIdentifiers):
|
|
let validOnlyRuleIdentifiers = validateRuleIdentifiers(
|
|
ruleIdentifiers: onlyRuleIdentifiers.map(aliasResolver),
|
|
validRuleIdentifiers: validRuleIdentifiers)
|
|
// Validate that rule identifiers aren't listed multiple times
|
|
if containsDuplicateIdentifiers(validOnlyRuleIdentifiers) {
|
|
return nil
|
|
}
|
|
return configuredRules.filter { rule in
|
|
return validOnlyRuleIdentifiers.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
|
|
}
|
|
}
|