From d0f9c0c69d9f0e3bd5ca1c35935b9941bd3d898b Mon Sep 17 00:00:00 2001 From: rlf-doordash Date: Wed, 16 Jul 2025 16:29:40 -0700 Subject: [PATCH 1/4] Change --- .../Configuration+CommandLine.swift | 15 ++-- .../Configuration+LintableFiles.swift | 74 +------------------ .../ExcludeByStrategy/ExcludeByType.swift | 57 ++++++++++++++ Tests/FrameworkTests/ConfigurationTests.swift | 44 +++++------ Tests/IntegrationTests/IntegrationTests.swift | 4 +- 5 files changed, 92 insertions(+), 102 deletions(-) create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 06549f9bbe..9226602bd7 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -255,11 +255,8 @@ extension Configuration { let scriptInputPaths = files.compactMap(\.path) - if options.useExcludingByPrefix { - return filterExcludedPathsByPrefix(in: scriptInputPaths) - .map(SwiftLintFile.init(pathDeferringReading:)) - } - return filterExcludedPaths(excludedPaths(), in: scriptInputPaths) + let excludeBy = ExcludeByStrategyFactory.createExcludeByStrategy(options: options, configuration: self) + return excludeBy.filterExcludedPaths(in: scriptInputPaths) .map(SwiftLintFile.init(pathDeferringReading:)) } if !options.quiet { @@ -272,14 +269,14 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } - let excludeLintableFilesBy = options.useExcludingByPrefix - ? Configuration.ExcludeBy.prefix - : .paths(excludedPaths: excludedPaths()) + + let excludeBy = ExcludeByStrategyFactory.createExcludeByStrategy(options: options, configuration: self) + return options.paths.flatMap { self.lintableFiles( inPath: $0, forceExclude: options.forceExclude, - excludeBy: excludeLintableFilesBy) + excludeBy: excludeBy) } } diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index c324845ff0..ad13efe912 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -1,11 +1,6 @@ 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. /// @@ -18,7 +13,7 @@ extension Configuration { /// - returns: Files to lint. public func lintableFiles(inPath path: String, forceExclude: Bool, - excludeBy: ExcludeBy) -> [SwiftLintFile] { + excludeBy: any ExcludeByStrategy) -> [SwiftLintFile] { lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy) .parallelCompactMap { SwiftLintFile(pathDeferringReading: $0) @@ -38,17 +33,12 @@ extension Configuration { internal func lintablePaths( inPath path: String, forceExclude: Bool, - excludeBy: ExcludeBy, + excludeBy: any ExcludeByStrategy, fileManager: some LintableFileManager = FileManager.default ) -> [String] { 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()]) - } + return excludeBy.filterExcludedPaths(in: [path.absolutePathStandardized()]) } // If path is a file and we're not forcing excludes, skip filtering with excluded/included paths return [path] @@ -59,62 +49,6 @@ extension Configuration { .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) - } - } - - /// 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 { $0 } - #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 } - } - - /// 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 { $0 } - let excludedPaths = self.excludedPaths - .parallelFlatMap { @Sendable in Glob.resolveGlob($0) } - .map { $0.absolutePathStandardized() } - return allPaths.filter { path in - !excludedPaths.contains { path.hasPrefix($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) } + return excludeBy.filterExcludedPaths(in: pathsForPath, includedPaths) } } diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift new file mode 100644 index 0000000000..8725e77fe5 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift @@ -0,0 +1,57 @@ +public protocol ExcludeByStrategy { + func filterExcludedPaths(in paths: [String]...) -> [String] +} + +struct ExcludeByPrefixStrategy: ExcludeByStrategy { + let excludedPaths: [String] + + func filterExcludedPaths(in paths: [String]...) -> [String] { + let allPaths = paths.flatMap { $0 } + let excludedPaths = self.excludedPaths + .parallelFlatMap { @Sendable in Glob.resolveGlob($0) } + .map { $0.absolutePathStandardized() } + return allPaths.filter { path in + !excludedPaths.contains { path.hasPrefix($0) } + } + } +} + +import Foundation + +public struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { + let excludedPaths: [String] + + public init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { + self.excludedPaths = configuration.excludedPaths + .flatMap(Glob.resolveGlob) + .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: configuration.rootDirectory) } + } + + public init(_ excludedPaths: [String]) { + self.excludedPaths = excludedPaths + } + + public func filterExcludedPaths(in paths: [String]...) -> [String] { + let allPaths = paths.flatMap { $0 } + #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 } + } +} + +class ExcludeByStrategyFactory { + static func createExcludeByStrategy(options: LintOrAnalyzeOptions, configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) -> any ExcludeByStrategy { + if options.useExcludingByPrefix { + return ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) + } + + return ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) + } +} diff --git a/Tests/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index f88852c94c..58ab7b71c5 100644 --- a/Tests/FrameworkTests/ConfigurationTests.swift +++ b/Tests/FrameworkTests/ConfigurationTests.swift @@ -288,10 +288,10 @@ final class ConfigurationTests: SwiftLintTestCase { excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"] ) - let excludedPaths = configuration.excludedPaths(fileManager: fileManager) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) let paths = configuration.lintablePaths(inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: excludedPaths), + excludeBy: excludeBy, fileManager: fileManager) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) } @@ -299,10 +299,10 @@ final class ConfigurationTests: SwiftLintTestCase { func testForceExcludesFile() { let fileManager = TestFileManager() let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"]) - let excludedPaths = configuration.excludedPaths(fileManager: fileManager) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) let paths = configuration.lintablePaths(inPath: "directory/ExcludedFile.swift", forceExclude: true, - excludeBy: .paths(excludedPaths: excludedPaths), + excludeBy: excludeBy, fileManager: fileManager) XCTAssertEqual([], paths) } @@ -311,10 +311,11 @@ final class ConfigurationTests: SwiftLintTestCase { let fileManager = TestFileManager() let configuration = Configuration(includedPaths: ["directory"], excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"]) - let excludedPaths = configuration.excludedPaths(fileManager: fileManager) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) + let paths = configuration.lintablePaths(inPath: "", forceExclude: true, - excludeBy: .paths(excludedPaths: excludedPaths), + excludeBy: excludeBy, fileManager: fileManager) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) } @@ -322,10 +323,10 @@ final class ConfigurationTests: SwiftLintTestCase { func testForceExcludesDirectory() { let fileManager = TestFileManager() let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]) - let excludedPaths = configuration.excludedPaths(fileManager: fileManager) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) let paths = configuration.lintablePaths(inPath: "directory", forceExclude: true, - excludeBy: .paths(excludedPaths: excludedPaths), + excludeBy: excludeBy, fileManager: fileManager) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) } @@ -333,19 +334,20 @@ final class ConfigurationTests: SwiftLintTestCase { func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { let fileManager = TestFileManager() let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]) - let excludedPaths = configuration.excludedPaths(fileManager: fileManager) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) let paths = configuration.lintablePaths(inPath: "directory", forceExclude: true, - excludeBy: .paths(excludedPaths: excludedPaths), + excludeBy: excludeBy, fileManager: fileManager) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) } func testLintablePaths() { - let excluded = Configuration.default.excludedPaths(fileManager: TestFileManager()) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: Configuration.default, fileManager: TestFileManager()) + let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, - excludeBy: .paths(excludedPaths: excluded)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = [ "DirectoryLevel1.swift", @@ -361,7 +363,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)) + excludeBy: ExcludeByPathsByExpandingSubPaths(configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] @@ -374,10 +376,10 @@ final class ConfigurationTests: SwiftLintTestCase { excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")] ) - let excludedPaths = configuration.excludedPaths() + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: configuration) let lintablePaths = configuration.lintablePaths(inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: excludedPaths)) + excludeBy: excludeBy) XCTAssertEqual(lintablePaths, []) } @@ -492,7 +494,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, - excludeBy: .prefix) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(filenames, ["Level2.swift"]) } @@ -502,7 +504,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) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) XCTAssertEqual([], paths) } @@ -512,7 +514,7 @@ extension ConfigurationTests { excludedPaths: ["Level1/Level1.swift"]) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: true, - excludeBy: .prefix) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } @@ -526,7 +528,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: .prefix) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } @@ -540,7 +542,7 @@ extension ConfigurationTests { ) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: .prefix) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(["Level0.swift"], filenames) } @@ -552,7 +554,7 @@ extension ConfigurationTests { excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: false, - excludeBy: .prefix) + excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(filenames, ["Level1.swift"]) } diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index 0914f97a8f..7f01844dbb 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -23,7 +23,7 @@ final class IntegrationTests: SwiftLintTestCase { let swiftFiles = config.lintableFiles( inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: config.excludedPaths())) + excludeBy: ExcludeByPathsByExpandingSubPaths(configuration: config)) XCTAssert( swiftFiles.contains(where: { #filePath.bridge().absolutePathRepresentation() == $0.path }), "current file should be included" @@ -48,7 +48,7 @@ final class IntegrationTests: SwiftLintTestCase { let swiftFiles = config.lintableFiles( inPath: "", forceExclude: false, - excludeBy: .paths(excludedPaths: config.excludedPaths())) + excludeBy: ExcludeByPathsByExpandingSubPaths(configuration: config)) let storage = RuleStorage() let corrections = swiftFiles.parallelMap { Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage) From 64da15d2bc4e3a010955938b8ee52844c8a46b15 Mon Sep 17 00:00:00 2001 From: rlf-doordash Date: Thu, 17 Jul 2025 17:06:19 -0700 Subject: [PATCH 2/4] Fix lint --- .../ExcludeByPathsByExpandingSubPaths.swift | 33 +++++++++++ .../ExcludeByPrefixStrategy.swift | 17 ++++++ .../ExcludeByStrategy/ExcludeByStrategy.swift | 7 +++ .../ExcludeByStrategyFactory.swift | 14 +++++ .../ExcludeByStrategy/ExcludeByType.swift | 57 ------------------- Tests/FrameworkTests/ConfigurationTests.swift | 24 +++++--- 6 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift delete mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift new file mode 100644 index 0000000000..e27e3ab3bd --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift @@ -0,0 +1,33 @@ +// +// ExcludeByPathsByExpandingSubPaths.swift +// + +import Foundation + +public struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { + let excludedPaths: [String] + + public init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { + self.excludedPaths = configuration.excludedPaths + .flatMap(Glob.resolveGlob) + .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: configuration.rootDirectory) } + } + + public init(_ excludedPaths: [String]) { + self.excludedPaths = excludedPaths + } + + public func filterExcludedPaths(in paths: [String]...) -> [String] { + let allPaths = paths.flatMap { $0 } + #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 } + } +} diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift new file mode 100644 index 0000000000..e37d1bb072 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift @@ -0,0 +1,17 @@ +// +// ExcludeByPrefixStrategy.swift +// + +struct ExcludeByPrefixStrategy: ExcludeByStrategy { + let excludedPaths: [String] + + func filterExcludedPaths(in paths: [String]...) -> [String] { + let allPaths = paths.flatMap { $0 } + let excludedPaths = self.excludedPaths + .parallelFlatMap { @Sendable in Glob.resolveGlob($0) } + .map { $0.absolutePathStandardized() } + return allPaths.filter { path in + !excludedPaths.contains { path.hasPrefix($0) } + } + } +} diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift new file mode 100644 index 0000000000..120a4ebed1 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift @@ -0,0 +1,7 @@ +// +// ExcludeByStrategy.swift +// + +public protocol ExcludeByStrategy { + func filterExcludedPaths(in paths: [String]...) -> [String] +} diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift new file mode 100644 index 0000000000..a1c1dc20ba --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift @@ -0,0 +1,14 @@ +import Foundation + +class ExcludeByStrategyFactory { + static func createExcludeByStrategy(options: LintOrAnalyzeOptions, + configuration: Configuration, + fileManager: some LintableFileManager = FileManager.default) + -> any ExcludeByStrategy { + if options.useExcludingByPrefix { + return ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) + } + + return ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) + } +} diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift deleted file mode 100644 index 8725e77fe5..0000000000 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByType.swift +++ /dev/null @@ -1,57 +0,0 @@ -public protocol ExcludeByStrategy { - func filterExcludedPaths(in paths: [String]...) -> [String] -} - -struct ExcludeByPrefixStrategy: ExcludeByStrategy { - let excludedPaths: [String] - - func filterExcludedPaths(in paths: [String]...) -> [String] { - let allPaths = paths.flatMap { $0 } - let excludedPaths = self.excludedPaths - .parallelFlatMap { @Sendable in Glob.resolveGlob($0) } - .map { $0.absolutePathStandardized() } - return allPaths.filter { path in - !excludedPaths.contains { path.hasPrefix($0) } - } - } -} - -import Foundation - -public struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { - let excludedPaths: [String] - - public init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { - self.excludedPaths = configuration.excludedPaths - .flatMap(Glob.resolveGlob) - .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: configuration.rootDirectory) } - } - - public init(_ excludedPaths: [String]) { - self.excludedPaths = excludedPaths - } - - public func filterExcludedPaths(in paths: [String]...) -> [String] { - let allPaths = paths.flatMap { $0 } - #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 } - } -} - -class ExcludeByStrategyFactory { - static func createExcludeByStrategy(options: LintOrAnalyzeOptions, configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) -> any ExcludeByStrategy { - if options.useExcludingByPrefix { - return ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) - } - - return ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) - } -} diff --git a/Tests/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index 58ab7b71c5..924346557f 100644 --- a/Tests/FrameworkTests/ConfigurationTests.swift +++ b/Tests/FrameworkTests/ConfigurationTests.swift @@ -343,7 +343,8 @@ final class ConfigurationTests: SwiftLintTestCase { } func testLintablePaths() { - let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: Configuration.default, fileManager: TestFileManager()) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration: Configuration.default, + fileManager: TestFileManager()) let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, @@ -361,9 +362,10 @@ final class ConfigurationTests: SwiftLintTestCase { func testGlobIncludePaths() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(includedPaths: ["**/Level2"]) + let excludeBy = ExcludeByPathsByExpandingSubPaths(configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true, - excludeBy: ExcludeByPathsByExpandingSubPaths(configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] @@ -492,9 +494,10 @@ extension ConfigurationTests { includedPaths: ["Level1"], excludedPaths: ["Level1/Level1.swift", "Level1/Level2/Level3"] ) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(filenames, ["Level2.swift"]) } @@ -502,9 +505,10 @@ extension ConfigurationTests { func testExcludeByPrefixForceExcludesFile() { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"]) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift", forceExclude: true, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) XCTAssertEqual([], paths) } @@ -512,9 +516,10 @@ extension ConfigurationTests { XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)) let configuration = Configuration(includedPaths: ["Level1"], excludedPaths: ["Level1/Level1.swift"]) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: true, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } @@ -526,9 +531,10 @@ extension ConfigurationTests { "Level1/Level2", "Directory.swift", "ChildConfig", "ParentConfig", "NestedConfig" ] ) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } @@ -540,9 +546,10 @@ extension ConfigurationTests { "Level1", "Directory.swift/DirectoryLevel1.swift", "ChildConfig", "ParentConfig", "NestedConfig" ] ) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: ".", forceExclude: true, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(["Level0.swift"], filenames) } @@ -552,9 +559,10 @@ extension ConfigurationTests { let configuration = Configuration( includedPaths: ["Level1"], excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) + let excludeBy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) let paths = configuration.lintablePaths(inPath: "Level1", forceExclude: false, - excludeBy: ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(filenames, ["Level1.swift"]) } From fb50d36be308fae8ad533333c83e3a021da790da Mon Sep 17 00:00:00 2001 From: rlf-doordash Date: Thu, 17 Jul 2025 17:11:20 -0700 Subject: [PATCH 3/4] Add removed comments --- .../ExcludeByPathsByExpandingSubPaths.swift | 6 ++++++ .../ExcludeByStrategy/ExcludeByPrefixStrategy.swift | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift index e27e3ab3bd..d9d3e05b9d 100644 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift @@ -4,6 +4,12 @@ import Foundation +/// 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 struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { let excludedPaths: [String] diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift index e37d1bb072..eecc4135f8 100644 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift @@ -2,6 +2,12 @@ // ExcludeByPrefixStrategy.swift // +/// 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. struct ExcludeByPrefixStrategy: ExcludeByStrategy { let excludedPaths: [String] From 3366cc4f595eb655497a2376ba0021fcffe31667 Mon Sep 17 00:00:00 2001 From: rlf-doordash Date: Wed, 3 Sep 2025 02:53:14 -0700 Subject: [PATCH 4/4] PR comments --- .../Configuration+CommandLine.swift | 5 ++-- .../Configuration+LintableFiles.swift | 2 +- .../ExcludeByPathsByExpandingSubPaths.swift | 12 +++----- .../ExcludeByPrefixStrategy.swift | 4 --- .../ExcludeByStrategy/ExcludeByStrategy.swift | 4 --- .../ExcludeByStrategyFactory.swift | 14 ---------- .../ExcludeByStrategyType.swift | 28 +++++++++++++++++++ Tests/IntegrationTests/IntegrationTests.swift | 2 +- 8 files changed, 37 insertions(+), 34 deletions(-) delete mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift create mode 100644 Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyType.swift diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 9226602bd7..459bf0891f 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -255,7 +255,8 @@ extension Configuration { let scriptInputPaths = files.compactMap(\.path) - let excludeBy = ExcludeByStrategyFactory.createExcludeByStrategy(options: options, configuration: self) + let excludeByType = ExcludeByStrategyType.createExcludeByStrategy(options: options, configuration: self) + let excludeBy = excludeByType.strategy return excludeBy.filterExcludedPaths(in: scriptInputPaths) .map(SwiftLintFile.init(pathDeferringReading:)) } @@ -270,7 +271,7 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } - let excludeBy = ExcludeByStrategyFactory.createExcludeByStrategy(options: options, configuration: self) + let excludeBy = ExcludeByStrategyType.createExcludeByStrategy(options: options, configuration: self).strategy return options.paths.flatMap { self.lintableFiles( diff --git a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift index ad13efe912..d84758b1dd 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift @@ -13,7 +13,7 @@ extension Configuration { /// - returns: Files to lint. public func lintableFiles(inPath path: String, forceExclude: Bool, - excludeBy: any ExcludeByStrategy) -> [SwiftLintFile] { + excludeBy: some ExcludeByStrategy) -> [SwiftLintFile] { lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy) .parallelCompactMap { SwiftLintFile(pathDeferringReading: $0) diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift index d9d3e05b9d..5520584e25 100644 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift @@ -1,7 +1,3 @@ -// -// ExcludeByPathsByExpandingSubPaths.swift -// - import Foundation /// Returns an array of file paths after removing the excluded paths as defined by this configuration. @@ -10,20 +6,20 @@ import Foundation /// - parameter paths: The input paths to filter. /// /// - returns: The input paths after removing the excluded paths. -public struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { +struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { let excludedPaths: [String] - public init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { + init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { self.excludedPaths = configuration.excludedPaths .flatMap(Glob.resolveGlob) .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: configuration.rootDirectory) } } - public init(_ excludedPaths: [String]) { + init(_ excludedPaths: [String]) { self.excludedPaths = excludedPaths } - public func filterExcludedPaths(in paths: [String]...) -> [String] { + func filterExcludedPaths(in paths: [String]...) -> [String] { let allPaths = paths.flatMap { $0 } #if os(Linux) let result = NSMutableOrderedSet(capacity: allPaths.count) diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift index eecc4135f8..6a6e3cffa8 100644 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift @@ -1,7 +1,3 @@ -// -// ExcludeByPrefixStrategy.swift -// - /// 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 diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift index 120a4ebed1..124faa3194 100644 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift @@ -1,7 +1,3 @@ -// -// ExcludeByStrategy.swift -// - public protocol ExcludeByStrategy { func filterExcludedPaths(in paths: [String]...) -> [String] } diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift deleted file mode 100644 index a1c1dc20ba..0000000000 --- a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyFactory.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -class ExcludeByStrategyFactory { - static func createExcludeByStrategy(options: LintOrAnalyzeOptions, - configuration: Configuration, - fileManager: some LintableFileManager = FileManager.default) - -> any ExcludeByStrategy { - if options.useExcludingByPrefix { - return ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) - } - - return ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) - } -} diff --git a/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyType.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyType.swift new file mode 100644 index 0000000000..479c60f317 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategyType.swift @@ -0,0 +1,28 @@ +import Foundation + +enum ExcludeByStrategyType { + case excludeByPrefix(ExcludeByPrefixStrategy) + case excludeByPathsByExpandingSubPaths(ExcludeByPathsByExpandingSubPaths) + + static func createExcludeByStrategy(options: LintOrAnalyzeOptions, + configuration: Configuration, + fileManager: some LintableFileManager = FileManager.default) + -> Self { + if options.useExcludingByPrefix { + let strategy = ExcludeByPrefixStrategy(excludedPaths: configuration.excludedPaths) + return .excludeByPrefix(strategy) + } + + let strategy = ExcludeByPathsByExpandingSubPaths(configuration: configuration, fileManager: fileManager) + return .excludeByPathsByExpandingSubPaths(strategy) + } + + var strategy: any ExcludeByStrategy { + switch self { + case .excludeByPrefix(let strategy): + return strategy + case .excludeByPathsByExpandingSubPaths(let strategy): + return strategy + } + } +} diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index 7f01844dbb..3c3587f3a4 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -1,5 +1,5 @@ import Foundation -import SwiftLintFramework +@testable import SwiftLintFramework import TestHelpers import XCTest