471 lines
22 KiB
Swift
471 lines
22 KiB
Swift
// 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 `<namespace>` element.
|
|
/// - Returns: A complete GIR document string ready to parse.
|
|
private func girDocument(_ body: String) -> String {
|
|
"""
|
|
<?xml version="1.0"?>
|
|
<repository version="1.2"
|
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
|
<namespace name="Test" version="1.0" c:identifier-prefixes="Test">
|
|
\(body)
|
|
</namespace>
|
|
</repository>
|
|
"""
|
|
}
|
|
|
|
@Suite("Parser semantics")
|
|
struct ParserSemanticsTests {
|
|
/// Record fields previously always parsed as `.void` because the `<type>`
|
|
/// handler never routed to the enclosing field.
|
|
@Test("Record field types are populated")
|
|
func fieldTypesArePopulated() throws {
|
|
let repo = try GIRParser().parse(
|
|
xmlString: girDocument(
|
|
"""
|
|
<record name="RGBA" c:type="GdkRGBA" glib:get-type="gdk_rgba_get_type">
|
|
<field name="red" writable="1">
|
|
<type name="gfloat" c:type="float"/>
|
|
</field>
|
|
<field name="alpha" writable="1">
|
|
<type name="gfloat" c:type="float"/>
|
|
</field>
|
|
</record>
|
|
"""))
|
|
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 `<callback>`
|
|
/// child rather than a `<type>`. 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(
|
|
"""
|
|
<record name="IOFuncs" c:type="GIOFuncs">
|
|
<field name="io_read">
|
|
<callback name="io_read">
|
|
<return-value transfer-ownership="none"><type name="gint"/></return-value>
|
|
<parameters>
|
|
<parameter name="channel"><type name="gpointer"/></parameter>
|
|
</parameters>
|
|
</callback>
|
|
</field>
|
|
<field name="depth"><type name="gint" c:type="int"/></field>
|
|
</record>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget">
|
|
<method name="measure" c:identifier="gtk_widget_measure">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
<parameters>
|
|
<instance-parameter name="widget"><type name="Widget"/></instance-parameter>
|
|
<parameter name="minimum" direction="out" caller-allocates="0">
|
|
<type name="gint" c:type="int*"/>
|
|
</parameter>
|
|
<parameter name="orientation" direction="in">
|
|
<type name="gint" c:type="int"/>
|
|
</parameter>
|
|
</parameters>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="File" c:type="GFile">
|
|
<method name="load" c:identifier="g_file_load" throws="1">
|
|
<return-value transfer-ownership="none"><type name="gboolean"/></return-value>
|
|
</method>
|
|
<method name="peek" c:identifier="g_file_peek">
|
|
<return-value transfer-ownership="none"><type name="gboolean"/></return-value>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget">
|
|
<method name="get_name" c:identifier="gtk_widget_get_name">
|
|
<return-value transfer-ownership="full" nullable="1">
|
|
<type name="utf8" c:type="char*"/>
|
|
</return-value>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<record name="Pixbuf" c:type="GdkPixbuf">
|
|
<method name="read_pixels" c:identifier="gdk_pixbuf_read_pixels">
|
|
<return-value transfer-ownership="none">
|
|
<type name="guint8" c:type="const guint8*"/>
|
|
</return-value>
|
|
</method>
|
|
</record>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Object" c:type="GObject">
|
|
<method name="set_valist" c:identifier="g_object_set_valist" introspectable="0">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
</method>
|
|
<method name="set" c:identifier="g_object_set" shadowed-by="set_property">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
</method>
|
|
<method name="get_data" c:identifier="g_object_get_data" deprecated="1" deprecated-version="2.4">
|
|
<return-value transfer-ownership="none"><type name="gpointer"/></return-value>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget" parent="GObject.InitiallyUnowned"
|
|
abstract="1" glib:type-name="GtkWidget" glib:get-type="gtk_widget_get_type">
|
|
<implements name="Accessible"/>
|
|
<implements name="Buildable"/>
|
|
</class>
|
|
<class name="Label" c:type="GtkLabel" parent="Widget" final="1"
|
|
glib:type-name="GtkLabel" glib:get-type="gtk_label_get_type"/>
|
|
"""))
|
|
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(
|
|
"""
|
|
<record name="WidgetClass" c:type="GtkWidgetClass" glib:is-gtype-struct-for="Widget"/>
|
|
<record name="RGBA" c:type="GdkRGBA" glib:get-type="gdk_rgba_get_type"/>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget">
|
|
<method name="set_names" c:identifier="gtk_widget_set_names">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
<parameters>
|
|
<parameter name="names">
|
|
<array length="1" zero-terminated="0" c:type="char**">
|
|
<type name="utf8" c:type="char*"/>
|
|
</array>
|
|
</parameter>
|
|
<parameter name="n_names"><type name="gint" c:type="int"/></parameter>
|
|
<parameter name="tags">
|
|
<array zero-terminated="1" c:type="char**">
|
|
<type name="utf8" c:type="char*"/>
|
|
</array>
|
|
</parameter>
|
|
<parameter name="quad">
|
|
<array fixed-size="4" c:type="int*">
|
|
<type name="gint" c:type="int"/>
|
|
</array>
|
|
</parameter>
|
|
<parameter name="argv">
|
|
<array c:type="char**">
|
|
<type name="utf8" c:type="char*"/>
|
|
</array>
|
|
</parameter>
|
|
<parameter name="data">
|
|
<array length="1" c:type="const guchar*">
|
|
<type name="guint8"/>
|
|
</array>
|
|
</parameter>
|
|
</parameters>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Container" c:type="GtkContainer">
|
|
<method name="get_children" c:identifier="gtk_container_get_children">
|
|
<return-value transfer-ownership="container">
|
|
<type name="GLib.List" c:type="GList*">
|
|
<type name="Widget"/>
|
|
</type>
|
|
</return-value>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget">
|
|
<method name="add_tick" c:identifier="gtk_widget_add_tick_callback">
|
|
<return-value transfer-ownership="none"><type name="guint"/></return-value>
|
|
<parameters>
|
|
<parameter name="callback" scope="notified" closure="1" destroy="2">
|
|
<type name="TickCallback" c:type="GtkTickCallback"/>
|
|
</parameter>
|
|
<parameter name="user_data"><type name="gpointer"/></parameter>
|
|
<parameter name="notify"><type name="GLib.DestroyNotify"/></parameter>
|
|
</parameters>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Label" c:type="GtkLabel">
|
|
<property name="label" writable="1" transfer-ownership="none"
|
|
getter="get_label" setter="set_label">
|
|
<type name="utf8" c:type="gchar*"/>
|
|
</property>
|
|
</class>
|
|
"""))
|
|
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(
|
|
"""
|
|
<interface name="Buildable" c:type="GtkBuildable" glib:get-type="gtk_buildable_get_type">
|
|
<prerequisite name="GObject.Object"/>
|
|
</interface>
|
|
"""))
|
|
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(
|
|
"""
|
|
<class name="Widget" c:type="GtkWidget">
|
|
<virtual-method name="snapshot">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
<parameters>
|
|
<instance-parameter name="widget"><type name="Widget"/></instance-parameter>
|
|
<parameter name="snapshot"><type name="Snapshot"/></parameter>
|
|
</parameters>
|
|
</virtual-method>
|
|
<method name="show" c:identifier="gtk_widget_show">
|
|
<return-value transfer-ownership="none"><type name="none"/></return-value>
|
|
</method>
|
|
</class>
|
|
"""))
|
|
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 <method>.
|
|
#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(
|
|
"""
|
|
<enumeration name="Align" c:type="GtkAlign" glib:type-name="GtkAlign"
|
|
glib:get-type="gtk_align_get_type">
|
|
<member name="fill" value="0" c:identifier="GTK_ALIGN_FILL"/>
|
|
<member name="baseline_fill" value="4" c:identifier="GTK_ALIGN_BASELINE_FILL"/>
|
|
<member name="baseline" value="4" c:identifier="GTK_ALIGN_BASELINE"/>
|
|
</enumeration>
|
|
<enumeration name="FileError" c:type="GFileError" glib:error-domain="g-file-error-quark">
|
|
<member name="exist" value="0" c:identifier="G_FILE_ERROR_EXIST"/>
|
|
</enumeration>
|
|
"""))
|
|
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))
|
|
}
|
|
}
|