1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Foundation
#if os(Windows)
import WinSDK
#endif
/// Iterator for looping over lists of files and directories. Directories are automatically
/// traversed recursively, and we check for files with a ".swift" extension.
@_spi(Internal)
public struct FileIterator: Sequence, IteratorProtocol {
/// List of file and directory URLs to iterate over.
private let urls: [URL]
/// If true, symlinks will be followed when iterating over directories and files. If not, they
/// will be ignored.
private let followSymlinks: Bool
/// Iterator for the list of URLs.
private var urlIterator: Array<URL>.Iterator
/// Iterator for recursing through directories.
private var dirIterator: FileManager.DirectoryEnumerator? = nil
/// The current working directory of the process, which is used to relativize URLs of files found
/// during iteration.
private let workingDirectory: URL
/// Keep track of the current directory we're recursing through.
private var currentDirectory = URL(fileURLWithPath: "")
/// Keep track of files we have visited to prevent duplicates.
private var visited: Set<String> = []
/// The file extension to check for when recursing through directories.
private let fileSuffix = ".swift"
/// Create a new file iterator over the given list of file URLs.
///
/// The given URLs may be files or directories. If they are directories, the iterator will recurse
/// into them. Symlinks are never followed on Windows platforms as Foundation doesn't support it.
/// - Parameters:
/// - urls: `Array` of files or directories to iterate.
/// - followSymlinks: `Bool` to indicate if symbolic links should be followed when iterating.
/// - workingDirectory: `URL` that indicates the current working directory. Used for testing.
public init(urls: [URL], followSymlinks: Bool, workingDirectory: URL = URL(fileURLWithPath: ".")) {
self.workingDirectory = workingDirectory
self.urls = urls
self.urlIterator = self.urls.makeIterator()
self.followSymlinks = followSymlinks
}
/// Iterate through the "paths" list, and emit the file paths in it. If we encounter a directory,
/// recurse through it and emit .swift file paths.
public mutating func next() -> URL? {
var output: URL? = nil
while output == nil {
// Check if we're recursing through a directory.
if dirIterator != nil {
output = nextInDirectory()
} else {
guard var next = urlIterator.next() else {
// If we've reached the end of all the URLs we wanted to iterate over, exit now.
return nil
}
guard let fileType = fileType(at: next) else {
continue
}
switch fileType {
case .typeSymbolicLink:
guard
followSymlinks,
let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: next.path)
else {
break
}
next = URL(fileURLWithPath: destination, relativeTo: next)
fallthrough
case .typeDirectory:
dirIterator = FileManager.default.enumerator(
at: next,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
currentDirectory = next
default:
// We'll get here if the path is a file, or if it doesn't exist. In the latter case,
// return the path anyway; we'll turn the error we get when we try to open the file into
// an appropriate diagnostic instead of trying to handle it here.
output = next
}
}
if let out = output, visited.contains(out.standardizedFileURL.path) {
output = nil
}
}
if let out = output {
visited.insert(out.standardizedFileURL.path)
}
return output
}
/// Use the FileManager API to recurse through directories and emit .swift file paths.
private mutating func nextInDirectory() -> URL? {
var output: URL? = nil
while output == nil {
guard let item = dirIterator?.nextObject() as? URL else {
break
}
#if os(Windows)
// Windows does not consider files and directories starting with `.` as hidden but we don't want to traverse
// into eg. `.build`. Manually skip any items starting with `.`.
if item.lastPathComponent.hasPrefix(".") {
dirIterator?.skipDescendants()
continue
}
#endif
guard item.lastPathComponent.hasSuffix(fileSuffix), let fileType = fileType(at: item) else {
continue
}
var path = item.path
switch fileType {
case .typeSymbolicLink where followSymlinks:
guard
let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: path)
else {
break
}
path = URL(fileURLWithPath: destination, relativeTo: item).path
fallthrough
case .typeRegular:
// We attempt to relativize the URLs based on the current working directory, not the
// directory being iterated over, so that they can be displayed better in diagnostics. Thus,
// if the user passes paths that are relative to the current working directory, they will
// be displayed as relative paths. Otherwise, they will still be displayed as absolute
// paths.
let relativePath: String
if !workingDirectory.isRoot, path.hasPrefix(workingDirectory.path) {
relativePath = String(path.dropFirst(workingDirectory.path.count).drop(while: { $0 == "/" || $0 == #"\"# }))
} else {
relativePath = path
}
output = URL(fileURLWithPath: relativePath, isDirectory: false, relativeTo: workingDirectory)
default:
break
}
}
// If we've exhausted the files in the directory recursion, unset the directory iterator.
if output == nil {
dirIterator = nil
}
return output
}
}
/// Returns the type of the file at the given URL.
private func fileType(at url: URL) -> FileAttributeType? {
// We cannot use `URL.resourceValues(forKeys:)` here because it appears to behave incorrectly on
// Linux.
return try? FileManager.default.attributesOfItem(atPath: url.path)[.type] as? FileAttributeType
}
|