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() } }