From 9cb6896373673dcb74a069a1195b9d9db6d9db1f Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 21 Oct 2024 08:46:03 -0700 Subject: [PATCH] Use `withUnsafeFileSystemRepresentation` to get the path of a URL on disk `URL.path` returns forward slashes in the path on Windows (https://github.com/swiftlang/swift-foundation/issues/973) where we expect backslashes. Work around that by defining our own `filePath` property that is backed by `withUnsafeFileSystemRepresentation`, which produces backslashes. rdar://137963660 --- .../BuildSystemManager.swift | 6 +- .../CompilationDatabase.swift | 2 +- .../DetermineBuildSystem.swift | 6 +- .../ExternalBuildSystemAdapter.swift | 3 +- .../SwiftPMBuildSystem.swift | 12 +-- Sources/Diagnose/DiagnoseCommand.swift | 16 ++-- Sources/Diagnose/ReproducerBundle.swift | 5 +- Sources/Diagnose/RequestInfo.swift | 4 +- .../Diagnose/SourceKitDRequestExecutor.swift | 10 ++- .../InProcessSourceKitLSPClient.swift | 2 +- .../JSONRPCConnection.swift | 2 +- .../SKLogging/SetGlobalLogFileHandler.swift | 8 +- .../SKSupport/DocumentURI+symlinkTarget.swift | 4 +- Sources/SKSupport/FileSystem.swift | 2 +- .../BuildServerTestProject.swift | 10 +-- .../IndexedSingleSwiftFileTestProject.swift | 26 +++---- .../SKTestSupport/MultiFileTestProject.swift | 2 +- Sources/SKTestSupport/SkipUnless.swift | 8 +- .../SwiftPMDependencyProject.swift | 4 +- .../SKTestSupport/SwiftPMTestProject.swift | 22 +++--- .../TestSourceKitLSPClient.swift | 4 +- Sources/SKTestSupport/Utils.swift | 26 ++++--- Sources/SemanticIndex/CheckedIndex.swift | 17 ++-- .../Clang/ClangLanguageService.swift | 4 +- .../IndexStoreDB+MainFilesProvider.swift | 6 +- Sources/SourceKitLSP/SourceKitLSPServer.swift | 2 +- .../SourceKitLSP/Swift/MacroExpansion.swift | 5 +- .../Swift/SyntacticTestIndex.swift | 4 +- Sources/SwiftExtensions/CMakeLists.txt | 3 +- .../FileManagerExtensions.swift | 23 ++++++ Sources/SwiftExtensions/URL+realpath.swift | 42 ---------- Sources/SwiftExtensions/URLExtensions.swift | 78 +++++++++++++++++++ .../BuildServerBuildSystemTests.swift | 14 ++-- .../CompilationDatabaseTests.swift | 22 +++++- .../LegacyBuildServerBuildSystemTests.swift | 6 +- .../SwiftPMBuildSystemTests.swift | 8 +- Tests/DiagnoseTests/DiagnoseTests.swift | 2 +- Tests/SKSupportTests/ProcessRunTests.swift | 3 +- .../BackgroundIndexingTests.swift | 4 +- .../MainFilesProviderTests.swift | 3 +- .../WorkspaceTestDiscoveryTests.swift | 5 +- Tests/SourceKitLSPTests/WorkspaceTests.swift | 3 +- 42 files changed, 270 insertions(+), 168 deletions(-) create mode 100644 Sources/SwiftExtensions/FileManagerExtensions.swift delete mode 100644 Sources/SwiftExtensions/URL+realpath.swift create mode 100644 Sources/SwiftExtensions/URLExtensions.swift diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 0c256c538..982962d1f 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -587,7 +587,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler { logger.error("Toolchain is not a file URL") return nil } - return try AbsolutePath(validating: toolchainUrl.path) + return try AbsolutePath(validating: toolchainUrl.filePath) } if let toolchainPath { if let toolchain = await self.toolchainRegistry.toolchain(withPath: toolchainPath) { @@ -645,10 +645,10 @@ package actor BuildSystemManager: QueueBasedMessageHandler { result.formUnion(targets) } if !filesAndDirectories.directories.isEmpty, - let documentPath = AbsolutePath(validatingOrNil: document.fileURL?.path) + let documentPath = AbsolutePath(validatingOrNil: try? document.fileURL?.filePath) { for (directory, info) in filesAndDirectories.directories { - guard let directoryPath = AbsolutePath(validatingOrNil: directory.fileURL?.path) else { + guard let directoryPath = AbsolutePath(validatingOrNil: try? directory.fileURL?.filePath) else { continue } if documentPath.isDescendant(of: directoryPath) { diff --git a/Sources/BuildSystemIntegration/CompilationDatabase.swift b/Sources/BuildSystemIntegration/CompilationDatabase.swift index 91037ce76..8051ead86 100644 --- a/Sources/BuildSystemIntegration/CompilationDatabase.swift +++ b/Sources/BuildSystemIntegration/CompilationDatabase.swift @@ -264,7 +264,7 @@ package struct JSONCompilationDatabase: CompilationDatabase, Equatable, Codable if let indices = pathToCommands[uri] { return indices.map { commands[$0] } } - if let fileURL = uri.fileURL, let indices = pathToCommands[DocumentURI(fileURL.realpath)] { + if let fileURL = try? uri.fileURL?.realpath, let indices = pathToCommands[DocumentURI(fileURL)] { return indices.map { commands[$0] } } return [] diff --git a/Sources/BuildSystemIntegration/DetermineBuildSystem.swift b/Sources/BuildSystemIntegration/DetermineBuildSystem.swift index 02bb98502..d8cac4e45 100644 --- a/Sources/BuildSystemIntegration/DetermineBuildSystem.swift +++ b/Sources/BuildSystemIntegration/DetermineBuildSystem.swift @@ -14,6 +14,7 @@ package import LanguageServerProtocol import SKLogging package import SKOptions +import SwiftExtensions import ToolchainRegistry import struct TSCBasic.AbsolutePath @@ -21,6 +22,7 @@ import struct TSCBasic.AbsolutePath import LanguageServerProtocol import SKLogging import SKOptions +import SwiftExtensions import ToolchainRegistry import struct TSCBasic.AbsolutePath @@ -42,7 +44,7 @@ package func determineBuildSystem( buildSystemPreference.insert(defaultBuildSystem, at: 0) } guard let workspaceFolderUrl = workspaceFolder.fileURL, - let workspaceFolderPath = try? AbsolutePath(validating: workspaceFolderUrl.path) + let workspaceFolderPath = try? AbsolutePath(validating: workspaceFolderUrl.filePath) else { return nil } @@ -58,7 +60,7 @@ package func determineBuildSystem( } case .swiftPM: if let projectRootURL = SwiftPMBuildSystem.projectRoot(for: workspaceFolderUrl, options: options), - let projectRoot = try? AbsolutePath(validating: projectRootURL.path) + let projectRoot = try? AbsolutePath(validating: projectRootURL.filePath) { return .swiftPM(projectRoot: projectRoot) } diff --git a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift index dc805f3eb..0572de978 100644 --- a/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift @@ -16,6 +16,7 @@ import LanguageServerProtocol import LanguageServerProtocolJSONRPC import SKLogging import SKOptions +import SwiftExtensions import struct TSCBasic.AbsolutePath import func TSCBasic.getEnvSearchPaths @@ -224,7 +225,7 @@ actor ExternalBuildSystemAdapter { .filter { $0.pathExtension == "json" } if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first, - let configFilePath = AbsolutePath(validatingOrNil: configFileURL.path) + let configFilePath = AbsolutePath(validatingOrNil: try? configFileURL.filePath) { return configFilePath } diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 304013d8e..5072d83b9 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -238,14 +238,16 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { private var targetDependencies: [BuildTargetIdentifier: Set] = [:] static package func projectRoot(for path: URL, options: SourceKitLSPOptions) -> URL? { - var path = path.realpath + guard var path = orLog("Getting realpath for project root", { try path.realpath }) else { + return nil + } while true { let packagePath = path.appending(component: "Package.swift") if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false { return path } - if (try? AbsolutePath(validating: path.path))?.isRoot ?? true { + if (try? AbsolutePath(validating: path.filePath))?.isRoot ?? true { break } path.deleteLastPathComponent() @@ -572,7 +574,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { package func sourceKitOptions( request: TextDocumentSourceKitOptionsRequest ) async throws -> TextDocumentSourceKitOptionsResponse? { - guard let url = request.textDocument.uri.fileURL, let path = try? AbsolutePath(validating: url.path) else { + guard let url = request.textDocument.uri.fileURL, let path = try? AbsolutePath(validating: url.filePath) else { // We can't determine build settings for non-file URIs. return nil } @@ -587,7 +589,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { } if !swiftPMTarget.sources.lazy.map(DocumentURI.init).contains(request.textDocument.uri), - let substituteFile = swiftPMTarget.sources.sorted(by: { $0.path < $1.path }).first + let substituteFile = swiftPMTarget.sources.sorted(by: { $0.description < $1.description }).first { logger.info("Getting compiler arguments for \(url) using substitute file \(substituteFile)") // If `url` is not part of the target's source, it's most likely a header file. Fake compiler arguments for it @@ -600,7 +602,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { let buildSettings = FileBuildSettings( compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget), workingDirectory: projectRoot.pathString - ).patching(newFile: DocumentURI(path.asURL.realpath), originalFile: DocumentURI(substituteFile)) + ).patching(newFile: DocumentURI(try path.asURL.realpath), originalFile: DocumentURI(substituteFile)) return TextDocumentSourceKitOptionsResponse( compilerArguments: buildSettings.compilerArguments, workingDirectory: buildSettings.workingDirectory diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift index 47c73e6f6..b4279e1c5 100644 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ b/Sources/Diagnose/DiagnoseCommand.swift @@ -14,6 +14,7 @@ package import ArgumentParser import Foundation import ToolchainRegistry +import SwiftExtensions import struct TSCBasic.AbsolutePath import class TSCBasic.Process @@ -22,6 +23,7 @@ import class TSCUtility.PercentProgressAnimation import ArgumentParser import Foundation import ToolchainRegistry +import SwiftExtensions import struct TSCBasic.AbsolutePath import class TSCBasic.Process @@ -172,7 +174,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { .deletingLastPathComponent() .deletingLastPathComponent() - guard let toolchain = try Toolchain(AbsolutePath(validating: toolchainPath.path)), + guard let toolchain = try Toolchain(AbsolutePath(validating: toolchainPath.filePath)), let sourcekitd = toolchain.sourcekitd else { continue @@ -223,7 +225,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { #if os(macOS) reportProgress(.collectingLogMessages(progress: 0), message: "Collecting log messages") let outputFileUrl = bundlePath.appendingPathComponent("log.txt") - guard FileManager.default.createFile(atPath: outputFileUrl.path, contents: nil) else { + guard FileManager.default.createFile(atPath: try outputFileUrl.filePath, contents: nil) else { throw GenericError("Failed to create log.txt") } let fileHandle = try FileHandle(forWritingTo: outputFileUrl) @@ -316,7 +318,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { @MainActor private func addSwiftVersion(toBundle bundlePath: URL) async throws { let outputFileUrl = bundlePath.appendingPathComponent("swift-versions.txt") - guard FileManager.default.createFile(atPath: outputFileUrl.path, contents: nil) else { + guard FileManager.default.createFile(atPath: try outputFileUrl.filePath, contents: nil) else { throw GenericError("Failed to create file at \(outputFileUrl)") } let fileHandle = try FileHandle(forWritingTo: outputFileUrl) @@ -333,9 +335,9 @@ package struct DiagnoseCommand: AsyncParsableCommand { continue } - try fileHandle.write(contentsOf: "\(swiftUrl.path) --version\n".data(using: .utf8)!) + try fileHandle.write(contentsOf: "\(swiftUrl.filePath) --version\n".data(using: .utf8)!) let process = Process( - arguments: [swiftUrl.path, "--version"], + arguments: [try swiftUrl.filePath, "--version"], outputRedirection: .stream( stdout: { try? fileHandle.write(contentsOf: $0) }, stderr: { _ in } @@ -417,7 +419,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { Bundle created. When filing an issue at https://github.com/swiftlang/sourcekit-lsp/issues/new, please attach the bundle located at - \(bundlePath.path) + \(try bundlePath.filePath) """ ) @@ -428,7 +430,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { // is responsible for showing the diagnose bundle location to the user if self.bundleOutputPath == nil { do { - _ = try await Process.run(arguments: ["open", "-R", bundlePath.path], workingDirectory: nil) + _ = try await Process.run(arguments: ["open", "-R", bundlePath.filePath], workingDirectory: nil) } catch { // If revealing the bundle in Finder should fail, we don't care. We still printed the bundle path to stdout. } diff --git a/Sources/Diagnose/ReproducerBundle.swift b/Sources/Diagnose/ReproducerBundle.swift index a17c8758c..d463d7d20 100644 --- a/Sources/Diagnose/ReproducerBundle.swift +++ b/Sources/Diagnose/ReproducerBundle.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftExtensions import ToolchainRegistry /// Create a folder that contains all files that should be necessary to reproduce a sourcekitd crash. @@ -26,7 +27,7 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu encoding: .utf8 ) if let toolchainPath = toolchain.path { - try toolchainPath.asURL.realpath.path + try toolchainPath.asURL.realpath.filePath .write( to: bundlePath.appendingPathComponent("toolchain.txt"), atomically: true, @@ -59,7 +60,7 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu // aren't user specific and would bloat the reproducer bundle. continue } - let dest = URL(fileURLWithPath: bundlePath.path + path) + let dest = URL(fileURLWithPath: try bundlePath.filePath + path) try? FileManager.default.createDirectory(at: dest.deletingLastPathComponent(), withIntermediateDirectories: true) try? FileManager.default.copyItem(at: URL(fileURLWithPath: String(path)), to: dest) } diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift index e5d713f9f..17866942c 100644 --- a/Sources/Diagnose/RequestInfo.swift +++ b/Sources/Diagnose/RequestInfo.swift @@ -13,9 +13,11 @@ #if compiler(>=6) package import Foundation import RegexBuilder +import SwiftExtensions #else import Foundation import RegexBuilder +import SwiftExtensions #endif /// All the information necessary to replay a sourcektid request. @@ -48,7 +50,7 @@ package struct RequestInfo: Sendable { requestTemplate .replacingOccurrences(of: "$OFFSET", with: String(offset)) .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) - .replacingOccurrences(of: "$FILE", with: file.path) + .replacingOccurrences(of: "$FILE", with: try file.filePath) } /// A fake value that is used to indicate that we are reducing a `swift-frontend` issue instead of a sourcekitd issue. diff --git a/Sources/Diagnose/SourceKitDRequestExecutor.swift b/Sources/Diagnose/SourceKitDRequestExecutor.swift index 0f717caa3..3e9604937 100644 --- a/Sources/Diagnose/SourceKitDRequestExecutor.swift +++ b/Sources/Diagnose/SourceKitDRequestExecutor.swift @@ -13,6 +13,7 @@ #if compiler(>=6) package import Foundation import SourceKitD +import SwiftExtensions import struct TSCBasic.AbsolutePath import class TSCBasic.Process @@ -20,6 +21,7 @@ import struct TSCBasic.ProcessResult #else import Foundation import SourceKitD +import SwiftExtensions import struct TSCBasic.AbsolutePath import class TSCBasic.Process @@ -147,9 +149,9 @@ package class OutOfProcessSourceKitRequestExecutor: SourceKitRequestExecutor { package func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult { try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let arguments = request.compilerArgs.replacing(["$FILE"], with: [temporarySourceFile.path]) + let arguments = request.compilerArgs.replacing(["$FILE"], with: [try temporarySourceFile.filePath]) - let process = Process(arguments: [swiftFrontend.path] + arguments) + let process = Process(arguments: [try swiftFrontend.filePath] + arguments) try process.launch() let result = try await process.waitUntilExit() @@ -167,9 +169,9 @@ package class OutOfProcessSourceKitRequestExecutor: SourceKitRequestExecutor { "debug", "run-sourcekitd-request", "--sourcekitd", - sourcekitd.path, + try sourcekitd.filePath, "--request-file", - temporaryRequestFile.path, + try temporaryRequestFile.filePath, ] ) try process.launch() diff --git a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift b/Sources/InProcessClient/InProcessSourceKitLSPClient.swift index f1bba4b35..561a1fc95 100644 --- a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift +++ b/Sources/InProcessClient/InProcessSourceKitLSPClient.swift @@ -68,7 +68,7 @@ public final class InProcessSourceKitLSPClient: Sendable { let serverToClientConnection = LocalConnection(receiverName: "client") self.server = SourceKitLSPServer( client: serverToClientConnection, - toolchainRegistry: ToolchainRegistry(installPath: AbsolutePath(validatingOrNil: toolchainPath?.path)), + toolchainRegistry: ToolchainRegistry(installPath: AbsolutePath(validatingOrNil: try? toolchainPath?.filePath)), options: options, testHooks: TestHooks(), onExit: { diff --git a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift index ace790dcf..d9edb2621 100644 --- a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift +++ b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift @@ -219,7 +219,7 @@ public final class JSONRPCConnection: Connection { } let process = Foundation.Process() logger.log( - "Launching build server at \(executable.path) with options [\(arguments.joined(separator: " "))]" + "Launching build server at \(executable.description) with options [\(arguments.joined(separator: " "))]" ) process.executableURL = executable process.arguments = arguments diff --git a/Sources/SKLogging/SetGlobalLogFileHandler.swift b/Sources/SKLogging/SetGlobalLogFileHandler.swift index d23dce482..1e3f5b202 100644 --- a/Sources/SKLogging/SetGlobalLogFileHandler.swift +++ b/Sources/SKLogging/SetGlobalLogFileHandler.swift @@ -65,8 +65,8 @@ func getOrCreateLogFileHandle(logDirectory: URL, logRotateCount: Int) -> FileHan do { try FileManager.default.createDirectory(at: logDirectory, withIntermediateDirectories: true) - if !FileManager.default.fileExists(atPath: logFileUrl.path) { - guard FileManager.default.createFile(atPath: logFileUrl.path, contents: nil) else { + if !FileManager.default.fileExists(at: logFileUrl) { + guard FileManager.default.createFile(atPath: try logFileUrl.filePath, contents: nil) else { throw FailedToCreateFileError(logFile: logFileUrl) } } @@ -79,7 +79,7 @@ func getOrCreateLogFileHandle(logDirectory: URL, logRotateCount: Int) -> FileHan // We will try creating a log file again once this section of the log reaches `maxLogFileSize` but that means that // we'll only log this error every `maxLogFileSize` bytes, which is a lot less spammy than logging it on every log // call. - fputs("Failed to open file handle for log file at \(logFileUrl.path): \(error)", stderr) + fputs("Failed to open file handle for log file at \(logFileUrl): \(error)", stderr) logFileHandle = FileHandle.standardError return FileHandle.standardError } @@ -176,7 +176,7 @@ private func cleanOldLogFilesImpl(logFileDirectory: URL, maxAge: TimeInterval) { guard let modificationDate = orLog( "Getting mtime of old log file", - { try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] } + { try FileManager.default.attributesOfItem(atPath: url.filePath)[.modificationDate] } ) as? Date, Date().timeIntervalSince(modificationDate) > maxAge else { diff --git a/Sources/SKSupport/DocumentURI+symlinkTarget.swift b/Sources/SKSupport/DocumentURI+symlinkTarget.swift index 34a15df6b..7635cd1dd 100644 --- a/Sources/SKSupport/DocumentURI+symlinkTarget.swift +++ b/Sources/SKSupport/DocumentURI+symlinkTarget.swift @@ -27,7 +27,9 @@ extension DocumentURI { guard let fileUrl = fileURL else { return nil } - let realpath = DocumentURI(fileUrl.realpath) + guard let realpath = try? DocumentURI(fileUrl.realpath) else { + return nil + } if realpath == self { return nil } diff --git a/Sources/SKSupport/FileSystem.swift b/Sources/SKSupport/FileSystem.swift index b0d3f692d..68a5ee2eb 100644 --- a/Sources/SKSupport/FileSystem.swift +++ b/Sources/SKSupport/FileSystem.swift @@ -27,7 +27,7 @@ extension AbsolutePath { package init(expandingTilde path: String) throws { if path.first == "~" { try self.init( - AbsolutePath(validating: FileManager.default.homeDirectoryForCurrentUser.path), + AbsolutePath(validating: FileManager.default.homeDirectoryForCurrentUser.filePath), validating: String(path.dropFirst(2)) ) } else { diff --git a/Sources/SKTestSupport/BuildServerTestProject.swift b/Sources/SKTestSupport/BuildServerTestProject.swift index 53389f965..2302e73f4 100644 --- a/Sources/SKTestSupport/BuildServerTestProject.swift +++ b/Sources/SKTestSupport/BuildServerTestProject.swift @@ -16,7 +16,7 @@ import XCTest fileprivate let sdkArgs = if let defaultSDKPath { """ - "-sdk", "\(defaultSDKPath.replacing(#"\"#, with: #"\\"#))", + "-sdk", r"\(defaultSDKPath)", """ } else { "" @@ -30,7 +30,7 @@ private let skTestSupportInputsDirectory: URL = { .appendingPathComponent("SourceKitLSP_SKTestSupport.bundle") .appendingPathComponent("Contents") .appendingPathComponent("Resources") - if !FileManager.default.fileExists(atPath: resources.path) { + if !FileManager.default.fileExists(at: resources) { // Xcode and command-line swiftpm differ about the path. resources.deleteLastPathComponent() resources.deleteLastPathComponent() @@ -40,8 +40,8 @@ private let skTestSupportInputsDirectory: URL = { productsDirectory .appendingPathComponent("SourceKitLSP_SKTestSupport.resources") #endif - guard FileManager.default.fileExists(atPath: resources.path) else { - fatalError("missing resources \(resources.path)") + guard FileManager.default.fileExists(at: resources) else { + fatalError("missing resources \(resources)") } return resources.appendingPathComponent("INPUTS", isDirectory: true).standardizedFileURL }() @@ -73,7 +73,7 @@ package class BuildServerTestProject: MultiFileTestProject { import sys from typing import Dict, List, Optional - sys.path.append("\(skTestSupportInputsDirectory.path)") + sys.path.append(r"\(try skTestSupportInputsDirectory.filePath)") from AbstractBuildServer import AbstractBuildServer, LegacyBuildServer diff --git a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift index 2f1291b01..d052a0be0 100644 --- a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift +++ b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift @@ -69,13 +69,13 @@ package struct IndexedSingleSwiftFileTestProject { try extractMarkers(markedText).textWithoutMarkers.write(to: testFileURL, atomically: false, encoding: .utf8) var compilerArguments: [String] = [ - testFileURL.path, - "-index-store-path", indexURL.path, + try testFileURL.filePath, + "-index-store-path", try indexURL.filePath, "-typecheck", ] - if let globalModuleCache { + if let globalModuleCache = try globalModuleCache { compilerArguments += [ - "-module-cache-path", globalModuleCache.path, + "-module-cache-path", try globalModuleCache.filePath, ] } if !indexSystemModules { @@ -98,7 +98,7 @@ package struct IndexedSingleSwiftFileTestProject { .appendingPathComponent("lib") .appendingPathComponent("swift") .appendingPathComponent("windows") - compilerArguments += ["-I", xctestModuleDir.path] + compilerArguments += ["-I", try xctestModuleDir.filePath] #else let usrLibDir = sdkUrl @@ -113,8 +113,8 @@ package struct IndexedSingleSwiftFileTestProject { .appendingPathComponent("Library") .appendingPathComponent("Frameworks") compilerArguments += [ - "-I", usrLibDir.path, - "-F", frameworksDir.path, + "-I", try usrLibDir.filePath, + "-F", try frameworksDir.filePath, ] #endif } @@ -122,9 +122,9 @@ package struct IndexedSingleSwiftFileTestProject { let compilationDatabase = JSONCompilationDatabase( [ JSONCompilationDatabase.Command( - directory: testWorkspaceDirectory.path, - filename: testFileURL.path, - commandLine: [swiftc.path] + compilerArguments + directory: try testWorkspaceDirectory.filePath, + filename: try testFileURL.filePath, + commandLine: [try swiftc.filePath] + compilerArguments ) ] ) @@ -136,7 +136,7 @@ package struct IndexedSingleSwiftFileTestProject { // Run swiftc to build the index store do { - try await Process.checkNonZeroExit(arguments: [swiftc.path] + compilerArguments) + try await Process.checkNonZeroExit(arguments: [swiftc.filePath] + compilerArguments) } catch { if !allowBuildFailure { throw error @@ -146,8 +146,8 @@ package struct IndexedSingleSwiftFileTestProject { // Create the test client var options = SourceKitLSPOptions.testDefault() options.indexOrDefault = SourceKitLSPOptions.IndexOptions( - indexStorePath: indexURL.path, - indexDatabasePath: indexDBURL.path + indexStorePath: try indexURL.filePath, + indexDatabasePath: try indexDBURL.filePath ) self.testClient = try await TestSourceKitLSPClient( options: options, diff --git a/Sources/SKTestSupport/MultiFileTestProject.swift b/Sources/SKTestSupport/MultiFileTestProject.swift index 93bcadcaa..09a71815d 100644 --- a/Sources/SKTestSupport/MultiFileTestProject.swift +++ b/Sources/SKTestSupport/MultiFileTestProject.swift @@ -105,7 +105,7 @@ package class MultiFileTestProject { let markedText = markedText .replacingOccurrences(of: "$TEST_DIR_URL", with: scratchDirectory.absoluteString) - .replacingOccurrences(of: "$TEST_DIR", with: scratchDirectory.path) + .replacingOccurrences(of: "$TEST_DIR", with: try scratchDirectory.filePath) let fileURL = fileLocation.url(relativeTo: scratchDirectory) try FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index 39d22d594..f5910c0df 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -220,7 +220,7 @@ package actor SkipUnless { .appendingPathComponent("debug") .appendingPathComponent("Modules") .appendingPathComponent("MyLibrary.swiftmodule") - return FileManager.default.fileExists(atPath: modulesDirectory.path) + return FileManager.default.fileExists(at: modulesDirectory) } } @@ -390,10 +390,10 @@ package actor SkipUnless { // of Lib when building Lib. for target in ["MyPlugin", "Lib"] { var arguments = [ - swift.pathString, "build", "--package-path", project.scratchDirectory.path, "--target", target, + swift.pathString, "build", "--package-path", try project.scratchDirectory.filePath, "--target", target, ] - if let globalModuleCache { - arguments += ["-Xswiftc", "-module-cache-path", "-Xswiftc", globalModuleCache.path] + if let globalModuleCache = try globalModuleCache { + arguments += ["-Xswiftc", "-module-cache-path", "-Xswiftc", try globalModuleCache.filePath] } try await Process.run(arguments: arguments, workingDirectory: nil) } diff --git a/Sources/SKTestSupport/SwiftPMDependencyProject.swift b/Sources/SKTestSupport/SwiftPMDependencyProject.swift index d5b62949f..cb9b69859 100644 --- a/Sources/SKTestSupport/SwiftPMDependencyProject.swift +++ b/Sources/SKTestSupport/SwiftPMDependencyProject.swift @@ -56,7 +56,7 @@ package class SwiftPMDependencyProject { // We can't use `workingDirectory` because Amazon Linux doesn't support working directories (or at least // TSCBasic.Process doesn't support working directories on Amazon Linux) let process = TSCBasic.Process( - arguments: [git.path, "-C", workingDirectory.path] + arguments + arguments: [try git.filePath, "-C", try workingDirectory.filePath] + arguments ) try process.launch() let processResult = try await process.waitUntilExit() @@ -102,7 +102,7 @@ package class SwiftPMDependencyProject { package func tag(changedFiles: [URL], version: String) async throws { try await runGitCommand( - ["add"] + changedFiles.map(\.path), + ["add"] + changedFiles.map { try $0.filePath }, workingDirectory: packageDirectory ) try await runGitCommand( diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift index a081013e6..fc2ca03ce 100644 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ b/Sources/SKTestSupport/SwiftPMTestProject.swift @@ -80,7 +80,7 @@ package class SwiftPMTestProject: MultiFileTestProject { .appendingPathComponent("include") .appendingPathComponent("module.modulemap") } - .first { FileManager.default.fileExists(atPath: $0.path) } + .first { FileManager.default.fileExists(at: $0) } guard let swiftSyntaxCShimsModulemap else { throw SwiftSyntaxCShimsModulemapNotFoundError() @@ -106,7 +106,7 @@ package class SwiftPMTestProject: MultiFileTestProject { let enumerator = FileManager.default.enumerator(at: dir, includingPropertiesForKeys: nil) while let file = enumerator?.nextObject() as? URL { if file.pathExtension == "o" { - objectFiles.append(file.path) + objectFiles.append(try file.filePath) } } } @@ -121,9 +121,9 @@ package class SwiftPMTestProject: MultiFileTestProject { if let toolchainVersion = try await ToolchainRegistry.forTesting.default?.swiftVersion, toolchainVersion < SwiftVersion(6, 0) { - moduleSearchPath = productsDirectory.path + moduleSearchPath = try productsDirectory.filePath } else { - moduleSearchPath = "\(productsDirectory.path)/Modules" + moduleSearchPath = "\(try productsDirectory.filePath)/Modules" } return """ @@ -140,7 +140,7 @@ package class SwiftPMTestProject: MultiFileTestProject { name: "MyMacros", swiftSettings: [.unsafeFlags([ "-I", "\(moduleSearchPath)", - "-Xcc", "-fmodule-map-file=\(swiftSyntaxCShimsModulemap.path)" + "-Xcc", "-fmodule-map-file=\(try swiftSyntaxCShimsModulemap.filePath)" ])], linkerSettings: [ .unsafeFlags([ @@ -233,16 +233,16 @@ package class SwiftPMTestProject: MultiFileTestProject { throw Error.swiftNotFound } var arguments = [ - swift.path, + try swift.filePath, "build", - "--package-path", path.path, + "--package-path", try path.filePath, "--build-tests", "-Xswiftc", "-index-ignore-system-modules", "-Xcc", "-index-ignore-system-symbols", ] - if let globalModuleCache { + if let globalModuleCache = try globalModuleCache { arguments += [ - "-Xswiftc", "-module-cache-path", "-Xswiftc", globalModuleCache.path, + "-Xswiftc", "-module-cache-path", "-Xswiftc", try globalModuleCache.filePath, ] } try await Process.checkNonZeroExit(arguments: arguments) @@ -254,10 +254,10 @@ package class SwiftPMTestProject: MultiFileTestProject { throw Error.swiftNotFound } let arguments = [ - swift.path, + try swift.filePath, "package", "resolve", - "--package-path", path.path, + "--package-path", try path.filePath, ] try await Process.checkNonZeroExit(arguments: arguments) } diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index 2072cb41e..935247846 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -125,9 +125,9 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { cleanUp: @Sendable @escaping () -> Void = {} ) async throws { var options = options - if let globalModuleCache { + if let globalModuleCache = try globalModuleCache { options.swiftPMOrDefault.swiftCompilerFlags = - (options.swiftPMOrDefault.swiftCompilerFlags ?? []) + ["-module-cache-path", globalModuleCache.path] + (options.swiftPMOrDefault.swiftCompilerFlags ?? []) + ["-module-cache-path", try globalModuleCache.filePath] } options.backgroundIndexing = enableBackgroundIndexing if options.sourcekitdRequestTimeout == nil { diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift index f223d28b8..d7ccc47eb 100644 --- a/Sources/SKTestSupport/Utils.swift +++ b/Sources/SKTestSupport/Utils.swift @@ -71,11 +71,11 @@ package func testScratchDir(testName: String = #function) throws -> URL { // Including the test name in the directory frequently makes path lengths of test files exceed the maximum path length // on Windows. Choose shorter directory names on that platform to avoid that issue. #if os(Windows) - let url = FileManager.default.temporaryDirectory.realpath + let url = try FileManager.default.temporaryDirectory.realpath .appendingPathComponent("lsp-test") .appendingPathComponent("\(uuid)") #else - let url = FileManager.default.temporaryDirectory.realpath + let url = try FileManager.default.temporaryDirectory.realpath .appendingPathComponent("sourcekit-lsp-test-scratch") .appendingPathComponent("\(testBaseName)-\(uuid)") #endif @@ -100,17 +100,19 @@ package func withTestScratchDir( try? FileManager.default.removeItem(at: scratchDirectory) } } - return try await body(try AbsolutePath(validating: scratchDirectory.path)) + return try await body(try AbsolutePath(validating: scratchDirectory.filePath)) } -let globalModuleCache: URL? = { - if let customModuleCache = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_TEST_MODULE_CACHE"] { - if customModuleCache.isEmpty { - return nil +var globalModuleCache: URL? { + get throws { + if let customModuleCache = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_TEST_MODULE_CACHE"] { + if customModuleCache.isEmpty { + return nil + } + return URL(fileURLWithPath: customModuleCache) } - return URL(fileURLWithPath: customModuleCache) + return try FileManager.default.temporaryDirectory.realpath + .appendingPathComponent("sourcekit-lsp-test-scratch") + .appendingPathComponent("shared-module-cache") } - return FileManager.default.temporaryDirectory.realpath - .appendingPathComponent("sourcekit-lsp-test-scratch") - .appendingPathComponent("shared-module-cache") -}() +} diff --git a/Sources/SemanticIndex/CheckedIndex.swift b/Sources/SemanticIndex/CheckedIndex.swift index 89e0d040e..417487c36 100644 --- a/Sources/SemanticIndex/CheckedIndex.swift +++ b/Sources/SemanticIndex/CheckedIndex.swift @@ -15,11 +15,13 @@ import Foundation @preconcurrency package import IndexStoreDB package import LanguageServerProtocol import SKLogging +import SwiftExtensions #else import Foundation @preconcurrency import IndexStoreDB import LanguageServerProtocol import SKLogging +import SwiftExtensions #endif /// Essentially a `DocumentManager` from the `SourceKitLSP` module. @@ -276,8 +278,9 @@ private struct IndexOutOfDateChecker { // If there are no in-memory modifications check if there are on-disk modifications. fallthrough case .modifiedFiles: - guard let fileURL = (mainFile ?? filePath).fileURL, - let lastUnitDate = index.dateOfLatestUnitFor(filePath: fileURL.path) + guard + let filePathStr = orLog("Realpath for up-to-date", { try (mainFile ?? filePath).fileURL?.realpath.filePath }), + let lastUnitDate = index.dateOfLatestUnitFor(filePath: filePathStr) else { return false } @@ -346,16 +349,18 @@ private struct IndexOutOfDateChecker { guard var fileURL = uri.fileURL else { return .fileDoesNotExist } - var modificationDate = try Self.modificationDate(atPath: fileURL.path) + var modificationDate = try Self.modificationDate(atPath: fileURL.filePath) // Get the maximum mtime in the symlink chain as the modification date of the URI. That way if either the symlink // is changed to point to a different file or if the underlying file is modified, the modification time is // updated. - while let relativeSymlinkDestination = try? FileManager.default.destinationOfSymbolicLink(atPath: fileURL.path), + while let relativeSymlinkDestination = try? FileManager.default.destinationOfSymbolicLink( + atPath: fileURL.filePath + ), let symlinkDestination = URL(string: relativeSymlinkDestination, relativeTo: fileURL) { fileURL = symlinkDestination - modificationDate = max(modificationDate, try Self.modificationDate(atPath: fileURL.path)) + modificationDate = max(modificationDate, try Self.modificationDate(atPath: fileURL.filePath)) } return .date(modificationDate) @@ -379,7 +384,7 @@ private struct IndexOutOfDateChecker { } let fileExists = if let fileUrl = uri.fileURL { - FileManager.default.fileExists(atPath: fileUrl.path) + FileManager.default.fileExists(at: fileUrl) } else { false } diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift index 8c0cb94a4..e19287961 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift @@ -456,14 +456,14 @@ extension ClangLanguageService { // The compile command changed, send over the new one. if let compileCommand = clangBuildSettings?.compileCommand, - let pathString = (try? AbsolutePath(validating: url.path))?.pathString + let pathString = AbsolutePath(validatingOrNil: try? url.filePath)?.pathString { let notification = DidChangeConfigurationNotification( settings: .clangd(ClangWorkspaceSettings(compilationDatabaseChanges: [pathString: compileCommand])) ) clangd.send(notification) } else { - logger.error("No longer have build settings for \(url.path) but can't send null build settings to clangd") + logger.error("No longer have build settings for \(url.description) but can't send null build settings to clangd") } } diff --git a/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift b/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift index f9c323905..3d346f2a3 100644 --- a/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift +++ b/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift @@ -16,19 +16,21 @@ import Foundation package import LanguageServerProtocol import SKLogging import SemanticIndex +import SwiftExtensions #else import BuildSystemIntegration import Foundation import LanguageServerProtocol import SKLogging import SemanticIndex +import SwiftExtensions #endif extension UncheckedIndex { package func mainFilesContainingFile(_ uri: DocumentURI) -> Set { let mainFiles: Set - if let url = uri.fileURL { - let mainFilePaths = Set(self.underlyingIndexStoreDB.mainFilesContainingFile(path: url.path)) + if let filePath = orLog("File path to get main files", { try uri.fileURL?.filePath }) { + let mainFilePaths = Set(self.underlyingIndexStoreDB.mainFilesContainingFile(path: filePath)) mainFiles = Set( mainFilePaths .filter { FileManager.default.fileExists(atPath: $0) } diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 09e6d7c3c..c8a785d72 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -2149,7 +2149,7 @@ extension SourceKitLSPServer { } // Add the file name and line to the detail string if let url = location.uri.fileURL, - let basename = (try? AbsolutePath(validating: url.path))?.basename + let basename = (try? AbsolutePath(validating: url.filePath))?.basename { detail = "Extension at \(basename):\(location.range.lowerBound.line + 1)" } else if let moduleName = moduleName { diff --git a/Sources/SourceKitLSP/Swift/MacroExpansion.swift b/Sources/SourceKitLSP/Swift/MacroExpansion.swift index 6ff5c9cac..bfa91c31a 100644 --- a/Sources/SourceKitLSP/Swift/MacroExpansion.swift +++ b/Sources/SourceKitLSP/Swift/MacroExpansion.swift @@ -17,6 +17,7 @@ import SKLogging import SKOptions import SKSupport import SourceKitD +import SwiftExtensions /// Caches the contents of macro expansions that were recently requested by the user. actor MacroExpansionManager { @@ -276,7 +277,7 @@ extension SwiftLanguageService { ) } catch { throw ResponseError.unknown( - "Failed to create directory for complete macro expansion at path: \(completeExpansionFilePath.path)" + "Failed to create directory for complete macro expansion at \(completeExpansionFilePath.description)" ) } @@ -286,7 +287,7 @@ extension SwiftLanguageService { try completeExpansionFileContent.write(to: completeExpansionFilePath, atomically: true, encoding: .utf8) } catch { throw ResponseError.unknown( - "Unable to write complete macro expansion to file path: \"\(completeExpansionFilePath.path)\"" + "Unable to write complete macro expansion to \"\(completeExpansionFilePath.description)\"" ) } diff --git a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift index 8b865c923..1e25666f6 100644 --- a/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift +++ b/Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift @@ -179,13 +179,13 @@ actor SyntacticTestIndex { guard !removedFiles.contains(uri) else { return } - guard FileManager.default.fileExists(atPath: url.path) else { + guard FileManager.default.fileExists(at: url) else { // File no longer exists. Probably deleted since we scheduled it for indexing. Nothing to worry about. logger.info("Not indexing \(uri.forLogging) for tests because it does not exist") return } guard - let fileModificationDate = try? FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] + let fileModificationDate = try? FileManager.default.attributesOfItem(atPath: url.filePath)[.modificationDate] as? Date else { logger.fault("Not indexing \(uri.forLogging) for tests because the modification date could not be determined") diff --git a/Sources/SwiftExtensions/CMakeLists.txt b/Sources/SwiftExtensions/CMakeLists.txt index ae50f20b6..65200a3f7 100644 --- a/Sources/SwiftExtensions/CMakeLists.txt +++ b/Sources/SwiftExtensions/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(SwiftExtensions STATIC CartesianProduct.swift Collection+Only.swift Collection+PartitionIntoBatches.swift + FileManagerExtensions.swift NSLock+WithLock.swift PipeAsStringHandler.swift ResultExtensions.swift @@ -16,7 +17,7 @@ add_library(SwiftExtensions STATIC Task+WithPriorityChangedHandler.swift ThreadSafeBox.swift TransitiveClosure.swift - URL+realpath.swift + URLExtensions.swift ) set_target_properties(SwiftExtensions PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SwiftExtensions/FileManagerExtensions.swift b/Sources/SwiftExtensions/FileManagerExtensions.swift new file mode 100644 index 000000000..8b8f15851 --- /dev/null +++ b/Sources/SwiftExtensions/FileManagerExtensions.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +package import Foundation + +extension FileManager { + /// Same as `fileExists(atPath:)` but takes a `URL` instead of a `String`. + package func fileExists(at url: URL) -> Bool { + guard let filePath = try? url.filePath else { + return false + } + return self.fileExists(atPath: filePath) + } +} diff --git a/Sources/SwiftExtensions/URL+realpath.swift b/Sources/SwiftExtensions/URL+realpath.swift deleted file mode 100644 index aa755a79a..000000000 --- a/Sources/SwiftExtensions/URL+realpath.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 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 -// -//===----------------------------------------------------------------------===// - -#if compiler(>=6) -package import Foundation -#else -import Foundation -#endif - -extension URL { - /// Assuming this is a file URL, resolves all symlinks in the path. - /// - /// - Note: We need this because `URL.resolvingSymlinksInPath()` not only resolves symlinks but also standardizes the - /// path by stripping away `private` prefixes. Since sourcekitd is not performing this standardization, using - /// `resolvingSymlinksInPath` can lead to slightly mismatched URLs between the sourcekit-lsp response and the test - /// assertion. - package var realpath: URL { - #if canImport(Darwin) - return self.path.withCString { path in - guard let realpath = Darwin.realpath(path, nil) else { - return self - } - let result = URL(fileURLWithPath: String(cString: realpath)) - free(realpath) - return result - } - #else - // Non-Darwin platforms don't have the `/private` stripping issue, so we can just use `self.resolvingSymlinksInPath` - // here. - return self.resolvingSymlinksInPath() - #endif - } -} diff --git a/Sources/SwiftExtensions/URLExtensions.swift b/Sources/SwiftExtensions/URLExtensions.swift new file mode 100644 index 000000000..242c240d6 --- /dev/null +++ b/Sources/SwiftExtensions/URLExtensions.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6) +package import Foundation +#else +import Foundation +#endif + +enum FilePathError: Error, CustomStringConvertible { + case noFileSystemRepresentation(URL) + case noFileURL(URL) + + var description: String { + switch self { + case .noFileSystemRepresentation(let url): + return "\(url.description) cannot be represented as a file system path" + case .noFileURL(let url): + return "\(url.description) is not a file URL" + } + } +} + +extension URL { + /// Assuming this is a file URL, resolves all symlinks in the path. + /// + /// - Note: We need this because `URL.resolvingSymlinksInPath()` not only resolves symlinks but also standardizes the + /// path by stripping away `private` prefixes. Since sourcekitd is not performing this standardization, using + /// `resolvingSymlinksInPath` can lead to slightly mismatched URLs between the sourcekit-lsp response and the test + /// assertion. + package var realpath: URL { + get throws { + #if canImport(Darwin) + return try self.filePath.withCString { path in + guard let realpath = Darwin.realpath(path, nil) else { + return self + } + let result = URL(fileURLWithPath: String(cString: realpath)) + free(realpath) + return result + } + #else + // Non-Darwin platforms don't have the `/private` stripping issue, so we can just use `self.resolvingSymlinksInPath` + // here. + return self.resolvingSymlinksInPath() + #endif + } + } + + package var filePath: String { + get throws { + guard self.isFileURL else { + throw FilePathError.noFileURL(self) + } + return try self.withUnsafeFileSystemRepresentation { buffer in + guard let buffer else { + throw FilePathError.noFileSystemRepresentation(self) + } + return String(cString: buffer) + } + } + } + + /// Deprecate `path` to encourage using `filePath`, at least if `SwiftExtensions` is imported. + @available(*, deprecated, message: "Use filePath instead", renamed: "filePath") + package var path: String { + return try! filePath + } +} diff --git a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift index 492c9f7a1..cc7073ac1 100644 --- a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift @@ -60,7 +60,7 @@ final class BuildServerBuildSystemTests: XCTestCase { def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: return { - "compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] } """ ) @@ -131,7 +131,7 @@ final class BuildServerBuildSystemTests: XCTestCase { def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: assert self.timer_has_fired return { - "compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] } """ ) @@ -205,12 +205,12 @@ final class BuildServerBuildSystemTests: XCTestCase { def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: if self.timer_has_fired: return { - "compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] } else: threading.Timer(1, self.timer_fired).start() return { - "compilerArguments": ["$TEST_DIR/Test.swift", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", $SDK_ARGS] } """ ) @@ -280,10 +280,10 @@ final class BuildServerBuildSystemTests: XCTestCase { } def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: - if os.path.exists("$TEST_DIR/should_crash"): + if os.path.exists(r"$TEST_DIR/should_crash"): assert False return { - "compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] } """ ) @@ -347,7 +347,7 @@ final class BuildServerBuildSystemTests: XCTestCase { def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: return { - "compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "compilerArguments": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] } """ ) diff --git a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift index c727be4b9..fe1cdf08d 100644 --- a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift +++ b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift @@ -332,8 +332,11 @@ final class CompilationDatabaseTests: XCTestCase { ] """ ) { buildSystem in - assertEqual(URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").path, "/b") - assertEqual(URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") + assertEqual(try URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").filePath, "\(pathSeparator)b") + assertEqual( + try URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").filePath, + "\(pathSeparator)IndexDatabase" + ) } } @@ -417,12 +420,23 @@ final class CompilationDatabaseTests: XCTestCase { ] """ ) { buildSystem in - assertEqual(URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").path, "/b") - assertEqual(URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") + assertEqual(try URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").filePath, "\(pathSeparator)b") + assertEqual( + try URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").filePath, + "\(pathSeparator)IndexDatabase" + ) } } } +fileprivate var pathSeparator: String { +#if os(Windows) + return #"\"# +#else + return "/" +#endif +} + private func checkCompilationDatabaseBuildSystem( _ compdb: ByteString, block: (CompilationDatabaseBuildSystem) async throws -> () diff --git a/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift index 14ab45437..445679163 100644 --- a/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift @@ -40,7 +40,7 @@ final class LegacyBuildServerBuildSystemTests: XCTestCase { { "uri": notification["uri"], "updatedOptions": { - "options": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] + "options": [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS] }, }, ) @@ -72,14 +72,14 @@ final class LegacyBuildServerBuildSystemTests: XCTestCase { class BuildServer(LegacyBuildServer): def send_delayed_options_changed(self, uri: str): - self.send_sourcekit_options_changed(uri, ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS]) + self.send_sourcekit_options_changed(uri, [r"$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS]) def register_for_changes(self, notification: Dict[str, object]): if notification["action"] != "register": return self.send_sourcekit_options_changed( notification["uri"], - ["$TEST_DIR/Test.swift", $SDK_ARGS] + [r"$TEST_DIR/Test.swift", $SDK_ARGS] ) threading.Timer(1, self.send_delayed_options_changed, [notification["uri"]]).start() """ diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index e14319a49..fdeada08a 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -169,7 +169,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { """, ] ) - let packageRoot = try AbsolutePath(validating: tempDir.appending(component: "pkg").asURL.realpath.path) + let packageRoot = try AbsolutePath(validating: tempDir.appending(component: "pkg").asURL.realpath.filePath) let buildSystemManager = await BuildSystemManager( buildSystemKind: .swiftPM(projectRoot: packageRoot), toolchainRegistry: .forTesting, @@ -182,7 +182,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { let aPlusSomething = packageRoot.appending(components: "Sources", "lib", "a+something.swift") assertNotNil(await buildSystemManager.initializationData?.indexStorePath) - let pathWithPlusEscaped = "\(aPlusSomething.asURL.path.replacing("+", with: "%2B"))" + let pathWithPlusEscaped = "\(try aPlusSomething.asURL.filePath.replacing("+", with: "%2B"))" #if os(Windows) let urlWithPlusEscaped = try XCTUnwrap(URL(string: "file:///\(pathWithPlusEscaped)")) #else @@ -629,7 +629,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: packageRoot.asURL, options: .testDefault())) let buildSystemManager = await BuildSystemManager( - buildSystemKind: .swiftPM(projectRoot: try AbsolutePath(validating: projectRoot.path)), + buildSystemKind: .swiftPM(projectRoot: try AbsolutePath(validating: projectRoot.filePath)), toolchainRegistry: .forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), @@ -705,7 +705,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { let projectRoot = try XCTUnwrap(SwiftPMBuildSystem.projectRoot(for: symlinkRoot.asURL, options: .testDefault())) let buildSystemManager = await BuildSystemManager( - buildSystemKind: .swiftPM(projectRoot: try AbsolutePath(validating: projectRoot.path)), + buildSystemKind: .swiftPM(projectRoot: try AbsolutePath(validating: projectRoot.filePath)), toolchainRegistry: .forTesting, options: SourceKitLSPOptions(), connectionToClient: DummyBuildSystemManagerConnectionToClient(), diff --git a/Tests/DiagnoseTests/DiagnoseTests.swift b/Tests/DiagnoseTests/DiagnoseTests.swift index 273823a48..8309f7241 100644 --- a/Tests/DiagnoseTests/DiagnoseTests.swift +++ b/Tests/DiagnoseTests/DiagnoseTests.swift @@ -317,7 +317,7 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { logger.info("Sending request: \(requestString)") let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate( - dylibPath: try! AbsolutePath(validating: sourcekitd.path) + dylibPath: try! AbsolutePath(validating: sourcekitd.filePath) ) let response = try await sourcekitd.run(requestYaml: requestString) diff --git a/Tests/SKSupportTests/ProcessRunTests.swift b/Tests/SKSupportTests/ProcessRunTests.swift index d9b069781..9ebab515f 100644 --- a/Tests/SKSupportTests/ProcessRunTests.swift +++ b/Tests/SKSupportTests/ProcessRunTests.swift @@ -12,6 +12,7 @@ import SKSupport import SKTestSupport +import SwiftExtensions import XCTest import struct TSCBasic.AbsolutePath @@ -38,7 +39,7 @@ final class ProcessRunTests: XCTestCase { """.write(to: pythonFile.asURL, atomically: true, encoding: .utf8) let result = try await Process.run( - arguments: [python.path, pythonFile.pathString], + arguments: [python.filePath, pythonFile.pathString], workingDirectory: workingDir ) let stdout = try unwrap(String(bytes: result.output.get(), encoding: .utf8)) diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index be48ba295..b1734200f 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -892,7 +892,7 @@ final class BackgroundIndexingTests: XCTestCase { .appendingPathComponent(".index-build") ) XCTAssertFalse( - FileManager.default.fileExists(atPath: nestedIndexBuildURL.path), + FileManager.default.fileExists(at: nestedIndexBuildURL), "No file should exist at \(nestedIndexBuildURL)" ) } @@ -1263,7 +1263,7 @@ final class BackgroundIndexingTests: XCTestCase { arguments: [ unwrap(ToolchainRegistry.forTesting.default?.swift?.pathString), "package", "update", - "--package-path", project.scratchDirectory.path, + "--package-path", project.scratchDirectory.filePath, ], workingDirectory: nil ) diff --git a/Tests/SourceKitLSPTests/MainFilesProviderTests.swift b/Tests/SourceKitLSPTests/MainFilesProviderTests.swift index a543c8698..86ecede86 100644 --- a/Tests/SourceKitLSPTests/MainFilesProviderTests.swift +++ b/Tests/SourceKitLSPTests/MainFilesProviderTests.swift @@ -13,6 +13,7 @@ import LanguageServerProtocol import SKTestSupport import SourceKitLSP +import SwiftExtensions import XCTest final class MainFilesProviderTests: XCTestCase { @@ -186,7 +187,7 @@ final class MainFilesProviderTests: XCTestCase { XCTAssertEqual(preEditDiag.message, "Unused variable 'fromMyLibrary'") let newFancyLibraryContents = """ - #include "\(project.scratchDirectory.path)/Sources/shared.h" + #include "\(try project.scratchDirectory.filePath)/Sources/shared.h" """ let fancyLibraryUri = try project.uri(for: "MyFancyLibrary.c") try newFancyLibraryContents.write(to: try XCTUnwrap(fancyLibraryUri.fileURL), atomically: false, encoding: .utf8) diff --git a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift index dfb154afb..3ecf4ed78 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift @@ -15,6 +15,7 @@ import Foundation import LanguageServerProtocol import SKTestSupport @_spi(Testing) import SourceKitLSP +import SwiftExtensions import ToolchainRegistry import XCTest @@ -784,9 +785,9 @@ final class WorkspaceTestDiscoveryTests: XCTestCase { let compilationDatabase = JSONCompilationDatabase([ JSONCompilationDatabase.Command( - directory: project.scratchDirectory.path, + directory: try project.scratchDirectory.filePath, filename: uri.pseudoPath, - commandLine: [swiftc.path, uri.pseudoPath] + commandLine: [try swiftc.filePath, uri.pseudoPath] ) ]) diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index f413c97ac..af6bce26c 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -16,6 +16,7 @@ import SKLogging import SKOptions import SKTestSupport import SourceKitLSP +import SwiftExtensions import TSCBasic import ToolchainRegistry import XCTest @@ -721,7 +722,7 @@ final class WorkspaceTests: XCTestCase { try await TSCBasic.Process.checkNonZeroExit(arguments: [ ToolchainRegistry.forTesting.default!.swift!.pathString, "build", - "--package-path", packageDir.path, + "--package-path", packageDir.filePath, "-Xswiftc", "-index-ignore-system-modules", "-Xcc", "-index-ignore-system-symbols", ])