diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 43a85af20e..b55e3b2767 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ Linting rules are defined in `Source/SwiftLintBuiltInRules/Rules`. If someone me User-facing changes must be documented in the `CHANGELOG.md` file, which is organized by version. New entries always go into the "Main" section. They give credit to the person who has made the change and they reference the issue which has been fixed by the change. -All changes on configuration options must be reflected in `Tests/IntegrationTests/default_rule_configurations.yml`. This can be achieved by running `swift run swiftlint-dev rules register`. Running this command is also necessary when new rules got added or removed to (un-)register them from/in the list of built-in rules and tests verifying all examples in rule descriptions. +All changes on configuration options must be reflected in `Tests/IntegrationTests/Resources/default_rule_configurations.yml`. This can be achieved by running `swift run swiftlint-dev rules register`. Running this command is also necessary when new rules got added or removed to (un-)register them from/in the list of built-in rules and tests verifying all examples in rule descriptions. For some rules, there are dedicated tests in `Tests/BuiltInRulesTests`. However, they are typically not required as all the examples in the rule descriptions are automatically tested. The examples in the rule descriptions are also used to generate documentation for the rules. If an example presents a very pathological case, that's helpful for testing but not for user documentation, you can add the `excludeFromDocumentation: true` parameter to the example initializer. Important is that all examples in the rule description are verified by running `RuleGeneratedTests` for rule modified rules. diff --git a/.swiftlint.yml b/.swiftlint.yml index 4007569390..83b507bc5f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -8,6 +8,7 @@ excluded: - assets - Tests/BuiltInRulesTests/Resources - Tests/FileSystemAccessTests/Resources + - Tests/IntegrationTests/Resources # Enabled/disabled rules analyzer_rules: diff --git a/BUILD b/BUILD index 354fdfa73f..535858f1ec 100644 --- a/BUILD +++ b/BUILD @@ -70,6 +70,7 @@ swift_library( visibility = ["//visibility:public"], deps = [ ":Yams.wrapper", + "@FilenameMatcher", "@SourceKittenFramework", "@SwiftSyntax//:SwiftIDEUtils_opt", "@SwiftSyntax//:SwiftOperators_opt", diff --git a/CHANGELOG.md b/CHANGELOG.md index b71da70bf4..59422c4caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ * Add new `unneeded_escaping` rule that detects closure parameters marked with `@escaping` that are never stored or captured escapingly. [SimplyDanny](https://github.com/SimplyDanny) + +* Improve performance when exclude patterns resolve to a large set of files. + [SimplyDanny](https://github.com/SimplyDanny) + [#5018](https://github.com/realm/SwiftLint/issues/5018) ### Bug Fixes diff --git a/MODULE.bazel b/MODULE.bazel index 08508411fd..77f37e97cf 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,6 +15,7 @@ bazel_dep(name = "rules_swift", version = "3.1.2", max_compatibility_level = 3, bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "SourceKittenFramework") bazel_dep(name = "swift_argument_parser", version = "1.6.2", repo_name = "SwiftArgumentParser") +bazel_dep(name = "swift-filename-matcher", version = "2.0.1", repo_name = "FilenameMatcher") bazel_dep(name = "yams", version = "6.2.0", repo_name = "Yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") diff --git a/Package.resolved b/Package.resolved index 3f956deaeb..e10b5dace8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -36,6 +36,15 @@ "version" : "1.6.2" } }, + { + "identity" : "swift-filename-matcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ileitch/swift-filename-matcher", + "state" : { + "revision" : "eef5ac0b6b3cdc64b3039b037bed2def8a1edaeb", + "version" : "2.0.1" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index cb777696a0..562d8cfdea 100644 --- a/Package.swift +++ b/Package.swift @@ -42,6 +42,7 @@ let package = Package( .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", .upToNextMajor(from: "0.2.0")), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.9.0")), + .package(url: "https://github.com/ileitch/swift-filename-matcher", .upToNextMinor(from: "2.0.1")), ], targets: [ .executableTarget( @@ -65,6 +66,7 @@ let package = Package( .target( name: "SwiftLintFramework", dependencies: [ + .product(name: "FilenameMatcher", package: "swift-filename-matcher"), "SwiftLintBuiltInRules", "SwiftLintCore", "SwiftLintExtraRules", @@ -96,6 +98,7 @@ let package = Package( dependencies: [ .product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])), .target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])), + .product(name: "FilenameMatcher", package: "swift-filename-matcher"), .product(name: "SourceKittenFramework", package: "SourceKitten"), .product(name: "SwiftIDEUtils", package: "swift-syntax"), .product(name: "SwiftOperators", package: "swift-syntax"), @@ -167,6 +170,7 @@ let package = Package( .testTarget( name: "FileSystemAccessTests", dependencies: [ + .product(name: "FilenameMatcher", package: "swift-filename-matcher"), "SwiftLintFramework", "TestHelpers", "SwiftLintCoreMacros", @@ -203,7 +207,7 @@ let package = Package( "TestHelpers", ], exclude: [ - "default_rule_configurations.yml" + "Resources", ], swiftSettings: swiftFeatures + targetedConcurrency // Set to strict once SwiftLintFramework is updated ), diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index 2d6a431a43..357faf01fe 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -68,6 +68,9 @@ public extension String { /// Returns a new string, converting the path to a canonical absolute path. /// + /// > Important: This method might use an incorrect working directory internally. This can cause test failures + /// in Bazel builds but does not seem to cause trouble in production. + /// /// - returns: A new `String`. func absolutePathStandardized() -> String { bridge().absolutePathRepresentation().bridge().standardizingPath diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 2935f2fc79..512c833d94 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -252,14 +252,8 @@ extension Configuration { guard options.forceExclude else { return files } - let scriptInputPaths = files.compactMap(\.path) - - if options.useExcludingByPrefix { - return filterExcludedPathsByPrefix(in: scriptInputPaths) - .map(SwiftLintFile.init(pathDeferringReading:)) - } - return filterExcludedPaths(excludedPaths(), in: scriptInputPaths) + return filteredPaths(in: scriptInputPaths, excludeByPrefix: visitor.options.useExcludingByPrefix) .map(SwiftLintFile.init(pathDeferringReading:)) } if !options.quiet { @@ -272,14 +266,12 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } - let excludeLintableFilesBy = options.useExcludingByPrefix - ? Configuration.ExcludeBy.prefix - : .paths(excludedPaths: excludedPaths()) - return options.paths.flatMap { + return visitor.options.paths.flatMap { self.lintableFiles( inPath: $0, - forceExclude: options.forceExclude, - excludeBy: excludeLintableFilesBy) + forceExclude: visitor.options.forceExclude, + excludeByPrefix: visitor.options.useExcludingByPrefix + ) } } diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index 1c91c0dbf5..2102cd55f9 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -1,25 +1,22 @@ +import FilenameMatcher import Foundation extension Configuration { - public enum ExcludeBy { - case prefix - case paths(excludedPaths: [String]) - } - // MARK: Lintable Paths + /// Returns the files that can be linted by SwiftLint in the specified parent path. /// /// - parameter path: The parent path in which to search for lintable files. Can be a directory or a /// file. /// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if /// `path` is an exact match. - /// - parameter excludeByPrefix: Whether or not uses excluding by prefix algorithm. + /// - parameter excludeByPrefix: Whether or not it uses the exclude-by-prefix algorithm. /// /// - returns: Files to lint. public func lintableFiles(inPath path: String, forceExclude: Bool, - excludeBy: ExcludeBy) -> [SwiftLintFile] { - lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy) + excludeByPrefix: Bool) -> [SwiftLintFile] { + lintablePaths(inPath: path, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix) .parallelCompactMap { SwiftLintFile(pathDeferringReading: $0) } @@ -31,90 +28,65 @@ extension Configuration { /// file. /// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if /// `path` is an exact match. - /// - parameter excludeByPrefix: Whether or not uses excluding by prefix algorithm. + /// - parameter excludeByPrefix: Whether or not it uses the exclude-by-prefix algorithm. /// - parameter fileManager: The lintable file manager to use to search for lintable files. /// /// - returns: Paths for files to lint. - internal func lintablePaths( + func lintablePaths( inPath path: String, forceExclude: Bool, - excludeBy: ExcludeBy, + excludeByPrefix: Bool, fileManager: some LintableFileManager = FileManager.default ) -> [String] { + let excluder = createExcluder(excludeByPrefix: excludeByPrefix) + + // Handle single file path. if fileManager.isFile(atPath: path) { - if forceExclude { - switch excludeBy { - case .prefix: - return filterExcludedPathsByPrefix(in: [path.absolutePathStandardized()]) - case .paths(let excludedPaths): - return filterExcludedPaths(excludedPaths, in: [path.absolutePathStandardized()]) - } - } - // If path is a file and we're not forcing excludes, skip filtering with excluded/included paths - return [path] + return fileManager.filesToLint( + inPath: path, + rootDirectory: nil, + excluder: forceExclude ? excluder : .noExclusion + ) } - let pathsForPath = includedPaths.isEmpty ? fileManager.filesToLint(inPath: path, rootDirectory: nil) : [] - let includedPaths = self.includedPaths - .flatMap(Glob.resolveGlob) - .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) } - - switch excludeBy { - case .prefix: - return filterExcludedPathsByPrefix(in: pathsForPath, includedPaths) - case .paths(let excludedPaths): - return filterExcludedPaths(excludedPaths, in: pathsForPath, includedPaths) + // With no included paths, we lint everything in the given path. + if includedPaths.isEmpty { + return fileManager.filesToLint( + inPath: path, + rootDirectory: nil, + excluder: excluder + ) } - } - - /// Returns an array of file paths after removing the excluded paths as defined by this configuration. - /// - /// - parameter fileManager: The lintable file manager to use to expand the excluded paths into all matching paths. - /// - parameter paths: The input paths to filter. - /// - /// - returns: The input paths after removing the excluded paths. - public func filterExcludedPaths( - _ excludedPaths: [String], - in paths: [String]... - ) -> [String] { - let allPaths = paths.flatMap(\.self) - #if os(Linux) - let result = NSMutableOrderedSet(capacity: allPaths.count) - result.addObjects(from: allPaths) - #else - let result = NSMutableOrderedSet(array: allPaths) - #endif - result.minusSet(Set(excludedPaths)) - // swiftlint:disable:next force_cast - return result.map { $0 as! String } + // With included paths, we only lint those paths (after resolving globs). + return includedPaths + .flatMap(Glob.resolveGlob) + .parallelFlatMap { + fileManager.filesToLint( + inPath: $0, + rootDirectory: rootDirectory, + excluder: excluder + ) + } } - /// Returns the file paths that are excluded by this configuration using filtering by absolute path prefix. - /// - /// For cases when excluded directories contain many lintable files (e. g. Pods) it works faster than default - /// algorithm `filterExcludedPaths`. - /// - /// - returns: The input paths after removing the excluded paths. - public func filterExcludedPathsByPrefix(in paths: [String]...) -> [String] { - let allPaths = paths.flatMap(\.self) - let excludedPaths = self.excludedPaths - .parallelFlatMap { @Sendable in Glob.resolveGlob($0) } - .map { $0.absolutePathStandardized() } - return allPaths.filter { path in - !excludedPaths.contains { path.hasPrefix($0) } - } + func filteredPaths(in paths: [String], excludeByPrefix: Bool) -> [String] { + let excluder = createExcluder(excludeByPrefix: excludeByPrefix) + return paths.filter { !excluder.excludes(path: $0) } } - /// Returns the file paths that are excluded by this configuration after expanding them using the specified file - /// manager. - /// - /// - parameter fileManager: The file manager to get child paths in a given parent location. - /// - /// - returns: The expanded excluded file paths. - public func excludedPaths(fileManager: some LintableFileManager = FileManager.default) -> [String] { - excludedPaths - .flatMap(Glob.resolveGlob) - .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) } + private func createExcluder(excludeByPrefix: Bool) -> Excluder { + if excludeByPrefix { + return .byPrefix( + prefixes: self.excludedPaths + .flatMap { Glob.resolveGlob($0) } + .map { $0.absolutePathStandardized() } + ) + } + return .matching( + matchers: self.excludedPaths.flatMap { + Glob.createFilenameMatchers(root: rootDirectory, pattern: $0) + } + ) } } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 31280a08ed..d8c3669398 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -1,17 +1,19 @@ +import FilenameMatcher import Foundation import SourceKittenFramework /// An interface for enumerating files that can be linted by SwiftLint. public protocol LintableFileManager { /// Returns all files that can be linted in the specified path. If the path is relative, it will be appended to the - /// specified root path, or currentt working directory if no root directory is specified. + /// specified root path, or current working directory if no root directory is specified. /// /// - parameter path: The path in which lintable files should be found. /// - parameter rootDirectory: The parent directory for the specified path. If none is provided, the current working /// directory will be used. + /// - parameter excluder: The excluder used to filter out files that should not be linted. /// /// - returns: Files to lint. - func filesToLint(inPath path: String, rootDirectory: String?) -> [String] + func filesToLint(inPath path: String, rootDirectory: String?, excluder: Excluder) -> [String] /// Returns the date when the file at the specified path was last modified. Returns `nil` if the file cannot be /// found or its last modification date cannot be determined. @@ -29,22 +31,56 @@ public protocol LintableFileManager { func isFile(atPath path: String) -> Bool } +/// An excluder for filtering out files that should not be linted. +public enum Excluder { + /// Full matching excluder using filename matchers. + case matching(matchers: [FilenameMatcher]) + /// Prefix-based excluder using path prefixes. + case byPrefix(prefixes: [String]) + /// An excluder that does not exclude any files. + case noExclusion + + func excludes(path: String) -> Bool { + switch self { + case let .matching(matchers): + matchers.contains(where: { $0.match(filename: path) }) + case let .byPrefix(prefixes): + prefixes.contains(where: { path.hasPrefix($0) }) + case .noExclusion: + false + } + } +} + extension FileManager: LintableFileManager { - public func filesToLint(inPath path: String, rootDirectory: String? = nil) -> [String] { + public func filesToLint(inPath path: String, + rootDirectory: String? = nil, + excluder: Excluder) -> [String] { let absolutePath = path.bridge() .absolutePathRepresentation(rootDirectory: rootDirectory ?? currentDirectoryPath).bridge() .standardizingPath - // if path is a file, it won't be returned in `enumerator(atPath:)` + // If path is a file, it won't be returned in `enumerator(atPath:)`. if absolutePath.bridge().isSwiftFile(), absolutePath.isFile { - return [absolutePath] + return excluder.excludes(path: absolutePath) ? [] : [absolutePath] + } + + guard let enumerator = enumerator(atPath: absolutePath) else { + return [] } - return subpaths(atPath: absolutePath)?.parallelCompactMap { element -> String? in - guard element.bridge().isSwiftFile() else { return nil } + var files = [String]() + while let element = enumerator.nextObject() as? String { let absoluteElementPath = absolutePath.bridge().appendingPathComponent(element) - return absoluteElementPath.isFile ? absoluteElementPath : nil - } ?? [] + if absoluteElementPath.bridge().isSwiftFile(), absoluteElementPath.isFile { + if !excluder.excludes(path: absoluteElementPath) { + files.append(absoluteElementPath) + } + } else if excluder.excludes(path: absoluteElementPath) { + enumerator.skipDescendants() + } + } + return files } public func modificationDate(forFileAtPath path: String) -> Date? { diff --git a/Source/SwiftLintFramework/Helpers/Glob.swift b/Source/SwiftLintFramework/Helpers/Glob.swift index 953819fd15..59df383635 100644 --- a/Source/SwiftLintFramework/Helpers/Glob.swift +++ b/Source/SwiftLintFramework/Helpers/Glob.swift @@ -1,3 +1,4 @@ +import FilenameMatcher import Foundation import SourceKittenFramework @@ -37,6 +38,29 @@ struct Glob { .map { $0.absolutePathStandardized() } } + static func createFilenameMatchers(root: String, pattern: String) -> [FilenameMatcher] { + var absolutPathPattern = pattern + if !pattern.starts(with: root) { + // If the root is not already part of the pattern, prepend it. + absolutPathPattern = root + (root.hasSuffix("/") ? "" : "/") + absolutPathPattern + } + absolutPathPattern = absolutPathPattern.absolutePathStandardized() + if pattern.hasSuffix(".swift") || pattern.hasSuffix("/**") { + // Suffix is already well defined. + return [FilenameMatcher(pattern: absolutPathPattern)] + } + if pattern.hasSuffix("/") { + // Matching all files in the folder. + return [FilenameMatcher(pattern: absolutPathPattern + "**")] + } + // The pattern could match files in the last folder in the path or all contained files if the last component + // represents folders. + return [ + FilenameMatcher(pattern: absolutPathPattern), + FilenameMatcher(pattern: absolutPathPattern + "/**"), + ] + } + // MARK: Private private static func expandGlobstar(pattern: String) -> [String] { diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index 51760bf302..c756734df4 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -269,6 +269,7 @@ private extension SwiftLintDev.Rules.Register { .write( to: testsParentDirectory .appendingPathComponent("IntegrationTests", isDirectory: true) + .appendingPathComponent("Resources", isDirectory: true) .appendingPathComponent("default_rule_configurations.yml", isDirectory: false), atomically: true, encoding: .utf8 diff --git a/Tests/BUILD b/Tests/BUILD index 476c4b7109..4b2a087d27 100644 --- a/Tests/BUILD +++ b/Tests/BUILD @@ -166,7 +166,7 @@ swift_library( swift_test( name = "IntegrationTests", data = [ - "IntegrationTests/default_rule_configurations.yml", + "IntegrationTests/Resources/default_rule_configurations.yml", "//:LintInputs", ], visibility = ["//visibility:public"], diff --git a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift index e4b6cfce41..e4a7fc5e15 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift @@ -33,6 +33,10 @@ internal extension ConfigurationTests { static var remoteConfigLocalRef: String { level0.stringByAppendingPathComponent("RemoteConfig/LocalRef") } static var remoteConfigCycle: String { level0.stringByAppendingPathComponent("RemoteConfig/Cycle") } static var emptyFolder: String { level0.stringByAppendingPathComponent("EmptyFolder") } + + static var exclusionTests: String { testResourcesPath.stringByAppendingPathComponent("ExclusionTests") } + static var directory: String { exclusionTests.stringByAppendingPathComponent("directory") } + static var directoryExcluded: String { directory.stringByAppendingPathComponent("excluded") } } // MARK: YAML File Paths diff --git a/Tests/FileSystemAccessTests/ConfigurationTests.swift b/Tests/FileSystemAccessTests/ConfigurationTests.swift index c17ca5e064..67adc66f28 100644 --- a/Tests/FileSystemAccessTests/ConfigurationTests.swift +++ b/Tests/FileSystemAccessTests/ConfigurationTests.swift @@ -9,7 +9,6 @@ import XCTest private let optInRules = RuleRegistry.shared.list.list.filter({ $0.1.init() is any OptInRule }).map(\.0) -// swiftlint:disable:next type_body_length final class ConfigurationTests: SwiftLintTestCase { // MARK: Setup & Teardown private var previousWorkingDir: String! // swiftlint:disable:this implicitly_unwrapped_optional @@ -282,97 +281,81 @@ final class ConfigurationTests: SwiftLintTestCase { 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 _: String) -> Date? { - nil - } - - func isFile(atPath path: String) -> Bool { - path.hasSuffix(".swift") - } - } - func testExcludedPaths() { - let fileManager = TestFileManager() + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) 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) + let paths = configuration.lintablePaths( + inPath: "", + forceExclude: false, + excludeByPrefix: false + ) + + assertEqual(["directory/File1.swift", "directory/File2.swift"], paths) } func testForceExcludesFile() { - let fileManager = TestFileManager() + XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests)) 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) + + let paths = configuration.lintablePaths( + inPath: "directory/ExcludedFile.swift", + forceExclude: true, + excludeByPrefix: false + ) + + XCTAssert(paths.isEmpty) } 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) + 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() { - 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) + 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() { - 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) + 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 excluded = Configuration.default.excludedPaths(fileManager: TestFileManager()) let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, - excludeBy: .paths(excludedPaths: excluded)) + excludeByPrefix: false) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = [ "DirectoryLevel1.swift", @@ -388,7 +371,7 @@ final class ConfigurationTests: SwiftLintTestCase { let configuration = Configuration(includedPaths: ["**/Level2"]) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, - excludeBy: .paths(excludedPaths: configuration.excludedPaths)) + excludeByPrefix: false) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] @@ -401,10 +384,9 @@ final class ConfigurationTests: SwiftLintTestCase { excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")] ) - let excludedPaths = configuration.excludedPaths() let lintablePaths = configuration.lintablePaths(inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: excludedPaths)) + excludeByPrefix: false) XCTAssertEqual(lintablePaths, []) } @@ -519,7 +501,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, - excludeBy: .prefix) + excludeByPrefix: true) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(filenames, ["Level2.swift"]) } @@ -529,7 +511,7 @@ extension ConfigurationTests { let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"]) let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift", forceExclude: true, - excludeBy: .prefix) + excludeByPrefix: true) XCTAssertEqual([], paths) } @@ -539,7 +521,7 @@ extension ConfigurationTests { excludedPaths: ["Level1/Level1.swift"]) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: true, - excludeBy: .prefix) + excludeByPrefix: true) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } @@ -553,7 +535,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: .prefix) + excludeByPrefix: true) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } @@ -567,7 +549,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: .prefix) + excludeByPrefix: true) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(["Level0.swift"], filenames) } @@ -579,7 +561,7 @@ extension ConfigurationTests { excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: false, - excludeBy: .prefix) + excludeByPrefix: true) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(filenames, ["Level1.swift"]) } @@ -629,11 +611,24 @@ extension ConfigurationTests { 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] { - map { $0.absolutePathStandardized() } + // In Bazel builds, absolute paths might be prefixed with `/private`. + map { String($0.absolutePathStandardized().trimmingPrefix("/private")) } } } diff --git a/Tests/FileSystemAccessTests/GlobTests.swift b/Tests/FileSystemAccessTests/GlobTests.swift index 84771f554d..ec8836f2a1 100644 --- a/Tests/FileSystemAccessTests/GlobTests.swift +++ b/Tests/FileSystemAccessTests/GlobTests.swift @@ -1,3 +1,4 @@ +import FilenameMatcher import TestHelpers import XCTest @@ -84,4 +85,31 @@ final class GlobTests: SwiftLintTestCase { let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("**/*.swift")) XCTAssertEqual(files.sorted(), expectedFiles.sorted()) } + + func testCreateFilenameMatchers() { + func assertGlobMatch(root: String = "", pattern: String, filename: String) { + let matchers = Glob.createFilenameMatchers(root: root, pattern: pattern) + XCTAssert(matchers.anyMatch(filename: filename)) + } + + assertGlobMatch(root: "/a/b/", pattern: "c/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(root: "/a", pattern: "**/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(root: "/a", pattern: "**/*.swift", filename: "/a/b.swift") + assertGlobMatch(root: "/", pattern: "**/*.swift", filename: "/a/b.swift") + assertGlobMatch(root: "/", pattern: "a/**/b.swift", filename: "/a/b.swift") + assertGlobMatch(root: "/", pattern: "a/**/b.swift", filename: "/a/c/b.swift") + assertGlobMatch(root: "/", pattern: "**/*.swift", filename: "/a.swift") + assertGlobMatch(root: "/", pattern: "a/**/*.swift", filename: "/a/b/c.swift") + assertGlobMatch(root: "/", pattern: "a/**/*.swift", filename: "/a/b.swift") + assertGlobMatch(root: "/a/b", pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(root: "/a/", pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") + + assertGlobMatch(pattern: "/a/b/c", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/a/b/c/", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/a/b/c/*.swift", filename: "/a/b/c/d.swift") + assertGlobMatch(pattern: "/d.swift/*.swift", filename: "/d.swift/e.swift") + assertGlobMatch(pattern: "/a/**", filename: "/a/b/c/d.swift") + assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/c/MyTest2.swift") + assertGlobMatch(root: "/", pattern: "**/*Test*", filename: "/a/b/MyTests/c.swift") + } } diff --git a/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/ExcludedFile.swift b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/ExcludedFile.swift new file mode 100644 index 0000000000..f9f7f1e44a --- /dev/null +++ b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/ExcludedFile.swift @@ -0,0 +1,4 @@ +// Excluded test file +func excludedFunction() { + print("Excluded File") +} diff --git a/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File1.swift b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File1.swift new file mode 100644 index 0000000000..cbd6604573 --- /dev/null +++ b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File1.swift @@ -0,0 +1,4 @@ +// Test file 1 +func testFunction1() { + print("File 1") +} diff --git a/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File2.swift b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File2.swift new file mode 100644 index 0000000000..1cf76f81e1 --- /dev/null +++ b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/File2.swift @@ -0,0 +1,4 @@ +// Test file 2 +func testFunction2() { + print("File 2") +} diff --git a/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/excluded/Excluded.swift b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/excluded/Excluded.swift new file mode 100644 index 0000000000..fc575e5367 --- /dev/null +++ b/Tests/FileSystemAccessTests/Resources/ExclusionTests/directory/excluded/Excluded.swift @@ -0,0 +1,4 @@ +// Excluded directory test file +func excludedDirectoryFunction() { + print("Excluded in directory") +} diff --git a/Tests/FrameworkTests/LinterCacheTests.swift b/Tests/FrameworkTests/LinterCacheTests.swift index 66c2cb6bda..de1c4c3fcd 100644 --- a/Tests/FrameworkTests/LinterCacheTests.swift +++ b/Tests/FrameworkTests/LinterCacheTests.swift @@ -54,7 +54,9 @@ private struct CacheTestHelper { } private class TestFileManager: LintableFileManager { - fileprivate func filesToLint(inPath _: String, rootDirectory _: String? = nil) -> [String] { + fileprivate func filesToLint(inPath _: String, + rootDirectory _: String? = nil, + excluder _: Excluder) -> [String] { [] } diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift new file mode 100644 index 0000000000..6959cd8025 --- /dev/null +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -0,0 +1,171 @@ +import SourceKittenFramework +import SwiftLintFramework +import TestHelpers +import XCTest + +final class ConfigPathResolutionTests: SwiftLintTestCase { + private func fixturePath(_ scenario: String) -> String { + #filePath.bridge() + .deletingLastPathComponent + .stringByAppendingPathComponent("Resources") + .stringByAppendingPathComponent(scenario) + } + + /// Returns the paths of lintable files relative to the fixture directory. + private func lintableFilePaths(in scenario: String, configFile: String? = nil, inPath: String = ".") -> [String] { + let scenarioPath = fixturePath(scenario) + + let previousDir = FileManager.default.currentDirectoryPath + defer { + _ = FileManager.default.changeCurrentDirectoryPath(previousDir) + } + XCTAssert(FileManager.default.changeCurrentDirectoryPath(scenarioPath)) + + let config = Configuration(configurationFiles: configFile.map { [$0] } ?? []) + let files = config.lintableFiles( + inPath: inPath, + forceExclude: false, + excludeByPrefix: false + ) + + return files.map { $0.path!.bridge().path(relativeTo: scenarioPath) }.sorted() + } + + func testParentChildSameDirectory() { + XCTAssertEqual( + lintableFilePaths(in: "_1_parent_child_same_dir", configFile: "parent.yml"), + ["Sources/CoreFile.swift"] + ) + } + + func testParentChildDifferentDirectories() { + XCTAssertEqual( + lintableFilePaths( + in: "_2_parent_child_different_dirs", + configFile: "project/.swiftlint.yml", + inPath: "project" + ), + ["project/Sources/Core/Service.swift"] + ) + } + + func testChildOverridesParentExclusion() { + XCTAssertEqual( + lintableFilePaths( + in: "_3_child_overrides_parent_exclusion", + configFile: "project/.swiftlint.yml", + inPath: "project" + ), + ["project/Vendor/Critical/Important.swift"] + ) + } + + func testParentIncludesChildExcludes() { + XCTAssertEqual( + lintableFilePaths(in: "_1_parent_child_same_dir", configFile: "parent.yml"), + ["Sources/CoreFile.swift"] + ) + } + + func testNestedConfigurationBasic() { + XCTAssertEqual( + lintableFilePaths(in: "_4_nested_basic", configFile: ".swiftlint.yml"), + ["ModuleA/File.swift", "ModuleA/Generated/File.swift", "ModuleB/File.swift"] + ) + } + + func testWildcardPatternCount() { + XCTAssertEqual( + lintableFilePaths( + in: "_5_wildcard_patterns", + configFile: "project/.swiftlint.yml", + inPath: "project" + ), + ["project/Sources/Models/User.swift"] + ) + } + + func testLintChildFolder() { + XCTAssertEqual( + lintableFilePaths( + in: "_2_parent_child_different_dirs", + configFile: "project/.swiftlint.yml", + inPath: "project" + ), + ["project/Sources/Core/Service.swift"] + ) + } + + func testEmptyIncludedDefaultsToAll() { + XCTAssertEqual( + lintableFilePaths( + in: "_6_wildcards_from_nested_folder", + configFile: ".swiftlint-exclude-thirdparty.yml" + ), + [ + "Generated/API.swift", + "MyProject/Package.swift", + "MyProject/Sources/App.swift", + "MyProject/SubModule/Package.swift", + ] + ) + } + + func testMultipleLevelsOfExclusion() { + XCTAssertEqual( + lintableFilePaths(in: "_1_parent_child_same_dir", configFile: "parent.yml"), + ["Sources/CoreFile.swift"] + ) + } + + func testConfigFromParentFolder() { + XCTAssertEqual( + lintableFilePaths(in: "_6_wildcards_from_nested_folder", configFile: ".swiftlint.yml"), + ["MyProject/Sources/App.swift"] + ) + + XCTAssertEqual( + lintableFilePaths(in: "_6_wildcards_from_nested_folder/MyProject", configFile: "../.swiftlint.yml"), + ["Sources/App.swift"] + ) + } + + func testNestedConfigurationAppliesOnlyToSubdirectory() { + let scenarioPath = fixturePath("_4_nested_basic") + let config = Configuration(configurationFiles: []) + + let moduleAFile = SwiftLintFile( + path: scenarioPath.stringByAppendingPathComponent("ModuleA/File.swift") + )! + let moduleBFile = SwiftLintFile( + path: scenarioPath.stringByAppendingPathComponent("ModuleB/File.swift") + )! + + XCTAssertTrue( + config.configuration(for: moduleAFile).rules + .map { type(of: $0).identifier } + .contains("explicit_type_interface") + ) + + XCTAssertFalse( + config.configuration(for: moduleBFile).rules + .map { type(of: $0).identifier } + .contains("explicit_type_interface") + ) + } + + func testNestedConfigurationDisabledByConfigFlag() { + let scenarioPath = fixturePath("_4_nested_basic") + let configFile = scenarioPath.stringByAppendingPathComponent("root.yml") + + let moduleAFile = SwiftLintFile( + path: scenarioPath.stringByAppendingPathComponent("ModuleB/File.swift") + )! + + XCTAssertFalse( + Configuration(configurationFiles: [configFile]).configuration(for: moduleAFile).rules + .map { type(of: $0).identifier } + .contains("explicit_type_interface") + ) + } +} diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index 4f19b49706..e753d96fb4 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -25,7 +25,7 @@ final class IntegrationTests: SwiftLintTestCase { let swiftFiles = config.lintableFiles( inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: config.excludedPaths())) + excludeByPrefix: false) XCTAssert( swiftFiles.contains(where: { #filePath.bridge().absolutePathRepresentation() == $0.path }), "current file should be included" @@ -50,7 +50,7 @@ final class IntegrationTests: SwiftLintTestCase { let swiftFiles = config.lintableFiles( inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: config.excludedPaths())) + excludeByPrefix: false) let storage = RuleStorage() let corrections = swiftFiles.parallelMap { Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage) diff --git a/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/CoreFile.swift b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/CoreFile.swift new file mode 100644 index 0000000000..0f480514d1 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/CoreFile.swift @@ -0,0 +1,4 @@ +// Should be included +struct CoreFile { + let name: String +} diff --git a/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Generated/GeneratedFile.swift b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Generated/GeneratedFile.swift new file mode 100644 index 0000000000..19566b994a --- /dev/null +++ b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Generated/GeneratedFile.swift @@ -0,0 +1,4 @@ +// Should be excluded by parent +struct GeneratedFile { + let id: Int +} diff --git a/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Models/Model.swift b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Models/Model.swift new file mode 100644 index 0000000000..b012831f22 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/Sources/Models/Model.swift @@ -0,0 +1,4 @@ +// Should be excluded by child +struct Model { + let data: String +} diff --git a/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/child.yml b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/child.yml new file mode 100644 index 0000000000..70754c6866 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/child.yml @@ -0,0 +1,5 @@ +excluded: + - Sources/Models + +opt_in_rules: + - explicit_type_interface diff --git a/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/parent.yml b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/parent.yml new file mode 100644 index 0000000000..6996c77db7 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_1_parent_child_same_dir/parent.yml @@ -0,0 +1,9 @@ +included: + - Sources + +excluded: + - Sources/Generated + +line_length: 100 + +child_config: child.yml diff --git a/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/base/parent.yml b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/base/parent.yml new file mode 100644 index 0000000000..007a7b5853 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/base/parent.yml @@ -0,0 +1,4 @@ +included: + - ../project/Sources + +line_length: 120 diff --git a/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/.swiftlint.yml b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/.swiftlint.yml new file mode 100644 index 0000000000..b33e6e22a5 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/.swiftlint.yml @@ -0,0 +1,4 @@ +parent_config: ../base/parent.yml + +excluded: + - Sources/Generated diff --git a/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Core/Service.swift b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Core/Service.swift new file mode 100644 index 0000000000..d26531eb71 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Core/Service.swift @@ -0,0 +1,4 @@ +// Should be included +struct CoreService { + func execute() {} +} diff --git a/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Generated/Model.swift b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Generated/Model.swift new file mode 100644 index 0000000000..215d08285e --- /dev/null +++ b/Tests/IntegrationTests/Resources/_2_parent_child_different_dirs/project/Sources/Generated/Model.swift @@ -0,0 +1,4 @@ +// Should be excluded by child +struct GeneratedModel { + let id: Int +} diff --git a/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/base/.swiftlint.yml b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/base/.swiftlint.yml new file mode 100644 index 0000000000..557df346d7 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/base/.swiftlint.yml @@ -0,0 +1,2 @@ +excluded: + - Vendor diff --git a/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/.swiftlint.yml b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/.swiftlint.yml new file mode 100644 index 0000000000..0b573089c2 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/.swiftlint.yml @@ -0,0 +1,4 @@ +parent_config: ../base/.swiftlint.yml + +included: + - Vendor/Critical diff --git a/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Critical/Important.swift b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Critical/Important.swift new file mode 100644 index 0000000000..4df88245e7 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Critical/Important.swift @@ -0,0 +1,4 @@ +// Should be included (child overrides parent exclusion) +struct CriticalVendorCode { + let version: String +} diff --git a/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Other/Library.swift b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Other/Library.swift new file mode 100644 index 0000000000..d3cdb664ca --- /dev/null +++ b/Tests/IntegrationTests/Resources/_3_child_overrides_parent_exclusion/project/Vendor/Other/Library.swift @@ -0,0 +1,4 @@ +// Should be excluded (parent excludes Vendor) +struct OtherVendorCode { + let data: Int +} diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/.swiftlint.yml b/Tests/IntegrationTests/Resources/_4_nested_basic/.swiftlint.yml new file mode 100644 index 0000000000..01f352fcdd --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/.swiftlint.yml @@ -0,0 +1,6 @@ +included: + - ModuleA + - ModuleB + +disabled_rules: + - line_length diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/.swiftlint.yml b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/.swiftlint.yml new file mode 100644 index 0000000000..9357ecdbf2 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/.swiftlint.yml @@ -0,0 +1,5 @@ +excluded: + - Generated + +opt_in_rules: + - explicit_type_interface diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/File.swift b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/File.swift new file mode 100644 index 0000000000..3bdbf4e5d7 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/File.swift @@ -0,0 +1,4 @@ +// Should be included with ModuleA nested config +struct ModuleAFile { + let name: String +} diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/Generated/File.swift b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/Generated/File.swift new file mode 100644 index 0000000000..4252417f34 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleA/Generated/File.swift @@ -0,0 +1,4 @@ +// Should be excluded by nested config +struct GeneratedFile { + let id: Int +} diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleB/File.swift b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleB/File.swift new file mode 100644 index 0000000000..66c03730b4 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/ModuleB/File.swift @@ -0,0 +1,4 @@ +// Should be included with root config only (no nested config) +struct ModuleBFile { + let data: String +} diff --git a/Tests/IntegrationTests/Resources/_4_nested_basic/root.yml b/Tests/IntegrationTests/Resources/_4_nested_basic/root.yml new file mode 100644 index 0000000000..c4f16ef51b --- /dev/null +++ b/Tests/IntegrationTests/Resources/_4_nested_basic/root.yml @@ -0,0 +1,5 @@ +included: + - ModuleA + +disabled_rules: + - line_length diff --git a/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/.swiftlint.yml b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/.swiftlint.yml new file mode 100644 index 0000000000..f44625c2cb --- /dev/null +++ b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/.swiftlint.yml @@ -0,0 +1,8 @@ +included: + - Sources + +excluded: + - "**/*.generated.swift" + +disabled_rules: + - line_length diff --git a/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Generated/API.generated.swift b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Generated/API.generated.swift new file mode 100644 index 0000000000..c0d3b84517 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Generated/API.generated.swift @@ -0,0 +1,4 @@ +// Should be excluded by wildcard pattern +struct GeneratedAPI { + let endpoint: String +} diff --git a/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.generated.swift b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.generated.swift new file mode 100644 index 0000000000..b32ef5a745 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.generated.swift @@ -0,0 +1,4 @@ +// Should be excluded by wildcard pattern +struct GeneratedUser { + let id: Int +} diff --git a/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.swift b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.swift new file mode 100644 index 0000000000..bd633fe22e --- /dev/null +++ b/Tests/IntegrationTests/Resources/_5_wildcard_patterns/project/Sources/Models/User.swift @@ -0,0 +1,4 @@ +// Should be included +struct User { + let name: String +} diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint-exclude-thirdparty.yml b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint-exclude-thirdparty.yml new file mode 100644 index 0000000000..d815ed52c1 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint-exclude-thirdparty.yml @@ -0,0 +1,4 @@ +excluded: + - "ThirdParty/**" + - "*/ThirdParty/**" + - "**/ThirdParty/**" diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint.yml b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint.yml new file mode 100644 index 0000000000..e9c04c7f29 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/.swiftlint.yml @@ -0,0 +1,6 @@ +excluded: + - "ThirdParty/**" + - "*/ThirdParty/**" + - "**/ThirdParty/**" + - "Generated" + - "**/Package.swift" diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/Generated/API.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/Generated/API.swift new file mode 100644 index 0000000000..5722826739 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/Generated/API.swift @@ -0,0 +1,4 @@ +// Should NOT be linted - in Generated +struct GeneratedCode { + var value: String +} diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Package.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Package.swift new file mode 100644 index 0000000000..8f7ea67e52 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Package.swift @@ -0,0 +1,7 @@ +// Should NOT be linted - Package.swift +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "MyPackage" +) diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Sources/App.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Sources/App.swift new file mode 100644 index 0000000000..8685cd6b3a --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/Sources/App.swift @@ -0,0 +1,4 @@ +// Should be linted +class MyApp { + func doSomething() {} +} diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/SubModule/Package.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/SubModule/Package.swift new file mode 100644 index 0000000000..35a7888372 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/SubModule/Package.swift @@ -0,0 +1,7 @@ +// Should NOT be linted - nested Package.swift +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "SubPackage" +) diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/ThirdParty/Lottie/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/ThirdParty/Lottie/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift new file mode 100644 index 0000000000..1d82293850 --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/MyProject/ThirdParty/Lottie/Private/Model/DotLottie/ZipFoundation/Archive+Writing.swift @@ -0,0 +1,4 @@ +// Should NOT be linted - in ThirdParty +class LottieAnimation { + func animate() {} +} diff --git a/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/ThirdParty/Library.swift b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/ThirdParty/Library.swift new file mode 100644 index 0000000000..6f626381bd --- /dev/null +++ b/Tests/IntegrationTests/Resources/_6_wildcards_from_nested_folder/ThirdParty/Library.swift @@ -0,0 +1,4 @@ +// Should NOT be linted - in ThirdParty at root +class ThirdPartyLib { + func process() {} +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml similarity index 100% rename from Tests/IntegrationTests/default_rule_configurations.yml rename to Tests/IntegrationTests/Resources/default_rule_configurations.yml diff --git a/bazel/repos.bzl b/bazel/repos.bzl index 1e9f257d6c..520184a446 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -66,6 +66,13 @@ def swiftlint_repos(bzlmod = False): url = "https://github.com/krzyzanowskim/CryptoSwift/archive/refs/tags/1.9.0.tar.gz", ) + http_archive( + name = "FilenameMatcher", + sha256 = "c0a6041be02ddd12f1cdde089f84dfa70e33e384cc476a786542a536d8401c6e", + strip_prefix = "swift-filename-matcher-2.0.1", + url = "https://github.com/ileitch/swift-filename-matcher/archive/refs/tags/2.0.1.tar.gz", + ) + def _swiftlint_repos_bzlmod(_): swiftlint_repos(bzlmod = True)