Files

108 lines
4.5 KiB
Swift

import FilenameMatcher
import Foundation
import SourceKittenFramework
/// An interface for enumerating files that can be linted by SwiftLint.
public protocol LintableFileManager {
/// Returns all files that can be linted in the specified path. If the path is relative, it will be appended to the
/// specified root path, or current working directory if no root directory is specified.
///
/// - parameter path: The path in which lintable files should be found.
/// - parameter rootDirectory: The parent directory for the specified path. If none is provided, the current working
/// directory will be used.
/// - parameter excluder: The excluder used to filter out files that should not be linted.
///
/// - returns: Files to lint.
func filesToLint(inPath path: String, rootDirectory: String?, excluder: Excluder) -> [String]
/// Returns the date when the file at the specified path was last modified. Returns `nil` if the file cannot be
/// found or its last modification date cannot be determined.
///
/// - parameter path: The file whose modification date should be determined.
///
/// - returns: A date, if one was determined.
func modificationDate(forFileAtPath path: String) -> Date?
}
/// An excluder for filtering out files that should not be linted.
public enum Excluder {
/// Full matching excluder using filename matchers.
case matching(matchers: [FilenameMatcher])
/// Prefix-based excluder using path prefixes.
case byPrefix(prefixes: [String])
/// An excluder that does not exclude any files.
case noExclusion
func excludes(path: String) -> Bool {
switch self {
case let .matching(matchers):
matchers.contains(where: { $0.match(filename: path) })
case let .byPrefix(prefixes):
prefixes.contains(where: { path.hasPrefix($0) })
case .noExclusion:
false
}
}
}
extension FileManager: LintableFileManager, @unchecked @retroactive Sendable {
public func filesToLint(inPath path: String,
rootDirectory: String? = nil,
excluder: Excluder) -> [String] {
let absolutePath = URL(
fileURLWithPath: path.absolutePathRepresentation(rootDirectory: rootDirectory ?? currentDirectoryPath)
)
// If path is a file, filter and return it directly.
if absolutePath.isSwiftFile {
let filePath = absolutePath.standardized.filepath
return excluder.excludes(path: filePath) ? [] : [filePath]
}
// Fast path when there are no exclusions.
if case .noExclusion = excluder {
return subpaths(atPath: absolutePath.filepath)?.parallelCompactMap { element in
let absoluteElementPath = URL(fileURLWithPath: element, relativeTo: absolutePath)
return absoluteElementPath.isSwiftFile ? absoluteElementPath.standardized.filepath : nil
} ?? []
}
return collectFiles(atPath: absolutePath, excluder: excluder)
}
private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [String] {
guard let root = absolutePath.filepathGuarded, let enumerator = enumerator(atPath: root) else {
return []
}
var files = [String]()
var directoriesToWalk = [String]()
while let element = enumerator.nextObject() as? String {
let absoluteElementPath = URL(fileURLWithPath: element, relativeTo: absolutePath)
guard let absoluteStandardizedElementPath = absoluteElementPath.standardized.filepathGuarded else {
continue
}
if absoluteElementPath.path.isFile {
if absoluteElementPath.pathExtension == "swift",
!excluder.excludes(path: absoluteStandardizedElementPath) {
files.append(absoluteStandardizedElementPath)
}
} else {
enumerator.skipDescendants()
if !excluder.excludes(path: absoluteStandardizedElementPath) {
directoriesToWalk.append(absoluteStandardizedElementPath)
}
}
}
return files + directoriesToWalk.parallelFlatMap {
collectFiles(atPath: URL(fileURLWithPath: $0, isDirectory: true), excluder: excluder)
}
}
public func modificationDate(forFileAtPath path: String) -> Date? {
(try? attributesOfItem(atPath: path))?[.modificationDate] as? Date
}
}