1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/XMLParser.swift

502 lines
23 KiB
Swift

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. The parser
/// handles all GIR element types including classes, interfaces, records,
/// enumerations, bitfields, callbacks, methods, properties, signals, functions,
/// constants, and aliases.
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: - XMLParser Delegate
/// SAX-style delegate for `XMLParser` that builds a `Repository` from GIR XML.
///
/// Tracks a stack of currently-open GIR elements via `current*` properties.
/// On `didStartElement` it creates the corresponding model object and populates
/// attributes. On `didEndElement` it appends the completed object to its parent.
/// The final `repository` property contains the fully parsed GIR document.
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?
var currentNamespace: Namespace?
var currentClass: Class?
var currentInterface: Interface?
var currentRecord: Record?
var currentEnum: Enumeration?
var currentBitfield: Bitfield?
var currentMethod: Method?
var currentConstructor: Constructor?
var currentSignal: Signal?
var currentProperty: Property?
var currentFunction: GlobalFunction?
var currentParameter: Parameter?
var currentCallback: Callback?
var currentConstant: Constant?
var currentAlias: Alias?
var currentReturnType: GIRType?
var currentText: String = ""
/// Counter for nested untracked elements that contain child elements.
/// When > 0, `<doc>` elements belong to untracked parents and should be discarded.
var untrackedDepth: Int = 0
/// Called when the XML parser encounters an opening element tag.
///
/// Creates the corresponding model object for the element and populates it
/// from XML attributes. Maintains a stack of `current*` properties so that
/// nested elements can attach themselves to their parent on close.
/// - Parameters:
/// - parser: The XML parser.
/// - elementName: The name of the XML element.
/// - namespaceURI: The namespace URI of the element.
/// - qualifiedName: The qualified name of the element.
/// - attributeDict: The element's attributes.
func parser(_ parser: XMLParser, didStartElement elementName: String,
namespaceURI: String?, qualifiedName: String?,
attributes attributeDict: [String: String] = [:]) {
switch elementName {
case "c:include":
guard let name = attributeDict["name"] else { return }
repository.cHeaderPath = name
case "package":
guard let name = attributeDict["name"] else { return }
repository.packageName = name
case "include":
guard let name = attributeDict["name"], let version = attributeDict["version"] else { return }
// GIR dependency includes appear before the <namespace> element
// Derive link name: lowercase(library) + "-" + major version (e.g. "Gdk-4.0" "gdk-4")
let majorVersion = version.split(separator: ".").first.map(String.init) ?? version
let linkName = "\(name.lowercased())-\(majorVersion)"
repository.includedLibraryLinks.append(linkName)
repository.includedPackages.append(IncludeEntry(name: name, version: version))
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 }
let sharedLibrary = attributeDict["shared-library"] ?? ""
let cIdentifierPrefix = attributeDict["c:identifier-prefixes"] ?? ""
currentNamespace = Namespace(name: name, version: version, cSharedLibrary: sharedLibrary, cIdentifierPrefix: cIdentifierPrefix)
case "class":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
let parent = attributeDict["parent"]
let isAbstract = attributeDict["abstract"] == "1"
currentClass = Class(name: name, cType: cType, parent: parent, isAbstract: isAbstract)
case "interface":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
let prereqs = attributeDict["prerequisite"]?.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } ?? []
currentInterface = Interface(name: name, cType: cType, prereqs: prereqs)
case "record":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentRecord = Record(name: name, cType: cType, isOpaque: attributeDict["opaque"] == "1",
isDisguised: attributeDict["disguised"] == "1")
case "enumeration":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentEnum = Enumeration(name: name, cType: cType)
case "bitfield":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentBitfield = Bitfield(name: name, cType: cType)
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)
currentEnum?.members.append(member)
currentBitfield?.members.append(member)
case "callback":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentCallback = Callback(name: name, cType: cType)
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 }
currentConstructor = Constructor(name: name, cIdentifier: cid)
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 }
currentMethod = Method(name: name, cIdentifier: cid)
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 }
currentFunction = GlobalFunction(name: name, cIdentifier: cid)
case "signal":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
case "glib:signal":
guard let name = attributeDict["name"] ?? attributeDict["glib:name"] else { return }
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
case "property":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
currentProperty = Property(name: name, type: .void,
isReadable: attributeDict["readable"] != "0",
isWritable: attributeDict["writable"] == "1",
isConstructOnly: attributeDict["construct-only"] == "1")
case "parameter", "instance-parameter":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let transfer = TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none
let cType = attributeDict["c:type"] ?? ""
currentParameter = Parameter(name: name, type: .void, cType: cType,
isNullable: attributeDict["nullable"] == "1",
isOptional: attributeDict["optional"] == "1",
transferOwnership: transfer,
isInstanceParameter: elementName == "instance-parameter")
case "return-value":
currentReturnType = .void
case "type":
let typeName = attributeDict["name"] ?? "none"
let resolvedType = parseGIRType(typeName)
let cType = attributeDict["c:type"] ?? ""
if currentParameter != nil {
currentParameter?.type = resolvedType
if !cType.isEmpty {
currentParameter?.cType = cType
}
} else if currentReturnType != nil {
currentReturnType = resolvedType
} else if currentProperty != nil {
currentProperty?.type = resolvedType
} else if currentConstant != nil {
currentConstant?.type = resolvedType
} else if currentAlias != nil {
currentAlias?.target = resolvedType
}
case "array":
if let param = currentParameter {
currentParameter?.type = .array(param.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 }
currentConstant = 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 }
currentAlias = Alias(name: name, cType: cType, target: .void)
case "implements":
if let name = attributeDict["name"] {
currentClass?.implements.append(name)
}
case "doc-version", "doc-deprecated", "doc-stability", "source-position":
break
case "doc":
currentText = ""
break
case "field":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let isReadable = attributeDict["readable"] != "0"
let isWritable = attributeDict["writable"] == "1"
currentRecord?.fields.append(Field(name: name, type: .void, isReadable: isReadable, isWritable: isWritable))
case "virtual-method", "parameters":
untrackedDepth += 1
default:
break
}
}
/// Called when the XML parser encounters a closing element tag.
///
/// Finalizes the current model object, resolves the return type if one was
/// collected, appends the object to its parent container, and clears the
/// corresponding `current*` property.
/// - Parameters:
/// - parser: The XML parser.
/// - elementName: The name of the XML element.
/// - namespaceURI: The namespace URI of the element.
/// - qualifiedName: The qualified name of the element.
func parser(_ parser: XMLParser, didEndElement elementName: String,
namespaceURI: String?, qualifiedName: String?) {
switch elementName {
case "namespace":
if let ns = currentNamespace { repository.namespaces.append(ns) }
currentNamespace = nil
case "class":
if let cls = currentClass { currentNamespace?.classes.append(cls) }
currentClass = nil
case "interface":
if let iface = currentInterface { currentNamespace?.interfaces.append(iface) }
currentInterface = nil
case "record":
if let record = currentRecord { currentNamespace?.records.append(record) }
currentRecord = nil
case "enumeration":
if let enm = currentEnum { currentNamespace?.enumerations.append(enm) }
currentEnum = nil
case "bitfield":
if let bf = currentBitfield { currentNamespace?.bitfields.append(bf) }
currentBitfield = nil
case "callback":
if let cb = currentCallback {
// Only add to namespace when at the top level (not inside a class/interface/record)
if currentClass == nil && currentInterface == nil && currentRecord == nil {
currentNamespace?.callbacks.append(cb)
}
}
currentCallback = nil
case "constructor":
if var ctor = currentConstructor {
if let rt = currentReturnType, rt != .void {
ctor.returnType = rt
}
currentClass?.constructors.append(ctor)
}
currentConstructor = nil
currentReturnType = nil
case "method":
if var method = currentMethod {
if let rt = currentReturnType, rt != .void {
method.returnType = rt
}
currentClass?.methods.append(method)
currentInterface?.methods.append(method)
currentRecord?.methods.append(method)
}
currentMethod = nil
currentReturnType = nil
case "function":
if var fn = currentFunction {
if let rt = currentReturnType, rt != .void {
fn.returnType = rt
}
// Class/interface/record-level functions are not globals
if currentClass != nil {
currentClass?.functions.append(fn)
} else if currentRecord != nil {
currentRecord?.methods.append(Method(name: fn.name, cIdentifier: fn.cIdentifier,
parameters: fn.parameters, returnType: fn.returnType))
} else if currentInterface != nil {
currentInterface?.functions.append(fn)
} else {
currentNamespace?.functions.append(fn)
}
}
currentFunction = nil
currentReturnType = nil
case "signal", "glib:signal":
if var sig = currentSignal {
if let rt = currentReturnType, rt != .void {
sig.returnType = rt
}
currentClass?.signals.append(sig)
currentInterface?.signals.append(sig)
}
currentSignal = nil
currentReturnType = nil
case "property":
if let prop = currentProperty {
currentClass?.properties.append(prop)
currentInterface?.properties.append(prop)
}
currentProperty = nil
case "parameter", "instance-parameter":
if let param = currentParameter {
currentMethod?.parameters.append(param)
currentConstructor?.parameters.append(param)
currentSignal?.parameters.append(param)
currentFunction?.parameters.append(param)
currentCallback?.parameters.append(param)
}
currentParameter = nil
case "return-value":
break
case "constant":
if let c = currentConstant { currentNamespace?.constants.append(c) }
currentConstant = nil
case "alias":
if let a = currentAlias { currentNamespace?.aliases.append(a) }
currentAlias = nil
case "virtual-method", "parameters":
untrackedDepth -= 1
case "doc":
let text = currentText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
// Skip doc inside untracked elements (virtual-method, field, etc.)
guard untrackedDepth == 0 else { return }
// Check innermost (most nested) elements first to handle hierarchy
// e.g. a signal inside a class check currentSignal before currentClass
if currentParameter != nil { currentParameter?.doc = text }
else if currentMethod != nil { currentMethod?.doc = text }
else if currentConstructor != nil { currentConstructor?.doc = text }
else if currentSignal != nil { currentSignal?.doc = text }
else if currentProperty != nil { currentProperty?.doc = text }
else if currentCallback != nil { currentCallback?.doc = text }
else if currentFunction != nil { currentFunction?.doc = text }
else if currentInterface != nil { currentInterface?.doc = text }
else if currentEnum != nil { currentEnum?.doc = text }
else if currentBitfield != nil { currentBitfield?.doc = text }
else if currentRecord != nil { currentRecord?.doc = text }
else if currentClass != nil { currentClass?.doc = text }
else if currentConstant != nil { currentConstant?.doc = text }
else if currentAlias != nil { currentAlias?.doc = text }
default:
break
}
}
/// Called when the XML parser encounters character data between elements.
///
/// Accumulates text content for the current element, used primarily for
/// capturing `<doc>` element text content.
/// - Parameters:
/// - parser: The XML parser.
/// - string: The character data found.
func parser(_ parser: XMLParser, foundCharacters string: String) {
currentText += string
}
// MARK: - Helpers
/// Requires that an attribute exists in the given dictionary, or aborts parsing.
/// - 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
}
/// Maps a GIR type name string to the corresponding `GIRType` enum case.
///
/// Recognizes primitive GLib types (`gboolean`, `gint32`, `utf8`, etc.) and
/// dotted namespace-qualified type references (e.g. `Gtk.Widget`). Unknown
/// names are returned as an unqualified `.typeRef`.
/// - Parameter name: The GIR type name (e.g. `"gint32"`, `"utf8"`, `"Gtk.Widget"`).
/// - Returns: The corresponding `GIRType` value.
private func parseGIRType(_ 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 "gfloat": return .float
case "gdouble": return .double
case "utf8": return .string
case "filename": return .filename
case "gpointer", "gconstpointer": return .pointer
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)
}
}
}