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 { // swiftlint:disable:this type_body_length // 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.filepath)) } 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.filepath)) // 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".url()]) 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) XCTAssertEqual(config.baseline, expectedConfig.baseline) XCTAssertEqual(config.writeBaseline, expectedConfig.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.filepath)) // 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: Mock.Dir.level0) let actualExcludedPath = configuration.excludedPaths.first! let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0) XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path) XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path) } func testIncludedExcludedRelativeLocationLevel0() { // Same as testIncludedPathRelatedToConfigurationFileLocationLevel1(), // but run from the directory the config file resides in XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration(configurationFiles: ["custom_included_excluded.yml".url()]) let actualIncludedPath = configuration.includedPaths.first! let desiredIncludedPath = "File1.swift".url(relativeTo: Mock.Dir.level0) let actualExcludedPath = configuration.excludedPaths.first! let desiredExcludedPath = "File2.swift".url(relativeTo: Mock.Dir.level0) XCTAssertEqual(actualIncludedPath.path, desiredIncludedPath.path) XCTAssertEqual(actualExcludedPath.path, desiredExcludedPath.path) } func testExcludedPaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) 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) } func testForceExcludesFile() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift".url()]) let paths = configuration.lintablePaths( inPath: "directory/ExcludedFile.swift".url(), forceExclude: true, excludeByPrefix: false ) XCTAssert(paths.isEmpty) } func testForceExcludesFileNotPresentInExcluded() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) 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) } func testForceExcludesDirectory() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) 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) } func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests.filepath)) 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) } func testLintablePaths() { let paths = Configuration.default.lintablePaths(inPath: Mock.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", ] XCTAssertEqual(Set(expectedFilenames), Set(filenames)) } func testGlobIncludePaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration(includedPaths: ["**/Level2".url()]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, excludeByPrefix: false) let filenames = paths.map(\.lastPathComponent).sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] XCTAssertEqual(Set(expectedFilenames), Set(filenames)) } func testDuplicatedGlobIncludePaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration(includedPaths: ["**/Level2".url(), "**/Level2".url()]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, excludeByPrefix: false) let filenames = paths.map(\.lastPathComponent).sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] XCTAssertEqual(expectedFilenames, filenames) } func testGlobExcludePaths() { let configuration = Configuration( includedPaths: [Mock.Dir.level3], excludedPaths: [Mock.Dir.level3.appending(path: "*.swift")] ) let lintablePaths = configuration.lintablePaths(inPath: URL.cwd, 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.url()) } func testWriteBaseline() throws { let baselinePath = "Baseline.json" let configuration = try Configuration(dict: ["write_baseline": baselinePath]) XCTAssertEqual(configuration.writeBaseline, baselinePath.url()) } func testCheckForUpdates() throws { let configuration = try Configuration(dict: ["check_for_updates": true]) XCTAssertTrue(configuration.checkForUpdates) } // MARK: - ExcludeByPrefix option tests func testExcludeByPrefixExcludedPaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( includedPaths: ["Level1".url()], excludedPaths: ["Level1/Level1.swift".url(), "Level1/Level2/Level3".url()] ) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, excludeByPrefix: true) let filenames = paths.map(\.lastPathComponent) XCTAssertEqual(filenames, ["Level2.swift"]) } func testExcludeByPrefixForceExcludesFile() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) 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) XCTAssertEqual([], paths) } func testExcludeByPrefixForceExcludesFileNotPresentInExcluded() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) 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() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } func testExcludeByPrefixForceExcludesDirectory() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( excludedPaths: [ "Level1/Level2".url(), "Directory.swift".url(), "ChildConfig".url(), "ParentConfig".url(), "NestedConfig".url(), ] ) let paths = configuration.lintablePaths(inPath: URL.cwd, forceExclude: true, excludeByPrefix: true) let filenames = paths.map(\.lastPathComponent).sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } func testExcludeByPrefixForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) let configuration = Configuration( excludedPaths: [ "Level1".url(), "Directory.swift/DirectoryLevel1.swift".url(), "ChildConfig".url(), "ParentConfig".url(), "NestedConfig".url(), ] ) let paths = configuration.lintablePaths(inPath: URL.cwd, forceExclude: true, excludeByPrefix: true) let filenames = paths.map(\.lastPathComponent) XCTAssertEqual(["Level0.swift"], filenames) } func testExcludeByPrefixGlobExcludePaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0.filepath)) 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() 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: [URL], file: StaticString = #filePath, line: UInt = #line) { XCTAssertEqual( relativeExpectedPaths.absolutePathsStandardized().sorted(), actualPaths.map(\.path).absolutePathsStandardized().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 { URL(filePath: $0).standardizedFileURL.resolvingSymlinksInPath().relativeDisplayPath } } } private extension Configuration { var enabledRuleIdentifiers: [String] { rules.map { type(of: $0).identifier }.sorted() } }