1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
|
//===----------------------------------------------------------------------===//
//
// 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 SwiftSyntax
/// Intermediate type schema representation for option types derived from Swift
/// syntax nodes
struct OptionTypeSchama {
struct Property {
var name: String
var type: OptionTypeSchama
var description: String?
var defaultValue: String?
}
struct Struct {
var name: String
/// Properties of the object, preserving the order of declaration
var properties: [Property]
}
struct Case {
var name: String
var description: String?
}
struct Enum {
var name: String
var cases: [Case]
}
enum Kind {
case boolean
case integer
case number
case string
indirect case array(value: OptionTypeSchama)
indirect case dictionary(value: OptionTypeSchama)
case `struct`(Struct)
case `enum`(Enum)
}
var kind: Kind
var isOptional: Bool
init(kind: Kind, isOptional: Bool = false) {
self.kind = kind
self.isOptional = isOptional
}
/// Accesses the property schema by name
subscript(_ key: String) -> OptionTypeSchama? {
get {
guard case .struct(let structInfo) = kind else {
return nil
}
return structInfo.properties.first { $0.name == key }?.type
}
set {
guard case .struct(var structInfo) = kind else {
fatalError("Cannot set property on non-object type")
}
guard let index = structInfo.properties.firstIndex(where: { $0.name == key }) else {
fatalError("Property not found: \(key)")
}
guard let newValue = newValue else {
fatalError("Cannot set property to nil")
}
structInfo.properties[index].type = newValue
kind = .struct(structInfo)
}
}
}
/// Context for resolving option schema from Swift syntax nodes
struct OptionSchemaContext {
private let typeNameResolver: TypeDeclResolver
init(typeNameResolver: TypeDeclResolver) {
self.typeNameResolver = typeNameResolver
}
/// Builds a schema from a type declaration
func buildSchema(from typeDecl: TypeDeclResolver.TypeDecl) throws -> OptionTypeSchama {
switch DeclSyntax(typeDecl).as(DeclSyntaxEnum.self) {
case .structDecl(let decl):
let structInfo = try buildStructProperties(decl)
return OptionTypeSchama(kind: .struct(structInfo))
case .enumDecl(let decl):
let enumInfo = try buildEnumCases(decl)
return OptionTypeSchama(kind: .enum(enumInfo))
default:
throw ConfigSchemaGenError("Unsupported type declaration: \(typeDecl)")
}
}
/// Resolves the type of a given type usage
private func resolveType(_ type: TypeSyntax) throws -> OptionTypeSchama {
switch type.as(TypeSyntaxEnum.self) {
case .optionalType(let type):
var wrapped = try resolveType(type.wrappedType)
guard !wrapped.isOptional else {
throw ConfigSchemaGenError("Nested optional type is not supported")
}
wrapped.isOptional = true
return wrapped
case .arrayType(let type):
let value = try resolveType(type.element)
return OptionTypeSchama(kind: .array(value: value))
case .dictionaryType(let type):
guard type.key.trimmedDescription == "String" else {
throw ConfigSchemaGenError("Dictionary key type must be String: \(type.key)")
}
let value = try resolveType(type.value)
return OptionTypeSchama(kind: .dictionary(value: value))
case .identifierType(let type):
let primitiveTypes: [String: OptionTypeSchama.Kind] = [
"String": .string,
"Int": .integer,
"Double": .number,
"Bool": .boolean,
]
if let primitiveType = primitiveTypes[type.trimmedDescription] {
return OptionTypeSchama(kind: primitiveType)
} else if type.name.trimmedDescription == "Set" {
guard let elementType = type.genericArgumentClause?.arguments.first?.argument else {
throw ConfigSchemaGenError("Set type must have one generic argument: \(type)")
}
return OptionTypeSchama(kind: .array(value: try resolveType(elementType)))
} else {
let type = try typeNameResolver.lookupType(for: type)
return try buildSchema(from: type)
}
default:
throw ConfigSchemaGenError("Unsupported type syntax: \(type)")
}
}
private func buildEnumCases(_ node: EnumDeclSyntax) throws -> OptionTypeSchama.Enum {
let cases = try node.memberBlock.members.flatMap { member -> [OptionTypeSchama.Case] in
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
return []
}
return try caseDecl.elements.map {
guard $0.parameterClause == nil else {
throw ConfigSchemaGenError("Associated values in enum cases are not supported: \(caseDecl)")
}
let name: String
if let rawValue = $0.rawValue?.value {
if let stringLiteral = rawValue.as(StringLiteralExprSyntax.self),
let literalValue = stringLiteral.representedLiteralValue
{
name = literalValue
} else {
throw ConfigSchemaGenError(
"Only string literals without interpolation are supported as enum case raw values: \(caseDecl)"
)
}
} else {
name = $0.name.text
}
return OptionTypeSchama.Case(name: name, description: Self.extractDocComment(caseDecl.leadingTrivia))
}
}
let typeName = node.name.text
return .init(name: typeName, cases: cases)
}
private func buildStructProperties(_ node: StructDeclSyntax) throws -> OptionTypeSchama.Struct {
var properties: [OptionTypeSchama.Property] = []
for member in node.memberBlock.members {
// Skip computed properties
guard let variable = member.decl.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let type = binding.typeAnnotation,
binding.accessorBlock == nil
else { continue }
let name = binding.pattern.trimmed.description
let defaultValue = binding.initializer?.value.description
let description = Self.extractDocComment(variable.leadingTrivia)
if description?.contains("- Note: Internal option") ?? false {
continue
}
let typeInfo = try resolveType(type.type)
properties.append(
.init(name: name, type: typeInfo, description: description, defaultValue: defaultValue)
)
}
let typeName = node.name.text
return .init(name: typeName, properties: properties)
}
private static func extractDocComment(_ trivia: Trivia) -> String? {
var docLines = trivia.flatMap { piece in
switch piece {
case .docBlockComment(let text):
// Remove `/**` and `*/`
assert(text.hasPrefix("/**") && text.hasSuffix("*/"), "Unexpected doc block comment format: \(text)")
return text.dropFirst(3).dropLast(2).split { $0.isNewline }
case .docLineComment(let text):
// Remove `///` and leading space
assert(text.hasPrefix("///"), "Unexpected doc line comment format: \(text)")
let text = text.dropFirst(3)
return [text]
default:
return []
}
}
guard !docLines.isEmpty else {
return nil
}
// Trim leading spaces for each line and skip empty lines
docLines = docLines.compactMap {
guard !$0.isEmpty else { return nil }
var trimmed = $0
while trimmed.first?.isWhitespace == true {
trimmed = trimmed.dropFirst()
}
return trimmed
}
return docLines.joined(separator: " ")
}
}
|