SwiftLint/Tests/SwiftLintFrameworkTests/ConfigurationTests.swift

552 lines
25 KiB
Swift

import Foundation
import SourceKittenFramework
@_spi(TestHelper)
@testable import SwiftLintCore
import XCTest
// swiftlint:disable file_length
private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is OptInRule }).map({ $0.0 })
class ConfigurationTests: SwiftLintTestCase {
// MARK: Setup & Teardown
private var previousWorkingDir: String!
override func setUp() {
super.setUp()
Configuration.resetCache()
previousWorkingDir = FileManager.default.currentDirectoryPath
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
}
override func tearDown() {
super.tearDown()
FileManager.default.changeCurrentDirectoryPath(previousWorkingDir)
}
// MARK: Tests
func testInit() {
XCTAssertNotNil(
try? Configuration(dict: [:]),
"initializing Configuration with empty Dictionary should succeed"
)
XCTAssertNotNil(
try? Configuration(dict: ["a": 1, "b": 2]),
"initializing Configuration with valid Dictionary should succeed"
)
}
func testNoConfiguration() {
// Change to a folder where there is no `.swiftlint.yml`
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.emptyFolder)
// Test whether the default configuration is used if there is no `.swiftlint.yml` or other config file
XCTAssertEqual(Configuration(configurationFiles: []), Configuration.default)
}
func testEmptyConfiguration() {
guard let config = try? Configuration(dict: [:]) else {
XCTFail("empty YAML string should yield non-nil Configuration")
return
}
XCTAssertEqual(config.rulesWrapper.disabledRuleIdentifiers, [])
XCTAssertEqual(config.includedPaths, [])
XCTAssertEqual(config.excludedPaths, [])
XCTAssertEqual(config.indentation, .spaces(count: 4))
XCTAssertEqual(config.reporter, "xcode")
XCTAssertEqual(reporterFrom(identifier: config.reporter).identifier, "xcode")
XCTAssertFalse(config.allowZeroLintableFiles)
}
func testInitWithRelativePathAndRootPath() {
let expectedConfig = Mock.Config._0
let config = Configuration(configurationFiles: [".swiftlint.yml"])
XCTAssertEqual(config.rulesWrapper.disabledRuleIdentifiers, expectedConfig.rulesWrapper.disabledRuleIdentifiers)
XCTAssertEqual(config.includedPaths, expectedConfig.includedPaths)
XCTAssertEqual(config.excludedPaths, expectedConfig.excludedPaths)
XCTAssertEqual(config.indentation, expectedConfig.indentation)
XCTAssertEqual(config.reporter, expectedConfig.reporter)
XCTAssertTrue(config.allowZeroLintableFiles)
}
func testEnableAllRulesConfiguration() throws {
let configuration = try Configuration(
dict: [:],
enableAllRules: true,
cachePath: nil
)
XCTAssertEqual(configuration.rules.count, RuleRegistry.shared.list.list.count)
}
func testOnlyRules() throws {
let only = ["nesting", "todo"]
let config = try Configuration(dict: ["only_rules": only])
let configuredIdentifiers = config.rules.map {
type(of: $0).description.identifier
}.sorted()
XCTAssertEqual(only, configuredIdentifiers)
}
func testOnlyRulesWithCustomRules() 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.first(where: { $0 is CustomRules }) as? CustomRules
else {
XCTFail("Custom rules are expected to be present")
return
}
XCTAssertTrue(
resultingCustomRules.configuration.customRuleConfigurations.contains {
$0.identifier == customRuleIdentifier
}
)
}
func testWarningThreshold_value() throws {
let config = try Configuration(dict: ["warning_threshold": 5])
XCTAssertEqual(config.warningThreshold, 5)
}
func testWarningThreshold_nil() throws {
let config = try Configuration(dict: [:])
XCTAssertNil(config.warningThreshold)
}
func testOtherRuleConfigurationsAlongsideOnlyRules() {
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)
XCTAssertNil(configuration)
configuration = try? Configuration(dict: disabledRulesConfigDict)
XCTAssertNil(configuration)
configuration = try? Configuration(dict: combinedRulesConfigDict)
XCTAssertNil(configuration)
}
func testDisabledRules() throws {
let disabledConfig = try Configuration(dict: ["disabled_rules": ["nesting", "todo"]])
XCTAssertEqual(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).description.identifier
})
XCTAssertEqual(expectedIdentifiers, configuredIdentifiers)
}
func testDisabledRulesWithUnknownRule() throws {
let validRule = "nesting"
let bogusRule = "no_sprites_with_elf_shoes"
let configuration = try Configuration(dict: ["disabled_rules": [validRule, bogusRule]])
XCTAssertEqual(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) }))
let configuredIdentifiers = Set(configuration.rules.map {
type(of: $0).description.identifier
})
XCTAssertEqual(expectedIdentifiers, configuredIdentifiers)
}
func testDuplicatedRules() {
let duplicateConfig1 = try? Configuration(dict: ["only_rules": ["todo", "todo"]])
XCTAssertEqual(
duplicateConfig1?.rules.count, 1, "duplicate rules should be removed when initializing Configuration"
)
let duplicateConfig2 = try? Configuration(dict: ["opt_in_rules": [optInRules.first!, optInRules.first!]])
XCTAssertEqual(
duplicateConfig2?.rules.filter { type(of: $0).description.identifier == optInRules.first! }.count, 1,
"duplicate rules should be removed when initializing Configuration"
)
let duplicateConfig3 = try? Configuration(dict: ["disabled_rules": ["todo", "todo"]])
XCTAssertEqual(
duplicateConfig3?.rulesWrapper.disabledRuleIdentifiers.count, 1,
"duplicate rules should be removed when initializing Configuration"
)
}
func testIncludedExcludedRelativeLocationLevel1() {
guard !isRunningWithBazel else {
return
}
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level1)
// 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"])
let actualIncludedPath = configuration.includedPaths.first!.bridge()
.absolutePathRepresentation(rootDirectory: configuration.rootDirectory)
let desiredIncludedPath = "File1.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0)
let actualExcludedPath = configuration.excludedPaths.first!.bridge()
.absolutePathRepresentation(rootDirectory: configuration.rootDirectory)
let desiredExcludedPath = "File2.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0)
XCTAssertEqual(actualIncludedPath, desiredIncludedPath)
XCTAssertEqual(actualExcludedPath, desiredExcludedPath)
}
func testIncludedExcludedRelativeLocationLevel0() {
// Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(),
// but run from the directory the config file resides in
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml"])
let actualIncludedPath = configuration.includedPaths.first!.bridge()
.absolutePathRepresentation(rootDirectory: configuration.rootDirectory)
let desiredIncludedPath = "File1.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0)
let actualExcludedPath = configuration.excludedPaths.first!.bridge()
.absolutePathRepresentation(rootDirectory: configuration.rootDirectory)
let desiredExcludedPath = "File2.swift".absolutePathRepresentation(rootDirectory: Mock.Dir.level0)
XCTAssertEqual(actualIncludedPath, desiredIncludedPath)
XCTAssertEqual(actualExcludedPath, desiredExcludedPath)
}
private class TestFileManager: LintableFileManager {
func filesToLint(inPath path: String, rootDirectory: String? = nil) -> [String] {
var filesToLint: [String] = []
switch path {
case "directory": filesToLint = ["directory/File1.swift", "directory/File2.swift",
"directory/excluded/Excluded.swift",
"directory/ExcludedFile.swift"]
case "directory/excluded": filesToLint = ["directory/excluded/Excluded.swift"]
case "directory/ExcludedFile.swift": filesToLint = ["directory/ExcludedFile.swift"]
default: XCTFail("Should not be called with path \(path)")
}
return filesToLint.absolutePathsStandardized()
}
func modificationDate(forFileAtPath path: String) -> Date? {
return nil
}
func isFile(atPath path: String) -> Bool {
path.hasSuffix(".swift")
}
}
func testExcludedPaths() {
let fileManager = TestFileManager()
let configuration = Configuration(includedPaths: ["directory"],
excludedPaths: ["directory/excluded",
"directory/ExcludedFile.swift"])
let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
let paths = configuration.lintablePaths(inPath: "",
forceExclude: false,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
}
func testForceExcludesFile() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
let paths = configuration.lintablePaths(inPath: "directory/ExcludedFile.swift",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual([], paths)
}
func testForceExcludesFileNotPresentInExcluded() {
let fileManager = TestFileManager()
let configuration = Configuration(includedPaths: ["directory"],
excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"])
let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
let paths = configuration.lintablePaths(inPath: "",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
}
func testForceExcludesDirectory() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
let paths = configuration.lintablePaths(inPath: "directory",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
}
func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
let paths = configuration.lintablePaths(inPath: "directory",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
}
func testLintablePaths() {
let excluded = Configuration.default.excludedPaths(fileManager: TestFileManager())
let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: false,
excludeBy: .paths(excludedPaths: excluded))
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = [
"DirectoryLevel1.swift",
"Level0.swift", "Level1.swift", "Level2.swift", "Level3.swift",
"Main.swift", "Sub.swift"
]
XCTAssertEqual(Set(expectedFilenames), Set(filenames))
}
func testGlobIncludePaths() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(includedPaths: ["**/Level2"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: true,
excludeBy: .paths(excludedPaths: configuration.excludedPaths))
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"]
XCTAssertEqual(Set(expectedFilenames), Set(filenames))
}
func testGlobExcludePaths() {
let configuration = Configuration(
includedPaths: [Mock.Dir.level3],
excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")]
)
let excludedPaths = configuration.excludedPaths()
let lintablePaths = configuration.lintablePaths(inPath: "",
forceExclude: false,
excludeBy: .paths(excludedPaths: excludedPaths))
XCTAssertEqual(lintablePaths, [])
}
// MARK: - Testing Configuration Equality
func testIsEqualTo() {
XCTAssertEqual(Mock.Config._0, Mock.Config._0)
}
func testIsNotEqualTo() {
XCTAssertNotEqual(Mock.Config._0, Mock.Config._2)
}
// MARK: - Testing Custom Configuration File
func testCustomConfiguration() {
let file = SwiftLintFile(path: Mock.Swift._0)!
XCTAssertNotEqual(Mock.Config._0.configuration(for: file),
Mock.Config._0Custom.configuration(for: file))
}
func testConfigurationWithSwiftFileAsRoot() {
let configuration = Configuration(configurationFiles: [Mock.Yml._0])
let file = SwiftLintFile(path: Mock.Swift._0)!
XCTAssertEqual(configuration.configuration(for: file), configuration)
}
func testConfigurationWithSwiftFileAsRootAndCustomConfiguration() {
let configuration = Mock.Config._0Custom
let file = SwiftLintFile(path: Mock.Swift._0)!
XCTAssertEqual(configuration.configuration(for: file), configuration)
}
// MARK: - Testing custom indentation
func testIndentationTabs() throws {
let configuration = try Configuration(dict: ["indentation": "tabs"])
XCTAssertEqual(configuration.indentation, .tabs)
}
func testIndentationSpaces() throws {
let configuration = try Configuration(dict: ["indentation": 2])
XCTAssertEqual(configuration.indentation, .spaces(count: 2))
}
func testIndentationFallback() throws {
let configuration = try Configuration(dict: ["indentation": "invalid"])
XCTAssertEqual(configuration.indentation, .spaces(count: 4))
}
// MARK: - Testing Rules from config dictionary
private let testRuleList = RuleList(rules: RuleWithLevelsMock.self)
func testConfiguresCorrectlyFromDict() throws {
let ruleConfiguration = [1, 2]
let config = [RuleWithLevelsMock.description.identifier: ruleConfiguration]
let rules = try testRuleList.allRulesWrapped(configurationDict: config).map { $0.rule }
// swiftlint:disable:next xct_specific_matcher
XCTAssertTrue(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)])
}
func testConfigureFallsBackCorrectly() throws {
let config = [RuleWithLevelsMock.description.identifier: ["a", "b"]]
let rules = try testRuleList.allRulesWrapped(configurationDict: config).map { $0.rule }
// swiftlint:disable:next xct_specific_matcher
XCTAssertTrue(rules == [RuleWithLevelsMock()])
}
func testAllowZeroLintableFiles() throws {
let configuration = try Configuration(dict: ["allow_zero_lintable_files": true])
XCTAssertTrue(configuration.allowZeroLintableFiles)
}
}
// MARK: - ExcludeByPrefix option tests
extension ConfigurationTests {
func testExcludeByPrefixExcludedPaths() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(includedPaths: ["Level1"],
excludedPaths: ["Level1/Level1.swift",
"Level1/Level2/Level3"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: false,
excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(filenames, ["Level2.swift"])
}
func testExcludeByPrefixForceExcludesFile() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"])
let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift",
forceExclude: true,
excludeBy: .prefix)
XCTAssertEqual([], paths)
}
func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(includedPaths: ["Level1"],
excludedPaths: ["Level1/Level1.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: true,
excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames)
}
func testExcludeByPrefixForceExcludesDirectory() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(
excludedPaths: [
"Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig"
]
)
let paths = configuration.lintablePaths(inPath: ".",
forceExclude: true,
excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames)
}
func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(
excludedPaths: [
"Level1", "Directory.swift/DirectoryLevel1.swift", "ChildConfig", "ParentConfig", "NestedConfig"
]
)
let paths = configuration.lintablePaths(inPath: ".",
forceExclude: true,
excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(["Level0.swift"], filenames)
}
func testExcludeByPrefixGlobExcludePaths() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(
includedPaths: ["Level1"],
excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: false,
excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(filenames, ["Level1.swift"])
}
func testDictInitWithCachePath() throws {
let configuration = try Configuration(
dict: ["cache_path": "cache/path/1"]
)
XCTAssertEqual(configuration.cachePath, "cache/path/1")
}
func testDictInitWithCachePathFromCommandLine() throws {
let configuration = try Configuration(
dict: ["cache_path": "cache/path/1"],
cachePath: "cache/path/2"
)
XCTAssertEqual(configuration.cachePath, "cache/path/2")
}
func testMainInitWithCachePath() {
let configuration = Configuration(
configurationFiles: [],
cachePath: "cache/path/1"
)
XCTAssertEqual(configuration.cachePath, "cache/path/1")
}
// This test demonstrates an existing bug: when the Configuration is obtained from the in-memory cache, the
// cachePath is not taken into account
//
// This issue may not be reproducible under normal execution: the cache is in memory, so when a user changes
// the cachePath from command line and re-runs swiftlint, cache is not reused leading to the correct behavior
func testMainInitWithCachePathAndCachedConfig() {
let configuration1 = Configuration(
configurationFiles: [],
cachePath: "cache/path/1"
)
let configuration2 = Configuration(
configurationFiles: [],
cachePath: "cache/path/2"
)
XCTAssertEqual(configuration1.cachePath, "cache/path/1")
XCTAssertEqual(configuration2.cachePath, "cache/path/1")
}
}
private extension Sequence where Element == String {
func absolutePathsStandardized() -> [String] {
map { $0.absolutePathStandardized() }
}
}