Skip to content

Commit 46f2e34

Browse files
committed
Use URL enumerator
1 parent 1bf2f3a commit 46f2e34

File tree

12 files changed

+77
-50
lines changed

12 files changed

+77
-50
lines changed

Source/SwiftLintCore/Extensions/String+SwiftLint.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,6 @@ public extension String {
6666
NSRange(location: 0, length: utf16.count)
6767
}
6868

69-
/// Returns a new string, converting the path to a canonical absolute path.
70-
///
71-
/// > Important: This method might use an incorrect working directory internally. This can cause test failures
72-
/// in Bazel builds but does not seem to cause trouble in production.
73-
///
74-
/// - returns: A new `String`.
75-
func absolutePathStandardized() -> String {
76-
URL(filePath: bridge().standardizingPath.absolutePathRepresentation()).filepath
77-
}
78-
7969
/// Count the number of occurrences of the given character in `self`
8070
/// - Parameter character: Character to count
8171
/// - Returns: Number of times `character` occurs in `self`

Source/SwiftLintCore/Extensions/URL+SwiftLint.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ public extension URL {
99
withUnsafeFileSystemRepresentation { String(cString: $0!) }
1010
}
1111

12-
var filepathGuarded: String? {
13-
withUnsafeFileSystemRepresentation { ptr in
14-
guard let ptr else {
15-
Issue.genericError(
16-
"File with URL '\(self)' cannot be represented as a file system path; skipping it"
17-
).print()
18-
return nil
19-
}
20-
return String(cString: ptr)
21-
}
22-
}
23-
2412
var isSwiftFile: Bool {
2513
isFile && pathExtension == "swift"
2614
}

Source/SwiftLintFramework/Configuration/Configuration+LintableFiles.swift

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ extension Configuration {
4848

4949
// With no included paths, we lint everything in the given path.
5050
if includedPaths.isEmpty {
51-
return fileManager.filesToLint(
52-
inPath: path,
53-
excluder: excluder
54-
)
51+
return makeUnique(paths: fileManager.filesToLint(inPath: path, excluder: excluder))
5552
}
5653

5754
// With included paths, only lint them (after resolving globs).
@@ -88,16 +85,8 @@ extension Configuration {
8885
return .noExclusion
8986
}
9087
if excludeByPrefix {
91-
return .byPrefix(
92-
prefixes: excludedPaths
93-
.flatMap { Glob.resolveGlob($0) }
94-
.map(\.path)
95-
)
88+
return .byPrefix(prefixes: excludedPaths.flatMap(Glob.resolveGlob).map(\.path))
9689
}
97-
return .matching(
98-
matchers: excludedPaths.flatMap {
99-
Glob.createFilenameMatchers(pattern: $0.path)
100-
}
101-
)
90+
return .matching(matchers: excludedPaths.flatMap { Glob.createFilenameMatchers(pattern: $0.path) })
10291
}
10392
}

Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ extension FileManager: @unchecked @retroactive Sendable {}
5151
#endif
5252

