mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
ac701c088c
The current algorithm is like "collect all included files and subtract all excluded files".
Collecting all included and all excluded files relies on the file system. This can become slow
when the patterns used to exclude files resolve to a large number of files.
The new approach only collects all lintable files and checks them against the exclude patterns.
This can be done by in-memory string-regex-match and does therefore not require file system accesses.
The new implementation also no longer traverses directories that already match an exclude pattern.
(cherry picked from commit 152355e36f)
322 lines
13 KiB
Swift
322 lines
13 KiB
Swift
import CollectionConcurrencyKit
|
|
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
private actor CounterActor {
|
|
private var count = 0
|
|
|
|
func next() -> Int {
|
|
count += 1
|
|
return count
|
|
}
|
|
}
|
|
|
|
private func readFilesFromScriptInputFiles() throws(SwiftLintError) -> [SwiftLintFile] {
|
|
let count = try fileCount(from: "SCRIPT_INPUT_FILE_COUNT")
|
|
return (0..<count).compactMap { fileNumber in
|
|
do {
|
|
let environment = ProcessInfo.processInfo.environment
|
|
let variable = "SCRIPT_INPUT_FILE_\(fileNumber)"
|
|
guard let path = environment[variable] else {
|
|
throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)")
|
|
}
|
|
if path.bridge().isSwiftFile() {
|
|
return SwiftLintFile(pathDeferringReading: path)
|
|
}
|
|
return nil
|
|
} catch {
|
|
queuedPrintError(String(describing: error))
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private func readFilesFromScriptInputFileLists() throws(SwiftLintError) -> [SwiftLintFile] {
|
|
let count = try fileCount(from: "SCRIPT_INPUT_FILE_LIST_COUNT")
|
|
return (0..<count).flatMap { fileNumber in
|
|
var filesToLint: [SwiftLintFile] = []
|
|
do {
|
|
let environment = ProcessInfo.processInfo.environment
|
|
let variable = "SCRIPT_INPUT_FILE_LIST_\(fileNumber)"
|
|
guard let path = environment[variable] else {
|
|
throw SwiftLintError.usageError(description: "Environment variable not set: \(variable)")
|
|
}
|
|
if path.bridge().pathExtension == "xcfilelist" {
|
|
guard let fileContents = FileManager.default.contents(atPath: path),
|
|
let textContents = String(data: fileContents, encoding: .utf8) else {
|
|
throw SwiftLintError.usageError(description: "Could not read file list at: \(path)")
|
|
}
|
|
textContents.enumerateLines { line, _ in
|
|
if line.isSwiftFile() {
|
|
filesToLint.append(SwiftLintFile(pathDeferringReading: line))
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
queuedPrintError(String(describing: error))
|
|
}
|
|
return filesToLint
|
|
}
|
|
}
|
|
|
|
private func fileCount(from envVar: String) throws(SwiftLintError) -> Int {
|
|
guard let countString = ProcessInfo.processInfo.environment[envVar] else {
|
|
throw .usageError(description: "\(envVar) variable not set")
|
|
}
|
|
guard let count = Int(countString) else {
|
|
throw .usageError(description: "\(envVar) did not specify a number")
|
|
}
|
|
return count
|
|
}
|
|
|
|
#if os(Linux) || os(Windows)
|
|
private func autoreleasepool<T>(block: () -> T) -> T { block() }
|
|
#endif
|
|
|
|
extension Configuration {
|
|
func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage) async throws -> [SwiftLintFile] {
|
|
let files = try Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") {
|
|
try getFiles(with: visitor)
|
|
}
|
|
let groupedFiles = try Signposts.record(name: "Configuration.VisitLintableFiles.GroupFiles") {
|
|
try groupFiles(files, visitor: visitor)
|
|
}
|
|
let lintersForFile = Signposts.record(name: "Configuration.VisitLintableFiles.LintersForFile") {
|
|
groupedFiles.map { file in
|
|
linters(for: [file.key: file.value], visitor: visitor)
|
|
}
|
|
}
|
|
let duplicateFileNames = Signposts.record(name: "Configuration.VisitLintableFiles.DuplicateFileNames") {
|
|
lintersForFile.map(\.duplicateFileNames)
|
|
}
|
|
let collected = await Signposts.record(name: "Configuration.VisitLintableFiles.Collect") {
|
|
await zip(lintersForFile, duplicateFileNames).asyncMap { linters, duplicateFileNames in
|
|
await collect(linters: linters, visitor: visitor, storage: storage,
|
|
duplicateFileNames: duplicateFileNames)
|
|
}
|
|
}
|
|
let result = await Signposts.record(name: "Configuration.VisitLintableFiles.Visit") {
|
|
await collected.asyncMap { linters, duplicateFileNames in
|
|
await visit(linters: linters, visitor: visitor, duplicateFileNames: duplicateFileNames)
|
|
}
|
|
}
|
|
return result.flatMap(\.self)
|
|
}
|
|
|
|
private func groupFiles(_ files: [SwiftLintFile], visitor: LintableFilesVisitor) throws(SwiftLintError)
|
|
-> [Configuration: [SwiftLintFile]] {
|
|
if files.isEmpty, !visitor.allowZeroLintableFiles {
|
|
throw .usageError(
|
|
description: "No lintable files found at paths: '\(visitor.options.paths.joined(separator: ", "))'"
|
|
)
|
|
}
|
|
|
|
return files.parallelFilterGroup { file in
|
|
let fileConfiguration = configuration(for: file)
|
|
let fileConfigurationRootPath = fileConfiguration.rootDirectory.bridge()
|
|
|
|
// Files whose configuration specifies they should be excluded will be skipped
|
|
let shouldSkip = fileConfiguration.excludedPaths.contains { excludedRelativePath in
|
|
let excludedPath = fileConfigurationRootPath.appendingPathComponent(excludedRelativePath)
|
|
let filePathComponents = file.path?.bridge().pathComponents ?? []
|
|
let excludedPathComponents = excludedPath.bridge().pathComponents
|
|
return filePathComponents.starts(with: excludedPathComponents)
|
|
}
|
|
|
|
return shouldSkip ? nil : fileConfiguration
|
|
}
|
|
}
|
|
|
|
private func outputFilename(for path: String, duplicateFileNames: Set<String>) -> String {
|
|
let basename = path.bridge().lastPathComponent
|
|
if !duplicateFileNames.contains(basename) {
|
|
return basename
|
|
}
|
|
|
|
var pathComponents = path.bridge().pathComponents
|
|
for component in rootDirectory.bridge().pathComponents where pathComponents.first == component {
|
|
pathComponents.removeFirst()
|
|
}
|
|
|
|
return pathComponents.joined(separator: "/")
|
|
}
|
|
|
|
private func linters(for filesPerConfiguration: [Configuration: [SwiftLintFile]],
|
|
visitor: LintableFilesVisitor) -> [Linter] {
|
|
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
|
|
|
|
var linters = [Linter]()
|
|
linters.reserveCapacity(fileCount)
|
|
for (config, files) in filesPerConfiguration {
|
|
let newConfig: Configuration
|
|
if visitor.cache != nil {
|
|
newConfig = config.withPrecomputedCacheDescription()
|
|
} else {
|
|
newConfig = config
|
|
}
|
|
linters += files.map { visitor.linter(forFile: $0, configuration: newConfig) }
|
|
}
|
|
return linters
|
|
}
|
|
|
|
private func collect(linters: [Linter],
|
|
visitor: LintableFilesVisitor,
|
|
storage: RuleStorage,
|
|
duplicateFileNames: Set<String>) async -> ([CollectedLinter], Set<String>) {
|
|
let counter = CounterActor()
|
|
let total = linters.filter(\.isCollecting).count
|
|
let progress = ProgressBar(count: total)
|
|
if visitor.options.progress, total > 0 {
|
|
await progress.initialize()
|
|
}
|
|
let collect = { (linter: Linter) -> CollectedLinter? in
|
|
let skipFile = visitor.shouldSkipFile(atPath: linter.file.path)
|
|
if !visitor.options.quiet, linter.isCollecting {
|
|
if visitor.options.progress {
|
|
await progress.printNext()
|
|
} else if let filePath = linter.file.path {
|
|
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
|
let collected = await counter.next()
|
|
if skipFile {
|
|
Issue.genericWarning(
|
|
"""
|
|
Skipping '\(outputFilename)' (\(collected)/\(total)) \
|
|
because its compiler arguments could not be found
|
|
"""
|
|
).print()
|
|
} else {
|
|
queuedPrintError("Collecting '\(outputFilename)' (\(collected)/\(total))")
|
|
}
|
|
}
|
|
}
|
|
|
|
guard !skipFile else {
|
|
return nil
|
|
}
|
|
|
|
return autoreleasepool {
|
|
linter.collect(into: storage)
|
|
}
|
|
}
|
|
|
|
let collectedLinters = await visitor.parallel ?
|
|
linters.concurrentCompactMap(collect) :
|
|
linters.asyncCompactMap(collect)
|
|
return (collectedLinters, duplicateFileNames)
|
|
}
|
|
|
|
private func visit(linters: [CollectedLinter],
|
|
visitor: LintableFilesVisitor,
|
|
duplicateFileNames: Set<String>) async -> [SwiftLintFile] {
|
|
let counter = CounterActor()
|
|
let progress = ProgressBar(count: linters.count)
|
|
if visitor.options.progress {
|
|
await progress.initialize()
|
|
}
|
|
let visit = { (linter: CollectedLinter) -> SwiftLintFile in
|
|
if !visitor.options.quiet {
|
|
if visitor.options.progress {
|
|
await progress.printNext()
|
|
} else if let filePath = linter.file.path {
|
|
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
|
let visited = await counter.next()
|
|
queuedPrintError(
|
|
"\(visitor.options.capitalizedVerb) '\(outputFilename)' (\(visited)/\(linters.count))"
|
|
)
|
|
}
|
|
}
|
|
|
|
await Signposts.record(name: "Configuration.Visit", span: .file(linter.file.path ?? "")) {
|
|
await visitor.block(linter)
|
|
}
|
|
return linter.file
|
|
}
|
|
return await visitor.parallel ?
|
|
linters.concurrentMap(visit) :
|
|
linters.asyncMap(visit)
|
|
}
|
|
|
|
fileprivate func getFiles(with visitor: LintableFilesVisitor) throws(SwiftLintError) -> [SwiftLintFile] {
|
|
let options = visitor.options
|
|
if options.useSTDIN {
|
|
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
|
|
if let stdinString = String(data: stdinData, encoding: .utf8) {
|
|
return [SwiftLintFile(contents: stdinString)]
|
|
}
|
|
throw .usageError(description: "stdin isn't a UTF8-encoded string")
|
|
}
|
|
if options.useScriptInputFiles || options.useScriptInputFileLists {
|
|
let files = try options.useScriptInputFiles
|
|
? readFilesFromScriptInputFiles()
|
|
: readFilesFromScriptInputFileLists()
|
|
guard options.forceExclude else {
|
|
return files
|
|
}
|
|
let scriptInputPaths = files.compactMap(\.path)
|
|
return (
|
|
visitor.options.useExcludingByPrefix
|
|
? filterExcludedPathsByPrefix(in: scriptInputPaths)
|
|
: filterExcludedPaths(in: scriptInputPaths)
|
|
).map(SwiftLintFile.init(pathDeferringReading:))
|
|
}
|
|
if !options.quiet {
|
|
let filesInfo: String
|
|
if options.paths.isEmpty || options.paths == [""] {
|
|
filesInfo = "in current working directory"
|
|
} else {
|
|
filesInfo = "at paths \(options.paths.joined(separator: ", "))"
|
|
}
|
|
|
|
queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)")
|
|
}
|
|
return visitor.options.paths.flatMap {
|
|
self.lintableFiles(
|
|
inPath: $0,
|
|
forceExclude: visitor.options.forceExclude,
|
|
excludeByPrefix: visitor.options.useExcludingByPrefix
|
|
)
|
|
}
|
|
}
|
|
|
|
func visitLintableFiles(options: LintOrAnalyzeOptions,
|
|
cache: LinterCache? = nil,
|
|
storage: RuleStorage,
|
|
visitorBlock: @escaping (CollectedLinter) async -> Void) async throws -> [SwiftLintFile] {
|
|
let visitor = try LintableFilesVisitor.create(options, cache: cache,
|
|
allowZeroLintableFiles: allowZeroLintableFiles,
|
|
block: visitorBlock)
|
|
return try await visitLintableFiles(with: visitor, storage: storage)
|
|
}
|
|
|
|
// MARK: LintOrAnalyze Command
|
|
|
|
init(options: LintOrAnalyzeOptions) {
|
|
self.init(
|
|
configurationFiles: options.configurationFiles,
|
|
enableAllRules: options.enableAllRules,
|
|
onlyRule: options.onlyRule,
|
|
cachePath: options.cachePath
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct DuplicateCollector {
|
|
var all = Set<String>()
|
|
var duplicates = Set<String>()
|
|
}
|
|
|
|
private extension Collection where Element == Linter {
|
|
var duplicateFileNames: Set<String> {
|
|
let collector = reduce(into: DuplicateCollector()) { result, linter in
|
|
if let filename = linter.file.path?.bridge().lastPathComponent {
|
|
if result.all.contains(filename) {
|
|
result.duplicates.insert(filename)
|
|
}
|
|
|
|
result.all.insert(filename)
|
|
}
|
|
}
|
|
return collector.duplicates
|
|
}
|
|
}
|