diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 06549f9bbe..459bf0891f 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -255,11 +255,9 @@ extension Configuration { let scriptInputPaths = files.compactMap(\.path) - if options.useExcludingByPrefix { - return filterExcludedPathsByPrefix(in: scriptInputPaths) - .map(SwiftLintFile.init(pathDeferringReading:)) - } - return filterExcludedPaths(excludedPaths(), in: scriptInputPaths) + let excludeByType = ExcludeByStrategyType.createExcludeByStrategy(options: options, configuration: self) + let excludeBy = excludeByType.strategy + return excludeBy.filterExcludedPaths(in: scriptInputPaths) .map(SwiftLintFile.init(pathDeferringReading:)) } if !options.quiet { @@ -272,14 +270,14 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } - let excludeLintableFilesBy = options.useExcludingByPrefix - ? Configuration.ExcludeBy.prefix - : .paths(excludedPaths: excludedPaths()) + + let excludeBy = ExcludeByStrategyType.createExcludeByStrategy(options: options, configuration: self).strategy + 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..d84758b1dd 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: some 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/ExcludeByPathsByExpandingSubPaths.swift b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift new file mode 100644 index 0000000000..5520584e25 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPathsByExpandingSubPaths.swift @@ -0,0 +1,35 @@ +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. +struct ExcludeByPathsByExpandingSubPaths: ExcludeByStrategy { + let excludedPaths: [String] + + init(configuration: Configuration, fileManager: some LintableFileManager = FileManager.default) { + self.excludedPaths = configuration.excludedPaths + .flatMap(Glob.resolveGlob) + .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: configuration.rootDirectory) } + } + + init(_ excludedPaths: [String]) { + self.excludedPaths = excludedPaths + } + + 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..6a6e3cffa8 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByPrefixStrategy.swift @@ -0,0 +1,19 @@ +/// 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] + + 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..124faa3194 --- /dev/null +++ b/Source/SwiftLintFramework/ExcludeByStrategy/ExcludeByStrategy.swift @@ -0,0 +1,3 @@ +public protocol ExcludeByStrategy { + func filterExcludedPaths(in paths: [String]...) -> [String] +} 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/FrameworkTests/ConfigurationTests.swift b/Tests/FrameworkTests/ConfigurationTests.swift index f88852c94c..924346557f 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,21 @@ 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", @@ -359,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: .paths(excludedPaths: configuration.excludedPaths)) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let expectedFilenames = ["Level2.swift", "Level3.swift"] @@ -374,10 +378,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, []) } @@ -490,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: .prefix) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(filenames, ["Level2.swift"]) } @@ -500,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: .prefix) + excludeBy: excludeBy) XCTAssertEqual([], paths) } @@ -510,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: .prefix) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) } @@ -524,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: .prefix) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) } @@ -538,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: .prefix) + excludeBy: excludeBy) let filenames = paths.map { $0.bridge().lastPathComponent } XCTAssertEqual(["Level0.swift"], filenames) } @@ -550,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: .prefix) + excludeBy: excludeBy) 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..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 @@ -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)