mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2025-12-17 12:03:01 +01:00
`appending(component:)` is the more modern API and can take multiple path components at the same time.
150 lines
5.3 KiB
Swift
150 lines
5.3 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Foundation
|
|
import SwiftParser
|
|
import SwiftSyntax
|
|
|
|
/// The main entry point for generating a JSON schema and Markdown documentation
|
|
/// for the SourceKit-LSP configuration file format
|
|
/// (`.sourcekit-lsp/config.json`) from the Swift type definitions in
|
|
/// `SKOptions` Swift module.
|
|
package struct ConfigSchemaGen {
|
|
private struct WritePlan {
|
|
fileprivate let category: String
|
|
fileprivate let path: URL
|
|
fileprivate let contents: () throws -> Data
|
|
|
|
fileprivate func write() throws {
|
|
try contents().write(to: path)
|
|
}
|
|
}
|
|
|
|
private static let projectRoot = URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
private static let sourceDir =
|
|
projectRoot
|
|
.appending(components: "Sources", "SKOptions")
|
|
private static let configSchemaJSONPath =
|
|
projectRoot
|
|
.appending(component: "config.schema.json")
|
|
private static let configSchemaDocPath =
|
|
projectRoot
|
|
.appending(components: "Documentation", "Configuration File.md")
|
|
|
|
/// Generates and writes the JSON schema and documentation for the SourceKit-LSP configuration file format.
|
|
package static func generate() throws {
|
|
let plans = try plan()
|
|
for plan in plans {
|
|
print("Writing \(plan.category) to \"\(plan.path.path)\"")
|
|
try plan.write()
|
|
}
|
|
}
|
|
|
|
/// Verifies that the generated JSON schema and documentation in the current source tree
|
|
/// are up-to-date with the Swift type definitions in `SKOptions`.
|
|
/// - Returns: `true` if the generated files are up-to-date, `false` otherwise.
|
|
package static func verify() throws -> Bool {
|
|
let plans = try plan()
|
|
for plan in plans {
|
|
print("Verifying \(plan.category) at \"\(plan.path.path)\"")
|
|
let expectedContents = try plan.contents()
|
|
let actualContents = try Data(contentsOf: plan.path)
|
|
guard expectedContents == actualContents else {
|
|
print("error: \(plan.category) is out-of-date!")
|
|
print("Please run `./sourcekit-lsp-dev-utils generate-config-schema` to update it.")
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static func plan() throws -> [WritePlan] {
|
|
let sourceFiles = FileManager.default.enumerator(at: sourceDir, includingPropertiesForKeys: nil)!
|
|
let typeNameResolver = TypeDeclResolver()
|
|
|
|
for case let fileURL as URL in sourceFiles {
|
|
guard fileURL.pathExtension == "swift" else {
|
|
continue
|
|
}
|
|
let sourceText = try String(contentsOf: fileURL)
|
|
let sourceFile = Parser.parse(source: sourceText)
|
|
typeNameResolver.collect(from: sourceFile)
|
|
}
|
|
let rootTypeDecl = try typeNameResolver.lookupType(fullyQualified: ["SourceKitLSPOptions"])
|
|
let context = OptionSchemaContext(typeNameResolver: typeNameResolver)
|
|
var schema = try context.buildSchema(from: rootTypeDecl)
|
|
|
|
// Manually annotate the logging level enum since LogLevel type exists
|
|
// outside of the SKOptions module
|
|
schema["logging"]?["level"]?.kind = .enum(
|
|
OptionTypeSchama.Enum(
|
|
name: "LogLevel",
|
|
cases: ["debug", "info", "default", "error", "fault"].map {
|
|
OptionTypeSchama.Case(name: $0)
|
|
}
|
|
)
|
|
)
|
|
schema["logging"]?["privacyLevel"]?.kind = .enum(
|
|
OptionTypeSchama.Enum(
|
|
name: "PrivacyLevel",
|
|
cases: ["public", "private", "sensitive"].map {
|
|
OptionTypeSchama.Case(name: $0)
|
|
}
|
|
)
|
|
)
|
|
|
|
return [
|
|
WritePlan(
|
|
category: "JSON Schema",
|
|
path: configSchemaJSONPath,
|
|
contents: { try generateJSONSchema(from: schema, context: context) }
|
|
),
|
|
WritePlan(
|
|
category: "Schema Documentation",
|
|
path: configSchemaDocPath,
|
|
contents: { try generateDocumentation(from: schema, context: context) }
|
|
),
|
|
]
|
|
}
|
|
|
|
private static func generateJSONSchema(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data {
|
|
let schemaBuilder = JSONSchemaBuilder(context: context)
|
|
var jsonSchema = try schemaBuilder.build(from: schema)
|
|
jsonSchema.title = "SourceKit-LSP Configuration"
|
|
jsonSchema.comment = "DO NOT EDIT THIS FILE. This file is generated by \(#fileID)."
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
return try encoder.encode(jsonSchema)
|
|
}
|
|
|
|
private static func generateDocumentation(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data
|
|
{
|
|
let docBuilder = OptionDocumentBuilder(context: context)
|
|
guard let data = try docBuilder.build(from: schema).data(using: .utf8) else {
|
|
throw ConfigSchemaGenError("Failed to encode documentation as UTF-8")
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
|
|
struct ConfigSchemaGenError: Error, CustomStringConvertible {
|
|
let description: String
|
|
|
|
init(_ description: String) {
|
|
self.description = description
|
|
}
|
|
}
|