From faa5859155a613f90fd74c98f453608bfeba7b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Thu, 15 Jan 2026 00:06:13 +0100 Subject: [PATCH] Introduce guarded `filepath` provider for file collection (#6435) --- .github/workflows/test.yml | 1 + .gitignore | 4 +- CHANGELOG.md | 5 +++ Package.swift | 1 + .../Extensions/URL+SwiftLint.swift | 12 ++++++ .../Extensions/FileManager+SwiftLint.swift | 6 ++- .../ConfigPathResolutionTests.swift | 40 ++++++++++++++++-- .../_8_unicode_private_use_area/app.zip | Bin 0 -> 946 bytes 8 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 Tests/IntegrationTests/Resources/_8_unicode_private_use_area/app.zip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28b29a3e1..b9f9f44f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,7 @@ jobs: name: SPM, macOS ${{ matrix.macOS }}, Xcode ${{ matrix.xcode }} runs-on: macos-${{ matrix.macOS }} strategy: + fail-fast: false matrix: include: - macOS: '14' diff --git a/.gitignore b/.gitignore index 41cde72e5..153c6688a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ default.profraw SwiftLint.xcodeproj SwiftLint.pkg -*.zip +/*.zip benchmark_* docs/ rule_docs/ @@ -73,4 +73,4 @@ oss-check-summary.md # VS Code -.vscode \ No newline at end of file +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 9493c2859..7f5c0d2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ [SimplyDanny](https://github.com/SimplyDanny) [#6423](https://github.com/realm/SwiftLint/issues/6423) +* Inform users about files being skipped due to impossible file system representation + instead of crashing. + [SimplyDanny](https://github.com/SimplyDanny) + [#6419](https://github.com/realm/SwiftLint/issues/6419) + * Ignore `override` functions in `async_without_await` rule. [SimplyDanny](https://github.com/SimplyDanny) [#6416](https://github.com/realm/SwiftLint/issues/6416) diff --git a/Package.swift b/Package.swift index d348d23a0..4dd6ae80c 100644 --- a/Package.swift +++ b/Package.swift @@ -203,6 +203,7 @@ let package = Package( .testTarget( name: "IntegrationTests", dependencies: [ + "SwiftLintCore", "SwiftLintFramework", "TestHelpers", ], diff --git a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift index 1acfb6215..b6781612e 100644 --- a/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/URL+SwiftLint.swift @@ -5,6 +5,18 @@ public extension URL { withUnsafeFileSystemRepresentation { String(cString: $0!) } } + var filepathGuarded: String? { + withUnsafeFileSystemRepresentation { ptr in + guard let ptr else { + Issue.genericError( + "File with URL '\(self)' cannot be represented as a file system path; skipping it" + ).print() + return nil + } + return String(cString: ptr) + } + } + var isSwiftFile: Bool { filepath.isFile && pathExtension == "swift" } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index 5f17b3903..766e763d0 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -71,7 +71,7 @@ extension FileManager: LintableFileManager, @unchecked @retroactive Sendable { } private func collectFiles(atPath absolutePath: URL, excluder: Excluder) -> [String] { - guard let enumerator = enumerator(atPath: absolutePath.filepath) else { + guard let root = absolutePath.filepathGuarded, let enumerator = enumerator(atPath: root) else { return [] } @@ -80,7 +80,9 @@ extension FileManager: LintableFileManager, @unchecked @retroactive Sendable { while let element = enumerator.nextObject() as? String { let absoluteElementPath = URL(fileURLWithPath: element, relativeTo: absolutePath) - let absoluteStandardizedElementPath = absoluteElementPath.standardized.filepath + guard let absoluteStandardizedElementPath = absoluteElementPath.standardized.filepathGuarded else { + continue + } if absoluteElementPath.path.isFile { if absoluteElementPath.pathExtension == "swift", !excluder.excludes(path: absoluteStandardizedElementPath) { diff --git a/Tests/IntegrationTests/ConfigPathResolutionTests.swift b/Tests/IntegrationTests/ConfigPathResolutionTests.swift index 839d31bf7..8c8819e49 100644 --- a/Tests/IntegrationTests/ConfigPathResolutionTests.swift +++ b/Tests/IntegrationTests/ConfigPathResolutionTests.swift @@ -3,7 +3,9 @@ import SwiftLintFramework import TestHelpers import XCTest -final class ConfigPathResolutionTests: SwiftLintTestCase { +@testable import SwiftLintCore + +final class ConfigPathResolutionTests: SwiftLintTestCase, @unchecked Sendable { private func fixturePath(_ scenario: String) -> URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() @@ -16,10 +18,8 @@ final class ConfigPathResolutionTests: SwiftLintTestCase { let scenarioPath = fixturePath(scenario).filepath let previousDir = FileManager.default.currentDirectoryPath - defer { - _ = FileManager.default.changeCurrentDirectoryPath(previousDir) - } XCTAssert(FileManager.default.changeCurrentDirectoryPath(scenarioPath)) + defer { _ = FileManager.default.changeCurrentDirectoryPath(previousDir) } let config = Configuration(configurationFiles: configFile.map { [$0] } ?? []) let files = config.lintableFiles( @@ -169,4 +169,36 @@ final class ConfigPathResolutionTests: SwiftLintTestCase { .contains("explicit_type_interface") ) } + + #if !os(Windows) + func testUnicodePrivateUseAreaCharacterInPath() async throws { + let fixture = fixturePath("_8_unicode_private_use_area") + + let process = Process() + process.executableURL = URL(filePath: "/usr/bin/env", directoryHint: .notDirectory) + process.arguments = ["unzip", "-o", fixture.appending(path: "app.zip").filepath, "-d", fixture.filepath] + try process.run() + process.waitUntilExit() + defer { try? FileManager.default.removeItem(at: fixture.appending(path: "App")) } + + if #available(macOS 26, *) { + XCTAssertEqual( + lintableFilePaths(in: "_8_unicode_private_use_area/App"), + ["Resources/Settings.bundle/androidx.core:core-bundle.swift"] + ) + } else { + let console = await Issue.captureConsole { + XCTAssert(lintableFilePaths(in: "_8_unicode_private_use_area/App").isEmpty) + } + XCTAssert( + console.contains( + """ + error: File with URL 'androidx.core:core-bundle.swift' \ + cannot be represented as a file system path; skipping it + """ + ) + ) + } + } + #endif } diff --git a/Tests/IntegrationTests/Resources/_8_unicode_private_use_area/app.zip b/Tests/IntegrationTests/Resources/_8_unicode_private_use_area/app.zip new file mode 100644 index 0000000000000000000000000000000000000000..9a71a1d52048e40d014ad479212d444a092078f2 GIT binary patch literal 946 zcmWIWW@h1H0D*Hm^O#s70EfJDXA$|TnY-h3Mr+zxs_Z9 z^ILYHx{eQOeo$(0erZv1YB9u}H-s`WK^Uhy!_a&whha`|YDr0EUV5=!QfXdFPAbIQ z6p@Tf5XNcl2cWreQ{Nv4g(V2v5;Qk4FQq6yGo?Z=Ilm~?3Pk8aY=i_EIH;O|!Q{n@ zVQo%oi9(`+t%4yifEbzNm~lm*1kfQMAi(g}5ky0xlNA!37+yg&2{keyCIO?JVM(JM z)Fkw%C2THcBqFJFrGA!Vb*HMF|gJuz*4q i;cj$02u3x~381LPh!R#dp!*qEf$%QSlKr6YX8-^{KgS#Z literal 0 HcmV?d00001