Files

654 lines
27 KiB
Swift

import Foundation
import SourceKittenFramework
import TestHelpers
import XCTest
@testable import SwiftLintFramework
// swiftlint:disable file_length
private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0)
final class ConfigurationTests: SwiftLintTestCase {
// MARK: Setup & Teardown
private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional
override func setUp() {
super.setUp()
Configuration.resetCache()
previousWorkingDir = FileManager.default.currentDirectoryPath
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
}
override func tearDown() {
super.tearDown()
XCTAssert(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`
XCTAssert(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)
XCTAssertFalse(config.strict)
XCTAssertFalse(config.lenient)
XCTAssertNil(config.baseline)
XCTAssertNil(config.writeBaseline)
XCTAssertFalse(config.checkForUpdates)
}
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)
XCTAssertTrue(config.strict)
XCTAssertNotNil(config.baseline)
XCTAssertNotNil(config.writeBaseline)
}
func testEnableAllRulesConfiguration() throws {
let configuration = try Configuration(
dict: [:],
enableAllRules: true,
cachePath: nil
)
XCTAssertEqual(configuration.rules.count, RuleRegistry.shared.list.list.count)
}
func testOnlyRule() throws {
let configuration = try Configuration(
dict: [:],
onlyRule: ["nesting"],
cachePath: nil
)
XCTAssertEqual(configuration.rules.count, 1)
}
func testOnlyRuleMultiple() throws {
let onlyRuleIdentifiers = ["nesting", "todo"].sorted()
let configuration = try Configuration(
dict: ["only_rules": "line_length"],
onlyRule: onlyRuleIdentifiers,
cachePath: nil
)
XCTAssertEqual(onlyRuleIdentifiers, configuration.enabledRuleIdentifiers)
let childConfiguration = try Configuration(dict: ["disabled_rules": onlyRuleIdentifiers.last ?? ""])
let mergedConfiguration = configuration.merged(withChild: childConfiguration)
XCTAssertEqual(onlyRuleIdentifiers.dropLast(), mergedConfiguration.enabledRuleIdentifiers)
}
func testOnlyRules() throws {
let only = ["nesting", "todo"]
let config = try Configuration(dict: ["only_rules": only])
let configuredIdentifiers = config.rules.map {
type(of: $0).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.customRules else {
XCTFail("Custom rules are expected to be present")
return
}
XCTAssertTrue(
resultingCustomRules.configuration.customRuleConfigurations.contains {
$0.identifier == customRuleIdentifier
}
)
}
func testOnlyRulesWithSpecificCustomRules() 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
XCTAssertNotNil(resultingCustomRules)
let enabledCustomRuleIdentifiers =
resultingCustomRules?.configuration.customRuleConfigurations.map(\.identifier)
XCTAssertEqual(enabledCustomRuleIdentifiers, [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).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) }))
XCTAssertEqual(expectedIdentifiers, Set(configuration.enabledRuleIdentifiers))
}
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).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
}
XCTAssert(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
XCTAssert(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)
}
func testExcludedPaths() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
let configuration = Configuration(
includedPaths: ["directory"],
excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]
)
let paths = configuration.lintablePaths(
inPath: "",
forceExclude: false,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
}
func testForceExcludesFile() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(
inPath: "directory/ExcludedFile.swift",
forceExclude: true,
excludeByPrefix: false
)
XCTAssert(paths.isEmpty)
}
func testForceExcludesFileNotPresentInExcluded() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
let configuration = Configuration(
includedPaths: ["directory"],
excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"]
)
let paths = configuration.lintablePaths(
inPath: "",
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
}
func testForceExcludesDirectory() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
let configuration = Configuration(excludedPaths: ["directory/excluded"])
let paths = configuration.lintablePaths(
inPath: "directory",
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/ExcludedFile.swift"], paths)
}
func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(
inPath: "directory",
forceExclude: true,
excludeByPrefix: false
)
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/excluded/Excluded.swift"], paths)
}
func testLintablePaths() {
let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: false,
excludeByPrefix: false)
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() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
let configuration = Configuration(includedPaths: ["**/Level2"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: true,
excludeByPrefix: false)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"]
XCTAssertEqual(Set(expectedFilenames), Set(filenames))
}
func testDuplicatedGlobIncludePaths() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
let configuration = Configuration(includedPaths: ["**/Level2", "**/Level2"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: true,
excludeByPrefix: false)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"]
XCTAssertEqual(expectedFilenames, filenames)
}
func testGlobExcludePaths() {
let configuration = Configuration(
includedPaths: [Mock.Dir.level3],
excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")]
)
let lintablePaths = configuration.lintablePaths(inPath: "",
forceExclude: false,
excludeByPrefix: false)
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.identifier: ruleConfiguration]
let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.rule)
// swiftlint:disable:next xct_specific_matcher
XCTAssertTrue(rules == [try RuleWithLevelsMock(configuration: ruleConfiguration)])
}
func testConfigureFallsBackCorrectly() throws {
let config = [RuleWithLevelsMock.identifier: ["a", "b"]]
let rules = try testRuleList.allRulesWrapped(configurationDict: config).map(\.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)
}
func testStrict() throws {
let configuration = try Configuration(dict: ["strict": true])
XCTAssertTrue(configuration.strict)
}
func testLenient() throws {
let configuration = try Configuration(dict: ["lenient": true])
XCTAssertTrue(configuration.lenient)
}
func testBaseline() throws {
let baselinePath = "Baseline.json"
let configuration = try Configuration(dict: ["baseline": baselinePath])
XCTAssertEqual(configuration.baseline, baselinePath)
}
func testWriteBaseline() throws {
let baselinePath = "Baseline.json"
let configuration = try Configuration(dict: ["write_baseline": baselinePath])
XCTAssertEqual(configuration.writeBaseline, baselinePath)
}
func testCheckForUpdates() throws {
let configuration = try Configuration(dict: ["check_for_updates": true])
XCTAssertTrue(configuration.checkForUpdates)
}
}
// MARK: - ExcludeByPrefix option tests
extension ConfigurationTests {
func testExcludeByPrefixExcludedPaths() {
XCTAssert(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,
excludeByPrefix: true)
let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(filenames, ["Level2.swift"])
}
func testExcludeByPrefixForceExcludesFile() {
XCTAssert(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,
excludeByPrefix: true)
XCTAssertEqual([], paths)
}
func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
let configuration = Configuration(includedPaths: ["Level1"],
excludedPaths: ["Level1/Level1.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: true,
excludeByPrefix: true)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames)
}
func testExcludeByPrefixForceExcludesDirectory() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
let configuration = Configuration(
excludedPaths: [
"Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig"
]
)
let paths = configuration.lintablePaths(inPath: ".",
forceExclude: true,
excludeByPrefix: true)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames)
}
func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
XCTAssert(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,
excludeByPrefix: true)
let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(["Level0.swift"], filenames)
}
func testExcludeByPrefixGlobExcludePaths() {
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0))
let configuration = Configuration(
includedPaths: ["Level1"],
excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"])
let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: false,
excludeByPrefix: true)
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 func assertEqual(_ relativeExpectedPaths: [String],
_ actualPaths: [String],
file: StaticString = #filePath,
line: UInt = #line) {
XCTAssertEqual(
relativeExpectedPaths.absolutePathsStandardized().sorted(),
actualPaths.sorted(),
file: file,
line: line
)
}
}
private extension Sequence where Element == String {
func absolutePathsStandardized() -> [String] {
// In Bazel builds, absolute paths might be prefixed with `/private`.
map { String($0.absolutePathStandardized().trimmingPrefix("/private")) }
}
}
private extension Configuration {
var enabledRuleIdentifiers: [String] {
rules.map {
type(of: $0).identifier
}.sorted()
}
}