SwiftLint/Source/SwiftLintCore/Models/Configuration.swift

327 lines
15 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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`: Configuration {
// 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
/// 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: [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
) {
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
}
/// Creates a Configuration by copying an existing configuration.
///
/// - parameter copying: The existing configuration to copy.
internal init(copying configuration: Configuration) {
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
}
/// 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 underlaying 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 whith this configuration.
/// - parameter pinnedVersion: The SwiftLint version defined in this configuration.
/// - parameter allowZeroLintableFiles: Allow SwiftLint to exit successfully when passed ignored or unlintable files
@_spi(TestHelper)
public init(
rulesMode: RulesMode = .default(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 = XcodeReporter.identifier,
cachePath: String? = nil,
pinnedVersion: String? = nil,
allowZeroLintableFiles: 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
)
}
// 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,
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 = enableAllRules ? .allEnabled : .default(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,
cachePath: cachePath
)
self.init(copying: resultingConfiguration)
self.fileGraph = fileGraph
setCached(forIdentifier: cacheIdentifier)
} catch {
let errorString: String
let initializationResult = FileGraphInitializationResult(
error: error, hasCustomConfigurationFiles: hasCustomConfigurationFiles
)
switch initializationResult {
case .initialImplicitFileNotFound:
// Silently fall back to default
self.init(rulesMode: rulesMode, cachePath: cachePath)
return
case .error(let message):
errorString = message
}
if useDefaultConfigOnFailure ?? !hasCustomConfigurationFiles {
// No files were explicitly specified, so maybe the user doesn't want a config at all -> warn
queuedPrintError("warning: \(errorString) Falling back to default configuration")
self.init(rulesMode: rulesMode, cachePath: cachePath)
} else {
// Files that were explicitly specified could not be loaded -> fail
queuedPrintError("error: \(errorString)")
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: - FileGraphInitializationResult
private enum FileGraphInitializationResult {
case initialImplicitFileNotFound
case error(message: String)
init(error: Error, hasCustomConfigurationFiles: Bool) {
switch error {
case let Issue.initialFileNotFound(path):
if hasCustomConfigurationFiles {
self = .error(message: "SwiftLint Configuration Error: Could not read file at path: \(path)")
} else {
// The initial configuration file wasn't found, but the user didn't explicitly specify one
// -> don't handle as error
self = .initialImplicitFileNotFound
}
case let Issue.genericWarning(message):
self = .error(message: "SwiftLint Configuration Error: \(message)")
case let Issue.yamlParsing(message):
self = .error(message: "YML Parsing Error: \(message)")
default:
self = .error(message: error.localizedDescription)
}
}
}
// 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(basedOnCustomConfigurationFiles)
hasher.combine(cachePath)
hasher.combine(rules.map { type(of: $0).description.identifier })
hasher.combine(fileGraph)
}
public static func == (lhs: Configuration, rhs: Configuration) -> Bool {
return 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
}
}
// MARK: - CustomStringConvertible
extension Configuration: CustomStringConvertible {
public var description: String {
return "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)\n"
+ "- Cache Path: \(cachePath as Optional)\n"
+ "- Computed Cache Description: \(computedCacheDescription as Optional)\n"
+ "- Rules: \(rules.map { type(of: $0).description.identifier })"
}
}