Files
sourcekit-lsp/Sources/BuildServerIntegration/CompilationDatabase.swift
2025-10-31 14:11:11 -07:00

217 lines
7.5 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 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
@_spi(SourceKitLSP) package import LanguageServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
@_spi(SourceKitLSP) import SKLogging
import SwiftExtensions
import TSCExtensions
#if os(Windows)
import WinSDK
#endif
/// A single compilation database command.
///
/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html
package struct CompilationDatabaseCompileCommand: Equatable, Codable {
/// The working directory for the compilation.
package var directory: String
/// The path of the main file for the compilation, which may be relative to `directory`.
package var filename: String
/// The compile command as a list of strings, with the program name first.
package var commandLine: [String]
/// The name of the build output, or nil.
package var output: String? = nil
package init(directory: String, filename: String, commandLine: [String], output: String? = nil) {
self.directory = directory
self.filename = filename
self.commandLine = commandLine
self.output = output
}
private enum CodingKeys: String, CodingKey {
case directory
case file
case command
case arguments
case output
}
package init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.directory = try container.decode(String.self, forKey: .directory)
self.filename = try container.decode(String.self, forKey: .file)
self.output = try container.decodeIfPresent(String.self, forKey: .output)
if let arguments = try container.decodeIfPresent([String].self, forKey: .arguments) {
self.commandLine = arguments
} else if let command = try container.decodeIfPresent(String.self, forKey: .command) {
#if os(Windows)
self.commandLine = splitWindowsCommandLine(command, initialCommandName: true)
#else
self.commandLine = splitShellEscapedCommand(command)
#endif
} else {
throw CompilationDatabaseDecodingError.missingCommandOrArguments
}
}
package func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(directory, forKey: .directory)
try container.encode(filename, forKey: .file)
try container.encode(commandLine, forKey: .arguments)
try container.encodeIfPresent(output, forKey: .output)
}
/// The `DocumentURI` for this file. If this a relative path, it will be interpreted relative to the compile command's
/// working directory, which in turn is relative to `compileCommandsDirectory`, the directory that contains the
/// `compile_commands.json` file.
package func uri(compileCommandsDirectory: URL) -> DocumentURI {
if filename.isAbsolutePath {
return DocumentURI(URL(fileURLWithPath: self.filename))
}
return DocumentURI(
URL(
fileURLWithPath: self.filename,
relativeTo: self.directoryURL(compileCommandsDirectory: compileCommandsDirectory)
)
)
}
/// A file URL representing `directory`. If `directory` is relative, it's interpreted relative to
/// `compileCommandsDirectory`, the directory that contains the `compile_commands.json` file.
func directoryURL(compileCommandsDirectory: URL) -> URL {
return URL(fileURLWithPath: directory, isDirectory: true, relativeTo: compileCommandsDirectory)
}
}
extension CodingUserInfoKey {
/// When decoding `JSONCompilationDatabase` a `URL` representing the directory that contains the
/// `compile_commands.json`.
package static let compileCommandsDirectoryKey: CodingUserInfoKey =
CodingUserInfoKey(rawValue: "lsp.compile-commands-dir")!
}
/// The JSON clang-compatible compilation database.
///
/// Example:
///
/// ```
/// [
/// {
/// "directory": "/src",
/// "file": "/src/file.cpp",
/// "command": "clang++ file.cpp"
/// }
/// ]
/// ```
///
/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html
package struct JSONCompilationDatabase: Equatable, Codable {
private var pathToCommands: [DocumentURI: [Int]] = [:]
var commands: [CompilationDatabaseCompileCommand] = []
/// The directory that contains the `compile_commands.json` file.
private let compileCommandsDirectory: URL
package init(_ commands: [CompilationDatabaseCompileCommand] = [], compileCommandsDirectory: URL) {
self.compileCommandsDirectory = compileCommandsDirectory
for command in commands {
add(command)
}
}
/// Decode the `JSONCompilationDatabase` from a decoder.
///
/// A `URL` representing the directory that contains the `compile_commands.json` must be passed in the decoder's
/// `userInfo` via the `compileCommandsDirectoryKey`.
package init(from decoder: Decoder) throws {
guard let compileCommandsDirectory = decoder.userInfo[.compileCommandsDirectoryKey] as? URL else {
struct MissingCompileCommandsDirectoryKeyError: Error {}
throw MissingCompileCommandsDirectoryKeyError()
}
self.compileCommandsDirectory = compileCommandsDirectory
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
self.add(try container.decode(CompilationDatabaseCompileCommand.self))
}
}
/// Loads the compilation database located in `directory`, if any.
///
/// - Returns: `nil` if `compile_commands.json` was not found
package init(directory: URL) throws {
let path = directory.appending(component: JSONCompilationDatabaseBuildServer.dbName)
try self.init(file: path)
}
/// Loads the compilation database from `file`
/// - Returns: `nil` if the file does not exist
package init(file: URL) throws {
let data = try Data(contentsOf: file)
let decoder = JSONDecoder()
decoder.userInfo[.compileCommandsDirectoryKey] = file.deletingLastPathComponent()
self = try decoder.decode(JSONCompilationDatabase.self, from: data)
}
package func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for command in commands {
try container.encode(command)
}
}
package subscript(_ uri: DocumentURI) -> [CompilationDatabaseCompileCommand] {
if let indices = pathToCommands[uri] {
return indices.map { commands[$0] }
}
if let fileURL = try? uri.fileURL?.realpath, let indices = pathToCommands[DocumentURI(fileURL)] {
return indices.map { commands[$0] }
}
return []
}
private mutating func add(_ command: CompilationDatabaseCompileCommand) {
let uri = command.uri(compileCommandsDirectory: compileCommandsDirectory)
pathToCommands[uri, default: []].append(commands.count)
if let symlinkTarget = uri.symlinkTarget {
pathToCommands[symlinkTarget, default: []].append(commands.count)
}
commands.append(command)
}
}
enum CompilationDatabaseDecodingError: Error {
case missingCommandOrArguments
case fixedDatabaseDecodingError
}
fileprivate extension String {
var isAbsolutePath: Bool {
#if os(Windows)
Array(self.utf16).withUnsafeBufferPointer { buffer in
return !PathIsRelativeW(buffer.baseAddress)
}
#else
return self.hasPrefix("/")
#endif
}
}