5353
extension FileManager: LintableFileManager {
54+
private static let enumeratorProperties: Set<URLResourceKey> = [
55+
.isRegularFileKey,
56+
.isSymbolicLinkKey,
57+
]
58+
private static let enumeratorOptions: DirectoryEnumerationOptions = [
59+
.producesRelativePathURLs,
60+
.skipsHiddenFiles,
61+
.skipsPackageDescendants,
62+
.skipsSubdirectoryDescendants,
63+
]
64+
5465
public func filesToLint(inPath path: URL, excluder: Excluder) -> [URL] {
5566
// If path is a file, filter and return it directly.
5667
if path.isSwiftFile {
@@ -69,25 +80,34 @@ extension FileManager: LintableFileManager {
6980
}
7081

7182
private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [URL] {
72-
guard let enumerator = enumerator(atPath: absolutePath.filepath) else {
83+
let absolutePath = absolutePath.standardized.resolvingSymlinksInPath()
84+
let enumerator = enumerator(
85+
at: absolutePath,
86+
includingPropertiesForKeys: Array(Self.enumeratorProperties),
87+
options: Self.enumeratorOptions
88+
)
89+
guard let enumerator else {
7390
return []
7491
}
7592

7693
var files = [URL]()
7794
var directoriesToWalk = [URL]()
7895

79-
while let element = enumerator.nextObject() as? String {
80-
let absoluteElementPath = element.url(relativeTo: absolutePath)
81-
if absoluteElementPath.isFile {
82-
if absoluteElementPath.pathExtension == "swift",
83-
!excluder.excludes(path: absoluteElementPath) {
84-
files.append(absoluteElementPath)
96+
while var element = (enumerator.nextObject() as? URL)?.relative(to: absolutePath) {
97+
var resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties)
98+
if resourceValues?.isSymbolicLink == true {
99+
if excluder.excludes(path: element) {
100+
continue
85101
}
86-
} else {
87-
enumerator.skipDescendants()
88-
if !excluder.excludes(path: absoluteElementPath) {
89-
directoriesToWalk.append(absoluteElementPath)
102+
element.resolveSymlinksInPath()
103+
resourceValues = try? element.resourceValues(forKeys: Self.enumeratorProperties)
104+
}
105+
if resourceValues?.isRegularFile == true {
106+
if element.pathExtension == "swift", !excluder.excludes(path: element) {
107+
files.append(element)
90108
}
109+
} else if resourceValues != nil, !excluder.excludes(path: element) {
110+
directoriesToWalk.append(element)
91111
}
92112
}
93113

Tests/FileSystemAccessTests/ConfigurationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ final class ConfigurationTests: SwiftLintTestCase { // swiftlint:disable:this ty
644644
private extension Sequence where Element == String {
645645
func absolutePathsStandardized() -> [String] {
646646
// In Bazel builds, absolute paths might be prefixed with `/private`.
647-
map { String($0.absolutePathStandardized().trimmingPrefix("/private")) }
647+
map { URL(filePath: $0).standardizedFileURL.resolvingSymlinksInPath().relativeDisplayPath }
648648
}
649649
}
650650

Tests/IntegrationTests/ConfigPathResolutionTests.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,43 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable {
170170
)
171171
}
172172

173-
#if !os(Windows)
173+
func testSymlinkedFileAndFolderAreFollowed() throws {
174+
#if os(Windows)
175+
try XCTSkip("Symlinks in fixture folder are not supported on Windows")
176+
#endif
177+
178+
let expectedPaths = ["Real/Folder/Nested.swift", "Real/Target.swift"]
179+
180+
// With symlinks
181+
XCTAssertEqual(lintableFilePaths(in: "_9_symlinked_paths", configFile: ".swiftlint.yml"), expectedPaths)
182+
183+
let fixture = fixturePath("_9_symlinked_paths")
184+
let fileLink = fixture.appending(path: "LinkToFile.swift", directoryHint: .notDirectory)
185+
var folderLink = fixture.appending(path: "LinkToFolder", directoryHint: .isDirectory)
186+
let targetFile = fixture.appending(path: "Real/Target.swift", directoryHint: .notDirectory)
187+
let targetFolder = fixture.appending(path: "Real/Folder", directoryHint: .isDirectory)
188+
189+
let fileManager = FileManager.default
190+
XCTAssert(fileManager.fileExists(atPath: fileLink.filepath))
191+
XCTAssert(fileManager.fileExists(atPath: folderLink.filepath))
192+
XCTAssert(try fileLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == true)
193+
XCTAssert(try folderLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == true)
194+
XCTAssertEqual(fileLink.resolvingSymlinksInPath(), targetFile)
195+
196+
XCTAssertNotEqual(folderLink, targetFile)
197+
folderLink.resolveSymlinksInPath()
198+
XCTAssert(try folderLink.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink == false)
199+
XCTAssertEqual(folderLink, targetFolder)
200+
201+
// Without symlinks
202+
XCTAssertEqual(lintableFilePaths(in: "_9_symlinked_paths", configFile: ".swiftlint.yml"), expectedPaths)
203+
}
204+
174205
func testUnicodePrivateUseAreaCharacterInPath() throws {
206+
#if os(Windows)
207+
try XCTSkip("Windows unzip does not support PUA characters in paths")
208+
#endif
209+
175210
let fixture = fixturePath("_8_unicode_private_use_area")
176211

177212
let process = Process()
@@ -186,5 +221,4 @@ final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable {
186221
["Resources/Settings.bundle/androidx.core:core-bundle.swift"]
187222
)
188223
}
189-
#endif
190224
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
excluded:
2+
- Excluded

Tests/IntegrationTests/Resources/_9_symlinked_paths/Excluded/Excluded.swift

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Real/Target.swift
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Real/Folder

0 commit comments

Comments
 (0)