// ParserSemanticsTests.swift // Covers the GIR attributes the binding planner depends on: parameter // direction, GError throwing, introspectability, GType registration, record // type-structs, array length metadata, container element types, and record // field types. Each of these was previously dropped on the floor by the parser. import Foundation import Testing @testable import GObjectGeneratorCore /// Wraps GIR element markup in a minimal well-formed repository document. /// /// - Parameter body: The XML to place inside the `` element. /// - Returns: A complete GIR document string ready to parse. private func girDocument(_ body: String) -> String { """ \(body) """ } @Suite("Parser semantics") struct ParserSemanticsTests { /// Record fields previously always parsed as `.void` because the `` /// handler never routed to the enclosing field. @Test("Record field types are populated") func fieldTypesArePopulated() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let record = try #require(repo.namespaces.first?.records.first) #expect(record.fields.count == 2) #expect(record.fields.allSatisfy { $0.type == .float }) #expect(record.isBoxed) } /// C vtable structs hold function-pointer members, spelled as a `` /// child rather than a ``. Such a field is an opaque function pointer /// at the ABI level, and must not be left as `.void`. @Test("Function-pointer fields are typed as pointers") func callbackFieldsAreTypedAsPointers() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let ns = try #require(repo.namespaces.first) let record = try #require(ns.records.first) #expect(record.fields.count == 2) #expect(record.fields[0].type == .pointer) #expect(record.fields[1].type == .int32) // A callback nested in a field is not a namespace-level declaration. #expect(ns.callbacks.isEmpty) } @Test("Parameter direction and caller-allocates are captured") func parameterDirectionIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let method = try #require(repo.namespaces.first?.classes.first?.methods.first) #expect(method.parameters.count == 3) #expect(method.parameters[0].isInstanceParameter) #expect(method.parameters[1].direction == .out) #expect(!method.parameters[1].callerAllocates) #expect(method.parameters[2].direction == .in) } @Test("GError-throwing callables are flagged") func throwingCallablesAreFlagged() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let methods = try #require(repo.namespaces.first?.classes.first?.methods) #expect(methods[0].throwsGError) #expect(!methods[1].throwsGError) } @Test("Return value ownership and nullability are captured") func returnValueSemanticsAreCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let method = try #require(repo.namespaces.first?.classes.first?.methods.first) #expect(method.returnValue.type == .string) #expect(method.returnValue.transferOwnership == .full) #expect(method.returnValue.isNullable) } @Test("Return value c:type is captured (pointer-to-scalar detection)") func returnValueCTypeIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let method = try #require(repo.namespaces.first?.records.first?.methods.first) #expect(method.returnValue.type == .uint8) #expect(method.returnValue.cType == "const guint8*") } @Test("Non-introspectable and shadowed symbols are marked unbindable") func symbolInfoIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let methods = try #require(repo.namespaces.first?.classes.first?.methods) #expect(!methods[0].symbolInfo.isIntrospectable) #expect(!methods[0].symbolInfo.isBindable) #expect(methods[1].symbolInfo.shadowedBy == "set_property") #expect(!methods[1].symbolInfo.isBindable) // Deprecation is recorded but does not by itself prevent binding. #expect(methods[2].symbolInfo.isDeprecated) #expect(methods[2].symbolInfo.deprecatedVersion == "2.4") #expect(methods[2].symbolInfo.isBindable) } @Test("Class GType registration, finality, and interfaces are captured") func classMetadataIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let classes = try #require(repo.namespaces.first?.classes) #expect(classes[0].isAbstract) #expect(!classes[0].isFinal) #expect(classes[0].parent == "GObject.InitiallyUnowned") #expect(classes[0].getTypeFunction == "gtk_widget_get_type") #expect(classes[0].typeName == "GtkWidget") #expect(classes[0].implements == ["Accessible", "Buildable"]) #expect(classes[1].isFinal) } @Test("GObject type-struct records are identified") func gtypeStructRecordsAreIdentified() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let records = try #require(repo.namespaces.first?.records) #expect(records[0].isGTypeStructFor == "Widget") #expect(!records[0].isBoxed) #expect(records[1].isGTypeStructFor == nil) #expect(records[1].isBoxed) } @Test("Array length metadata is captured") func arrayLengthMetadataIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let params = try #require(repo.namespaces.first?.classes.first?.methods.first?.parameters) guard case .cArray(let element, let info) = params[0].type else { Issue.record("expected a C array for 'names', got \(params[0].type)") return } #expect(element == .string) #expect(info.lengthParameterIndex == 1) #expect(!info.isZeroTerminated) #expect(info.hasKnownLength) guard case .cArray(_, let zeroInfo) = params[2].type else { Issue.record("expected a C array for 'tags'") return } #expect(zeroInfo.isZeroTerminated) #expect(zeroInfo.hasKnownLength) guard case .cArray(_, let fixedInfo) = params[3].type else { Issue.record("expected a C array for 'quad'") return } #expect(fixedInfo.fixedSize == 4) // No zero-terminated/length/fixed-size attribute at all: GIR omits // zero-terminated for a plain NULL-terminated array (e.g. GStrv), so // absence must default to true — not false. guard case .cArray(_, let absentInfo) = params[4].type else { Issue.record("expected a C array for 'argv'") return } #expect(absentInfo.isZeroTerminated) #expect(absentInfo.hasKnownLength) // The element's own c:type is what distinguishes a pointer vector // (`char**` of `char*`) from a contiguous value buffer (`const guchar*` // of `guint8`); the parser must carry it through, and leave it empty // when the GIR omits it. #expect(info.elementCType == "char*") guard case .cArray(_, let bufferInfo) = params[5].type else { Issue.record("expected a C array for 'data'") return } #expect(bufferInfo.elementCType == "") #expect(bufferInfo.cType == "const guchar*") } @Test("Container element types are captured") func containerElementTypesAreCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let method = try #require(repo.namespaces.first?.classes.first?.methods.first) guard case .container(let kind, let elements) = method.returnValue.type else { Issue.record("expected a container type, got \(method.returnValue.type)") return } #expect(kind == .list) #expect(elements == [.typeRef("Widget")]) #expect(method.returnValue.transferOwnership == .container) } @Test("Callback scope, closure, and destroy indices are captured") func callbackScopeIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let param = try #require(repo.namespaces.first?.classes.first?.methods.first?.parameters.first) #expect(param.scope == .notified) #expect(param.closureIndex == 1) #expect(param.destroyIndex == 2) } @Test("Property getter, setter, and transfer are captured") func propertyAccessorsAreCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let prop = try #require(repo.namespaces.first?.classes.first?.properties.first) #expect(prop.type == .string) #expect(prop.getter == "get_label") #expect(prop.setter == "set_label") #expect(prop.isWritable) } @Test("Interface prerequisites parse from child elements") func interfacePrerequisitesParse() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let iface = try #require(repo.namespaces.first?.interfaces.first) #expect(iface.prereqs == ["GObject.Object"]) #expect(iface.getTypeFunction == "gtk_buildable_get_type") } @Test("Virtual method bodies do not leak into class members") func virtualMethodsDoNotLeak() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let cls = try #require(repo.namespaces.first?.classes.first) // The virtual method contributes no method, and — critically — its // parameters do not attach to the sibling . #expect(cls.methods.count == 1) #expect(cls.methods[0].name == "show") #expect(cls.methods[0].parameters.isEmpty) } @Test("Enum GType, error domain, and aliased values are captured") func enumMetadataIsCaptured() throws { let repo = try GIRParser().parse( xmlString: girDocument( """ """)) let enums = try #require(repo.namespaces.first?.enumerations) #expect(enums[0].getTypeFunction == "gtk_align_get_type") // Aliased raw values survive parsing intact; de-duplicating them into // Swift `static var` aliases is the renderer's job. #expect(enums[0].members.count == 3) #expect(enums[0].members[1].value == "4") #expect(enums[0].members[2].value == "4") #expect(enums[1].errorDomain == "g-file-error-quark") } @Test("Full primitive type table maps without fabricating type refs") func primitiveTypeTableIsComplete() { let expected: [String: GIRType] = [ "none": .void, "gboolean": .boolean, "gint8": .int8, "gint16": .int16, "gint": .int32, "gint32": .int32, "gint64": .int64, "guint8": .uint8, "guint16": .uint16, "guint": .uint32, "guint32": .uint32, "guint64": .uint64, "glong": .long, "gulong": .ulong, "gsize": .size, "gssize": .ssize, "gchar": .char, "guchar": .uchar, "gunichar": .unichar, "GType": .gtype, "gfloat": .float, "gdouble": .double, "utf8": .string, "filename": .filename, "gpointer": .pointer, "gconstpointer": .pointer, "va_list": .vaList, ] for (name, type) in expected { #expect(GIRXMLDelegate.girType(forName: name) == type, "\(name) should map to \(type)") } // Namespace-qualified references keep their namespace for the registry. #expect(GIRXMLDelegate.girType(forName: "Gtk.Widget") == .typeRef("Widget", namespace: "Gtk")) #expect(GIRXMLDelegate.girType(forName: "Widget") == .typeRef("Widget", namespace: nil)) } }