1
0
Fork 0

Emit isolated deinit on root class wrappers, fix five review findings

This commit is contained in:
Brendan Szymanski 2026-07-21 00:24:22 -04:00
parent 57ff84bce5
commit 4f1016f682
12 changed files with 288 additions and 111 deletions

View file

@ -81,6 +81,48 @@ struct CallbackGenerationTests {
#expect(entry.reason == .callbackWithoutUserData)
}
// MARK: - Parameter/return nullability (review finding #2)
@Test("Callback param and return nullability propagate into the mapped closure type")
func callbackNullabilityPropagates() throws {
let glib = Repository(namespaces: [
Namespace(name: "GLib", version: "2.0",
callbacks: [Callback(
name: "NullableFunc", cType: "GNullableFunc",
parameters: [
Parameter(name: "value", type: .string, cType: "const char*", isNullable: true),
],
returnValue: ReturnValue(type: .string, isNullable: true)
)])
])
let registry = TypeRegistry(repositories: ["GLib": glib])
let ctx = MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib")
let mapping = try map(.typeRef("NullableFunc", namespace: "GLib"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType.contains("String?"))
#expect(mapping.swiftType.hasSuffix("-> String?"))
}
@Test("A non-nullable callback param/return stays non-optional")
func callbackNonNullableStaysNonOptional() throws {
let glib = Repository(namespaces: [
Namespace(name: "GLib", version: "2.0",
callbacks: [Callback(
name: "PlainFunc", cType: "GPlainFunc",
parameters: [
Parameter(name: "value", type: .string, cType: "const char*", isNullable: false),
],
returnValue: ReturnValue(type: .string, isNullable: false)
)])
])
let registry = TypeRegistry(repositories: ["GLib": glib])
let ctx = MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib")
let mapping = try map(.typeRef("PlainFunc", namespace: "GLib"),
nullable: false, transfer: .none, context: ctx)
#expect(!mapping.swiftType.contains("String?"))
#expect(mapping.swiftType.hasSuffix("-> String"))
}
// MARK: - Renderer infrastructure (constructed directly D4.3 render-side proof)
/// A `.callbackBox(scope: .call, )` parameter that doubles as its own

View file

@ -255,6 +255,11 @@ struct ParserSemanticsTests {
<type name="gint" c:type="int"/>
</array>
</parameter>
<parameter name="argv">
<array c:type="char**">
<type name="utf8" c:type="char*"/>
</array>
</parameter>
</parameters>
</method>
</class>
@ -282,6 +287,16 @@ struct ParserSemanticsTests {
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)
}
@Test("Container element types are captured")

View file

@ -129,6 +129,29 @@ struct PropertyGenerationTests {
#expect(source.contains(" public var length: Int32"))
}
@Test("A root class's isolated deinit unrefs its pointer via g_object_unref")
func rootClassEmitsUnrefDeinit() throws {
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
let (source, plan, _) = renderClass(named: "Buffer", properties: [prop])
#expect(plan.unrefFunc == "g_object_unref")
#expect(source.contains("isolated deinit {"))
#expect(source.contains("g_object_unref(pointer)"))
}
@Test("A class rooting a non-GObject fundamental unrefs through _instancePointer")
func fundamentalRootClassUnrefsTypedPointer() throws {
let plan = ClassPlan(
name: "ParamSpec", girName: "GObject.ParamSpec", cType: "GParamSpec",
getTypeFunction: "g_param_spec_get_type",
refFunc: "g_param_spec_ref", unrefFunc: "g_param_spec_unref"
)
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let source = renderModule(module)["ParamSpec.swift"] ?? ""
#expect(source.contains("isolated deinit {"))
#expect(source.contains("g_param_spec_unref(_instancePointer(pointer))"))
}
@Test("Writable string property generates both a getter and a setter")
func writableStringProperty() throws {
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)

View file

@ -376,15 +376,40 @@ struct TypeMapperTests {
@Test("Alias follows to underlying type")
func aliasFollowsToUnderlying() throws {
let ctx = makeContext()
// GLib.Strv is an alias for a zero-terminated string array.
// Without array bridging, this throws .arrayWithoutLength but the
// alias is *followed*, which is what this test asserts.
// GLib.Strv is an alias for a zero-terminated string array, which has
// a known length (zero-termination) but array bridging itself isn't
// implemented yet .arrayBridgingUnimplemented, not
// .arrayWithoutLength. The alias is *followed*, which is what this
// test asserts.
do {
_ = try map(.typeRef("Strv", namespace: "GLib"),
nullable: false, transfer: .none, context: ctx)
Issue.record("Expected arrayWithoutLength error")
Issue.record("Expected arrayBridgingUnimplemented error")
} catch {
#expect(error.reason == .arrayWithoutLength)
#expect(error.reason == .arrayBridgingUnimplemented)
}
}
@Test("Cyclic type alias throws instead of overflowing the stack")
func cyclicAliasThrows() throws {
let cyclic = Repository(namespaces: [
Namespace(
name: "Cyclic", version: "1.0",
aliases: [
Alias(name: "A", cType: "CyclicA", target: .typeRef("B", namespace: "Cyclic")),
Alias(name: "B", cType: "CyclicB", target: .typeRef("A", namespace: "Cyclic")),
]
)
])
let registry = TypeRegistry(repositories: ["Cyclic": cyclic])
let ctx = MapContext(registry: registry, currentModule: "Cyclic", currentNamespace: "Cyclic")
do {
_ = try map(.typeRef("A", namespace: "Cyclic"),
nullable: false, transfer: .none, context: ctx)
Issue.record("Expected unknownType error from cyclic alias detection")
} catch {
#expect(error.reason == .unknownType)
#expect(error.detail.contains("cyclic"))
}
}
@ -425,15 +450,15 @@ struct TypeMapperTests {
}
}
@Test("cArray with length throws arrayWithoutLength (not yet bridged)")
@Test("cArray with length throws arrayBridgingUnimplemented (not yet bridged)")
func cArrayWithLengthNotYetBridged() {
let ctx = makeContext()
do {
_ = try map(.cArray(.int32, ArrayInfo(lengthParameterIndex: 0)),
nullable: false, transfer: .none, context: ctx)
Issue.record("Expected arrayWithoutLength error")
Issue.record("Expected arrayBridgingUnimplemented error")
} catch {
#expect(error.reason == .arrayWithoutLength)
#expect(error.reason == .arrayBridgingUnimplemented)
}
}