Tier 3 compile-gate compliance: Pango, GdkPixbuf, Graphene
Root causes fixed, all in TypeMapper.swift / TypeRegistry.swift / Planner.swift / PlanRenderer.swift / BindingPlan.swift / IRModel.swift / XMLParser.swift: - Global (cross-module) inherited-member post-pass in planModules replaces the old in-module-only version, dropping properties/methods redeclared by an ancestor in a DIFFERENT module (e.g. Pango.Coverage.ref()/unref() vs. GObject.Object) - transfer-ownership="none" GObject-class returns now route through init(retaining:) instead of adopting a borrowed pointer as owned - Non-GObject fundamental root classes (GParamSpec) capture their GIR glib:ref-func/glib:unref-func instead of hardcoding g_object_ref - Interface conformance clauses annotate each interface with @MainActor to satisfy isolated-conformance checking across module boundaries - Scalar marshalOut (.direct/.numericCast) returned through a pointer C type with no <array> length now skips instead of misreading a raw buffer pointer as its pointee value - Property/method Swift-name collisions on the same class are resolved by dropping the property (e.g. Pango.FontFamily.isVariable) - MultiPackageAnalyzer only walks includes for configured packages - No unconditional Foundation import (collided with Gio.InputStream) - gdk_pixbuf_non_anim_new added to knownMissingCFunctions (GIR-declared, not exported through the public umbrella header) Smoke tests: smoke/tier3/PixbufSmoke.swift (Pixbuf construction + dimension round-trip against real libgdk_pixbuf) and smoke/tier3/PangoSmoke.swift (findBaseDir against real libpango, Latin vs. Hebrew script direction). 208 unit tests pass (+14: InheritedMemberTests + E2 sections in PropertyGenerationTests/RendererCallableTests/others). compile-gate.sh 1, 2, and 3 all PASS. smoke-test.sh 3 passes 21/21 (18 tier-1 base + 1 GdkPixbuf + 2 Pango).
This commit is contained in:
parent
29872707f2
commit
ea41629a1f
18 changed files with 518 additions and 80 deletions
|
|
@ -503,6 +503,12 @@ public struct ClassPlan: Equatable, Sendable {
|
|||
/// Whether this class descends from `InitiallyUnowned`, requiring
|
||||
/// `g_object_ref_sink` on construction.
|
||||
public let descendsFromInitiallyUnowned: Bool
|
||||
/// The C function that takes a reference on this class's underlying
|
||||
/// pointer (`init(retaining:)`). `"g_object_ref"` for ordinary
|
||||
/// `GObject`-derived classes; overridden for classes that root their own
|
||||
/// non-`GObject` fundamental type hierarchy (e.g. `GParamSpec`'s
|
||||
/// `g_param_spec_ref_sink` — see `TypeRegistry.refUnrefFunctions`).
|
||||
public let refFunc: String
|
||||
/// Planned constructors (empty for abstract classes).
|
||||
public let constructors: [CallablePlan]
|
||||
/// Planned instance methods.
|
||||
|
|
@ -522,6 +528,7 @@ public struct ClassPlan: Equatable, Sendable {
|
|||
isOpen: Bool = false, isAbstract: Bool = false,
|
||||
getTypeFunction: String? = nil,
|
||||
descendsFromInitiallyUnowned: Bool = false,
|
||||
refFunc: String = "g_object_ref",
|
||||
interfaces: [String] = [],
|
||||
constructors: [CallablePlan] = [], methods: [CallablePlan] = [],
|
||||
functions: [CallablePlan] = [], properties: [PropertyPlan] = [],
|
||||
|
|
@ -531,6 +538,7 @@ public struct ClassPlan: Equatable, Sendable {
|
|||
self.isOpen = isOpen; self.isAbstract = isAbstract
|
||||
self.getTypeFunction = getTypeFunction
|
||||
self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned
|
||||
self.refFunc = refFunc
|
||||
self.interfaces = interfaces
|
||||
self.constructors = constructors; self.methods = methods
|
||||
self.functions = functions; self.properties = properties
|
||||
|
|
|
|||
|
|
@ -129,6 +129,14 @@ public struct Class {
|
|||
public var signals: [Signal]
|
||||
/// The functions associated with this class.
|
||||
public var functions: [GlobalFunction]
|
||||
/// The GIR `glib:ref-func` attribute, present only on the root class of a
|
||||
/// non-`GObject` fundamental type hierarchy (e.g. `GParamSpec`'s
|
||||
/// `g_param_spec_ref_sink`). `nil` for ordinary `GObject`-derived classes,
|
||||
/// which use `g_object_ref`/`g_object_ref_sink` instead.
|
||||
public var refFunc: String?
|
||||
/// The GIR `glib:unref-func` attribute — see `refFunc`. `nil` for ordinary
|
||||
/// `GObject`-derived classes, which use `g_object_unref` instead.
|
||||
public var unrefFunc: String?
|
||||
/// Creates a new class definition.
|
||||
/// - Parameters:
|
||||
/// - name: The class name, e.g. `"Widget"`.
|
||||
|
|
@ -146,12 +154,14 @@ public struct Class {
|
|||
/// - signals: The signals. Defaults to empty.
|
||||
/// - functions: The associated functions. Defaults to empty.
|
||||
/// - doc: Documentation comment from the GIR XML.
|
||||
/// - refFunc: The GIR `glib:ref-func` override, if any.
|
||||
/// - unrefFunc: The GIR `glib:unref-func` override, if any.
|
||||
public init(name: String, cType: String, parent: String?, isAbstract: Bool = false,
|
||||
isFinal: Bool = false, getTypeFunction: String? = nil, typeName: String? = nil,
|
||||
symbolInfo: SymbolInfo = SymbolInfo(),
|
||||
implements: [String] = [], constructors: [Constructor] = [], methods: [Method] = [],
|
||||
properties: [Property] = [], signals: [Signal] = [], functions: [GlobalFunction] = [],
|
||||
doc: String? = nil) {
|
||||
doc: String? = nil, refFunc: String? = nil, unrefFunc: String? = nil) {
|
||||
self.name = name; self.cType = cType; self.parent = parent
|
||||
self.isAbstract = isAbstract; self.isFinal = isFinal
|
||||
self.getTypeFunction = getTypeFunction; self.typeName = typeName
|
||||
|
|
@ -159,6 +169,7 @@ public struct Class {
|
|||
self.constructors = constructors; self.methods = methods
|
||||
self.properties = properties; self.signals = signals; self.functions = functions
|
||||
self.doc = doc
|
||||
self.refFunc = refFunc; self.unrefFunc = unrefFunc
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -917,6 +928,10 @@ public struct ReturnValue: Equatable, Sendable {
|
|||
public var transferOwnership: TransferOwnership
|
||||
/// Documentation comment from the GIR XML `<doc>` element.
|
||||
public var doc: String?
|
||||
/// The raw GIR `c:type` attribute, e.g. `"const guint8*"`. Empty when
|
||||
/// the GIR omitted it. Used to detect scalar types returned through a
|
||||
/// pointer (no `<array>` length) that the type name alone hides.
|
||||
public var cType: String
|
||||
|
||||
/// Creates a return value description.
|
||||
///
|
||||
|
|
@ -925,12 +940,14 @@ public struct ReturnValue: Equatable, Sendable {
|
|||
/// - isNullable: Whether `NULL` may be returned. Defaults to `false`.
|
||||
/// - transferOwnership: Ownership transfer to the caller. Defaults to `.none`.
|
||||
/// - doc: Documentation comment from the GIR XML.
|
||||
/// - cType: The raw GIR `c:type` attribute. Defaults to `""`.
|
||||
public init(type: GIRType = .void, isNullable: Bool = false,
|
||||
transferOwnership: TransferOwnership = .none, doc: String? = nil) {
|
||||
transferOwnership: TransferOwnership = .none, doc: String? = nil, cType: String = "") {
|
||||
self.type = type
|
||||
self.isNullable = isNullable
|
||||
self.transferOwnership = transferOwnership
|
||||
self.doc = doc
|
||||
self.cType = cType
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,12 +84,14 @@ public struct MultiPackageAnalyzer {
|
|||
|
||||
let parser = GIRParser()
|
||||
|
||||
let configuredModules = Set(config.packages.map(\.name))
|
||||
|
||||
for entry in config.packages {
|
||||
let repo = try parser.parse(fileURL: URL(fileURLWithPath: entry.girPath))
|
||||
repositories[entry.name] = repo
|
||||
|
||||
var deps = Set<String>()
|
||||
for include in repo.includedPackages {
|
||||
for include in repo.includedPackages where configuredModules.contains(include.swiftModule) {
|
||||
deps.insert(include.swiftModule)
|
||||
}
|
||||
directDeps[entry.name] = deps
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
|
|||
// Generated by SwiftGtkGen. DO NOT EDIT.
|
||||
|
||||
import C\(plan.module)
|
||||
import Foundation
|
||||
\(depImports)
|
||||
"""
|
||||
|
||||
|
|
@ -283,7 +282,6 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
// Generated by SwiftGtkGen. DO NOT EDIT.
|
||||
|
||||
import C\(moduleName)
|
||||
import Foundation
|
||||
\(depImports)
|
||||
/// Reinterprets a wrapper's raw instance pointer as an `OpaquePointer`,
|
||||
/// selected when the C function's parameter is an opaque struct pointer.
|
||||
|
|
@ -599,11 +597,11 @@ private func renderClass(_ plan: ClassPlan) -> String {
|
|||
let access = plan.isOpen ? "open" : "public"
|
||||
let parentDecl: String
|
||||
if let parent = plan.parent {
|
||||
let ifaces = plan.interfaces.isEmpty ? "" : ", \(plan.interfaces.joined(separator: ", "))"
|
||||
let ifaces = plan.interfaces.isEmpty ? "" : ", \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))"
|
||||
|
||||
parentDecl = ": \(parent)\(ifaces)"
|
||||
} else if !plan.interfaces.isEmpty {
|
||||
parentDecl = ": \(plan.interfaces.joined(separator: ", "))"
|
||||
parentDecl = ": \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))"
|
||||
} else {
|
||||
parentDecl = ""
|
||||
}
|
||||
|
|
@ -642,15 +640,20 @@ private func renderClass(_ plan: ClassPlan) -> String {
|
|||
}
|
||||
lines.append("")
|
||||
|
||||
// Retaining init
|
||||
|
||||
// Retaining init. `g_object_ref`/`g_object_ref_sink` take a plain
|
||||
// `gpointer` and accept `pointer` as-is; a class rooting its own
|
||||
// fundamental hierarchy (e.g. `GParamSpec`) declares a ref function
|
||||
// that takes its typed C struct pointer, requiring `_instancePointer`.
|
||||
let refArg = plan.refFunc == "g_object_ref" ? "pointer" : "_instancePointer(pointer)"
|
||||
if isRoot {
|
||||
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" self.pointer = pointer")
|
||||
lines.append(" g_object_ref(pointer)")
|
||||
lines.append(" \(plan.refFunc)(\(refArg))")
|
||||
lines.append(" }")
|
||||
} else {
|
||||
lines.append(" public override init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" g_object_ref(pointer)")
|
||||
lines.append(" \(plan.refFunc)(\(refArg))")
|
||||
lines.append(" super.init(takingOwnership: pointer)")
|
||||
lines.append(" }")
|
||||
}
|
||||
|
|
@ -1419,12 +1422,19 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String {
|
|||
return "String(cString: \(cCall)) /* TODO: g_free */"
|
||||
}
|
||||
return "String(cString: \(cCall))"
|
||||
case .objectWrap, .objectRetain:
|
||||
case .objectWrap:
|
||||
let isOptional = mapping.swiftType.hasSuffix("?")
|
||||
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
|
||||
let wrapExpr = "\(baseType)(takingOwnership: _rawPointer(\(cCall)))"
|
||||
if isOptional { return "\(cCall).map { \(baseType)(takingOwnership: _rawPointer($0)) }" }
|
||||
return wrapExpr
|
||||
return "\(baseType)(takingOwnership: _rawPointer(\(cCall)))"
|
||||
case .objectRetain:
|
||||
// transfer-ownership="none": the C call keeps its own reference, so
|
||||
// the wrapper must take a new one instead of adopting the borrowed
|
||||
// pointer outright (see TypeMapper.swift's `.object` case).
|
||||
let isOptional = mapping.swiftType.hasSuffix("?")
|
||||
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
|
||||
if isOptional { return "\(cCall).map { \(baseType)(retaining: _rawPointer($0)) }" }
|
||||
return "\(baseType)(retaining: _rawPointer(\(cCall)))"
|
||||
case .interfaceWrap(let adopt):
|
||||
let isOptional = mapping.swiftType.hasSuffix("?")
|
||||
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
|
||||
|
|
|
|||
|
|
@ -51,6 +51,83 @@ public func planModules(
|
|||
modulePlans[moduleName] = plan
|
||||
}
|
||||
|
||||
// Global post-pass: drop class properties/methods already declared,
|
||||
// with the identical Swift name, by an ancestor class — possibly in a
|
||||
// different module (e.g. `Pango.Coverage.ref()`/`unref()` already
|
||||
// declared by `GObject.Object`). GObject inheritance provides these for
|
||||
// free, and Swift errors on the redundant redeclaration. Keyed by
|
||||
// unqualified Swift name: safe because `duplicateOfDependency` skipping
|
||||
// guarantees surviving cross-module type names are unique along
|
||||
// dependency edges, and inheritance only follows those edges.
|
||||
var classesByName: [String: ClassPlan] = [:]
|
||||
for (_, plan) in modulePlans {
|
||||
for case .class(let p) in plan.types { classesByName[p.name] = p }
|
||||
}
|
||||
|
||||
func methodSignature(_ m: CallablePlan) -> String {
|
||||
"\(m.name)/\(m.parameters.filter { !$0.isInstanceParameter }.count)"
|
||||
}
|
||||
|
||||
func ancestorNames(of plan: ClassPlan) -> (props: Set<String>, methods: Set<String>) {
|
||||
var propNames: Set<String> = []
|
||||
var methodSigs: Set<String> = []
|
||||
var current = plan.parent
|
||||
var seen: Set<String> = [plan.name]
|
||||
while let parentName = current, !seen.contains(parentName), let parentPlan = classesByName[parentName] {
|
||||
seen.insert(parentName)
|
||||
propNames.formUnion(parentPlan.properties.map(\.swiftName))
|
||||
methodSigs.formUnion(parentPlan.methods.map(methodSignature))
|
||||
current = parentPlan.parent
|
||||
}
|
||||
return (propNames, methodSigs)
|
||||
}
|
||||
|
||||
for (moduleName, modulePlan) in modulePlans {
|
||||
var skips = modulePlan.skips
|
||||
var changed = false
|
||||
let newTypes = modulePlan.types.map { typePlan -> TypePlan in
|
||||
guard case .class(let plan) = typePlan else { return typePlan }
|
||||
let inherited = ancestorNames(of: plan)
|
||||
guard !inherited.props.isEmpty || !inherited.methods.isEmpty else { return typePlan }
|
||||
|
||||
let filteredProps = plan.properties.filter { prop in
|
||||
guard inherited.props.contains(prop.swiftName) else { return true }
|
||||
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
|
||||
cIdentifier: prop.girName, reason: .inheritedMember,
|
||||
detail: "property '\(prop.swiftName)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
let filteredMethods = plan.methods.filter { method in
|
||||
guard inherited.methods.contains(methodSignature(method)) else { return true }
|
||||
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(method.name)",
|
||||
cIdentifier: method.cIdentifier, reason: .inheritedMember,
|
||||
detail: "method '\(method.name)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count else {
|
||||
return typePlan
|
||||
}
|
||||
changed = true
|
||||
let newPlan = ClassPlan(
|
||||
name: plan.name, cType: plan.cType, parent: plan.parent,
|
||||
isOpen: plan.isOpen, isAbstract: plan.isAbstract,
|
||||
getTypeFunction: plan.getTypeFunction,
|
||||
descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned,
|
||||
refFunc: plan.refFunc,
|
||||
interfaces: plan.interfaces,
|
||||
constructors: plan.constructors, methods: filteredMethods,
|
||||
functions: plan.functions, properties: filteredProps,
|
||||
signals: plan.signals, doc: plan.doc
|
||||
)
|
||||
return .class(newPlan)
|
||||
}
|
||||
guard changed else { continue }
|
||||
modulePlans[moduleName] = ModulePlan(
|
||||
module: modulePlan.module, dependencyModules: modulePlan.dependencyModules,
|
||||
types: newTypes, skips: skips, coverage: modulePlan.coverage
|
||||
)
|
||||
}
|
||||
|
||||
return modulePlans
|
||||
}
|
||||
|
||||
|
|
@ -213,64 +290,6 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
|
|||
boundCallables -= lostCallables
|
||||
}
|
||||
|
||||
// Post-pass: drop class properties/methods already declared, with the
|
||||
// identical Swift name, by an in-module ancestor — GObject inheritance
|
||||
// provides them for free, and Swift errors on the redundant redeclaration
|
||||
// (e.g. `SimpleIOStream`'s construct-only `inputStream`/`outputStream`
|
||||
// properties, already declared by its ancestor `IOStream`).
|
||||
do {
|
||||
var classesByName: [String: ClassPlan] = [:]
|
||||
for case .class(let p) in types { classesByName[p.name] = p }
|
||||
|
||||
func ancestorNames(of plan: ClassPlan) -> Set<String> {
|
||||
var propNames: Set<String> = []
|
||||
var methodNames: Set<String> = []
|
||||
var current = plan.parent
|
||||
var seen: Set<String> = [plan.name]
|
||||
while let parentName = current, !seen.contains(parentName), let parentPlan = classesByName[parentName] {
|
||||
seen.insert(parentName)
|
||||
propNames.formUnion(parentPlan.properties.map(\.swiftName))
|
||||
methodNames.formUnion(parentPlan.methods.map(\.name))
|
||||
current = parentPlan.parent
|
||||
}
|
||||
return propNames.union(methodNames)
|
||||
}
|
||||
|
||||
types = types.map { typePlan in
|
||||
guard case .class(let plan) = typePlan else { return typePlan }
|
||||
let inherited = ancestorNames(of: plan)
|
||||
guard !inherited.isEmpty else { return typePlan }
|
||||
|
||||
let filteredProps = plan.properties.filter { prop in
|
||||
guard inherited.contains(prop.swiftName) else { return true }
|
||||
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.name).\(prop.swiftName)",
|
||||
cIdentifier: prop.girName, reason: .inheritedMember,
|
||||
detail: "property '\(prop.swiftName)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
let filteredMethods = plan.methods.filter { method in
|
||||
guard inherited.contains(method.name) else { return true }
|
||||
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.name).\(method.name)",
|
||||
cIdentifier: method.cIdentifier, reason: .inheritedMember,
|
||||
detail: "method '\(method.name)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count else {
|
||||
return typePlan
|
||||
}
|
||||
let newPlan = ClassPlan(
|
||||
name: plan.name, cType: plan.cType, parent: plan.parent,
|
||||
isOpen: plan.isOpen, isAbstract: plan.isAbstract,
|
||||
getTypeFunction: plan.getTypeFunction,
|
||||
descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned,
|
||||
interfaces: plan.interfaces,
|
||||
constructors: plan.constructors, methods: filteredMethods,
|
||||
functions: plan.functions, properties: filteredProps,
|
||||
signals: plan.signals, doc: plan.doc
|
||||
)
|
||||
return .class(newPlan)
|
||||
}
|
||||
}
|
||||
let coverage = CoverageStats(
|
||||
boundCallables: boundCallables, totalCallables: totalCallables,
|
||||
boundTypes: boundTypes, totalTypes: totalTypes,
|
||||
|
|
@ -547,6 +566,9 @@ private let knownMissingCFunctions: Set<String> = [
|
|||
"g_null_settings_backend_new", "g_memory_settings_backend_new",
|
||||
// Declared in <gio/gnetworking.h>, likewise excluded from <gio/gio.h>.
|
||||
"g_networking_init",
|
||||
// Declared in the GdkPixbuf GIR but not exported through the public
|
||||
// <gdk-pixbuf/gdk-pixbuf.h> umbrella header.
|
||||
"gdk_pixbuf_non_anim_new",
|
||||
]
|
||||
|
||||
let knownMisleadingCFunctions: Set<String> = [
|
||||
|
|
@ -872,13 +894,14 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
|
||||
let isOpen = registry.subclassedTypes().contains(girName)
|
||||
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
|
||||
let refFunc = registry.refUnrefFunctions(for: girName).ref
|
||||
|
||||
// Resolve implemented interfaces to their Swift names, excluding any
|
||||
// already provided by an ancestor class (redundant conformance is a
|
||||
// Swift error, e.g. `DataInputStream: …, Seekable` when its ancestor
|
||||
// `BufferedInputStream` already conforms).
|
||||
let ancestorInterfaceGirNames: Set<String> = Set(registry.ancestry(of: girName).flatMap { ancestor -> [String] in
|
||||
if case .object(_, _, _, let ifaces) = ancestor.category { return ifaces }
|
||||
if case .object(_, _, _, let ifaces, _, _) = ancestor.category { return ifaces }
|
||||
return []
|
||||
})
|
||||
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
|
||||
|
|
@ -932,6 +955,21 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
}
|
||||
}
|
||||
|
||||
// A property whose Swift name collides with a planned method/constructor/
|
||||
// function of the same class is dropped — Swift rejects the redundant
|
||||
// redeclaration (e.g. `Pango.FontFamily.isVariable` property vs.
|
||||
// `isVariable()` method, both derived from the same GIR member).
|
||||
let takenCallableNames: Set<String> = Set(
|
||||
methodPlans.map(\.name) + constructorPlans.map(\.name) + functionPlans.map(\.name)
|
||||
)
|
||||
propertyPlans = propertyPlans.filter { prop in
|
||||
guard takenCallableNames.contains(prop.swiftName) else { return true }
|
||||
memberSkips.append(SkipEntry(symbol: "\(girName).\(prop.swiftName)",
|
||||
cIdentifier: prop.girName, reason: .nameCollision,
|
||||
detail: "property '\(prop.swiftName)' collides with a method of the same name"))
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Signals ──
|
||||
var signalPlans: [SignalPlan] = []
|
||||
for signal in klass.signals where signal.symbolInfo.isBindable {
|
||||
|
|
@ -950,6 +988,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
isAbstract: klass.isAbstract,
|
||||
getTypeFunction: klass.getTypeFunction,
|
||||
descendsFromInitiallyUnowned: descendsIU,
|
||||
refFunc: refFunc,
|
||||
interfaces: interfaceNames,
|
||||
constructors: constructorPlans,
|
||||
methods: methodPlans,
|
||||
|
|
@ -1119,6 +1158,21 @@ private func planCallable(
|
|||
reason: .callbackWithoutUserData,
|
||||
detail: "callback return type '\(returnMap.swiftType)' deferred"))
|
||||
}
|
||||
// A scalar type (`.direct`/`.numericCast` marshalOut) returned
|
||||
// through a pointer C type with no `<array>` length (e.g.
|
||||
// `guint8` from `c:type="const guint8*"`) is a raw buffer
|
||||
// pointer the mapper's scalar-name match hid — cannot be
|
||||
// marshalled as the pointee value.
|
||||
if returnValue.cType.hasSuffix("*") {
|
||||
switch returnMap.marshalOut {
|
||||
case .direct, .numericCast:
|
||||
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
|
||||
reason: .unknownType,
|
||||
detail: "return C type '\(returnValue.cType)' is a pointer to a scalar with no array length"))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
returnMapping = returnMap
|
||||
case .failure(let error as MapError):
|
||||
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
|
||||
|
|
@ -1272,6 +1326,11 @@ private func planSignal(_ signal: Signal, onClass className: String,
|
|||
/// - Returns: A constructor plan, or a skip entry.
|
||||
func planConstructor(_ ctor: Constructor, className: String, descendsIU: Bool, context: MapContext) -> CallablePlanResult {
|
||||
let fullName = "\(context.currentNamespace).\(className).\(ctor.name)"
|
||||
if knownMissingCFunctions.contains(ctor.cIdentifier) {
|
||||
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
|
||||
reason: .unknownType,
|
||||
detail: "C symbol '\(ctor.cIdentifier)' is not visible through the public umbrella header"))
|
||||
}
|
||||
// GError** is implicit in throws="1", not in the parameter list.
|
||||
let paramPlanResult = planParameters(ctor.parameters, context: context)
|
||||
guard case .success(let paramPlans) = paramPlanResult else {
|
||||
|
|
|
|||
|
|
@ -241,8 +241,16 @@ private func mapTypeRef(
|
|||
|
||||
case .object:
|
||||
let swiftType = context.registry.swiftTypeName(for: resolved, in: context.currentModule)
|
||||
// `transfer-ownership="none"` means the C call does not hand us a new
|
||||
// reference (e.g. GBinding.get_source()/get_target()): the wrapper
|
||||
// must take its own ref via `init(retaining:)`, or its `deinit` will
|
||||
// unref a reference it never owned (over-release -> use-after-free).
|
||||
// `"full"` (and the degenerate `"container"` case, which doesn't
|
||||
// apply to a bare object pointer) hands us an owned reference already,
|
||||
// so the wrapper just adopts it via `init(takingOwnership:)`.
|
||||
return Mapping(swiftType: swiftType, cSwiftType: "UnsafeMutableRawPointer?",
|
||||
marshalIn: .objectPointer, marshalOut: .objectWrap(sink: false),
|
||||
marshalIn: .objectPointer,
|
||||
marshalOut: transfer == .full ? .objectWrap(sink: false) : .objectRetain,
|
||||
gvalue: GValueOps(typeMacro: "G_TYPE_OBJECT",
|
||||
getterSuffix: "object", setterSuffix: "object"),
|
||||
category: .needsClass)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ public enum TypeCategory: Equatable, Sendable {
|
|||
/// - isAbstract: Whether the class cannot be instantiated directly.
|
||||
/// - isFinal: Whether GIR forbids subclassing.
|
||||
/// - interfaces: Fully qualified names of implemented interfaces.
|
||||
case object(parentGIRName: String?, isAbstract: Bool, isFinal: Bool, interfaces: [String])
|
||||
/// - refFunc: GIR `glib:ref-func` override, present only on the root of
|
||||
/// a non-`GObject` fundamental hierarchy (e.g. `GParamSpec`).
|
||||
/// - unrefFunc: GIR `glib:unref-func` override — see `refFunc`.
|
||||
case object(parentGIRName: String?, isAbstract: Bool, isFinal: Bool, interfaces: [String],
|
||||
refFunc: String? = nil, unrefFunc: String? = nil)
|
||||
/// A GObject interface.
|
||||
/// - Parameter prereqs: Fully qualified prerequisite type names.
|
||||
case interface(prereqs: [String])
|
||||
|
|
@ -196,7 +200,9 @@ public struct TypeRegistry: Sendable {
|
|||
parentGIRName: cls.parent.map(qualify),
|
||||
isAbstract: cls.isAbstract,
|
||||
isFinal: cls.isFinal,
|
||||
interfaces: cls.implements.map(qualify)
|
||||
interfaces: cls.implements.map(qualify),
|
||||
refFunc: cls.refFunc,
|
||||
unrefFunc: cls.unrefFunc
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -327,7 +333,7 @@ public struct TypeRegistry: Sendable {
|
|||
var seen: Set<String> = [girName]
|
||||
var current = girName
|
||||
while let resolved = resolve(girName: current),
|
||||
case .object(let parent, _, _, _) = resolved.category,
|
||||
case .object(let parent, _, _, _, _, _) = resolved.category,
|
||||
let parentName = parent,
|
||||
!seen.contains(parentName),
|
||||
let parentType = resolve(girName: parentName)
|
||||
|
|
@ -362,6 +368,31 @@ public struct TypeRegistry: Sendable {
|
|||
return ancestry(of: girName).contains { $0.girName == Self.objectGIRName }
|
||||
}
|
||||
|
||||
/// Resolves the C ref/unref functions that manage a class's lifetime.
|
||||
///
|
||||
/// Ordinary `GObject`-derived classes use `g_object_ref`/`g_object_unref`
|
||||
/// (with `g_object_ref_sink` substituted separately for
|
||||
/// `InitiallyUnowned` construction — see `descendsFromInitiallyUnowned`).
|
||||
/// Some classes are the root of their own *non*-`GObject` fundamental
|
||||
/// type hierarchy — e.g. `GParamSpec`, whose GIR declares
|
||||
/// `glib:ref-func="g_param_spec_ref_sink"` /
|
||||
/// `glib:unref-func="g_param_spec_unref"` on the root class only.
|
||||
/// Calling `g_object_ref` on such a pointer trips
|
||||
/// `G_IS_OBJECT`/`G_IS_OBJECT_CLASS` assertions at runtime, since it was
|
||||
/// never actually a `GObject`.
|
||||
///
|
||||
/// - Parameter girName: The fully qualified class name.
|
||||
/// - Returns: The ref/unref C function names to use for this class.
|
||||
public func refUnrefFunctions(for girName: String) -> (ref: String, unref: String) {
|
||||
for type in [resolve(girName: girName)].compactMap({ $0 }) + ancestry(of: girName) {
|
||||
if case .object(_, _, _, _, let refFunc, let unrefFunc) = type.category,
|
||||
let refFunc, let unrefFunc {
|
||||
return (refFunc, unrefFunc)
|
||||
}
|
||||
}
|
||||
return ("g_object_ref", "g_object_unref")
|
||||
}
|
||||
|
||||
/// The Swift spelling of a resolved type as written from `module`.
|
||||
///
|
||||
/// Types from another module are qualified (`GObject.Object`); types from
|
||||
|
|
@ -386,7 +417,7 @@ public struct TypeRegistry: Sendable {
|
|||
public func subclassedTypes() -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
for type in types.values {
|
||||
if case .object(let parent, _, _, _) = type.category, let parent {
|
||||
if case .object(let parent, _, _, _, _, _) = type.category, let parent {
|
||||
result.insert(parent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,9 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
|||
isFinal: attributeDict["final"] == "1",
|
||||
getTypeFunction: attributeDict["glib:get-type"],
|
||||
typeName: attributeDict["glib:type-name"],
|
||||
symbolInfo: Self.symbolInfo(from: attributeDict)
|
||||
symbolInfo: Self.symbolInfo(from: attributeDict),
|
||||
refFunc: attributeDict["glib:ref-func"],
|
||||
unrefFunc: attributeDict["glib:unref-func"]
|
||||
)))
|
||||
|
||||
case "interface":
|
||||
|
|
@ -587,6 +589,7 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
|||
$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
|
||||
|
|
|
|||
|
|
@ -134,6 +134,21 @@ struct FunctionGenerationTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test("A scalar return type whose c:type is a pointer with no array length is skipped")
|
||||
func pointerToScalarReturnSkip() throws {
|
||||
// Mirrors gdk_pixbuf_read_pixels: GIR names the return `guint8` but
|
||||
// c:type is `const guint8*` — a raw buffer pointer, not a scalar.
|
||||
let fn = GlobalFunction(
|
||||
name: "read_pixels", cIdentifier: "gdk_pixbuf_read_pixels",
|
||||
parameters: [],
|
||||
returnValue: ReturnValue(type: .uint8, cType: "const guint8*")
|
||||
)
|
||||
guard case .skip(let entry) = planFunction(fn, context: makeContext()) else {
|
||||
Issue.record("expected pointer-to-scalar return to be skipped"); return
|
||||
}
|
||||
#expect(entry.reason == .unknownType)
|
||||
}
|
||||
|
||||
@Test("Unexported C symbols never reach a plan")
|
||||
func missingSymbolSkip() throws {
|
||||
// g_open is a `#define g_open open` macro on non-Windows: not a real
|
||||
|
|
|
|||
133
Tests/SwiftGtkGenCoreTests/InheritedMemberTests.swift
Normal file
133
Tests/SwiftGtkGenCoreTests/InheritedMemberTests.swift
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// InheritedMemberTests.swift
|
||||
// Covers Phase E2's global (cross-module) inherited-member post-pass in
|
||||
// `planModules`: a class in one Swift module that redeclares a same-name,
|
||||
// same-arity method already provided by an ancestor class in a DIFFERENT
|
||||
// module (e.g. `Pango.Coverage.ref()`/`unref()` vs. `GObject.Object.ref()`/
|
||||
// `unref()`) must have its own copy dropped — Swift rejects the redundant
|
||||
// redeclaration across module boundaries. A same-name method with a
|
||||
// DIFFERENT arity (legitimate overload, e.g. `MemoryOutputStream.getData()`
|
||||
// vs. inherited `Object.getData(key:)`) must NOT be dropped.
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import SwiftGtkGenCore
|
||||
|
||||
@Suite("Cross-module inherited-member dedup")
|
||||
struct InheritedMemberTests {
|
||||
/// Builds a two-module `MultiPackageAnalysis` + `TypeRegistry`: module A
|
||||
/// declares root class `Object` with `ref()` (no args) and `getData(key:)`
|
||||
/// (one arg); module B declares `Coverage: Object` (redeclaring `ref()`
|
||||
/// with the SAME arity) and `Stream: Object` (redeclaring `getData()`
|
||||
/// with a DIFFERENT arity — a legitimate overload).
|
||||
private func planTwoModules() throws -> [String: ModulePlan] {
|
||||
let tmpDir = NSTemporaryDirectory() + "inherited_member_test_\(UUID().uuidString)"
|
||||
try FileManager.default.createDirectory(atPath: tmpDir, withIntermediateDirectories: true)
|
||||
|
||||
let moduleAGIR = """
|
||||
<?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">
|
||||
<c:include name="gobject.h"/>
|
||||
<package name="gobject-2.0"/>
|
||||
<namespace name="AMod" version="1.0"
|
||||
shared-library="libamod.so.0" c:identifier-prefixes="A">
|
||||
<class name="Object" c:type="AObject" parent="" glib:type-name="AObject"
|
||||
glib:get-type="a_object_get_type">
|
||||
<method name="ref" c:identifier="a_object_ref">
|
||||
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
|
||||
</method>
|
||||
<method name="get_data" c:identifier="a_object_get_data">
|
||||
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
|
||||
<parameters>
|
||||
<instance-parameter name="self" transfer-ownership="none">
|
||||
<type name="Object" c:type="AObject*"/>
|
||||
</instance-parameter>
|
||||
<parameter name="key" transfer-ownership="none">
|
||||
<type name="utf8" c:type="const char*"/>
|
||||
</parameter>
|
||||
</parameters>
|
||||
</method>
|
||||
</class>
|
||||
</namespace>
|
||||
</repository>
|
||||
"""
|
||||
let moduleAPath = tmpDir + "/AMod-1.0.gir"
|
||||
try moduleAGIR.write(toFile: moduleAPath, atomically: true, encoding: .utf8)
|
||||
|
||||
let moduleBGIR = """
|
||||
<?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">
|
||||
<include name="AMod" version="1.0"/>
|
||||
<c:include name="bmod.h"/>
|
||||
<package name="bmod-1.0"/>
|
||||
<namespace name="BMod" version="1.0"
|
||||
shared-library="libbmod.so.0" c:identifier-prefixes="B">
|
||||
<class name="Coverage" c:type="BCoverage" parent="AMod.Object"
|
||||
glib:type-name="BCoverage" glib:get-type="b_coverage_get_type">
|
||||
<method name="ref" c:identifier="b_coverage_ref">
|
||||
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
|
||||
</method>
|
||||
</class>
|
||||
<class name="Stream" c:type="BStream" parent="AMod.Object"
|
||||
glib:type-name="BStream" glib:get-type="b_stream_get_type">
|
||||
<method name="get_data" c:identifier="b_stream_get_data">
|
||||
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
|
||||
<parameters>
|
||||
<instance-parameter name="self" transfer-ownership="none">
|
||||
<type name="Stream" c:type="BStream*"/>
|
||||
</instance-parameter>
|
||||
</parameters>
|
||||
</method>
|
||||
</class>
|
||||
</namespace>
|
||||
</repository>
|
||||
"""
|
||||
let moduleBPath = tmpDir + "/BMod-1.0.gir"
|
||||
try moduleBGIR.write(toFile: moduleBPath, atomically: true, encoding: .utf8)
|
||||
|
||||
let packages: [PackageEntry] = [
|
||||
PackageEntry(name: "AMod", girPath: moduleAPath),
|
||||
PackageEntry(name: "BMod", girPath: moduleBPath),
|
||||
]
|
||||
let config = MonorepoConfig(outputDir: tmpDir, packages: packages)
|
||||
let analysis = try MultiPackageAnalyzer(config: config).analyze()
|
||||
let registry = TypeRegistry(repositories: analysis.repositories)
|
||||
return planModules(analysis: analysis, registry: registry)
|
||||
}
|
||||
|
||||
@Test("A same-arity method redeclared across a cross-module inheritance edge is dropped")
|
||||
func crossModuleSameArityCollisionIsDropped() throws {
|
||||
let plans = try planTwoModules()
|
||||
guard let bPlan = plans["BMod"],
|
||||
case .class(let coverage)? = bPlan.types.first(where: {
|
||||
if case .class(let c) = $0 { return c.name == "Coverage" }
|
||||
return false
|
||||
}) else {
|
||||
Issue.record("expected BMod.Coverage to plan"); return
|
||||
}
|
||||
#expect(!coverage.methods.contains { $0.name == "ref" })
|
||||
#expect(bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Coverage.ref") })
|
||||
}
|
||||
|
||||
@Test("A different-arity method with the same base name across modules is kept (legitimate overload)")
|
||||
func crossModuleDifferentArityOverloadIsKept() throws {
|
||||
let plans = try planTwoModules()
|
||||
guard let bPlan = plans["BMod"],
|
||||
case .class(let stream)? = bPlan.types.first(where: {
|
||||
if case .class(let c) = $0 { return c.name == "Stream" }
|
||||
return false
|
||||
}) else {
|
||||
Issue.record("expected BMod.Stream to plan"); return
|
||||
}
|
||||
// Stream.getData() (0 args) must survive despite Object.getData(key:)
|
||||
// (1 arg) sharing the base name — different arity, not a collision.
|
||||
#expect(stream.methods.contains { $0.name == "getData" })
|
||||
#expect(!bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Stream.getData") })
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ struct InterfaceConformanceTests {
|
|||
let classSrc = files["TypeModule.swift"] ?? ""
|
||||
let ifaceSrc = files["TypePlugin.swift"] ?? ""
|
||||
|
||||
#expect(classSrc.contains("class TypeModule: Object, TypePlugin {"))
|
||||
#expect(classSrc.contains("class TypeModule: Object, @MainActor TypePlugin {"))
|
||||
|
||||
// Protocol body is bare — only the `pointer` requirement.
|
||||
#expect(ifaceSrc.contains("public protocol TypePlugin {"))
|
||||
|
|
|
|||
|
|
@ -88,4 +88,27 @@ struct MultiPackageAnalysisTests {
|
|||
let gobjExtLibs = analysis.packageConfigs["GObject"]?.externalLibraries
|
||||
#expect(gobjExtLibs?.contains("GLib") == true)
|
||||
}
|
||||
|
||||
@Test func excludesForeignIncludesNotInConfig() throws {
|
||||
// GObject's GIR <include>s both GLib (configured) and a foreign
|
||||
// namespace ("HarfBuzz") that has no package entry — mirroring
|
||||
// Pango's real-world <include name="HarfBuzz"/>. The foreign
|
||||
// include must not surface as a direct/transitive dependency,
|
||||
// an external library, or a re-export target.
|
||||
let config = try buildConfig()
|
||||
var gobjSrc = try String(contentsOfFile: config.packages[1].girPath, encoding: .utf8)
|
||||
gobjSrc = gobjSrc.replacingOccurrences(
|
||||
of: "<include name=\"GLib\" version=\"2.0\"/>",
|
||||
with: "<include name=\"GLib\" version=\"2.0\"/>\n <include name=\"HarfBuzz\" version=\"0.0\"/>"
|
||||
)
|
||||
try gobjSrc.write(toFile: config.packages[1].girPath, atomically: true, encoding: .utf8)
|
||||
|
||||
let analyzer = MultiPackageAnalyzer(config: config)
|
||||
let analysis = try analyzer.analyze()
|
||||
|
||||
#expect(analysis.directDependencies["GObject"]?.contains("GLib") == true)
|
||||
#expect(analysis.directDependencies["GObject"]?.contains("HarfBuzz") == false)
|
||||
#expect(analysis.transitiveDependencies["GObject"]?.contains("HarfBuzz") == false)
|
||||
#expect(analysis.packageConfigs["GObject"]?.externalLibraries.contains("HarfBuzz") == false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,24 @@ struct ParserSemanticsTests {
|
|||
#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(
|
||||
|
|
|
|||
|
|
@ -402,4 +402,22 @@ struct PropertyGenerationTests {
|
|||
#expect(plan.properties[0].swiftType == "String?")
|
||||
#expect(source.contains("g_value_get_string(&gvalue).map { String(cString: $0) }"))
|
||||
}
|
||||
|
||||
// MARK: - E2: property/method name collision (Pango.FontFamily.isVariable)
|
||||
|
||||
@Test("A property whose Swift name collides with a planned method is dropped, keeping the method")
|
||||
func propertyCollidingWithMethodIsSkipped() throws {
|
||||
// GIR `is-variable` property and `is_variable` method both yield the
|
||||
// Swift name `isVariable` — `var isVariable: Bool` and
|
||||
// `func isVariable() -> Bool` are an invalid redeclaration.
|
||||
let prop = Property(name: "is-variable", type: .boolean, isReadable: true, isWritable: false)
|
||||
let method = getterMethod("is_variable", returns: .boolean)
|
||||
let (source, plan, skips) = renderClass(named: "FontFamily", properties: [prop], methods: [method])
|
||||
|
||||
#expect(plan.properties.isEmpty)
|
||||
#expect(plan.methods.contains { $0.name == "isVariable" })
|
||||
#expect(skips.contains { $0.reason == .nameCollision })
|
||||
#expect(!source.contains("public var isVariable"))
|
||||
#expect(source.contains("func isVariable("))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,4 +295,15 @@ struct RendererCallableTests {
|
|||
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
|
||||
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
|
||||
}
|
||||
|
||||
// MARK: - E2: no unconditional Foundation import (collides with Gio.InputStream)
|
||||
|
||||
@Test("Generated module and support headers do not import Foundation")
|
||||
func generatedHeadersOmitFoundationImport() throws {
|
||||
let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats())
|
||||
let files = renderModule(module)
|
||||
for (name, content) in files {
|
||||
#expect(!content.contains("import Foundation"), "\(name) unexpectedly imports Foundation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ struct TypeMapperTests {
|
|||
|
||||
// MARK: - Type references
|
||||
|
||||
@Test("Object typeRef maps to pointer wrapper")
|
||||
@Test("Object typeRef with transfer=none maps to objectRetain (must take its own ref)")
|
||||
func objectTypeRef() throws {
|
||||
let ctx = makeContext()
|
||||
let mapping = try map(.typeRef("Object", namespace: "GObject"),
|
||||
|
|
@ -285,6 +285,14 @@ struct TypeMapperTests {
|
|||
#expect(mapping.swiftType == "Object")
|
||||
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
|
||||
#expect(mapping.marshalIn == .objectPointer)
|
||||
#expect(mapping.marshalOut == .objectRetain)
|
||||
}
|
||||
|
||||
@Test("Object typeRef with transfer=full maps to objectWrap (adopts the owned ref)")
|
||||
func objectTypeRefFullTransfer() throws {
|
||||
let ctx = makeContext()
|
||||
let mapping = try map(.typeRef("Object", namespace: "GObject"),
|
||||
nullable: false, transfer: .full, context: ctx)
|
||||
#expect(mapping.marshalOut == .objectWrap(sink: false))
|
||||
}
|
||||
|
||||
|
|
|
|||
38
smoke/tier3/PangoSmoke.swift
Normal file
38
smoke/tier3/PangoSmoke.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// PangoSmoke.swift
|
||||
// Tier-3-only runtime smoke test for Pango (Phase E2). Proves — against
|
||||
// the REAL libpango, linked at runtime — that:
|
||||
// 1. `findBaseDir(text:length:)` reaches the real `pango_find_base_dir`
|
||||
// C call, correctly marshalling a Swift `String` through `withCString`
|
||||
// and marshalling the returned `PangoDirection` C enum back into the
|
||||
// Swift `Direction` enum via `numericCast`.
|
||||
// 2. The result reflects the REAL Unicode bidirectional algorithm run by
|
||||
// libpango — not stubbed/placeholder data — by exercising both a
|
||||
// strong-LTR script (Latin) and a strong-RTL script (Hebrew) and
|
||||
// asserting the opposite directions libpango is known to report.
|
||||
//
|
||||
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
|
||||
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
|
||||
// running `swift test`. Only installed for tier >= 3 (Pango's module).
|
||||
|
||||
import Testing
|
||||
|
||||
import GLib
|
||||
import GObject
|
||||
import Pango
|
||||
|
||||
@Suite("Tier 3 Pango smoke tests")
|
||||
struct PangoSmokeTests {
|
||||
@Test("findBaseDir(text:length:) reaches the real pango_find_base_dir and reports LTR for Latin script")
|
||||
func findBaseDirLatinIsLTR() throws {
|
||||
let text = "hello world"
|
||||
let direction = findBaseDir(text: text, length: -1)
|
||||
#expect(direction == .ltr)
|
||||
}
|
||||
|
||||
@Test("findBaseDir(text:length:) reaches the real pango_find_base_dir and reports RTL for Hebrew script")
|
||||
func findBaseDirHebrewIsRTL() throws {
|
||||
let text = "שלום עולם"
|
||||
let direction = findBaseDir(text: text, length: -1)
|
||||
#expect(direction == .rtl)
|
||||
}
|
||||
}
|
||||
36
smoke/tier3/PixbufSmoke.swift
Normal file
36
smoke/tier3/PixbufSmoke.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// PixbufSmoke.swift
|
||||
// Tier-3-only runtime smoke test for GdkPixbuf (Phase E2). Proves — against
|
||||
// the REAL libgdk_pixbuf, linked at runtime — that:
|
||||
// 1. `Pixbuf(colorspace:hasAlpha:bitsPerSample:width:height:)` reaches the
|
||||
// real `gdk_pixbuf_new` C call, correctly marshalling an enum param
|
||||
// (`Colorspace`), a `Bool` param (`hasAlpha` → `gboolean`), and `Int32`
|
||||
// params (`bitsPerSample`/`width`/`height`), and wraps the returned
|
||||
// `GdkPixbuf*` as a live GObject via `takingOwnership`.
|
||||
// 2. `getWidth()`/`getHeight()` round-trip through `gdk_pixbuf_get_width`/
|
||||
// `gdk_pixbuf_get_height`, returning the values passed to the
|
||||
// constructor — not stubbed/placeholder data.
|
||||
//
|
||||
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
|
||||
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
|
||||
// running `swift test`. Only installed for tier ≥ 3 (GdkPixbuf's module).
|
||||
|
||||
import Testing
|
||||
|
||||
import GLib
|
||||
import GObject
|
||||
import GdkPixbuf
|
||||
|
||||
@Suite("Tier 3 GdkPixbuf smoke tests")
|
||||
struct PixbufSmokeTests {
|
||||
@Test("Pixbuf(colorspace:hasAlpha:bitsPerSample:width:height:) constructs a real GdkPixbuf and getWidth()/getHeight() round-trip")
|
||||
func pixbufConstructionAndDimensions() throws {
|
||||
let pixbuf = Pixbuf(colorspace: .rgb, hasAlpha: true, bitsPerSample: 8, width: 4, height: 7)
|
||||
|
||||
// Dimension round-trip: these values only match if the constructor
|
||||
// actually reached `gdk_pixbuf_new` with the right arguments and the
|
||||
// getters call back into the real C object rather than returning
|
||||
// stub data.
|
||||
#expect(pixbuf.getWidth() == 4)
|
||||
#expect(pixbuf.getHeight() == 7)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue