@@ -14,54 +14,82 @@ import Foundation
1414
1515/// Iterator for looping over lists of files and directories. Directories are automatically
1616/// traversed recursively, and we check for files with a ".swift" extension.
17- struct FileIterator : Sequence , IteratorProtocol {
17+ @_spi ( Testing)
18+ public struct FileIterator : Sequence , IteratorProtocol {
1819
1920 /// List of file and directory URLs to iterate over.
20- let urls : [ URL ]
21+ private let urls : [ URL ]
22+
23+ /// If true, symlinks will be followed when iterating over directories and files. If not, they
24+ /// will be ignored.
25+ private let followSymlinks : Bool
2126
2227 /// Iterator for the list of URLs.
23- var urlIterator : Array < URL > . Iterator
28+ private var urlIterator : Array < URL > . Iterator
2429
2530 /// Iterator for recursing through directories.
26- var dirIterator : FileManager . DirectoryEnumerator ? = nil
31+ private var dirIterator : FileManager . DirectoryEnumerator ? = nil
2732
2833 /// The current working directory of the process, which is used to relativize URLs of files found
2934 /// during iteration.
30- let workingDirectory = URL ( fileURLWithPath: " . " )
35+ private let workingDirectory = URL ( fileURLWithPath: " . " )
3136
3237 /// Keep track of the current directory we're recursing through.
33- var currentDirectory = URL ( fileURLWithPath: " " )
38+ private var currentDirectory = URL ( fileURLWithPath: " " )
3439
3540 /// Keep track of files we have visited to prevent duplicates.
36- var visited : Set < String > = [ ]
41+ private var visited : Set < String > = [ ]
3742
3843 /// The file extension to check for when recursing through directories.
39- let fileSuffix = " .swift "
44+ private let fileSuffix = " .swift "
4045
4146 /// Create a new file iterator over the given list of file URLs.
4247 ///
4348 /// The given URLs may be files or directories. If they are directories, the iterator will recurse
4449 /// into them.
45- init ( urls: [ URL ] ) {
50+ public init ( urls: [ URL ] , followSymlinks : Bool ) {
4651 self . urls = urls
4752 self . urlIterator = self . urls. makeIterator ( )
53+ self . followSymlinks = followSymlinks
4854 }
4955
5056 /// Iterate through the "paths" list, and emit the file paths in it. If we encounter a directory,
5157 /// recurse through it and emit .swift file paths.
52- mutating func next( ) -> URL ? {
58+ public mutating func next( ) -> URL ? {
5359 var output : URL ? = nil
5460 while output == nil {
5561 // Check if we're recursing through a directory.
5662 if dirIterator != nil {
5763 output = nextInDirectory ( )
5864 } else {
59- guard let next = urlIterator. next ( ) else { return nil }
60- var isDir : ObjCBool = false
61- if FileManager . default. fileExists ( atPath: next. path, isDirectory: & isDir) , isDir. boolValue {
62- dirIterator = FileManager . default. enumerator ( at: next, includingPropertiesForKeys: nil )
65+ guard var next = urlIterator. next ( ) else {
66+ // If we've reached the end of all the URLs we wanted to iterate over, exit now.
67+ return nil
68+ }
69+
70+ guard let fileType = fileType ( at: next) else {
71+ continue
72+ }
73+
74+ switch fileType {
75+ case . typeSymbolicLink:
76+ guard
77+ followSymlinks,
78+ let destination = try ? FileManager . default. destinationOfSymbolicLink ( atPath: next. path)
79+ else {
80+ break
81+ }
82+ next = URL ( fileURLWithPath: destination)
83+ fallthrough
84+
85+ case . typeDirectory:
86+ dirIterator = FileManager . default. enumerator (
87+ at: next,
88+ includingPropertiesForKeys: nil ,
89+ options: [ . skipsHiddenFiles] )
6390 currentDirectory = next
64- } else {
91+
92+ default :
6593 // We'll get here if the path is a file, or if it doesn't exist. In the latter case,
6694 // return the path anyway; we'll turn the error we get when we try to open the file into
6795 // an appropriate diagnostic instead of trying to handle it here.
@@ -82,23 +110,41 @@ struct FileIterator: Sequence, IteratorProtocol {
82110 private mutating func nextInDirectory( ) -> URL ? {
83111 var output : URL ? = nil
84112 while output == nil {
85- if let item = dirIterator? . nextObject ( ) as? URL {
86- if item . lastPathComponent . hasSuffix ( fileSuffix ) {
87- var isDir : ObjCBool = false
88- if FileManager . default . fileExists ( atPath : item . path , isDirectory : & isDir )
89- && !isDir . boolValue
90- {
91- // We can't use the `.producesRelativePathURLs` enumeration option because it isn't
92- // supported yet on Linux, so we need to relativize the URL ourselves.
93- let relativePath =
94- item . path . hasPrefix ( workingDirectory . path )
95- ? String ( item . path . dropFirst ( workingDirectory . path . count + 1 ) )
96- : item . path
97- output =
98- URL ( fileURLWithPath : relativePath , isDirectory : false , relativeTo : workingDirectory )
99- }
113+ guard let item = dirIterator? . nextObject ( ) as? URL else {
114+ break
115+ }
116+
117+ guard item . lastPathComponent . hasSuffix ( fileSuffix ) , let fileType = fileType ( at : item ) else {
118+ continue
119+ }
120+
121+ var path = item . path
122+ switch fileType {
123+ case . typeSymbolicLink where followSymlinks :
124+ guard
125+ let destination = try ? FileManager . default . destinationOfSymbolicLink ( atPath : path )
126+ else {
127+ break
100128 }
101- } else { break }
129+ path = destination
130+ fallthrough
131+
132+ case . typeRegular:
133+ // We attempt to relativize the URLs based on the current working directory, not the
134+ // directory being iterated over, so that they can be displayed better in diagnostics. Thus,
135+ // if the user passes paths that are relative to the current working diectory, they will
136+ // be displayed as relative paths. Otherwise, they will still be displayed as absolute
137+ // paths.
138+ let relativePath =
139+ path. hasPrefix ( workingDirectory. path)
140+ ? String ( path. dropFirst ( workingDirectory. path. count + 1 ) )
141+ : path
142+ output =
143+ URL ( fileURLWithPath: relativePath, isDirectory: false , relativeTo: workingDirectory)
144+
145+ default :
146+ break
147+ }
102148 }
103149 // If we've exhausted the files in the directory recursion, unset the directory iterator.
104150 if output == nil {
@@ -107,3 +153,10 @@ struct FileIterator: Sequence, IteratorProtocol {
107153 return output
108154 }
109155}
156+
157+ /// Returns the type of the file at the given URL.
158+ private func fileType( at url: URL ) -> FileAttributeType ? {
159+ // We cannot use `URL.resourceValues(forKeys:)` here because it appears to behave incorrectly on
160+ // Linux.
161+ return try ? FileManager . default. attributesOfItem ( atPath: url. path) [ . type] as? FileAttributeType
162+ }
0 commit comments