mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
Ideally, SwiftLintCore would some day only contain components that are needed to define rules. Consequently, it would be the only bundle required to import for (external) rule development.
361 lines
16 KiB
Swift
361 lines
16 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 {
|
||
// MARK: - Properties: Static
|
||
/// The default Configuration resulting from an empty configuration file.
|
||
public static var `default`: Self {
|
||
// This is realized via a getter to account for differences of the current working directory
|
||
Self()
|
||
}
|
||
|
||
/// The default file name to look for user-defined configurations.
|
||
public static let defaultFileName = ".swiftlint.yml"
|
||
|
||
// MARK: Public Instance
|
||
/// The paths that should be included when linting
|
||
public private(set) var includedPaths: [String]
|
||
|
||
/// The paths that should be excluded when linting
|
||
public private(set) var excludedPaths: [String]
|
||
|
||
/// The style to use when indenting Swift source code.
|
||
public let indentation: IndentationStyle
|
||
|
||
/// The threshold for the number of warnings to tolerate before treating the lint as having failed.
|
||
public let warningThreshold: Int?
|
||
|
||
/// The identifier for the `Reporter` to use to report style violations.
|
||
public let reporter: String?
|
||
|
||
/// The location of the persisted cache to use with this configuration.
|
||
public let cachePath: String?
|
||
|
||
/// Allow or disallow SwiftLint to exit successfully when passed only ignored or unlintable files.
|
||
public let allowZeroLintableFiles: Bool
|
||
|
||
/// Treat warnings as errors.
|
||
public let strict: Bool
|
||
|
||
/// Treat errors as warnings.
|
||
public let lenient: Bool
|
||
|
||
/// The path to read a baseline from.
|
||
public let baseline: String?
|
||
|
||
/// The path to write a baseline to.
|
||
public let writeBaseline: String?
|
||
|
||
/// Check for updates.
|
||
public let checkForUpdates: Bool
|
||
|
||
/// This value is `true` iff the `--config` parameter was used to specify (a) configuration file(s)
|
||
/// In particular, this means that the value is also `true` if the `--config` parameter
|
||
/// was used to explicitly specify the default `.swiftlint.yml` as the configuration file
|
||
public private(set) var basedOnCustomConfigurationFiles = false
|
||
|
||
// MARK: Public Computed
|
||
/// All rules enabled in this configuration
|
||
public var rules: [any Rule] { rulesWrapper.resultingRules }
|
||
|
||
/// The root directory is the directory that included & excluded paths relate to.
|
||
/// By default, the root directory is the current working directory,
|
||
/// but during some merging algorithms it may be used differently.
|
||
/// The rootDirectory also serves as the stopping point when searching for nested configs along the file hierarchy.
|
||
public var rootDirectory: String { fileGraph.rootDirectory }
|
||
|
||
/// The rules mode used for this configuration.
|
||
public var rulesMode: RulesMode { rulesWrapper.mode }
|
||
|
||
// MARK: Internal Instance
|
||
internal var fileGraph: FileGraph
|
||
internal private(set) var rulesWrapper: RulesWrapper
|
||
internal var computedCacheDescription: String?
|
||
|
||
// MARK: - Initializers: Internal
|
||
/// Initialize with all properties
|
||
internal init(
|
||
rulesWrapper: RulesWrapper,
|
||
fileGraph: FileGraph,
|
||
includedPaths: [String],
|
||
excludedPaths: [String],
|
||
indentation: IndentationStyle,
|
||
warningThreshold: Int?,
|
||
reporter: String?,
|
||
cachePath: String?,
|
||
allowZeroLintableFiles: Bool,
|
||
strict: Bool,
|
||
lenient: Bool,
|
||
baseline: String?,
|
||
writeBaseline: String?,
|
||
checkForUpdates: Bool
|
||
) {
|
||
self.rulesWrapper = rulesWrapper
|
||
self.fileGraph = fileGraph
|
||
self.includedPaths = includedPaths
|
||
self.excludedPaths = excludedPaths
|
||
self.indentation = indentation
|
||
self.warningThreshold = warningThreshold
|
||
self.reporter = reporter
|
||
self.cachePath = cachePath
|
||
self.allowZeroLintableFiles = allowZeroLintableFiles
|
||
self.strict = strict
|
||
self.lenient = lenient
|
||
self.baseline = baseline
|
||
self.writeBaseline = writeBaseline
|
||
self.checkForUpdates = checkForUpdates
|
||
}
|
||
|
||
/// Creates a Configuration by copying an existing configuration.
|
||
///
|
||
/// - parameter copying: The existing configuration to copy.
|
||
internal init(copying configuration: Self) {
|
||
rulesWrapper = configuration.rulesWrapper
|
||
fileGraph = configuration.fileGraph
|
||
includedPaths = configuration.includedPaths
|
||
excludedPaths = configuration.excludedPaths
|
||
indentation = configuration.indentation
|
||
warningThreshold = configuration.warningThreshold
|
||
reporter = configuration.reporter
|
||
basedOnCustomConfigurationFiles = configuration.basedOnCustomConfigurationFiles
|
||
cachePath = configuration.cachePath
|
||
allowZeroLintableFiles = configuration.allowZeroLintableFiles
|
||
strict = configuration.strict
|
||
lenient = configuration.lenient
|
||
baseline = configuration.baseline
|
||
writeBaseline = configuration.writeBaseline
|
||
checkForUpdates = configuration.checkForUpdates
|
||
}
|
||
|
||
/// Creates a `Configuration` by specifying its properties directly,
|
||
/// except that rules are still to be synthesized from rulesMode, ruleList & allRulesWrapped
|
||
/// and a check against the pinnedVersion is performed if given.
|
||
///
|
||
/// - parameter rulesMode: The `RulesMode` for this configuration.
|
||
/// - parameter allRulesWrapped: The rules with their own configurations already applied.
|
||
/// - parameter ruleList: The list of all rules. Used for alias resolving and as a fallback
|
||
/// if `allRulesWrapped` is nil.
|
||
/// - parameter filePath The underlying file graph. If `nil` is specified, a empty file graph
|
||
/// with the current working directory as the `rootDirectory` will be used.
|
||
/// - parameter includedPaths: Included paths to lint.
|
||
/// - parameter excludedPaths: Excluded paths to not lint.
|
||
/// - parameter indentation: The style to use when indenting Swift source code.
|
||
/// - 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 cachePath: The location of the persisted cache to use with this configuration.
|
||
/// - parameter pinnedVersion: The SwiftLint version defined in this configuration.
|
||
/// - parameter allowZeroLintableFiles: Allow SwiftLint to exit successfully when passed ignored or unlintable
|
||
/// files.
|
||
/// - parameter strict: Treat warnings as errors.
|
||
/// - parameter lenient: Treat errors as warnings.
|
||
/// - parameter baseline: The path to read a baseline from.
|
||
/// - parameter writeBaseline: The path to write a baseline to.
|
||
/// - parameter checkForUpdates: Check for updates to SwiftLint.
|
||
package init(
|
||
rulesMode: RulesMode = .defaultConfiguration(disabled: [], optIn: []),
|
||
allRulesWrapped: [ConfigurationRuleWrapper]? = nil,
|
||
ruleList: RuleList = RuleRegistry.shared.list,
|
||
fileGraph: FileGraph? = nil,
|
||
includedPaths: [String] = [],
|
||
excludedPaths: [String] = [],
|
||
indentation: IndentationStyle = .default,
|
||
warningThreshold: Int? = nil,
|
||
reporter: String? = nil,
|
||
cachePath: String? = nil,
|
||
pinnedVersion: String? = nil,
|
||
allowZeroLintableFiles: Bool = false,
|
||
strict: Bool = false,
|
||
lenient: Bool = false,
|
||
baseline: String? = nil,
|
||
writeBaseline: String? = nil,
|
||
checkForUpdates: Bool = false
|
||
) {
|
||
if let pinnedVersion, pinnedVersion != Version.current.value {
|
||
queuedPrintError(
|
||
"warning: Currently running SwiftLint \(Version.current.value) but " +
|
||
"configuration specified version \(pinnedVersion)."
|
||
)
|
||
exit(2)
|
||
}
|
||
|
||
self.init(
|
||
rulesWrapper: RulesWrapper(
|
||
mode: rulesMode,
|
||
allRulesWrapped: allRulesWrapped ?? (try? ruleList.allRulesWrapped()) ?? [],
|
||
aliasResolver: { ruleList.identifier(for: $0) ?? $0 }
|
||
),
|
||
fileGraph: fileGraph ?? FileGraph(
|
||
rootDirectory: FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized()
|
||
),
|
||
includedPaths: includedPaths,
|
||
excludedPaths: excludedPaths,
|
||
indentation: indentation,
|
||
warningThreshold: warningThreshold,
|
||
reporter: reporter,
|
||
cachePath: cachePath,
|
||
allowZeroLintableFiles: allowZeroLintableFiles,
|
||
strict: strict,
|
||
lenient: lenient,
|
||
baseline: baseline,
|
||
writeBaseline: writeBaseline,
|
||
checkForUpdates: checkForUpdates
|
||
)
|
||
}
|
||
|
||
// MARK: Public
|
||
/// Creates a `Configuration` with convenience parameters.
|
||
///
|
||
/// - parameter configurationFiles: The path on disk to one or multiple configuration files. If this array
|
||
/// is empty, the default `.swiftlint.yml` file will be used.
|
||
/// - parameter enableAllRules: Enable all available rules.
|
||
/// - parameter cachePath: The location of the persisted cache to use whith this configuration.
|
||
/// - parameter ignoreParentAndChildConfigs:If `true`, child and parent config references will be ignored.
|
||
/// - parameter mockedNetworkResults: For testing purposes only. Instead of loading the specified urls,
|
||
/// the mocked value will be used. Example: ["http://mock.com": "content"]
|
||
/// - parameter useDefaultConfigOnFailure: If this value is specified, it will override the normal behavior.
|
||
/// This is only intended for tests checking whether invalid configs fail.
|
||
public init(
|
||
configurationFiles: [String], // No default value here to avoid ambiguous Configuration() initializer
|
||
enableAllRules: Bool = false,
|
||
onlyRule: [String] = [],
|
||
cachePath: String? = nil,
|
||
ignoreParentAndChildConfigs: Bool = false,
|
||
mockedNetworkResults: [String: String] = [:],
|
||
useDefaultConfigOnFailure: Bool? = nil // swiftlint:disable:this discouraged_optional_boolean
|
||
) {
|
||
// Handle mocked network results if needed
|
||
Self.FileGraph.FilePath.mockedNetworkResults = mockedNetworkResults
|
||
defer {
|
||
if !mockedNetworkResults.isEmpty {
|
||
Self.FileGraph.FilePath.deleteGitignoreAndSwiftlintCache()
|
||
}
|
||
}
|
||
|
||
// Store whether there are custom configuration files; use default config file name if there are none
|
||
let hasCustomConfigurationFiles: Bool = configurationFiles.isNotEmpty
|
||
let configurationFiles = configurationFiles.isEmpty ? [Self.defaultFileName] : configurationFiles
|
||
defer { basedOnCustomConfigurationFiles = hasCustomConfigurationFiles }
|
||
|
||
let currentWorkingDirectory = FileManager.default.currentDirectoryPath.bridge().absolutePathStandardized()
|
||
let rulesMode: RulesMode = if enableAllRules {
|
||
.allCommandLine
|
||
} else if onlyRule.isNotEmpty {
|
||
.onlyCommandLine(Set(onlyRule))
|
||
} else {
|
||
.defaultConfiguration(disabled: [], optIn: [])
|
||
}
|
||
|
||
// Try obtaining cached config
|
||
let cacheIdentifier = "\(currentWorkingDirectory) - \(configurationFiles)"
|
||
if let cachedConfig = Self.getCached(forIdentifier: cacheIdentifier) {
|
||
self.init(copying: cachedConfig)
|
||
return
|
||
}
|
||
|
||
// Try building configuration via the file graph
|
||
do {
|
||
var fileGraph = FileGraph(
|
||
commandLineChildConfigs: configurationFiles,
|
||
rootDirectory: currentWorkingDirectory,
|
||
ignoreParentAndChildConfigs: ignoreParentAndChildConfigs
|
||
)
|
||
let resultingConfiguration = try fileGraph.resultingConfiguration(
|
||
enableAllRules: enableAllRules,
|
||
onlyRule: onlyRule,
|
||
cachePath: cachePath
|
||
)
|
||
|
||
self.init(copying: resultingConfiguration)
|
||
self.fileGraph = fileGraph
|
||
setCached(forIdentifier: cacheIdentifier)
|
||
} catch {
|
||
if case Issue.initialFileNotFound = error, !hasCustomConfigurationFiles {
|
||
// The initial configuration file wasn't found, but the user didn't explicitly specify one
|
||
// Don't handle as error. Instead, silently fall back to default.
|
||
self.init(rulesMode: rulesMode, cachePath: cachePath)
|
||
return
|
||
}
|
||
if useDefaultConfigOnFailure ?? !hasCustomConfigurationFiles {
|
||
// No files were explicitly specified, so maybe the user doesn't want a config at all -> warn
|
||
queuedPrintError(
|
||
"\(Issue.wrap(error: error).errorDescription) – Falling back to default configuration"
|
||
)
|
||
self.init(rulesMode: rulesMode, cachePath: cachePath)
|
||
} else {
|
||
// Files that were explicitly specified could not be loaded -> fail
|
||
queuedPrintError(Issue.wrap(error: error).asError.localizedDescription)
|
||
queuedFatalError("Could not read configuration")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Methods: Internal
|
||
mutating func makeIncludedAndExcludedPaths(relativeTo newBasePath: String, previousBasePath: String) {
|
||
includedPaths = includedPaths.map {
|
||
$0.bridge().absolutePathRepresentation(rootDirectory: previousBasePath).path(relativeTo: newBasePath)
|
||
}
|
||
|
||
excludedPaths = excludedPaths.map {
|
||
$0.bridge().absolutePathRepresentation(rootDirectory: previousBasePath).path(relativeTo: newBasePath)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Hashable
|
||
extension Configuration: Hashable {
|
||
public func hash(into hasher: inout Hasher) {
|
||
hasher.combine(includedPaths)
|
||
hasher.combine(excludedPaths)
|
||
hasher.combine(indentation)
|
||
hasher.combine(warningThreshold)
|
||
hasher.combine(reporter)
|
||
hasher.combine(allowZeroLintableFiles)
|
||
hasher.combine(strict)
|
||
hasher.combine(lenient)
|
||
hasher.combine(baseline)
|
||
hasher.combine(writeBaseline)
|
||
hasher.combine(checkForUpdates)
|
||
hasher.combine(basedOnCustomConfigurationFiles)
|
||
hasher.combine(cachePath)
|
||
hasher.combine(rules.map { type(of: $0).identifier })
|
||
hasher.combine(fileGraph)
|
||
}
|
||
|
||
public static func == (lhs: Configuration, rhs: Configuration) -> Bool {
|
||
lhs.includedPaths == rhs.includedPaths &&
|
||
lhs.excludedPaths == rhs.excludedPaths &&
|
||
lhs.indentation == rhs.indentation &&
|
||
lhs.warningThreshold == rhs.warningThreshold &&
|
||
lhs.reporter == rhs.reporter &&
|
||
lhs.basedOnCustomConfigurationFiles == rhs.basedOnCustomConfigurationFiles &&
|
||
lhs.cachePath == rhs.cachePath &&
|
||
lhs.rules == rhs.rules &&
|
||
lhs.fileGraph == rhs.fileGraph &&
|
||
lhs.allowZeroLintableFiles == rhs.allowZeroLintableFiles &&
|
||
lhs.strict == rhs.strict &&
|
||
lhs.lenient == rhs.lenient &&
|
||
lhs.baseline == rhs.baseline &&
|
||
lhs.writeBaseline == rhs.writeBaseline &&
|
||
lhs.checkForUpdates == rhs.checkForUpdates &&
|
||
lhs.rulesMode == rhs.rulesMode
|
||
}
|
||
}
|
||
|
||
// MARK: - CustomStringConvertible
|
||
extension Configuration: CustomStringConvertible {
|
||
public var description: String {
|
||
"Configuration: \n"
|
||
+ "- Indentation Style: \(indentation)\n"
|
||
+ "- Included Paths: \(includedPaths)\n"
|
||
+ "- Excluded Paths: \(excludedPaths)\n"
|
||
+ "- Warning Threshold: \(warningThreshold as Optional)\n"
|
||
+ "- Root Directory: \(rootDirectory as Optional)\n"
|
||
+ "- Reporter: \(reporter ?? "default")\n"
|
||
+ "- Cache Path: \(cachePath as Optional)\n"
|
||
+ "- Computed Cache Description: \(computedCacheDescription as Optional)\n"
|
||
+ "- Rules: \(rules.map { type(of: $0).identifier })"
|
||
}
|
||
}
|