772 lines
33 KiB
Swift
772 lines
33 KiB
Swift
// XMLParser.swift
|
|
// GIR XML → IR. The delegate maintains an explicit element stack so that every
|
|
// element attaches to its true parent, and nested constructs (container element
|
|
// types, callback-typed parameters, virtual-method bodies) nest correctly
|
|
// instead of leaking into whatever `current*` slot happened to be set.
|
|
|
|
import Foundation
|
|
#if canImport(FoundationXML)
|
|
import FoundationXML
|
|
#endif
|
|
|
|
/// Errors that can occur during GIR XML parsing.
|
|
public enum GIRParserError: Error {
|
|
/// The XML document is malformed or could not be parsed.
|
|
/// - Parameter description: A human-readable error description.
|
|
case invalidXML(String)
|
|
/// An unexpected XML element was encountered.
|
|
/// - Parameter description: Details about the unexpected element.
|
|
case unexpectedElement(String)
|
|
/// A required attribute is missing from an XML element.
|
|
/// - Parameter description: Description of the missing attribute.
|
|
case missingAttribute(String)
|
|
}
|
|
|
|
/// Parses GIR XML files into the intermediate representation model.
|
|
///
|
|
/// Uses Foundation's `XMLParser` in a SAX-style delegate pattern, driving an
|
|
/// explicit element stack. All GIR element types are handled: classes,
|
|
/// interfaces, records, enumerations, bitfields, callbacks, methods,
|
|
/// properties, signals, functions, constants, and aliases.
|
|
///
|
|
/// The parser is deliberately non-judgemental: it records what the GIR says,
|
|
/// including symbols that cannot be bound. Deciding what to *bind* is the
|
|
/// planner's job, which reports skips with reasons.
|
|
///
|
|
/// ### Example
|
|
/// ```swift
|
|
/// let parser = GIRParser()
|
|
/// let repo = try parser.parse(fileURL: URL(fileURLWithPath: "/usr/share/gir-1.0/GLib-2.0.gir"))
|
|
/// print(repo.namespaces.first?.classes.count ?? 0)
|
|
/// ```
|
|
public struct GIRParser {
|
|
/// Creates a new GIR parser.
|
|
public init() {}
|
|
|
|
/// Parses a GIR XML document from a string.
|
|
/// - Parameter xmlString: The XML content to parse.
|
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
|
/// - Throws: `GIRParserError` if the XML cannot be encoded as UTF-8 or parsing fails.
|
|
public func parse(xmlString: String) throws -> Repository {
|
|
guard let data = xmlString.data(using: .utf8) else {
|
|
throw GIRParserError.invalidXML("Failed to encode XML string as UTF-8")
|
|
}
|
|
return try parse(data: data)
|
|
}
|
|
|
|
/// Parses a GIR XML document from a file URL.
|
|
/// - Parameter fileURL: The URL of the .gir file to parse.
|
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
|
/// - Throws: `GIRParserError` if the file cannot be read or parsing fails.
|
|
public func parse(fileURL: URL) throws -> Repository {
|
|
let data = try Data(contentsOf: fileURL)
|
|
return try parse(data: data)
|
|
}
|
|
|
|
/// Parses raw XML data using a SAX-style delegate.
|
|
/// - Parameter data: The XML data to parse.
|
|
/// - Returns: A `Repository` with the parsed content.
|
|
/// - Throws: `GIRParserError` if parsing fails.
|
|
private func parse(data: Data) throws -> Repository {
|
|
let delegate = GIRXMLDelegate()
|
|
let parser = XMLParser(data: data)
|
|
parser.delegate = delegate
|
|
if parser.parse(), delegate.parseError == nil {
|
|
return delegate.repository
|
|
}
|
|
throw delegate.parseError ?? GIRParserError.invalidXML(parser.parserError?.localizedDescription ?? "unknown error")
|
|
}
|
|
}
|
|
|
|
// MARK: - Element Stack
|
|
|
|
/// A partially-built `<type>` element.
|
|
///
|
|
/// Nested `<type>` children supply container element types, e.g.
|
|
/// `<type name="GLib.List"><type name="utf8"/></type>`.
|
|
struct TypeBuilder {
|
|
/// The GIR type name attribute, e.g. `"utf8"` or `"GLib.List"`.
|
|
var name: String
|
|
/// The `c:type` attribute, e.g. `"gchar*"`.
|
|
var cType: String
|
|
/// Element types collected from nested `<type>` children.
|
|
var children: [GIRType] = []
|
|
}
|
|
|
|
/// A partially-built `<array>` element.
|
|
struct ArrayBuilder {
|
|
/// The array's `name` attribute; set for GLib container arrays.
|
|
var name: String?
|
|
/// Length metadata gathered from the element's attributes.
|
|
var info: ArrayInfo
|
|
/// The element type, from the nested `<type>` or `<array>` child.
|
|
var children: [GIRType] = []
|
|
}
|
|
|
|
/// One open XML element during parsing.
|
|
///
|
|
/// The delegate pushes a frame on every element it tracks and pops it on close,
|
|
/// attaching the completed value to the frame beneath. Untracked elements push
|
|
/// ``ignored`` so that their children attach to nothing rather than leaking into
|
|
/// an ancestor.
|
|
indirect enum Frame {
|
|
case namespace(Namespace)
|
|
case klass(Class)
|
|
case interface(Interface)
|
|
case record(Record)
|
|
case enumeration(Enumeration)
|
|
case bitfield(Bitfield)
|
|
case callback(Callback)
|
|
case constructor(Constructor)
|
|
case method(Method)
|
|
case function(GlobalFunction)
|
|
case signal(Signal)
|
|
case property(Property)
|
|
case parameterList([Parameter])
|
|
case parameter(Parameter)
|
|
case returnValue(ReturnValue)
|
|
case field(Field)
|
|
case constant(Constant)
|
|
case alias(Alias)
|
|
case type(TypeBuilder)
|
|
case array(ArrayBuilder)
|
|
case doc
|
|
/// An element whose content is deliberately discarded (virtual methods,
|
|
/// unions, source positions, and anything else not modelled).
|
|
case ignored(String)
|
|
}
|
|
|
|
// MARK: - XMLParser Delegate
|
|
|
|
/// SAX-style delegate for `XMLParser` that builds a `Repository` from GIR XML.
|
|
///
|
|
/// Maintains an explicit ``Frame`` stack: `didStartElement` pushes a frame,
|
|
/// `didEndElement` pops it and attaches the finished value to its parent. This
|
|
/// makes parent-child relationships exact, which flat `current*` slots could not
|
|
/// express for nested constructs.
|
|
final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
|
/// The repository being populated during parsing.
|
|
var repository = Repository()
|
|
/// Set to the first error encountered, or `nil` if parsing succeeds.
|
|
var parseError: GIRParserError?
|
|
|
|
/// The stack of currently-open elements, outermost first.
|
|
private var stack: [Frame] = []
|
|
/// Accumulated character data for the innermost `<doc>` element.
|
|
private var currentText: String = ""
|
|
|
|
// MARK: Element start
|
|
|
|
func parser(_ parser: XMLParser, didStartElement elementName: String,
|
|
namespaceURI: String?, qualifiedName: String?,
|
|
attributes attributeDict: [String: String] = [:]) {
|
|
switch elementName {
|
|
case "c:include":
|
|
if let name = attributeDict["name"] { repository.cHeaderPath = name }
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "package":
|
|
if let name = attributeDict["name"] { repository.packageName = name }
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "include":
|
|
// GIR dependency includes appear before the <namespace> element.
|
|
// Derive the link name as lowercase(library) + "-" + major version,
|
|
// e.g. "Gdk-4.0" → "gdk-4".
|
|
if let name = attributeDict["name"], let version = attributeDict["version"] {
|
|
let majorVersion = version.split(separator: ".").first.map(String.init) ?? version
|
|
repository.includedLibraryLinks.append("\(name.lowercased())-\(majorVersion)")
|
|
repository.includedPackages.append(IncludeEntry(name: name, version: version))
|
|
}
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "namespace":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let version = requireAttribute("version", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.namespace(Namespace(
|
|
name: name, version: version,
|
|
cSharedLibrary: attributeDict["shared-library"] ?? "",
|
|
cIdentifierPrefix: attributeDict["c:identifier-prefixes"] ?? ""
|
|
)))
|
|
|
|
case "class":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.klass(Class(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
parent: attributeDict["parent"],
|
|
isAbstract: attributeDict["abstract"] == "1",
|
|
isFinal: attributeDict["final"] == "1",
|
|
getTypeFunction: attributeDict["glib:get-type"],
|
|
typeName: attributeDict["glib:type-name"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict),
|
|
refFunc: attributeDict["glib:ref-func"],
|
|
unrefFunc: attributeDict["glib:unref-func"]
|
|
)))
|
|
|
|
case "interface":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.interface(Interface(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
getTypeFunction: attributeDict["glib:get-type"],
|
|
typeName: attributeDict["glib:type-name"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "prerequisite":
|
|
// GIR spells prerequisites as child elements, not an attribute.
|
|
if let name = attributeDict["name"] {
|
|
mutateTop { if case .interface(var iface) = $0 { iface.prereqs.append(name); $0 = .interface(iface) } }
|
|
}
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "record":
|
|
guard let name = attributeDict["name"] else {
|
|
// Anonymous nested <record> (a C struct inside a <union>, e.g.
|
|
// GskPathPoint). Not a namespace-level type; discard it and its
|
|
// fields — record fields are deferred and the parent union is
|
|
// already .ignored.
|
|
stack.append(.ignored(elementName))
|
|
return
|
|
}
|
|
stack.append(.record(Record(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
isOpaque: attributeDict["opaque"] == "1",
|
|
isDisguised: attributeDict["disguised"] == "1",
|
|
isGTypeStructFor: attributeDict["glib:is-gtype-struct-for"],
|
|
getTypeFunction: attributeDict["glib:get-type"],
|
|
typeName: attributeDict["glib:type-name"],
|
|
copyFunction: attributeDict["copy-function"],
|
|
freeFunction: attributeDict["free-function"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "enumeration":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.enumeration(Enumeration(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
getTypeFunction: attributeDict["glib:get-type"],
|
|
typeName: attributeDict["glib:type-name"],
|
|
errorDomain: attributeDict["glib:error-domain"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "bitfield":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.bitfield(Bitfield(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
getTypeFunction: attributeDict["glib:get-type"],
|
|
typeName: attributeDict["glib:type-name"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "member":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
let member = EnumMember(name: name, value: value, cIdentifier: cid)
|
|
mutateTop {
|
|
switch $0 {
|
|
case .enumeration(var e): e.members.append(member); $0 = .enumeration(e)
|
|
case .bitfield(var b): b.members.append(member); $0 = .bitfield(b)
|
|
default: break
|
|
}
|
|
}
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "callback":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.callback(Callback(
|
|
name: name,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
throwsGError: attributeDict["throws"] == "1",
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "constructor":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.constructor(Constructor(
|
|
name: name, cIdentifier: cid,
|
|
throwsGError: attributeDict["throws"] == "1",
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "method":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.method(Method(
|
|
name: name, cIdentifier: cid,
|
|
throwsGError: attributeDict["throws"] == "1",
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "function":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.function(GlobalFunction(
|
|
name: name, cIdentifier: cid,
|
|
throwsGError: attributeDict["throws"] == "1",
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "signal", "glib:signal":
|
|
guard let name = attributeDict["name"] ?? attributeDict["glib:name"] else {
|
|
parseError = .missingAttribute("Missing 'name' on <\(elementName)>")
|
|
parser.abortParsing()
|
|
return
|
|
}
|
|
stack.append(.signal(Signal(
|
|
name: name,
|
|
isDetailed: attributeDict["detailed"] == "1",
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "property":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.property(Property(
|
|
name: name, type: .void,
|
|
isReadable: attributeDict["readable"] != "0",
|
|
isWritable: attributeDict["writable"] == "1",
|
|
isConstructOnly: attributeDict["construct-only"] == "1",
|
|
isNullable: attributeDict["nullable"] == "1",
|
|
transferOwnership: TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none,
|
|
getter: attributeDict["getter"],
|
|
setter: attributeDict["setter"],
|
|
symbolInfo: Self.symbolInfo(from: attributeDict)
|
|
)))
|
|
|
|
case "parameters":
|
|
stack.append(.parameterList([]))
|
|
|
|
case "parameter", "instance-parameter":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.parameter(Parameter(
|
|
name: name, type: .void,
|
|
cType: attributeDict["c:type"] ?? "",
|
|
isNullable: attributeDict["nullable"] == "1",
|
|
isOptional: attributeDict["optional"] == "1",
|
|
transferOwnership: TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none,
|
|
isInstanceParameter: elementName == "instance-parameter",
|
|
direction: ParameterDirection(rawValue: attributeDict["direction"] ?? "in") ?? .in,
|
|
callerAllocates: attributeDict["caller-allocates"] == "1",
|
|
scope: attributeDict["scope"].flatMap(CallbackScope.init(rawValue:)),
|
|
closureIndex: attributeDict["closure"].flatMap(Int.init),
|
|
destroyIndex: attributeDict["destroy"].flatMap(Int.init)
|
|
)))
|
|
|
|
case "return-value":
|
|
stack.append(.returnValue(ReturnValue(
|
|
type: .void,
|
|
isNullable: attributeDict["nullable"] == "1",
|
|
transferOwnership: TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none
|
|
)))
|
|
|
|
case "type":
|
|
stack.append(.type(TypeBuilder(
|
|
name: attributeDict["name"] ?? "none",
|
|
cType: attributeDict["c:type"] ?? ""
|
|
)))
|
|
|
|
case "array":
|
|
stack.append(.array(ArrayBuilder(
|
|
name: attributeDict["name"],
|
|
info: ArrayInfo(
|
|
lengthParameterIndex: attributeDict["length"].flatMap(Int.init),
|
|
fixedSize: attributeDict["fixed-size"].flatMap(Int.init),
|
|
isZeroTerminated: attributeDict["zero-terminated"].map { $0 == "1" }
|
|
?? (attributeDict["length"] == nil && attributeDict["fixed-size"] == nil),
|
|
cType: attributeDict["c:type"] ?? ""
|
|
)
|
|
)))
|
|
|
|
case "constant":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.constant(Constant(name: name, value: value, type: .void)))
|
|
|
|
case "alias":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
|
let cType = requireAttribute("c:type", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.alias(Alias(name: name, cType: cType, target: .void)))
|
|
|
|
case "implements":
|
|
if let name = attributeDict["name"] {
|
|
mutateTop { if case .klass(var cls) = $0 { cls.implements.append(name); $0 = .klass(cls) } }
|
|
}
|
|
stack.append(.ignored(elementName))
|
|
|
|
case "field":
|
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
|
stack.append(.field(Field(
|
|
name: name, type: .void,
|
|
isReadable: attributeDict["readable"] != "0",
|
|
isWritable: attributeDict["writable"] == "1"
|
|
)))
|
|
|
|
case "doc":
|
|
currentText = ""
|
|
stack.append(.doc)
|
|
|
|
default:
|
|
stack.append(.ignored(elementName))
|
|
}
|
|
}
|
|
|
|
// MARK: Element end
|
|
|
|
func parser(_ parser: XMLParser, didEndElement elementName: String,
|
|
namespaceURI: String?, qualifiedName: String?) {
|
|
guard let frame = stack.popLast() else { return }
|
|
|
|
switch frame {
|
|
case .namespace(let ns):
|
|
repository.namespaces.append(ns)
|
|
|
|
case .klass(let cls):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.classes.append(cls); $0 = .namespace(ns) } }
|
|
|
|
case .interface(let iface):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.interfaces.append(iface); $0 = .namespace(ns) } }
|
|
|
|
case .record(let record):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.records.append(record); $0 = .namespace(ns) } }
|
|
|
|
case .enumeration(let e):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.enumerations.append(e); $0 = .namespace(ns) } }
|
|
|
|
case .bitfield(let b):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.bitfields.append(b); $0 = .namespace(ns) } }
|
|
|
|
case .callback(let cb):
|
|
// Namespace-level callbacks are bindable types in their own right.
|
|
// A callback nested in a <field> is a function-pointer member of a
|
|
// C struct (vtables such as GIOFuncs): at the ABI level that field
|
|
// is exactly an opaque pointer, which is what it is recorded as.
|
|
// Callbacks nested anywhere else describe a signature in place and
|
|
// are not separate declarations.
|
|
mutateTop {
|
|
switch $0 {
|
|
case .namespace(var ns): ns.callbacks.append(cb); $0 = .namespace(ns)
|
|
case .field(var f):
|
|
f = Field(name: f.name, type: .pointer, isReadable: f.isReadable,
|
|
isWritable: f.isWritable, doc: f.doc)
|
|
$0 = .field(f)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .constructor(let ctor):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var cls): cls.constructors.append(ctor); $0 = .klass(cls)
|
|
case .record(var rec): rec.constructors.append(ctor); $0 = .record(rec)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .method(let method):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var cls): cls.methods.append(method); $0 = .klass(cls)
|
|
case .interface(var iface): iface.methods.append(method); $0 = .interface(iface)
|
|
case .record(var rec): rec.methods.append(method); $0 = .record(rec)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .function(let fn):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var cls): cls.functions.append(fn); $0 = .klass(cls)
|
|
case .interface(var iface): iface.functions.append(fn); $0 = .interface(iface)
|
|
case .record(var rec): rec.functions.append(fn); $0 = .record(rec)
|
|
case .namespace(var ns): ns.functions.append(fn); $0 = .namespace(ns)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .signal(let sig):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var cls): cls.signals.append(sig); $0 = .klass(cls)
|
|
case .interface(var iface): iface.signals.append(sig); $0 = .interface(iface)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .property(let prop):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var cls): cls.properties.append(prop); $0 = .klass(cls)
|
|
case .interface(var iface): iface.properties.append(prop); $0 = .interface(iface)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .parameterList(let params):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .method(var m): m.parameters = params; $0 = .method(m)
|
|
case .constructor(var c): c.parameters = params; $0 = .constructor(c)
|
|
case .function(var f): f.parameters = params; $0 = .function(f)
|
|
case .signal(var s): s.parameters = params; $0 = .signal(s)
|
|
case .callback(var cb): cb.parameters = params; $0 = .callback(cb)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .parameter(let param):
|
|
mutateTop { if case .parameterList(var list) = $0 { list.append(param); $0 = .parameterList(list) } }
|
|
|
|
case .returnValue(let rv):
|
|
mutateTop {
|
|
switch $0 {
|
|
case .method(var m): m.returnValue = rv; $0 = .method(m)
|
|
case .constructor(var c): c.returnValue = rv; $0 = .constructor(c)
|
|
case .function(var f): f.returnValue = rv; $0 = .function(f)
|
|
case .signal(var s): s.returnValue = rv; $0 = .signal(s)
|
|
case .callback(var cb): cb.returnValue = rv; $0 = .callback(cb)
|
|
default: break
|
|
}
|
|
}
|
|
|
|
case .field(let field):
|
|
mutateTop { if case .record(var rec) = $0 { rec.fields.append(field); $0 = .record(rec) } }
|
|
|
|
case .constant(let c):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.constants.append(c); $0 = .namespace(ns) } }
|
|
|
|
case .alias(let a):
|
|
mutateTop { if case .namespace(var ns) = $0 { ns.aliases.append(a); $0 = .namespace(ns) } }
|
|
|
|
case .type(let builder):
|
|
assignType(Self.buildType(from: builder), cType: builder.cType)
|
|
|
|
case .array(let builder):
|
|
let element = builder.children.first ?? .pointer
|
|
if let name = builder.name, let kind = Self.containerKind(forTypeName: name) {
|
|
assignType(.container(kind, elements: builder.children), cType: builder.info.cType)
|
|
} else {
|
|
assignType(.cArray(element, builder.info), cType: builder.info.cType)
|
|
}
|
|
|
|
case .doc:
|
|
assignDoc(currentText.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
currentText = ""
|
|
|
|
case .ignored:
|
|
break
|
|
}
|
|
}
|
|
|
|
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
|
currentText += string
|
|
}
|
|
|
|
// MARK: - Attachment helpers
|
|
|
|
/// Mutates the innermost open frame in place.
|
|
///
|
|
/// - Parameter body: A closure receiving the top frame for mutation. Not
|
|
/// called when the stack is empty.
|
|
private func mutateTop(_ body: (inout Frame) -> Void) {
|
|
guard !stack.isEmpty else { return }
|
|
body(&stack[stack.count - 1])
|
|
}
|
|
|
|
/// Assigns a resolved type to whichever construct encloses it.
|
|
///
|
|
/// Handles every element that has a `<type>` or `<array>` child: parameters,
|
|
/// return values, properties, constants, aliases, record fields, and nested
|
|
/// container/array element types.
|
|
///
|
|
/// - Parameters:
|
|
/// - type: The resolved GIR type.
|
|
/// - cType: The `c:type` spelling, applied to parameters when non-empty.
|
|
private func assignType(_ type: GIRType, cType: String) {
|
|
mutateTop {
|
|
switch $0 {
|
|
case .parameter(var p):
|
|
p.type = type
|
|
if !cType.isEmpty { p.cType = cType }
|
|
$0 = .parameter(p)
|
|
case .returnValue(var rv):
|
|
rv.type = type
|
|
if !cType.isEmpty { rv.cType = cType }
|
|
$0 = .returnValue(rv)
|
|
case .property(var prop):
|
|
prop.type = type
|
|
$0 = .property(prop)
|
|
case .constant(var c):
|
|
c.type = type
|
|
$0 = .constant(c)
|
|
case .alias(var a):
|
|
a.target = type
|
|
$0 = .alias(a)
|
|
case .field(var f):
|
|
// Field is immutable in the IR, so rebuild it with the type
|
|
// that only becomes known when the child <type> closes.
|
|
f = Field(name: f.name, type: type, isReadable: f.isReadable,
|
|
isWritable: f.isWritable, doc: f.doc)
|
|
$0 = .field(f)
|
|
case .type(var builder):
|
|
builder.children.append(type)
|
|
$0 = .type(builder)
|
|
case .array(var builder):
|
|
if builder.children.isEmpty { builder.info.elementCType = cType }
|
|
builder.children.append(type)
|
|
$0 = .array(builder)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attaches documentation text to whichever construct encloses the `<doc>`.
|
|
///
|
|
/// - Parameter text: The trimmed documentation text; ignored when empty.
|
|
private func assignDoc(_ text: String) {
|
|
guard !text.isEmpty else { return }
|
|
mutateTop {
|
|
switch $0 {
|
|
case .klass(var x): x.doc = text; $0 = .klass(x)
|
|
case .interface(var x): x.doc = text; $0 = .interface(x)
|
|
case .record(var x): x.doc = text; $0 = .record(x)
|
|
case .enumeration(var x): x.doc = text; $0 = .enumeration(x)
|
|
case .bitfield(var x): x.doc = text; $0 = .bitfield(x)
|
|
case .callback(var x): x.doc = text; $0 = .callback(x)
|
|
case .constructor(var x): x.doc = text; $0 = .constructor(x)
|
|
case .method(var x): x.doc = text; $0 = .method(x)
|
|
case .function(var x): x.doc = text; $0 = .function(x)
|
|
case .signal(var x): x.doc = text; $0 = .signal(x)
|
|
case .property(var x): x.doc = text; $0 = .property(x)
|
|
case .parameter(var x): x.doc = text; $0 = .parameter(x)
|
|
case .returnValue(var x): x.doc = text; $0 = .returnValue(x)
|
|
case .constant(var x): x.doc = text; $0 = .constant(x)
|
|
case .alias(var x): x.doc = text; $0 = .alias(x)
|
|
case .field(var x):
|
|
x = Field(name: x.name, type: x.type, isReadable: x.isReadable,
|
|
isWritable: x.isWritable, doc: text)
|
|
$0 = .field(x)
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Requires that an attribute exists, or aborts parsing with an error.
|
|
/// - Parameters:
|
|
/// - key: The attribute name to look up.
|
|
/// - dict: The attribute dictionary from the current XML element.
|
|
/// - element: The name of the XML element (for error messages).
|
|
/// - parser: The XML parser to abort on failure.
|
|
/// - Returns: The attribute value, or `nil` if the attribute is missing.
|
|
private func requireAttribute(_ key: String, from dict: [String: String], for element: String, parser: XMLParser) -> String? {
|
|
guard let value = dict[key] else {
|
|
parseError = .missingAttribute("Missing '\(key)' on <\(element)>")
|
|
parser.abortParsing()
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
// MARK: - Type mapping
|
|
|
|
/// Extracts the binding-relevant GIR metadata common to all symbols.
|
|
///
|
|
/// - Parameter dict: The element's attribute dictionary.
|
|
/// - Returns: The parsed ``SymbolInfo``.
|
|
static func symbolInfo(from dict: [String: String]) -> SymbolInfo {
|
|
SymbolInfo(
|
|
isIntrospectable: dict["introspectable"] != "0",
|
|
isDeprecated: dict["deprecated"] == "1",
|
|
deprecatedVersion: dict["deprecated-version"],
|
|
shadowedBy: dict["shadowed-by"],
|
|
shadows: dict["shadows"],
|
|
movedTo: dict["moved-to"]
|
|
)
|
|
}
|
|
|
|
/// Builds a `GIRType` from a completed `<type>` element.
|
|
///
|
|
/// Container type names (`GLib.List`, `GLib.HashTable`, …) become
|
|
/// ``GIRType/container(_:elements:)`` carrying their nested element types;
|
|
/// everything else maps through ``girType(forName:)``.
|
|
///
|
|
/// - Parameter builder: The accumulated `<type>` element state.
|
|
/// - Returns: The resolved GIR type.
|
|
static func buildType(from builder: TypeBuilder) -> GIRType {
|
|
if let kind = containerKind(forTypeName: builder.name) {
|
|
return .container(kind, elements: builder.children)
|
|
}
|
|
return girType(forName: builder.name)
|
|
}
|
|
|
|
/// Maps a GIR container type name to its ``ContainerKind``.
|
|
///
|
|
/// - Parameter name: A GIR type name, e.g. `"GLib.List"`.
|
|
/// - Returns: The container kind, or `nil` if the name is not a container.
|
|
static func containerKind(forTypeName name: String) -> ContainerKind? {
|
|
switch name {
|
|
case "GLib.List": return .list
|
|
case "GLib.SList": return .slist
|
|
case "GLib.HashTable": return .hashTable
|
|
case "GLib.Array": return .array
|
|
case "GLib.PtrArray": return .ptrArray
|
|
case "GLib.ByteArray": return .byteArray
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
/// Maps a GIR type name string to the corresponding `GIRType` case.
|
|
///
|
|
/// Recognizes the full set of GLib primitive names and dotted
|
|
/// namespace-qualified references (e.g. `Gtk.Widget`). Unknown names become
|
|
/// an unqualified `.typeRef`, which the ``TypeRegistry`` later resolves —
|
|
/// or reports as unresolvable, rather than fabricating a Swift type.
|
|
///
|
|
/// - Parameter name: The GIR type name (e.g. `"gint32"`, `"utf8"`, `"Gtk.Widget"`).
|
|
/// - Returns: The corresponding `GIRType` value.
|
|
static func girType(forName name: String) -> GIRType {
|
|
switch name {
|
|
case "none": return .void
|
|
case "gboolean": return .boolean
|
|
case "gint8": return .int8
|
|
case "gint16": return .int16
|
|
case "gint", "gint32": return .int32
|
|
case "gint64": return .int64
|
|
case "guint8": return .uint8
|
|
case "guint16": return .uint16
|
|
case "guint", "guint32": return .uint32
|
|
case "guint64": return .uint64
|
|
case "glong", "gintptr", "time_t": return .long
|
|
case "gulong", "guintptr": return .ulong
|
|
case "gsize": return .size
|
|
case "gssize", "goffset": return .ssize
|
|
case "gchar": return .char
|
|
case "gshort": return .int16
|
|
case "guchar": return .uchar
|
|
case "gushort": return .uint16
|
|
case "gunichar": return .unichar
|
|
case "gunichar2": return .uint16
|
|
case "GType": return .gtype
|
|
case "gfloat": return .float
|
|
case "gdouble": return .double
|
|
case "utf8": return .string
|
|
case "filename": return .filename
|
|
case "gpointer", "gconstpointer": return .pointer
|
|
case "va_list": return .vaList
|
|
default:
|
|
if let dotIndex = name.firstIndex(of: ".") {
|
|
let ns = String(name[..<dotIndex])
|
|
let type = String(name[name.index(after: dotIndex)...])
|
|
return .typeRef(type, namespace: ns)
|
|
}
|
|
return .typeRef(name)
|
|
}
|
|
}
|
|
}
|