Skip to content

Commit 884c016

Browse files
committed
Collect files with directory enumerator
1 parent bd0b616 commit 884c016

File tree

10 files changed

+174
-121
lines changed

10 files changed

+174
-121
lines changed

Source/SwiftLintFramework/Configuration+CommandLine.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,8 @@ extension Configuration {
253253
return files
254254
}
255255
let scriptInputPaths = files.compactMap(\.path)
256-
return (
257-
visitor.options.useExcludingByPrefix
258-
? filterExcludedPathsByPrefix(in: scriptInputPaths)
259-
: filterExcludedPaths(in: scriptInputPaths)
260-
).map(SwiftLintFile.init(pathDeferringReading:))
256+
return filteredPaths(in: scriptInputPaths, excludeByPrefix: visitor.options.useExcludingByPrefix)
257+
.map(SwiftLintFile.init(pathDeferringReading:))
261258
}
262259
if !options.quiet {
263260
let filesInfo: String

Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,65 +32,61 @@ extension Configuration {
3232
/// - parameter fileManager: The lintable file manager to use to search for lintable files.
3333
///
3434
/// - returns: Paths for files to lint.
35-
internal func lintablePaths(
35+
func lintablePaths(
3636
inPath path: String,
3737
forceExclude: Bool,
3838
excludeByPrefix: Bool,
3939
fileManager: some LintableFileManager = FileManager.default
4040
) -> [String] {
41+
let excluder = createExcluder(excludeByPrefix: excludeByPrefix)
42+
43+
// Handle single file path.
4144
if fileManager.isFile(atPath: path) {
42-
let file = fileManager.filesToLint(inPath: path, rootDirectory: nil)
43-
if forceExclude {
44-
return excludeByPrefix
45-
? filterExcludedPathsByPrefix(in: file)
46-
: filterExcludedPaths(in: file)
47-
}
48-
// If path is a file and we're not forcing excludes, skip filtering with excluded/included paths
49-
return file
45+
return fileManager.filesToLint(
46+
inPath: path,
47+
rootDirectory: nil,
48+
excluder: forceExclude ? excluder : .noExclusion
49+
)
5050
}
5151

52-
let pathsForPath = includedPaths.isEmpty ? fileManager.filesToLint(inPath: path, rootDirectory: nil) : []
53-
let includedPaths = self.includedPaths
54-
.flatMap(Glob.resolveGlob)
55-
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }
52+
// With no included paths, we lint everything in the given path.
53+
if includedPaths.isEmpty {
54+
return fileManager.filesToLint(
55+
inPath: path,
56+
rootDirectory: nil,
57+
excluder: excluder
58+
)
59+
}
5660

57-
return excludeByPrefix
58-
? filterExcludedPathsByPrefix(in: pathsForPath + includedPaths)
59-
: filterExcludedPaths(in: pathsForPath + includedPaths)
61+
// With included paths, we only lint those paths (after resolving globs).
62+
return includedPaths
63+
.flatMap(Glob.resolveGlob)
64+
.parallelFlatMap {
65+
fileManager.filesToLint(
66+
inPath: $0,
67+
rootDirectory: rootDirectory,
68+
excluder: excluder
69+
)
70+
}
6071
}
6172

62-
/// Returns an array of file paths after removing the excluded paths as defined by this configuration.
63-
///
64-
/// - parameter paths: The input paths to filter.
65-
///
66-
/// - returns: The input paths after removing the excluded paths.
67-
public func filterExcludedPaths(in paths: [String]) -> [String] {
68-
#if os(Linux)
69-
let result = NSMutableOrderedSet(capacity: paths.count)
70-
result.addObjects(from: paths)
71-
#else
72-
let result = NSOrderedSet(array: paths)
73-
#endif
74-
let exclusionPatterns = self.excludedPaths.flatMap {
75-
Glob.createFilenameMatchers(root: rootDirectory, pattern: $0)
76-
}
77-
return result.array
78-
.parallelCompactMap { exclusionPatterns.anyMatch(filename: $0 as! String) ? nil : $0 as? String }
79-
// swiftlint:disable:previous force_cast
73+
func filteredPaths(in paths: [String], excludeByPrefix: Bool) -> [String] {
74+
let excluder = createExcluder(excludeByPrefix: excludeByPrefix)
75+
return paths.filter { !excluder.excludes(path: $0) }
8076
}
8177

82-
/// Returns the file paths that are excluded by this configuration using filtering by absolute path prefix.
83-
///
84-
/// For cases when excluded directories contain many lintable files (e. g. Pods) it works faster than default
85-
/// algorithm `filterExcludedPaths`.
86-
///
87-
/// - returns: The input paths after removing the excluded paths.
88-
public func filterExcludedPathsByPrefix(in paths: [String]) -> [String] {
89-
let excludedPaths = self.excludedPaths
90-
.parallelFlatMap { Glob.resolveGlob($0) }
91-
.map { $0.absolutePathStandardized() }
92-
return paths.filter { path in
93-
!excludedPaths.contains { path.hasPrefix($0) }
78+
private func createExcluder(excludeByPrefix: Bool) -> Excluder {
79+
if excludeByPrefix {
80+
return .byPrefix(
81+
prefixes: self.excludedPaths
82+
.flatMap { Glob.resolveGlob($0) }
83+
.map { $0.absolutePathStandardized() }
84+
)
9485
}
86+
return .matching(
87+
matchers: self.excludedPaths.flatMap {
88+
Glob.createFilenameMatchers(root: rootDirectory, pattern: $0)
89+
}
90+
)
9591
}
9692
}

Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
import FilenameMatcher
12
import Foundation
23
import SourceKittenFramework
34

45
/// An interface for enumerating files that can be linted by SwiftLint.
56
public protocol LintableFileManager {
67
/// Returns all files that can be linted in the specified path. If the path is relative, it will be appended to the
7-
/// specified root path, or currentt working directory if no root directory is specified.
8+
/// specified root path, or current working directory if no root directory is specified.
89
///
910
/// - parameter path: The path in which lintable files should be found.
1011
/// - parameter rootDirectory: The parent directory for the specified path. If none is provided, the current working
1112
/// directory will be used.
13+
/// - parameter excluder: The excluder used to filter out files that should not be linted.
1214
///
1315
/// - returns: Files to lint.
14-
func filesToLint(inPath path: String, rootDirectory: String?) -> [String]
16+
func filesToLint(inPath path: String, rootDirectory: String?, excluder: Excluder) -> [String]
1517

1618
/// Returns the date when the file at the specified path was last modified. Returns `nil` if the file cannot be
1719
/// found or its last modification date cannot be determined.
@@ -29,22 +31,56 @@ public protocol LintableFileManager {
2931
func isFile(atPath path: String) -> Bool
3032
}
3133

34+
/// An excluder for filtering out files that should not be linted.
35+
public enum Excluder {
36+
/// Full matching excluder using filename matchers.
37+
case matching(matchers: [FilenameMatcher])
38+
/// Prefix-based excluder using path prefixes.
39+
case byPrefix(prefixes: [String])
40+
/// An excluder that does not exclude any files.
41+
case noExclusion
42+
43+
func excludes(path: String) -> Bool {
44+
switch self {
45+
case let .matching(matchers):
46+
matchers.contains(where: { $0.match(filename: path) })
47+
case let .byPrefix(prefixes):
48+
prefixes.contains(where: { path.hasPrefix($0) })
49+
case .noExclusion:
50+
false
51+
}
52+
}
53+
}
54+
3255
extension FileManager: LintableFileManager {
33-
public func filesToLint(inPath path: String, rootDirectory: String? = nil) -> [String] {
56+
public func filesToLint(inPath path: String,
57+
rootDirectory: String? = nil,
58+
excluder: Excluder) -> [String] {
3459
let absolutePath = path.bridge()
3560
.absolutePathRepresentation(rootDirectory: rootDirectory ?? currentDirectoryPath).bridge()
3661
.standardizingPath
3762

38-
// if path is a file, it won't be returned in `enumerator(atPath:)`
63+
// If path is a file, it won't be returned in `enumerator(atPath:)`.
3964
if absolutePath.bridge().isSwiftFile(), absolutePath.isFile {
40-
return [absolutePath]
65+
return excluder.excludes(path: absolutePath) ? [] : [absolutePath]
66+
}
67+
68+
guard let enumerator = enumerator(atPath: absolutePath) else {
69+
return []
4170
}
4271

43-
return subpaths(atPath: absolutePath)?.parallelCompactMap { element -> String? in
44-
guard element.bridge().isSwiftFile() else { return nil }
72+
var files = [String]()
73+
while let element = enumerator.nextObject() as? String {
4574
let absoluteElementPath = absolutePath.bridge().appendingPathComponent(element)
46-
return absoluteElementPath.isFile ? absoluteElementPath : nil
47-
} ?? []
75+
if absoluteElementPath.bridge().isSwiftFile(), absoluteElementPath.isFile {
76+
if !excluder.excludes(path: absoluteElementPath) {
77+
files.append(absoluteElementPath)
78+
}
79+
} else if excluder.excludes(path: absoluteElementPath) {
80+
enumerator.skipDescendants()
81+
}
82+
}
83+
return files
4884
}
4985

5086
public func modificationDate(forFileAtPath path: String) -> Date? {

Tests/FileSystemAccessTests/ConfigurationTests+Mock.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ internal extension ConfigurationTests {
3333
static var remoteConfigLocalRef: String { level0.stringByAppendingPathComponent("RemoteConfig/LocalRef") }
3434
static var remoteConfigCycle: String { level0.stringByAppendingPathComponent("RemoteConfig/Cycle") }
3535
static var emptyFolder: String { level0.stringByAppendingPathComponent("EmptyFolder") }
36+
37+
static var exclusionTests: String { testResourcesPath.stringByAppendingPathComponent("ExclusionTests") }
38+
static var directory: String { exclusionTests.stringByAppendingPathComponent("directory") }
39+
static var directoryExcluded: String { directory.stringByAppendingPathComponent("excluded") }
3640
}
3741

3842
// MARK: YAML File Paths

Tests/FileSystemAccessTests/ConfigurationTests.swift

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -281,85 +281,75 @@ final class ConfigurationTests: SwiftLintTestCase {
281281
XCTAssertEqual(actualExcludedPath, desiredExcludedPath)
282282
}
283283

284-
private class TestFileManager: LintableFileManager {
285-
func filesToLint(inPath path: String, rootDirectory _: String? = nil) -> [String] {
286-
var filesToLint: [String] = []
287-
switch path {
288-
case "directory": filesToLint = [
289-
"directory/File1.swift",
290-
"directory/File2.swift",
291-
"directory/excluded/Excluded.swift",
292-
"directory/ExcludedFile.swift",
293-
]
294-
case "directory/excluded": filesToLint = ["directory/excluded/Excluded.swift"]
295-
case "directory/ExcludedFile.swift": filesToLint = ["directory/ExcludedFile.swift"]
296-
default: XCTFail("Should not be called with path \(path)")
297-
}
298-
return filesToLint.absolutePathsStandardized()
299-
}
300-
301-
func modificationDate(forFileAtPath _: String) -> Date? {
302-
nil
303-
}
304-
305-
func isFile(atPath path: String) -> Bool {
306-
path.hasSuffix(".swift")
307-
}
308-
}
309-
310284
func testExcludedPaths() {
311-
let fileManager = TestFileManager()
285+
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
312286
let configuration = Configuration(
313287
includedPaths: ["directory"],
314288
excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]
315289
)
316290

317-
let paths = configuration.lintablePaths(inPath: "",
318-
forceExclude: false,
319-
excludeByPrefix: false,
320-
fileManager: fileManager)
321-
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
291+
let paths = configuration.lintablePaths(
292+
inPath: "",
293+
forceExclude: false,
294+
excludeByPrefix: false
295+
)
296+
297+
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
322298
}
323299

324300
func testForceExcludesFile() {
325-
let fileManager = TestFileManager()
301+
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
326302
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
327-
let paths = configuration.lintablePaths(inPath: "directory/ExcludedFile.swift",
328-
forceExclude: true,
329-
excludeByPrefix: false,
330-
fileManager: fileManager)
331-
XCTAssertEqual([], paths)
303+
304+
let paths = configuration.lintablePaths(
305+
inPath: "directory/ExcludedFile.swift",
306+
forceExclude: true,
307+
excludeByPrefix: false
308+
)
309+
310+
XCTAssert(paths.isEmpty)
332311
}
333312

334313
func testForceExcludesFileNotPresentInExcluded() {
335-
let fileManager = TestFileManager()
336-
let configuration = Configuration(includedPaths: ["directory"],
337-
excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"])
338-
let paths = configuration.lintablePaths(inPath: "",
339-
forceExclude: true,
340-
excludeByPrefix: false,
341-
fileManager: fileManager)
342-
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
314+
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
315+
let configuration = Configuration(
316+
includedPaths: ["directory"],
317+
excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"]
318+
)
319+
320+
let paths = configuration.lintablePaths(
321+
inPath: "",
322+
forceExclude: true,
323+
excludeByPrefix: false
324+
)
325+
326+
assertEqual(["directory/File1.swift", "directory/File2.swift"], paths)
343327
}
344328

345329
func testForceExcludesDirectory() {
346-
let fileManager = TestFileManager()
347-
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
348-
let paths = configuration.lintablePaths(inPath: "directory",
349-
forceExclude: true,
350-
excludeByPrefix: false,
351-
fileManager: fileManager)
352-
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
330+
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
331+
let configuration = Configuration(excludedPaths: ["directory/excluded"])
332+
333+
let paths = configuration.lintablePaths(
334+
inPath: "directory",
335+
forceExclude: true,
336+
excludeByPrefix: false
337+
)
338+
339+
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/ExcludedFile.swift"], paths)
353340
}
354341

355342
func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
356-
let fileManager = TestFileManager()
357-
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
358-
let paths = configuration.lintablePaths(inPath: "directory",
359-
forceExclude: true,
360-
excludeByPrefix: false,
361-
fileManager: fileManager)
362-
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
343+
XCTAssert(FileManager.default.changeCurrentDirectoryPath(Mock.Dir.exclusionTests))
344+
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
345+
346+
let paths = configuration.lintablePaths(
347+
inPath: "directory",
348+
forceExclude: true,
349+
excludeByPrefix: false
350+
)
351+
352+
assertEqual(["directory/File1.swift", "directory/File2.swift", "directory/excluded/Excluded.swift"], paths)
363353
}
364354

365355
func testLintablePaths() {
@@ -621,6 +611,18 @@ extension ConfigurationTests {
621611
XCTAssertEqual(configuration1.cachePath, "cache/path/1")
622612
XCTAssertEqual(configuration2.cachePath, "cache/path/1")
623613
}
614+
615+
private func assertEqual(_ relativeExpectedPaths: [String],
616+
_ actualPaths: [String],
617+
file: StaticString = #filePath,
618+
line: UInt = #line) {
619+
XCTAssertEqual(
620+
relativeExpectedPaths.absolutePathsStandardized().sorted(),
621+
actualPaths.sorted(),
622+
file: file,
623+
line: line
624+
)
625+
}
624626
}
625627

626628
private extension Sequence where Element == String {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Excluded test file
2+
func excludedFunction() {
3+
print("Excluded File")
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Test file 1
2+
func testFunction1() {
3+
print("File 1")
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Test file 2
2+
func testFunction2() {
3+
print("File 2")
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Excluded directory test file
2+
func excludedDirectoryFunction() {
3+
print("Excluded in directory")
4+
}

0 commit comments

Comments
 (0)