Files
SwiftLint/Tests/FileSystemAccessTests/ConfigurationTests.swift

656 lines
24 KiB
Swift

import Foundation
import SourceKittenFramework
import TestHelpers
import Testing
@testable import SwiftLintFramework
// swiftlint:disable file_length
private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0)
@Suite(.rulesRegistered)
struct ConfigurationTests { // swiftlint:disable:this type_body_length
init() {
Configuration.resetCache()
}
@Test
func basicInit() throws {
_ = try Configuration(dict: [:])
_ = try Configuration(dict: ["a": 1, "b": 2])
}
@Test(.workingDirectory(Constants.Dir.emptyFolder))
func useDefaultIfNoConfiguration() {
#expect(Configuration(configurationFiles: []) == Configuration.default)
}
@Test
func emptyConfiguration() {
guard let config = try? Configuration(dict: [:]) else {
Issue.record("empty YAML string should yield non-nil Configuration")
return
}
#expect(config.rulesWrapper.disabledRuleIdentifiers.isEmpty)
#expect(config.includedPaths.isEmpty)
#expect(config.excludedPaths.isEmpty)
#expect(config.indentation == .spaces(count: 4))
#expect(config.reporter == "xcode")
#expect(reporterFrom(identifier: config.reporter).identifier == "xcode")
#expect(!config.allowZeroLintableFiles)
#expect(!config.strict)
#expect(!config.lenient)
#expect(config.baseline == nil)
#expect(config.writeBaseline == nil)
#expect(!config.checkForUpdates)
}
@Test(.workingDirectory(Constants.Dir.level0))
func initWithRelativePathAndRootPath() {
let expectedConfig = Constants.Config._0
let config = Configuration(configurationFiles: [".swiftlint.yml".url()])
#expect(config.rulesWrapper.disabledRuleIdentifiers == expectedConfig.rulesWrapper.disabledRuleIdentifiers)
#expect(config.includedPaths == expectedConfig.includedPaths)
#expect(config.excludedPaths == expectedConfig.excludedPaths)
#expect(config.indentation == expectedConfig.indentation)
#expect(config.reporter == expectedConfig.reporter)
#expect(config.allowZeroLintableFiles)
#expect(config.strict)
#expect(config.baseline != nil)
#expect(config.writeBaseline != nil)
}
@Test
func enableAllRulesConfiguration() throws {
let configuration = try Configuration(
dict: [:],
enableAllRules: true,
cachePath: nil
)
#expect(configuration.rules.count == RuleRegistry.shared.list.list.count)
}
@Test
func onlyRule() throws {
let configuration = try Configuration(
dict: [:],
onlyRule: ["nesting"],
cachePath: nil
)
#expect(configuration.rules.count == 1)
}
@Test
func onlyRuleMultiple() throws {
let onlyRuleIdentifiers = ["nesting", "todo"].sorted()
let configuration = try Configuration(
dict: ["only_rules": "line_length"],
onlyRule: onlyRuleIdentifiers,
cachePath: nil
)
#expect(onlyRuleIdentifiers == configuration.enabledRuleIdentifiers)
let childConfiguration = try Configuration(dict: ["disabled_rules": onlyRuleIdentifiers.last ?? ""])
let mergedConfiguration = configuration.merged(withChild: childConfiguration)
#expect(onlyRuleIdentifiers.dropLast() == mergedConfiguration.enabledRuleIdentifiers)
}
@Test
func onlyRules() throws {
let only = ["nesting", "todo"]
let config = try Configuration(dict: ["only_rules": only])
let configuredIdentifiers = config.rules.map {
type(of: $0).identifier
}.sorted()
#expect(only == configuredIdentifiers)
}
@Test
func onlyRulesWithCustomRules() throws {
// All custom rules from a config file should be active if the `custom_rules` is included in the `only_rules`
// As the behavior is different for custom rules from parent configs, this test is helpful
let only = ["custom_rules"]
let customRuleIdentifier = "my_custom_rule"
let customRules = [customRuleIdentifier: ["name": "A name for this custom rule", "regex": "this is illegal"]]
let config = try Configuration(dict: ["only_rules": only, "custom_rules": customRules])
guard let resultingCustomRules = config.rules.customRules else {
Issue.record("Custom rules are expected to be present")
return
}
#expect(
resultingCustomRules.configuration.customRuleConfigurations.contains {
$0.identifier == customRuleIdentifier
}
)
}
@Test
func onlyRulesWithSpecificCustomRules() throws {
// Individual custom rules can be specified on the command line without specifying `custom_rules` as well.
let customRuleIdentifier = "my_custom_rule"
let customRuleIdentifier2 = "my_custom_rule2"
let only = ["custom_rules"]
let customRules = [
customRuleIdentifier: ["name": "A custom rule", "regex": "this is illegal"],
customRuleIdentifier2: ["name": "Another custom rule", "regex": "this is also illegal"],
]
let configuration = try Configuration(
dict: [
"only_rules": only,
"custom_rules": customRules,
],
onlyRule: [customRuleIdentifier]
)
let resultingCustomRules = configuration.rules.customRules
#expect(resultingCustomRules != nil)
let enabledCustomRuleIdentifiers =
resultingCustomRules?.configuration.customRuleConfigurations.map(\.identifier)
#expect(enabledCustomRuleIdentifiers == [customRuleIdentifier])
}
@Test
func warningThreshold_value() throws {
let config = try Configuration(dict: ["warning_threshold": 5])
#expect(config.warningThreshold == 5)
}
@Test
func warningThreshold_nil() throws {
let config = try Configuration(dict: [:])
#expect(config.warningThreshold == nil)
}
@Test
func otherRuleConfigurationsAlongsideOnlyRules() {
let only = ["nesting", "todo"]
let enabledRulesConfigDict = [
"opt_in_rules": ["line_length"],
"only_rules": only,
]
let disabledRulesConfigDict = [
"disabled_rules": ["identifier_name"],
"only_rules": only,
]
let combinedRulesConfigDict = enabledRulesConfigDict.reduce(into: disabledRulesConfigDict) { $0[$1.0] = $1.1 }
var configuration = try? Configuration(dict: enabledRulesConfigDict)
#expect(configuration == nil)
configuration = try? Configuration(dict: disabledRulesConfigDict)
#expect(configuration == nil)
configuration = try? Configuration(dict: combinedRulesConfigDict)
#expect(configuration == nil)
}
@Test
func disabledRules() throws {
let disabledConfig = try Configuration(dict: ["disabled_rules": ["nesting", "todo"]])
#expect(disabledConfig.rulesWrapper.disabledRuleIdentifiers == ["nesting", "todo"],
"initializing Configuration with valid rules in Dictionary should succeed")
let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
.filter({ !(["nesting", "todo"] + optInRules).contains($0) }))
let configuredIdentifiers = Set(disabledConfig.rules.map {
type(of: $0).identifier
})
#expect(expectedIdentifiers == configuredIdentifiers)
}
@Test
func disabledRulesWithUnknownRule() throws {
let validRule = "nesting"
let bogusRule = "no_sprites_with_elf_shoes"
let configuration = try Configuration(dict: ["disabled_rules": [validRule, bogusRule]])
#expect(configuration.rulesWrapper.disabledRuleIdentifiers == [validRule],
"initializing Configuration with valid rules in YAML string should succeed")
let expectedIdentifiers = Set(RuleRegistry.shared.list.list.keys
.filter({ !([validRule] + optInRules).contains($0) }))
#expect(expectedIdentifiers == Set(configuration.enabledRuleIdentifiers))
}
@Test
func duplicatedRules() {
let duplicateConfig1 = try? Configuration(dict: ["only_rules": ["todo", "todo"]])
#expect(
duplicateConfig1?.rules.count == 1, "duplicate rules should be removed when initializing Configuration"
)
let duplicateConfig2 = try? Configuration(dict: ["opt_in_rules": [optInRules.first!, optInRules.first!]])
#expect(
duplicateConfig2?.rules.filter { type(of: $0).identifier == optInRules.first! }.count == 1,
"duplicate rules should be removed when initializing Configuration"
)
let duplicateConfig3 = try? Configuration(dict: ["disabled_rules": ["todo", "todo"]])
#expect(
duplicateConfig3?.rulesWrapper.disabledRuleIdentifiers.count == 1,
"duplicate rules should be removed when initializing Configuration"
)
}
@Test(.disabled(if: isRunningWithBazel), .workingDirectory(Constants.Dir.level1))
func includedExcludedRelativeLocationLevel1() {
// The included path "File.swift" should be put relative to the configuration file
// (~> Resources/ProjectMock/File.swift) and not relative to the path where
// SwiftLint is run from (~> Resources/ProjectMock/Level1/File.swift)
let configuration = Configuration(configurationFiles: ["../custom_included_excluded.yml".url()])
let actualIncludedPath = configuration.includedPaths.first!
let desiredIncludedPath = "File1.swift".url(relativeTo: Constants.Dir.level0)
let actualExcludedPath = configuration.excludedPaths.first!
let desiredExcludedPath = "File2.swift".url(relativeTo: Constants.Dir.level0)
#expect(actualIncludedPath == desiredIncludedPath)
#expect(actualExcludedPath == desiredExcludedPath)
}
@Test(.workingDirectory(Constants.Dir.level0))
func includedExcludedRelativeLocationLevel0() {
// Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(),
// but run from the directory the config file resides in
let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml".url()])
let actualIncludedPath = configuration.includedPaths.first!
let desiredIncludedPath = "File1.swift".url(relativeTo: Constants.Dir.level0)
let actualExcludedPath = configuration.excludedPaths.first!
let desiredExcludedPath = "File2.swift".url(relativeTo: Constants.Dir.level0)
#expect(actualIncludedPath == desiredIncludedPath)
#expect(actualExcludedPath == desiredExcludedPath)
}
@Test(.workingDirectory(Constants.Dir.exclusionTests))
func excludedPaths() {
let configuration = Configuration(
includedPaths: ["directory".url()],
excludedPaths: ["directory/excluded".url(), "directory/ExcludedFile.swift".url()]
)
let paths = configuration.lintablePaths(
inPath: URL.cwd,
forceExclude: false,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
}
@Test(.workingDirectory(Constants.Dir.exclusionTests))
func forceExcludesFile() {
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()])
let paths = configuration.lintablePaths(
inPath: "directory/ExcludedFile.swift".url(),
forceExclude: true,
excludeByPrefix: false
)
#expect(paths.isEmpty)
}
@Test(.workingDirectory(Constants.Dir.exclusionTests))
func forceExcludesFileNotPresentInExcluded() {
let configuration = Configuration(
includedPaths: ["directory".url()],
excludedPaths: ["directory/ExcludedFile.swift".url(), "directory/excluded".url()]
)
let paths = configuration.lintablePaths(
inPath: URL.cwd,
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
}
@Test(.workingDirectory(Constants.Dir.exclusionTests))
func forceExcludesDirectory() {
let configuration = Configuration(excludedPaths: ["directory/excluded".url()])
let paths = configuration.lintablePaths(
inPath: "directory".url(),
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/ExcludedFile.swift"], paths)
}
@Test(.workingDirectory(Constants.Dir.exclusionTests))
func forceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()])
let paths = configuration.lintablePaths(
inPath: "directory".url(),
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/excluded/Excluded.swift"], paths)
}
@Test
func lintablePaths() {
let paths = Configuration.default.lintablePaths(inPath: Constants.Dir.level0,
forceExclude: false,
excludeByPrefix: false)
let filenames = paths.map(\.lastPathComponent).sorted()
let expectedFilenames = [
"DirectoryLevel1.swift",
"Level0.swift", "Level1.swift", "Level2.swift", "Level3.swift",
"Main.swift", "Sub.swift",
]
#expect(Set(expectedFilenames) == Set(filenames))
}
@Test(.workingDirectory(Constants.Dir.level0))
func globIncludePaths() {
let configuration = Configuration(includedPaths: ["**/Level2".url()])
let paths = configuration.lintablePaths(inPath: Constants.Dir.level0,
forceExclude: true,
excludeByPrefix: false)
let filenames = paths.map(\.lastPathComponent).sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"]
#expect(Set(expectedFilenames) == Set(filenames))
}
@Test(.workingDirectory(Constants.Dir.level0))
func duplicatedGlobIncludePaths() {
let configuration = Configuration(includedPaths: ["**/Level2".url(), "**/Level2".url()])
let paths = configuration.lintablePaths(inPath: Constants.Dir.level0,
forceExclude: true,
excludeByPrefix: false)
let filenames = paths.map(\.lastPathComponent).sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"]
#expect(expectedFilenames == filenames)
}
@Test
func globExcludePaths() {
let configuration = Configuration(
includedPaths: [Constants.Dir.level3],
excludedPaths: [Constants.Dir.level3.appending(path: "*.swift", directoryHint: .notDirectory)]
)
let lintablePaths = configuration.lintablePaths(inPath: URL.cwd,
forceExclude: false,
excludeByPrefix: false)
#expect(lintablePaths.isEmpty)
}
// MARK: - Testing Configuration Equality
@Test
func isEqualTo() {
#expect(Constants.Config._0 == Constants.Config._0) // swiftlint:disable:this identical_operands
}
@Test
func isNotEqualTo() {
#expect(Constants.Config._0 != Constants.Config._2)
}
// MARK: - Testing Custom Configuration File
@Test
func customConfiguration() {
let file = SwiftLintFile(path: Constants.Swift._0)!
#expect(Constants.Config._0.configuration(for: file) != Constants.Config._0Custom.configuration(for: file))
}
@Test
func configurationWithSwiftFileAsRoot() {
let configuration = Configuration(configurationFiles: [Constants.Yml._0])
let file = SwiftLintFile(path: Constants.Swift._0)!
#expect(configuration.configuration(for: file) == configuration)
}
@Test
func configurationWithSwiftFileAsRootAndCustomConfiguration() {
let configuration = Constants.Config._0Custom
let file = SwiftLintFile(path: Constants.Swift._0)!
#expect(configuration.configuration(for: file) == configuration)
}
// MARK: - Testing custom indentation
@Test
func indentationTabs() throws {
let configuration = try Configuration(dict: ["indentation": "tabs"])
#expect(configuration.indentation == .tabs)
}
@Test
func indentationSpaces() throws {
let configuration = try Configuration(dict: ["indentation": 2])
#expect(configuration.indentation == .spaces(count: 2))
}
@Test
func indentationFallback() throws {
let configuration = try Configuration(dict: ["indentation": "invalid"])
#expect(configuration.indentation == .spaces(count: 4))
}
// MARK: - Testing Rules from config dictionary
nonisolated(unsafe) private static let testRuleList = RuleList(rules: RuleWithLevelsMock.self)
@Test
func configuresCorrectlyFromDict() throws {
let ruleConfiguration = [1, 2]
let config = [RuleWithLevelsMock.identifier: ruleConfiguration]
let rules = try Self.testRuleList.allRulesWrapped(configurationDict: config).map(\.rule)
#expect(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)])
}
@Test
func configureFallsBackCorrectly() throws {
let config = [RuleWithLevelsMock.identifier: ["a", "b"]]
let rules = try Self.testRuleList.allRulesWrapped(configurationDict: config).map(\.rule)
#expect(rules == [RuleWithLevelsMock()])
}
@Test
func allowZeroLintableFiles() throws {
let configuration = try Configuration(dict: ["allow_zero_lintable_files": true])
#expect(configuration.allowZeroLintableFiles)
}
@Test
func strict() throws {
let configuration = try Configuration(dict: ["strict": true])
#expect(configuration.strict)
}
@Test
func lenient() throws {
let configuration = try Configuration(dict: ["lenient": true])
#expect(configuration.lenient)
}
@Test
func baseline() throws {
let baselinePath = "Baseline.json"
let configuration = try Configuration(dict: ["baseline": baselinePath])
#expect(configuration.baseline == baselinePath.url())
}
@Test
func writeBaseline() throws {
let baselinePath = "Baseline.json"
let configuration = try Configuration(dict: ["write_baseline": baselinePath])
#expect(configuration.writeBaseline == baselinePath.url())
}
@Test
func checkForUpdates() throws {
let configuration = try Configuration(dict: ["check_for_updates": true])
#expect(configuration.checkForUpdates)
}
@Test(.workingDirectory(Constants.Dir.level0))
func excludeByPrefixExcludedPaths() {
let configuration = Configuration(
includedPaths: ["Level1".url()],
excludedPaths: ["Level1/Level1.swift".url(), "Level1/Level2/Level3".url()]
)
let paths = configuration.lintablePaths(
inPath: Constants.Dir.level0,
forceExclude: false,
excludeByPrefix: true
)
let filenames = paths.map(\.lastPathComponent)
#expect(filenames == ["Level2.swift"])
}
@Test
func excludeByPrefixForceExcludesFile() {
let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift".url()])
let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift".url(),
forceExclude: true,
excludeByPrefix: true)
#expect(paths.isEmpty)
}
@Test(.workingDirectory(Constants.Dir.level0))
func excludeByPrefixForceExcludesFileNotPresentInExcluded() {
let configuration = Configuration(
includedPaths: ["Level1".url()],
excludedPaths: ["Level1/Level1.swift".url()])
let paths = configuration.lintablePaths(
inPath: "Level1".url(),
forceExclude: true,
excludeByPrefix: true
)
let filenames = paths.map(\.lastPathComponent).sorted()
#expect(["Level2.swift", "Level3.swift"] == filenames)
}
@Test(.workingDirectory(Constants.Dir.level0))
func excludeByPrefixForceExcludesDirectory() {
let configuration = Configuration(
excludedPaths: [
"Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig"
].map { $0.url() }
)
let paths = configuration.lintablePaths(
inPath: URL.cwd,
forceExclude: true,
excludeByPrefix: true
)
let filenames = paths.map(\.lastPathComponent).sorted()
#expect(["Level0.swift", "Level1.swift"] == filenames)
}
@Test(.workingDirectory(Constants.Dir.level0))
func excludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
let configuration = Configuration(
excludedPaths: [
"Level1", "Directory.swift/DirectoryLevel1.swift", "ChildConfig", "ParentConfig", "NestedConfig"
].map { $0.url() }
)
let paths = configuration.lintablePaths(
inPath: URL.cwd,
forceExclude: true,
excludeByPrefix: true
)
let filenames = paths.map(\.lastPathComponent)
#expect(["Level0.swift"] == filenames)
}
@Test(.workingDirectory(Constants.Dir.level0))
func excludeByPrefixGlobExcludePaths() {
let configuration = Configuration(
includedPaths: ["Level1".url()],
excludedPaths: ["Level1/*/*.swift".url(), "Level1/*/*/*.swift".url()])
let paths = configuration.lintablePaths(
inPath: "Level1".url(),
forceExclude: false,
excludeByPrefix: true
)
let filenames = paths.map(\.lastPathComponent).sorted()
#expect(filenames == ["Level1.swift"])
}
@Test
func dictInitWithCachePath() throws {
let configuration = try Configuration(
dict: ["cache_path": "cache/path/1"]
)
#expect(configuration.cachePath == "cache/path/1")
}
@Test
func dictInitWithCachePathFromCommandLine() throws {
let configuration = try Configuration(
dict: ["cache_path": "cache/path/1"],
cachePath: "cache/path/2"
)
#expect(configuration.cachePath == "cache/path/2")
}
@Test
func mainInitWithCachePath() {
let configuration = Configuration(
configurationFiles: [],
cachePath: "cache/path/1"
)
#expect(configuration.cachePath == "cache/path/1")
}
@Test(.temporaryDirectory)
func mainInitWithCachePathAndCachedConfig() {
let configuration1 = Configuration(
configurationFiles: [],
cachePath: "cache/path/1"
)
let configuration2 = Configuration(
configurationFiles: [],
cachePath: "cache/path/2"
)
#expect(configuration1.cachePath == "cache/path/1")
#expect(configuration2.cachePath == "cache/path/2")
}
private func assertEqual(_ relativeExpectedPaths: [String],
_ actualPaths: [URL],
sourceLocation: Testing.SourceLocation = #_sourceLocation) {
#expect(
relativeExpectedPaths.absolutePathsStandardized().sorted()
== actualPaths.map(\.path).absolutePathsStandardized().sorted(),
sourceLocation: sourceLocation
)
}
}
private extension Sequence where Element == String {
func absolutePathsStandardized() -> [String] {
// In Bazel builds, absolute paths might be prefixed with `/private`.
map { $0.url().standardizedFileURL.resolvingSymlinksInPath().relativeDisplayPath }
}
}
private extension Configuration {
var enabledRuleIdentifiers: [String] {
rules.map { type(of: $0).identifier }.sorted()
}
}