File: OptionSchema.swift

package info (click to toggle)
swiftlang 6.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 2,791,532 kB
  • sloc: cpp: 9,901,743; ansic: 2,201,431; asm: 1,091,827; python: 308,252; objc: 82,166; f90: 80,126; lisp: 38,358; pascal: 25,559; sh: 20,429; ml: 5,058; perl: 4,745; makefile: 4,484; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (233 lines) | stat: -rw-r--r-- 8,025 bytes parent folder | download | duplicates (2)
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: " ")
  }
}