Move `excludedPaths` out of iteration loop to speed up lint times (#4955)

This commit is contained in:
Andyy Hope 2023-05-07 04:43:33 +08:00 committed by GitHub
parent 7756793356
commit 5e15039554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 37 deletions

View File

@ -96,6 +96,11 @@
[keith](https://github.com/keith) [keith](https://github.com/keith)
[#4782](https://github.com/realm/SwiftLint/issues/4782) [#4782](https://github.com/realm/SwiftLint/issues/4782)
* Improve lint times of SwiftLintPlugin by moving the
`excludedPaths(fileManager:)` operation out of the linting iterations.
[andyyhope](https://github.com/andyyhope)
[#4844](https://github.com/realm/SwiftLint/issues/4844)
## 0.51.0: bzllint ## 0.51.0: bzllint
#### Breaking #### Breaking

View File

@ -1,6 +1,11 @@
import Foundation import Foundation
extension Configuration { extension Configuration {
public enum ExcludeBy {
case prefix
case paths(excludedPaths: [String])
}
// MARK: Lintable Paths // MARK: Lintable Paths
/// Returns the files that can be linted by SwiftLint in the specified parent path. /// Returns the files that can be linted by SwiftLint in the specified parent path.
/// ///
@ -12,8 +17,8 @@ extension Configuration {
/// ///
/// - returns: Files to lint. /// - returns: Files to lint.
public func lintableFiles(inPath path: String, forceExclude: Bool, public func lintableFiles(inPath path: String, forceExclude: Bool,
excludeByPrefix: Bool = false) -> [SwiftLintFile] { excludeBy: ExcludeBy) -> [SwiftLintFile] {
return lintablePaths(inPath: path, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix) return lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy)
.compactMap(SwiftLintFile.init(pathDeferringReading:)) .compactMap(SwiftLintFile.init(pathDeferringReading:))
} }
@ -30,14 +35,17 @@ extension Configuration {
internal func lintablePaths( internal func lintablePaths(
inPath path: String, inPath path: String,
forceExclude: Bool, forceExclude: Bool,
excludeByPrefix: Bool = false, excludeBy: ExcludeBy,
fileManager: LintableFileManager = FileManager.default fileManager: LintableFileManager = FileManager.default
) -> [String] { ) -> [String] {
if fileManager.isFile(atPath: path) { if fileManager.isFile(atPath: path) {
if forceExclude { if forceExclude {
return excludeByPrefix switch excludeBy {
? filterExcludedPathsByPrefix(in: [path.absolutePathStandardized()]) case .prefix:
: filterExcludedPaths(fileManager: fileManager, in: [path.absolutePathStandardized()]) 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 // If path is a file and we're not forcing excludes, skip filtering with excluded/included paths
return [path] return [path]
@ -48,9 +56,12 @@ extension Configuration {
.flatMap(Glob.resolveGlob) .flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) } .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }
return excludeByPrefix switch excludeBy {
? filterExcludedPathsByPrefix(in: pathsForPath, includedPaths) case .prefix:
: filterExcludedPaths(fileManager: fileManager, in: pathsForPath, includedPaths) 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. /// Returns an array of file paths after removing the excluded paths as defined by this configuration.
@ -60,7 +71,7 @@ extension Configuration {
/// ///
/// - returns: The input paths after removing the excluded paths. /// - returns: The input paths after removing the excluded paths.
public func filterExcludedPaths( public func filterExcludedPaths(
fileManager: LintableFileManager = FileManager.default, _ excludedPaths: [String],
in paths: [String]... in paths: [String]...
) -> [String] { ) -> [String] {
let allPaths = paths.flatMap { $0 } let allPaths = paths.flatMap { $0 }
@ -71,7 +82,6 @@ extension Configuration {
let result = NSMutableOrderedSet(array: allPaths) let result = NSMutableOrderedSet(array: allPaths)
#endif #endif
let excludedPaths = self.excludedPaths(fileManager: fileManager)
result.minusSet(Set(excludedPaths)) result.minusSet(Set(excludedPaths))
// swiftlint:disable:next force_cast // swiftlint:disable:next force_cast
return result.map { $0 as! String } return result.map { $0 as! String }
@ -98,7 +108,7 @@ extension Configuration {
/// - parameter fileManager: The file manager to get child paths in a given parent location. /// - parameter fileManager: The file manager to get child paths in a given parent location.
/// ///
/// - returns: The expanded excluded file paths. /// - returns: The expanded excluded file paths.
private func excludedPaths(fileManager: LintableFileManager) -> [String] { public func excludedPaths(fileManager: LintableFileManager = FileManager.default) -> [String] {
return excludedPaths return excludedPaths
.flatMap(Glob.resolveGlob) .flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) } .parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }

View File

@ -222,10 +222,14 @@ extension Configuration {
} }
let scriptInputPaths = files.compactMap { $0.path } let scriptInputPaths = files.compactMap { $0.path }
let filesToLint = visitor.useExcludingByPrefix ?
filterExcludedPathsByPrefix(in: scriptInputPaths) : if visitor.useExcludingByPrefix {
filterExcludedPaths(in: scriptInputPaths) return filterExcludedPathsByPrefix(in: scriptInputPaths)
return filesToLint.map(SwiftLintFile.init(pathDeferringReading:)) .map(SwiftLintFile.init(pathDeferringReading:))
} else {
return filterExcludedPaths(excludedPaths(), in: scriptInputPaths)
.map(SwiftLintFile.init(pathDeferringReading:))
}
} }
if !visitor.quiet { if !visitor.quiet {
let filesInfo: String let filesInfo: String
@ -238,8 +242,12 @@ extension Configuration {
queuedPrintError("\(visitor.action) Swift files \(filesInfo)") queuedPrintError("\(visitor.action) Swift files \(filesInfo)")
} }
return visitor.paths.flatMap { return visitor.paths.flatMap {
self.lintableFiles(inPath: $0, forceExclude: visitor.forceExclude, self.lintableFiles(
excludeByPrefix: visitor.useExcludingByPrefix) inPath: $0,
forceExclude: visitor.forceExclude,
excludeBy: visitor.useExcludingByPrefix
? .prefix
: .paths(excludedPaths: excludedPaths()))
} }
} }

View File

@ -17,7 +17,10 @@ private let config: Configuration = {
class IntegrationTests: SwiftLintTestCase { class IntegrationTests: SwiftLintTestCase {
func testSwiftLintLints() { func testSwiftLintLints() {
// This is as close as we're ever going to get to a self-hosting linter. // This is as close as we're ever going to get to a self-hosting linter.
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false) let swiftFiles = config.lintableFiles(
inPath: "",
forceExclude: false,
excludeBy: .paths(excludedPaths: config.excludedPaths()))
XCTAssert( XCTAssert(
swiftFiles.contains(where: { #file.bridge().absolutePathRepresentation() == $0.path }), swiftFiles.contains(where: { #file.bridge().absolutePathRepresentation() == $0.path }),
"current file should be included" "current file should be included"
@ -35,7 +38,10 @@ class IntegrationTests: SwiftLintTestCase {
} }
func testSwiftLintAutoCorrects() { func testSwiftLintAutoCorrects() {
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false) let swiftFiles = config.lintableFiles(
inPath: "",
forceExclude: false,
excludeBy: .paths(excludedPaths: config.excludedPaths()))
let storage = RuleStorage() let storage = RuleStorage()
let corrections = swiftFiles.parallelFlatMap { let corrections = swiftFiles.parallelFlatMap {
Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage) Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage)

View File

@ -251,43 +251,69 @@ class ConfigurationTests: SwiftLintTestCase {
} }
func testExcludedPaths() { func testExcludedPaths() {
let fileManager = TestFileManager()
let configuration = Configuration(includedPaths: ["directory"], let configuration = Configuration(includedPaths: ["directory"],
excludedPaths: ["directory/excluded", excludedPaths: ["directory/excluded",
"directory/ExcludedFile.swift"]) "directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(inPath: "", forceExclude: false, fileManager: TestFileManager())
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) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
} }
func testForceExcludesFile() { func testForceExcludesFile() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"]) let configuration = Configuration(excludedPaths: ["directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(inPath: "directory/ExcludedFile.swift", forceExclude: true, let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
fileManager: TestFileManager()) let paths = configuration.lintablePaths(inPath: "directory/ExcludedFile.swift",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual([], paths) XCTAssertEqual([], paths)
} }
func testForceExcludesFileNotPresentInExcluded() { func testForceExcludesFileNotPresentInExcluded() {
let fileManager = TestFileManager()
let configuration = Configuration(includedPaths: ["directory"], let configuration = Configuration(includedPaths: ["directory"],
excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"]) excludedPaths: ["directory/ExcludedFile.swift", "directory/excluded"])
let paths = configuration.lintablePaths(inPath: "", forceExclude: true, fileManager: TestFileManager()) 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) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
} }
func testForceExcludesDirectory() { func testForceExcludesDirectory() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]) let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(inPath: "directory", forceExclude: true, let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
fileManager: TestFileManager()) let paths = configuration.lintablePaths(inPath: "directory",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
} }
func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() { func testForceExcludesDirectoryThatIsNotInExcludedButHasChildrenThatAre() {
let fileManager = TestFileManager()
let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"]) let configuration = Configuration(excludedPaths: ["directory/excluded", "directory/ExcludedFile.swift"])
let paths = configuration.lintablePaths(inPath: "directory", forceExclude: true, let excludedPaths = configuration.excludedPaths(fileManager: fileManager)
fileManager: TestFileManager()) let paths = configuration.lintablePaths(inPath: "directory",
forceExclude: true,
excludeBy: .paths(excludedPaths: excludedPaths),
fileManager: fileManager)
XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths) XCTAssertEqual(["directory/File1.swift", "directory/File2.swift"].absolutePathsStandardized(), paths)
} }
func testLintablePaths() { func testLintablePaths() {
let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0, forceExclude: false) let excluded = Configuration.default.excludedPaths(fileManager: TestFileManager())
let paths = Configuration.default.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: false,
excludeBy: .paths(excludedPaths: excluded))
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = [ let expectedFilenames = [
"DirectoryLevel1.swift", "DirectoryLevel1.swift",
@ -301,7 +327,9 @@ class ConfigurationTests: SwiftLintTestCase {
func testGlobIncludePaths() { func testGlobIncludePaths() {
FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0) FileManager.default.changeCurrentDirectoryPath(Mock.Dir.level0)
let configuration = Configuration(includedPaths: ["**/Level2"]) let configuration = Configuration(includedPaths: ["**/Level2"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, forceExclude: true) let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: true,
excludeBy: .paths(excludedPaths: configuration.excludedPaths))
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
let expectedFilenames = ["Level2.swift", "Level3.swift"] let expectedFilenames = ["Level2.swift", "Level3.swift"]
@ -314,7 +342,11 @@ class ConfigurationTests: SwiftLintTestCase {
excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")] excludedPaths: [Mock.Dir.level3.stringByAppendingPathComponent("*.swift")]
) )
XCTAssertEqual(configuration.lintablePaths(inPath: "", forceExclude: false), []) let excludedPaths = configuration.excludedPaths()
let lintablePaths = configuration.lintablePaths(inPath: "",
forceExclude: false,
excludeBy: .paths(excludedPaths: excludedPaths))
XCTAssertEqual(lintablePaths, [])
} }
// MARK: - Testing Configuration Equality // MARK: - Testing Configuration Equality
@ -400,7 +432,7 @@ extension ConfigurationTests {
"Level1/Level2/Level3"]) "Level1/Level2/Level3"])
let paths = configuration.lintablePaths(inPath: Mock.Dir.level0, let paths = configuration.lintablePaths(inPath: Mock.Dir.level0,
forceExclude: false, forceExclude: false,
excludeByPrefix: true) excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent } let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(filenames, ["Level2.swift"]) XCTAssertEqual(filenames, ["Level2.swift"])
} }
@ -410,7 +442,7 @@ extension ConfigurationTests {
let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"]) let configuration = Configuration(excludedPaths: ["Level1/Level2/Level3/Level3.swift"])
let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift", let paths = configuration.lintablePaths(inPath: "Level1/Level2/Level3/Level3.swift",
forceExclude: true, forceExclude: true,
excludeByPrefix: true) excludeBy: .prefix)
XCTAssertEqual([], paths) XCTAssertEqual([], paths)
} }
@ -420,7 +452,7 @@ extension ConfigurationTests {
excludedPaths: ["Level1/Level1.swift"]) excludedPaths: ["Level1/Level1.swift"])
let paths = configuration.lintablePaths(inPath: "Level1", let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: true, forceExclude: true,
excludeByPrefix: true) excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames) XCTAssertEqual(["Level2.swift", "Level3.swift"], filenames)
} }
@ -434,7 +466,7 @@ extension ConfigurationTests {
) )
let paths = configuration.lintablePaths(inPath: ".", let paths = configuration.lintablePaths(inPath: ".",
forceExclude: true, forceExclude: true,
excludeByPrefix: true) excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames) XCTAssertEqual(["Level0.swift", "Level1.swift"], filenames)
} }
@ -448,7 +480,7 @@ extension ConfigurationTests {
) )
let paths = configuration.lintablePaths(inPath: ".", let paths = configuration.lintablePaths(inPath: ".",
forceExclude: true, forceExclude: true,
excludeByPrefix: true) excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent } let filenames = paths.map { $0.bridge().lastPathComponent }
XCTAssertEqual(["Level0.swift"], filenames) XCTAssertEqual(["Level0.swift"], filenames)
} }
@ -460,7 +492,7 @@ extension ConfigurationTests {
excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"]) excludedPaths: ["Level1/*/*.swift", "Level1/*/*/*.swift"])
let paths = configuration.lintablePaths(inPath: "Level1", let paths = configuration.lintablePaths(inPath: "Level1",
forceExclude: false, forceExclude: false,
excludeByPrefix: true) excludeBy: .prefix)
let filenames = paths.map { $0.bridge().lastPathComponent }.sorted() let filenames = paths.map { $0.bridge().lastPathComponent }.sorted()
XCTAssertEqual(filenames, ["Level1.swift"]) XCTAssertEqual(filenames, ["Level1.swift"])
} }