@@ -14,54 +14,82 @@ import Foundation
14
14
15
15
/// Iterator for looping over lists of files and directories. Directories are automatically
16
16
/// 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 {
18
19
19
20
/// 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
21
26
22
27
/// Iterator for the list of URLs.
23
- var urlIterator : Array < URL > . Iterator
28
+ private var urlIterator : Array < URL > . Iterator
24
29
25
30
/// Iterator for recursing through directories.
26
- var dirIterator : FileManager . DirectoryEnumerator ? = nil
31
+ private var dirIterator : FileManager . DirectoryEnumerator ? = nil
27
32
28
33
/// The current working directory of the process, which is used to relativize URLs of files found
29
34
/// during iteration.
30
- let workingDirectory = URL ( fileURLWithPath: " . " )
35
+ private let workingDirectory = URL ( fileURLWithPath: " . " )
31
36
32
37
/// Keep track of the current directory we're recursing through.
33
- var currentDirectory = URL ( fileURLWithPath: " " )
38
+ private var currentDirectory = URL ( fileURLWithPath: " " )
34
39
35
40
/// Keep track of files we have visited to prevent duplicates.
36
- var visited : Set < String > = [ ]
41
+ private var visited : Set < String > = [ ]
37
42
38
43
/// The file extension to check for when recursing through directories.
39
- let fileSuffix = " .swift "
44
+ private let fileSuffix = " .swift "
40
45
41
46
/// Create a new file iterator over the given list of file URLs.
42
47
///
43
48
/// The given URLs may be files or directories. If they are directories, the iterator will recurse
44
49
/// into them.
45
- init ( urls: [ URL ] ) {
50
+ public init ( urls: [ URL ] , followSymlinks : Bool ) {
46
51
self . urls = urls
47
52
self . urlIterator = self . urls. makeIterator ( )
53
+ self . followSymlinks = followSymlinks
48
54
}
49
55
50
56
/// Iterate through the "paths" list, and emit the file paths in it. If we encounter a directory,
51
57
/// recurse through it and emit .swift file paths.
52
- mutating func next( ) -> URL ? {
58
+ public mutating func next( ) -> URL ? {
53
59
var output : URL ? = nil
54
60
while output == nil {
55
61
// Check if we're recursing through a directory.
56
62
if dirIterator != nil {
57
63
output = nextInDirectory ( )
58
64
} 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] )
63
90
currentDirectory = next
64
- } else {
91
+
92
+ default :
65
93
// We'll get here if the path is a file, or if it doesn't exist. In the latter case,
66
94
// return the path anyway; we'll turn the error we get when we try to open the file into
67
95
// an appropriate diagnostic instead of trying to handle it here.
@@ -82,23 +110,41 @@ struct FileIterator: Sequence, IteratorProtocol {
82
110
private mutating func nextInDirectory( ) -> URL ? {
83
111
var output : URL ? = nil
84
112
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
100
128
}
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
+ }
102
148
}
103
149
// If we've exhausted the files in the directory recursion, unset the directory iterator.
104
150
if output == nil {
@@ -107,3 +153,10 @@ struct FileIterator: Sequence, IteratorProtocol {
107
153
return output
108
154
}
109
155
}
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