Files
sourcekit-lsp/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/ConfigSchemaGen.swift
2024-12-09 02:12:19 +09:00

134 lines
4.5 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.
public struct ConfigSchemaGen {
public struct WritePlan {
public let category: String
public let path: URL
public let contents: () throws -> Data
public func write() throws {
try contents().write(to: path)
}
}
static let projectRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
static let sourceDir =
projectRoot
.appendingPathComponent("Sources")
.appendingPathComponent("SKOptions")
static let configSchemaJSONPath =
projectRoot
.appendingPathComponent("config.schema.json")
static let configSchemaDocPath =
projectRoot
.appendingPathComponent("Documentation")
.appendingPathComponent("Configuration File.md")
public static func generate() throws {
let plans = try plan()
for plan in plans {
print("Writing \(plan.category) to \"\(plan.path.path)\"")
try plan.write()
}
}
public 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: ["sensitive", "private", "public"].map {
OptionTypeSchama.Case(name: $0)
}
)
)
var plans: [WritePlan] = []
plans.append(WritePlan(
category: "JSON Schema",
path: configSchemaJSONPath,
contents: { try generateJSONSchema(from: schema, context: context) }
))
plans.append(WritePlan(
category: "Schema Documentation",
path: configSchemaDocPath,
contents: { try generateDocumentation(from: schema, context: context) }
))
return plans
}
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]
return try encoder.encode(jsonSchema)
}
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
}
}