Complete Phase E5 tier-6 Adw/Soup/Gst compile-gate compliance
Relaxes the generated-filename PascalCase validator's uppercase-run cap (>3 -> >4) so legitimate acronym type names (PluginAPIFlags, AuthNTLM) stop crashing generation. Fixes four compounding cross-module correctness bugs the larger tier-6 GIR set exposed at scale: - The duplicate-of-dependency drop only checked simple-name collision, wrongly dropping genuinely distinct C types that merely share a post-namespace-stripping Swift name (Gst.Object/GstObject vs GObject.Object/GObject, plus three tier-4/5 cases: Gdk.AppLaunchContext, Gdk.Gravity, Gdk.Rectangle). Now also requires matching cType. - Qualifying a cross-module reference as "GObject.X" broke wherever a raw C struct also named GObject was in scope (every C target's import), since Swift resolved the module name to the shadowing struct. GObject's Support.swift now exports collision-free GLibObject/GLibValueArray aliases used instead. - Missing `override` keyword: added ancestor-method-selector detection (name + parameter labels, same-module only, since none of these members are open) so a subclass narrowing an ancestor's return type compiles. - The pre-existing cross-module inherited-member dedup pass keyed ancestors by bare Swift name; its cycle guard falsely self-terminated once two classes shared a name, missing real inherited members (e.g. GstObject's own ref()/unref() were never recognized as duplicating GObject.Object's, producing an illegal redeclaration). Rewired to walk by unambiguous GIR name via new ClassPlan.girName/parentGIRName fields. Also fixes bitfield/enum-typed global constants (wraps the raw literal in Type(rawValue:)) and a void-returning ref function (gst_atomic_queue_ref, unlike GstBuffer/GObject's T*-returning convention) via new RecordPlan.copyReturnsVoid. Adds tier-6 smoke tests (Gst/Soup/Adw version calls against the real linked libraries) and refreshes the tier-4/5 skip baselines for the duplicate-detection fix's legitimate coverage growth. Zero skip-baseline drift on tiers 1-6; 220/220 unit tests and all tier smoke suites pass.
This commit is contained in:
parent
c20015be48
commit
525aefa7aa
9 changed files with 545 additions and 121 deletions
|
|
@ -123,10 +123,17 @@ public struct GValueOps: Equatable, Sendable {
|
|||
public let getterSuffix: String
|
||||
/// The `g_value_set_*` function suffix, e.g. `"int"`, `"enum"`, `"boxed"`.
|
||||
public let setterSuffix: String
|
||||
/// For `getterSuffix == "boxed"` only: whether the boxed record has a
|
||||
/// known GIR copy function. Selects `init(retaining:)` (copies the
|
||||
/// borrowed `g_value_get_boxed` pointer) when `true`, or
|
||||
/// `init(takingOwnership:)` (adopts it as-is — the only initializer a
|
||||
/// no-copy-function record exposes, see `renderRecord`) when `false`.
|
||||
/// Ignored for every other suffix.
|
||||
public let hasCopyFunction: Bool
|
||||
|
||||
public init(typeMacro: String, getterSuffix: String, setterSuffix: String) {
|
||||
public init(typeMacro: String, getterSuffix: String, setterSuffix: String, hasCopyFunction: Bool = false) {
|
||||
self.typeMacro = typeMacro; self.getterSuffix = getterSuffix
|
||||
self.setterSuffix = setterSuffix
|
||||
self.setterSuffix = setterSuffix; self.hasCopyFunction = hasCopyFunction
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -493,11 +500,20 @@ public struct CallbackTypePlan: Equatable, Sendable {
|
|||
public struct ClassPlan: Equatable, Sendable {
|
||||
/// The Swift class name (unqualified), e.g. `"Object"`.
|
||||
public let name: String
|
||||
/// The fully qualified GIR name (`"<Namespace>.<Name>"`), e.g.
|
||||
/// `"GObject.Object"`. Unlike `name`/`parent`, which are Swift spellings
|
||||
/// that may collide across modules sharing a simple name (e.g.
|
||||
/// `Gst.Object` vs `GObject.Object`), this and `parentGIRName` are
|
||||
/// always unambiguous — the safe key for any cross-module ancestor
|
||||
/// lookup (see `planModules`'s inherited-member dedup post-pass).
|
||||
public let girName: String
|
||||
/// The C type name, e.g. `"GObject"`.
|
||||
public let cType: String
|
||||
/// The module-qualified Swift parent class name, or `nil` for root.
|
||||
/// E.g. `nil` for `GObject.Object`, `"GObject.InitiallyUnowned"` for `Gtk.Widget`.
|
||||
public let parent: String?
|
||||
/// The parent's fully qualified GIR name, or `nil` for root. See `girName`.
|
||||
public let parentGIRName: String?
|
||||
/// Whether the class is `open` (subclassed by at least one other type).
|
||||
public let isOpen: Bool
|
||||
/// Whether the class is abstract — no constructors emitted.
|
||||
|
|
@ -528,7 +544,8 @@ public struct ClassPlan: Equatable, Sendable {
|
|||
/// Documentation from the GIR `<doc>` element.
|
||||
public let doc: String?
|
||||
|
||||
public init(name: String, cType: String, parent: String? = nil,
|
||||
public init(name: String, girName: String, cType: String, parent: String? = nil,
|
||||
parentGIRName: String? = nil,
|
||||
isOpen: Bool = false, isAbstract: Bool = false,
|
||||
getTypeFunction: String? = nil,
|
||||
descendsFromInitiallyUnowned: Bool = false,
|
||||
|
|
@ -538,7 +555,8 @@ public struct ClassPlan: Equatable, Sendable {
|
|||
functions: [CallablePlan] = [], properties: [PropertyPlan] = [],
|
||||
signals: [SignalPlan] = [],
|
||||
doc: String? = nil) {
|
||||
self.name = name; self.cType = cType; self.parent = parent
|
||||
self.name = name; self.girName = girName; self.cType = cType
|
||||
self.parent = parent; self.parentGIRName = parentGIRName
|
||||
self.isOpen = isOpen; self.isAbstract = isAbstract
|
||||
self.getTypeFunction = getTypeFunction
|
||||
self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned
|
||||
|
|
@ -557,15 +575,23 @@ public struct RecordPlan: Equatable, Sendable {
|
|||
public let cType: String
|
||||
public let getTypeFunction: String?
|
||||
public let copyFunction: String?
|
||||
/// Whether `copyFunction` returns `void` rather than the (possibly new)
|
||||
/// pointer — true for plain refcounting functions like
|
||||
/// `gst_atomic_queue_ref` that bump the refcount in place instead of
|
||||
/// following the `GstBuffer`/`GObject`-style `T *ref(T *)` convention.
|
||||
/// Determines whether `init(retaining:)` reassigns `self.pointer` from
|
||||
/// the call's return value or keeps the argument pointer as-is.
|
||||
public let copyReturnsVoid: Bool
|
||||
public let freeFunction: String?
|
||||
public let doc: String?
|
||||
|
||||
public init(name: String, cType: String, getTypeFunction: String? = nil,
|
||||
copyFunction: String? = nil, freeFunction: String? = nil,
|
||||
doc: String? = nil) {
|
||||
copyFunction: String? = nil, copyReturnsVoid: Bool = false,
|
||||
freeFunction: String? = nil, doc: String? = nil) {
|
||||
self.name = name; self.cType = cType
|
||||
self.getTypeFunction = getTypeFunction
|
||||
self.copyFunction = copyFunction; self.freeFunction = freeFunction
|
||||
self.copyFunction = copyFunction; self.copyReturnsVoid = copyReturnsVoid
|
||||
self.freeFunction = freeFunction
|
||||
self.doc = doc
|
||||
}
|
||||
}
|
||||
|
|
@ -627,7 +653,7 @@ public enum PropertyAccessorPlan: Equatable, Sendable {
|
|||
/// Read or write the value through the GObject GValue machinery:
|
||||
/// `g_value_init(&v, typeMacro)` + `g_object_get_property` / `_set_property`
|
||||
/// + `g_value_get_<suffix>` / `g_value_set_<suffix>`.
|
||||
case gvalue(typeMacro: String, valueSuffix: String)
|
||||
case gvalue(typeMacro: String, valueSuffix: String, hasCopyFunction: Bool = false)
|
||||
/// Forward to an already-generated accessor method on the same type, named
|
||||
/// by the GIR `getter=` / `setter=` attribute (e.g. `getLabel`/`setLabel`).
|
||||
/// `argumentLabel` is the setter method's parameter label, or `nil` for a
|
||||
|
|
@ -722,17 +748,23 @@ public struct CallablePlan: Equatable, Sendable {
|
|||
public let ownershipInit: OwnershipInit?
|
||||
/// `true` when this callable takes a trailing `GError**` and can throw.
|
||||
public let throwsError: Bool
|
||||
/// `true` when an ancestor class declares an instance method of the
|
||||
/// same Swift selector (name + arity) — e.g. `Gst.Pipeline.getBus()`
|
||||
/// narrowing `Gst.Element.getBus()`'s return type. Emits the `override`
|
||||
/// keyword, required by Swift whenever a subclass redeclares a
|
||||
/// same-selector member rather than overloading it.
|
||||
public let isOverride: Bool
|
||||
/// Documentation from the GIR `<doc>` element.
|
||||
public let doc: String?
|
||||
|
||||
public init(name: String, cIdentifier: String, parameters: [ParameterPlan],
|
||||
returnMapping: Mapping? = nil, isStatic: Bool = false,
|
||||
isConstructor: Bool = false, ownershipInit: OwnershipInit? = nil,
|
||||
throwsError: Bool = false, doc: String? = nil) {
|
||||
throwsError: Bool = false, isOverride: Bool = false, doc: String? = nil) {
|
||||
self.name = name; self.cIdentifier = cIdentifier; self.parameters = parameters
|
||||
self.returnMapping = returnMapping; self.isStatic = isStatic
|
||||
self.isConstructor = isConstructor; self.ownershipInit = ownershipInit
|
||||
self.throwsError = throwsError; self.doc = doc
|
||||
self.throwsError = throwsError; self.isOverride = isOverride; self.doc = doc
|
||||
}
|
||||
}
|
||||
/// How a constructor takes ownership of the new GObject instance.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import Foundation
|
|||
public func renderModule(_ plan: ModulePlan) -> [String: String] {
|
||||
var files: [String: String] = [:]
|
||||
|
||||
let depImports = plan.dependencyModules.map { "import \($0)\n" }.joined()
|
||||
let depImports = plan.dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined()
|
||||
let header = """
|
||||
// Generated by SwiftGtkGen. DO NOT EDIT.
|
||||
|
||||
|
|
@ -97,11 +97,12 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
|
|||
}
|
||||
/// Tests whether a generated filename follows the PascalCase convention — starts
|
||||
/// with an uppercase ASCII letter, contains only ASCII letters/digits, and
|
||||
/// never has a run of more than 3 consecutive uppercase letters immediately
|
||||
/// never has a run of more than 4 consecutive uppercase letters immediately
|
||||
/// preceded by a lowercase letter. That last rule rejects unconverted C
|
||||
/// spellings (`MarshalBOOLEAN`) while still accepting legitimate acronym
|
||||
/// runs, since those never follow a lowercase letter mid-name (`RGBA`,
|
||||
/// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`).
|
||||
/// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`, `PluginAPIFlags`,
|
||||
/// `AuthNTLM`).
|
||||
/// Per-symbol filenames are only ever derived from authoritative GIR type
|
||||
/// names (constants/functions/callbacks are merged into fixed-name files).
|
||||
///
|
||||
|
|
@ -117,7 +118,7 @@ public func isValidGeneratedFileName(_ filename: String) -> Bool {
|
|||
for ch in base {
|
||||
if ch.isUppercase && ch.isASCII {
|
||||
run += 1
|
||||
if run > 3 && precededByLower { return false }
|
||||
if run > 4 && precededByLower { return false }
|
||||
} else {
|
||||
run = 0
|
||||
precededByLower = ch.isLowercase && ch.isASCII
|
||||
|
|
@ -163,7 +164,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
public let domain: UInt32
|
||||
public let code: Int32
|
||||
public let message: String
|
||||
public init(consuming error: UnsafeMutablePointer<GError>) {
|
||||
@_spi(SGTKInternal) public init(consuming error: UnsafeMutablePointer<GError>) {
|
||||
self.domain = error.pointee.domain
|
||||
self.code = error.pointee.code
|
||||
self.message = String(cString: error.pointee.message)
|
||||
|
|
@ -203,6 +204,21 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
public let G_TYPE_OBJECT: UInt = 80
|
||||
public let G_TYPE_GTYPE: UInt = 88
|
||||
public let G_TYPE_VARIANT: UInt = 96
|
||||
|
||||
// MARK: - Collision-free aliases for cross-module qualification
|
||||
/// `GObject` (this Swift module) shares its spelling with the raw C
|
||||
/// struct `GObject` that every generated module's C target
|
||||
/// transitively vends from glib-object.h. Writing the qualifier
|
||||
/// `GObject.Object` from another module therefore resolves `GObject`
|
||||
/// to the locally visible C struct, not this module — `Object` is
|
||||
/// then not one of its members and the reference fails to
|
||||
/// typecheck. These bare, collision-free aliases are what
|
||||
/// `TypeRegistry.swiftTypeName` emits instead whenever a
|
||||
/// cross-module reference to one of these types would otherwise
|
||||
/// need qualifying (e.g. from `Gst`, which declares its own
|
||||
/// distinct `Object`/`ValueArray` classes).
|
||||
public typealias GLibObject = Object
|
||||
public typealias GLibValueArray = ValueArray
|
||||
"""
|
||||
} else {
|
||||
gtypeConstants = ""
|
||||
|
|
@ -238,9 +254,9 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
/// as a fallback per Phase D contingency; `~Copyable` would also work.
|
||||
public struct SignalHandle {
|
||||
public let id: UInt
|
||||
public let instance: UnsafeMutableRawPointer
|
||||
@_spi(SGTKInternal) public let instance: UnsafeMutableRawPointer
|
||||
private var isDisconnected: Bool = false
|
||||
public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance }
|
||||
@_spi(SGTKInternal) public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance }
|
||||
public mutating func disconnect() {
|
||||
guard !isDisconnected else { return }
|
||||
isDisconnected = true
|
||||
|
|
@ -254,7 +270,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
/// Never emitted with `@_cdecl` — per-call-site wrapping via a
|
||||
/// `@convention(c)` literal closure avoids duplicate-symbol link
|
||||
/// errors when multiple modules with signals link together.
|
||||
public nonisolated func _sgtk_destroy_notify_impl(
|
||||
@_spi(SGTKInternal) public nonisolated func _sgtk_destroy_notify_impl(
|
||||
_ data: UnsafeMutableRawPointer?,
|
||||
_ closure: UnsafeMutableRawPointer?
|
||||
) {
|
||||
|
|
@ -273,13 +289,13 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
// MARK: - manual primitives (Phase E: replace with planned bindings)
|
||||
|
||||
@_silgen_name("g_signal_connect_data")
|
||||
public nonisolated func _sgtk_signal_connect_data(
|
||||
@_spi(SGTKInternal) public nonisolated func _sgtk_signal_connect_data(
|
||||
_ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer<CChar>,
|
||||
_ cHandler: UnsafeRawPointer, _ data: UnsafeMutableRawPointer?,
|
||||
_ destroyData: UnsafeRawPointer?, _ connectFlags: UInt32
|
||||
) -> UInt
|
||||
@_silgen_name("g_signal_handler_disconnect")
|
||||
public nonisolated func _sgtk_signal_handler_disconnect(
|
||||
@_spi(SGTKInternal) public nonisolated func _sgtk_signal_handler_disconnect(
|
||||
_ instance: UnsafeMutableRawPointer, _ handlerId: UInt
|
||||
)
|
||||
"""
|
||||
|
|
@ -287,7 +303,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
|
|||
primitiveShims = ""
|
||||
}
|
||||
|
||||
let depImports = dependencyModules.map { "import \($0)\n" }.joined()
|
||||
let depImports = dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined()
|
||||
return """
|
||||
// Generated by SwiftGtkGen. DO NOT EDIT.
|
||||
|
||||
|
|
@ -363,7 +379,7 @@ private func renderEnum(_ plan: EnumPlan) -> String {
|
|||
if let doc = plan.doc {
|
||||
lines.append(contentsOf: renderDocComment(doc))
|
||||
}
|
||||
lines.append("public enum \(plan.name): Int, Sendable {")
|
||||
lines.append("public nonisolated enum \(plan.name): Int, Sendable {")
|
||||
|
||||
for c in plan.cases {
|
||||
lines.append(" case \(c.name) = \(c.rawValue)")
|
||||
|
|
@ -389,7 +405,7 @@ private func renderBitfield(_ plan: BitfieldPlan) -> String {
|
|||
if let doc = plan.doc {
|
||||
lines.append(contentsOf: renderDocComment(doc))
|
||||
}
|
||||
lines.append("public struct \(plan.name): OptionSet, Sendable {")
|
||||
lines.append("public nonisolated struct \(plan.name): OptionSet, Sendable {")
|
||||
lines.append(" public let rawValue: UInt32")
|
||||
lines.append(" public init(rawValue: UInt32) { self.rawValue = rawValue }")
|
||||
|
||||
|
|
@ -439,6 +455,45 @@ private func renderAlias(_ plan: AliasPlan) -> String {
|
|||
return lines.joined(separator: "\n") + "\n"
|
||||
}
|
||||
|
||||
// MARK: - Raw pointer detection (no-unsafe-pointer public API policy)
|
||||
|
||||
/// Raw-pointer type markers that must never appear in a plain-`public`
|
||||
/// declaration's signature, property type, or typealias — anything matching
|
||||
/// is demoted to `@_spi(SGTKInternal)` instead of being skipped (Phase E4).
|
||||
private let rawPointerMarkers = [
|
||||
"UnsafeMutableRawPointer", "UnsafeRawPointer", "UnsafeMutablePointer",
|
||||
"UnsafePointer", "OpaquePointer", "@convention(c)",
|
||||
]
|
||||
|
||||
/// `true` when a Swift-facing type string exposes a raw pointer or C
|
||||
/// function-pointer type.
|
||||
private func swiftTypeHasRawPointer(_ swiftType: String) -> Bool {
|
||||
rawPointerMarkers.contains { swiftType.contains($0) }
|
||||
}
|
||||
|
||||
/// `true` when any parameter or the return type of a callable's Swift-facing
|
||||
/// signature (including out-param-derived return types) exposes a raw
|
||||
/// pointer — such callables must be rendered `@_spi(SGTKInternal) public`
|
||||
/// rather than plain `public`.
|
||||
private func signatureHasRawPointer(_ plan: CallablePlan) -> Bool {
|
||||
let paramHit = plan.parameters.contains { param in
|
||||
!param.isInstanceParameter && swiftTypeHasRawPointer(param.mapping.swiftType)
|
||||
}
|
||||
let returnHit = plan.returnMapping.map { swiftTypeHasRawPointer($0.swiftType) } ?? false
|
||||
return paramHit || returnHit
|
||||
}
|
||||
|
||||
/// `true` when a property's Swift-facing type exposes a raw pointer.
|
||||
private func propertyHasRawPointer(_ plan: PropertyPlan) -> Bool {
|
||||
swiftTypeHasRawPointer(plan.swiftType)
|
||||
}
|
||||
|
||||
/// Renders the `@_spi(SGTKInternal) ` prefix when `hidden` is true, else
|
||||
/// the empty string — used to keep call sites terse.
|
||||
private func spiPrefix(_ hidden: Bool) -> String {
|
||||
hidden ? "@_spi(SGTKInternal) " : ""
|
||||
}
|
||||
|
||||
// ── Callback typealias ──
|
||||
|
||||
/// Renders a namespace-level callback type as both a C-compatible
|
||||
|
|
@ -455,8 +510,8 @@ private func renderCallbackType(_ plan: CallbackTypePlan) -> String {
|
|||
if let doc = plan.doc {
|
||||
lines.append(contentsOf: renderDocComment(doc))
|
||||
}
|
||||
lines.append("public typealias \(plan.name) = \(plan.cSwiftType)")
|
||||
lines.append("public typealias \(plan.name)Swift = \(plan.swiftType)")
|
||||
lines.append("@_spi(SGTKInternal) public typealias \(plan.name) = \(plan.cSwiftType)")
|
||||
lines.append("\(spiPrefix(swiftTypeHasRawPointer(plan.swiftType)))public typealias \(plan.name)Swift = \(plan.swiftType)")
|
||||
return lines.joined(separator: "\n") + "\n"
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +524,7 @@ private func renderRecord(_ plan: RecordPlan) -> String {
|
|||
lines.append(contentsOf: renderDocComment(doc))
|
||||
}
|
||||
lines.append("public final class \(plan.name) {")
|
||||
lines.append(" public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append("")
|
||||
if plan.freeFunction != nil {
|
||||
lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility")
|
||||
|
|
@ -479,18 +534,27 @@ private func renderRecord(_ plan: RecordPlan) -> String {
|
|||
lines.append(" /// never frees the pointee — safe for borrowed (transfer-none) values such")
|
||||
lines.append(" /// as signal parameters.")
|
||||
}
|
||||
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" self.pointer = pointer")
|
||||
lines.append(" }")
|
||||
|
||||
// init(retaining:) — copies a borrowed pointer so the wrapper owns its own
|
||||
// instance. Only emitted when a copy/ref function is known.
|
||||
// instance. Only emitted when a copy/ref function is known. Most C
|
||||
// ref/copy functions follow the `T *fn(T *)` convention and return the
|
||||
// (possibly new) pointer; a plain refcount bump like
|
||||
// `gst_atomic_queue_ref` returns `void` instead, so the argument
|
||||
// pointer itself is retained rather than the call's result.
|
||||
if let copy = plan.copyFunction {
|
||||
lines.append("")
|
||||
lines.append(" /// Copies a borrowed boxed pointer so the wrapper owns an independent")
|
||||
lines.append(" /// instance. Use for `transfer-ownership=\"none\"` returns.")
|
||||
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))")
|
||||
lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
if plan.copyReturnsVoid {
|
||||
lines.append(" \(copy)(_instancePointer(pointer))")
|
||||
lines.append(" self.pointer = pointer")
|
||||
} else {
|
||||
lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))")
|
||||
}
|
||||
lines.append(" }")
|
||||
}
|
||||
|
||||
|
|
@ -527,11 +591,11 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
|
|||
|
||||
var protocolInherits = ""
|
||||
if !plan.prereqs.isEmpty {
|
||||
protocolInherits = ": " + plan.prereqs.joined(separator: ", ")
|
||||
protocolInherits = ": " + plan.prereqs.map { "@MainActor \($0)" }.joined(separator: ", ")
|
||||
}
|
||||
|
||||
lines.append("public protocol \(plan.name)\(protocolInherits) {")
|
||||
lines.append(" var pointer: UnsafeMutableRawPointer { get }")
|
||||
lines.append(" @_spi(SGTKInternal) var pointer: UnsafeMutableRawPointer { get }")
|
||||
lines.append("}")
|
||||
|
||||
// Method/property bodies live in a protocol extension as default
|
||||
|
|
@ -571,18 +635,18 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
|
|||
// `Self: \(classPrereq)` (a class-typed prerequisite), so the Ref
|
||||
// must literally subclass it — inheriting its pointer storage,
|
||||
// inits, and deinit rather than declaring its own.
|
||||
lines.append("public final class \(plan.name)Ref: \(classPrereq), \(plan.name) {")
|
||||
lines.append("public final class \(plan.name)Ref: \(classPrereq), @MainActor \(plan.name) {")
|
||||
lines.append("}")
|
||||
} else {
|
||||
lines.append("public final class \(plan.name)Ref: \(plan.name) {")
|
||||
lines.append(" public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append("public final class \(plan.name)Ref: @MainActor \(plan.name) {")
|
||||
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append("")
|
||||
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" g_object_ref(pointer)")
|
||||
lines.append(" self.pointer = pointer")
|
||||
lines.append(" }")
|
||||
lines.append("")
|
||||
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" self.pointer = pointer")
|
||||
lines.append(" }")
|
||||
lines.append("")
|
||||
|
|
@ -627,7 +691,7 @@ private func renderClass(_ plan: ClassPlan) -> String {
|
|||
|
||||
// Storage — only for root classes
|
||||
if isRoot {
|
||||
lines.append(" public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
|
||||
lines.append("")
|
||||
}
|
||||
|
||||
|
|
@ -636,14 +700,14 @@ private func renderClass(_ plan: ClassPlan) -> String {
|
|||
let needsInits = isRoot || !plan.isAbstract
|
||||
|
||||
if needsInits {
|
||||
let initModifier = isRoot ? "" : "public override "
|
||||
let initModifier = isRoot ? "" : "@_spi(SGTKInternal) public override "
|
||||
|
||||
// takingOwnership init
|
||||
if isRoot {
|
||||
let body = plan.descendsFromInitiallyUnowned
|
||||
? ["g_object_ref_sink(pointer)", "self.pointer = pointer"]
|
||||
: ["self.pointer = pointer"]
|
||||
lines.append(" public required init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" @_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||
for line in body { lines.append(" \(line)") }
|
||||
lines.append(" }")
|
||||
} else {
|
||||
|
|
@ -663,12 +727,12 @@ private func renderClass(_ plan: ClassPlan) -> String {
|
|||
// 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(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" self.pointer = pointer")
|
||||
lines.append(" \(plan.refFunc)(\(refArg))")
|
||||
lines.append(" }")
|
||||
} else {
|
||||
lines.append(" public override init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" @_spi(SGTKInternal) public override init(retaining pointer: UnsafeMutableRawPointer) {")
|
||||
lines.append(" \(plan.refFunc)(\(refArg))")
|
||||
lines.append(" super.init(takingOwnership: pointer)")
|
||||
lines.append(" }")
|
||||
|
|
@ -1300,7 +1364,7 @@ private func renderCallable(_ plan: CallablePlan) -> String {
|
|||
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) }
|
||||
let retType = callableReturnType(plan) ?? ""
|
||||
let throwsKeyword = plan.throwsError ? " throws" : ""
|
||||
lines.append("public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
lines.append("\(spiPrefix(signatureHasRawPointer(plan)))public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
if hasOutParams(plan) {
|
||||
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
|
||||
} else if plan.throwsError {
|
||||
|
|
@ -1317,7 +1381,8 @@ private func renderMethod(_ plan: CallablePlan) -> [String] {
|
|||
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
|
||||
let retType = callableReturnType(plan) ?? ""
|
||||
let throwsKeyword = plan.throwsError ? " throws" : ""
|
||||
lines.append(" public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
let overrideKeyword = plan.isOverride ? "override " : ""
|
||||
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public \(overrideKeyword)func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
if hasOutParams(plan) {
|
||||
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
|
||||
} else if plan.throwsError {
|
||||
|
|
@ -1334,7 +1399,7 @@ private func renderStaticFunction(_ plan: CallablePlan) -> [String] {
|
|||
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
|
||||
let retType = callableReturnType(plan) ?? ""
|
||||
let throwsKeyword = plan.throwsError ? " throws" : ""
|
||||
lines.append(" public static func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public static func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
|
||||
if hasOutParams(plan) {
|
||||
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
|
||||
} else if plan.throwsError {
|
||||
|
|
@ -1354,7 +1419,7 @@ private func renderConstructor(_ plan: CallablePlan) -> [String] {
|
|||
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
|
||||
|
||||
let throwsKeyword = plan.throwsError ? " throws" : ""
|
||||
lines.append(" public convenience init(\(swiftSignature(plan)))\(throwsKeyword) {")
|
||||
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public convenience init(\(swiftSignature(plan)))\(throwsKeyword) {")
|
||||
|
||||
if plan.throwsError {
|
||||
let indent = " "
|
||||
|
|
@ -1529,10 +1594,11 @@ private func renderProperty(_ plan: PropertyPlan) -> [String] {
|
|||
if let doc = plan.doc {
|
||||
lines.append(contentsOf: renderDocComment(doc).map { " \($0)" })
|
||||
}
|
||||
let prefix = spiPrefix(propertyHasRawPointer(plan))
|
||||
let getterBody = propertyGetterBody(plan.getter, plan: plan)
|
||||
if let setter = plan.setter {
|
||||
let setterBody = propertySetterBody(setter, plan: plan)
|
||||
lines.append(" public var \(plan.swiftName): \(plan.swiftType) {")
|
||||
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
|
||||
lines.append(" get {")
|
||||
for line in getterBody { lines.append(" \(line)") }
|
||||
lines.append(" }")
|
||||
|
|
@ -1541,7 +1607,7 @@ private func renderProperty(_ plan: PropertyPlan) -> [String] {
|
|||
lines.append(" }")
|
||||
lines.append(" }")
|
||||
} else {
|
||||
lines.append(" public var \(plan.swiftName): \(plan.swiftType) {")
|
||||
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
|
||||
for line in getterBody { lines.append(" \(line)") }
|
||||
lines.append(" }")
|
||||
}
|
||||
|
|
@ -1553,9 +1619,9 @@ private func propertyGetterBody(_ accessor: PropertyAccessorPlan, plan: Property
|
|||
switch accessor {
|
||||
case .delegate(let method, _):
|
||||
return ["\(method)()"]
|
||||
case .gvalue(let typeMacro, let suffix):
|
||||
case .gvalue(let typeMacro, let suffix, let hasCopyFunction):
|
||||
return gvalueGetterBody(swiftType: plan.swiftType, girName: plan.girName,
|
||||
typeMacro: typeMacro, suffix: suffix)
|
||||
typeMacro: typeMacro, suffix: suffix, hasCopyFunction: hasCopyFunction)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1565,7 +1631,7 @@ private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: Property
|
|||
case .delegate(let method, let label):
|
||||
if let label { return ["\(method)(\(label): newValue)"] }
|
||||
return ["\(method)(newValue)"]
|
||||
case .gvalue(let typeMacro, let suffix):
|
||||
case .gvalue(let typeMacro, let suffix, _):
|
||||
return gvalueSetterBody(swiftType: plan.swiftType, girName: plan.girName,
|
||||
typeMacro: typeMacro, suffix: suffix)
|
||||
}
|
||||
|
|
@ -1574,7 +1640,7 @@ private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: Property
|
|||
/// Builds a GValue-machinery getter body: init a GValue, read the property into
|
||||
/// it, extract the Swift value (with the per-category bridge), and clean up.
|
||||
private func gvalueGetterBody(swiftType: String, girName: String,
|
||||
typeMacro: String, suffix: String) -> [String] {
|
||||
typeMacro: String, suffix: String, hasCopyFunction: Bool = false) -> [String] {
|
||||
let resultExpr: String
|
||||
if suffix == "boolean" {
|
||||
resultExpr = "g_value_get_boolean(&gvalue) != 0"
|
||||
|
|
@ -1601,7 +1667,9 @@ private func gvalueGetterBody(swiftType: String, girName: String,
|
|||
resultExpr = "\(baseType)(retaining: g_value_get_object(&gvalue))"
|
||||
}
|
||||
} else if suffix == "boxed" {
|
||||
resultExpr = "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
|
||||
resultExpr = hasCopyFunction
|
||||
? "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
|
||||
: "\(swiftType)(takingOwnership: g_value_get_boxed(&gvalue))"
|
||||
} else {
|
||||
resultExpr = "g_value_get_\(suffix)(&gvalue)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,25 +59,46 @@ public func planModules(
|
|||
// 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] = [:]
|
||||
var classesByGIRName: [String: ClassPlan] = [:]
|
||||
for (_, plan) in modulePlans {
|
||||
for case .class(let p) in plan.types { classesByName[p.name] = p }
|
||||
for case .class(let p) in plan.types { classesByGIRName[p.girName] = p }
|
||||
}
|
||||
|
||||
// Swift's override-conflict diagnostic ("requires an 'override' keyword"
|
||||
// / "overriding non-open instance method outside of its defining
|
||||
// module") matches candidates by SELECTOR — name + parameter labels/
|
||||
// types — regardless of return type. So the ancestor-collision key must
|
||||
// do the same: excluding return type catches true selector collisions
|
||||
// (e.g. `Pango.Coverage.ref()` vs. inherited `GObject.Object.ref()` —
|
||||
// same selector, would silently violate the override rule if kept) while
|
||||
// still keeping legitimate different-selector overloads across an
|
||||
// inheritance edge (e.g. `MenuButton.setDirection(direction: ArrowType)`
|
||||
// vs. inherited `Widget.setDirection(dir: TextDirection)` — different
|
||||
// labels AND types, a different selector, no relation to include).
|
||||
func methodSignature(_ m: CallablePlan) -> String {
|
||||
"\(m.name)/\(m.parameters.filter { !$0.isInstanceParameter }.count)"
|
||||
let params = m.parameters
|
||||
.filter { !$0.isInstanceParameter && !$0.isOutParameter }
|
||||
.map { "\($0.swiftName):\($0.mapping.swiftType)" }
|
||||
.joined(separator: ",")
|
||||
return "\(m.name)|throws:\(m.throwsError)|(\(params))"
|
||||
}
|
||||
|
||||
// Walked by GIR name, not Swift spelling: two classes in different
|
||||
// modules can share a Swift simple name (`Gst.Object` / `GObject.Object`
|
||||
// both spell `Object`), which would make a bare-name-keyed walk and its
|
||||
// cycle guard falsely conflate them — terminating the ancestor walk one
|
||||
// level early and missing real inherited members. GIR names are always
|
||||
// unique.
|
||||
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)
|
||||
var current = plan.parentGIRName
|
||||
var seen: Set<String> = [plan.girName]
|
||||
while let parentGIRName = current, !seen.contains(parentGIRName), let parentPlan = classesByGIRName[parentGIRName] {
|
||||
seen.insert(parentGIRName)
|
||||
propNames.formUnion(parentPlan.properties.map(\.swiftName))
|
||||
methodSigs.formUnion(parentPlan.methods.map(methodSignature))
|
||||
current = parentPlan.parent
|
||||
current = parentPlan.parentGIRName
|
||||
}
|
||||
return (propNames, methodSigs)
|
||||
}
|
||||
|
|
@ -90,13 +111,6 @@ public func planModules(
|
|||
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)",
|
||||
|
|
@ -104,12 +118,45 @@ public func planModules(
|
|||
detail: "method '\(method.name)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
// Own-method selectors that survived filtering — used below to
|
||||
// catch properties left delegating to a method this pass just
|
||||
// removed (the property was planned before this global pass ran,
|
||||
// using the class's full pre-filter method list).
|
||||
let survivingSelectors = Set(filteredMethods.map {
|
||||
"\($0.name)/\($0.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter }.count)"
|
||||
})
|
||||
func delegatesToDroppedMethod(_ accessor: PropertyAccessorPlan?) -> Bool {
|
||||
guard case .delegate(let method, let label) = accessor else { return false }
|
||||
let arity = label == nil ? 0 : 1
|
||||
return !survivingSelectors.contains("\(method)/\(arity)")
|
||||
}
|
||||
let filteredProps = plan.properties.filter { prop in
|
||||
if inherited.props.contains(prop.swiftName) {
|
||||
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
|
||||
cIdentifier: prop.girName, reason: .inheritedMember,
|
||||
detail: "property '\(prop.swiftName)' already declared by ancestor"))
|
||||
return false
|
||||
}
|
||||
if delegatesToDroppedMethod(prop.getter) || delegatesToDroppedMethod(prop.setter) {
|
||||
// The delegated getter/setter method collided with an
|
||||
// ancestor's identically-selectored method and was
|
||||
// dropped above — the property can no longer delegate to
|
||||
// it safely (it would silently resolve to the ancestor's
|
||||
// differently-typed method instead).
|
||||
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
|
||||
cIdentifier: prop.girName, reason: .inheritedMember,
|
||||
detail: "property '\(prop.swiftName)' delegated to a getter=/setter= method that collided with an ancestor's identically-selectored method and was dropped"))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
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,
|
||||
name: plan.name, girName: plan.girName, cType: plan.cType,
|
||||
parent: plan.parent, parentGIRName: plan.parentGIRName,
|
||||
isOpen: plan.isOpen, isAbstract: plan.isAbstract,
|
||||
getTypeFunction: plan.getTypeFunction,
|
||||
descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned,
|
||||
|
|
@ -589,11 +636,12 @@ let reservedSwiftTypes: Set<String> = [
|
|||
/// Plans a boxed record as an opaque pointer wrapper, resolving the copy and
|
||||
/// free functions that give it correct memory management (C4).
|
||||
func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
|
||||
let pair = resolvedCopyFreePair(record)
|
||||
return RecordPlan(
|
||||
name: record.name, cType: record.cType,
|
||||
getTypeFunction: record.getTypeFunction,
|
||||
copyFunction: resolvedCopyFunction(record),
|
||||
freeFunction: resolvedFreeFunction(record),
|
||||
copyFunction: pair.copy, copyReturnsVoid: pair.copyReturnsVoid,
|
||||
freeFunction: pair.free,
|
||||
doc: record.doc
|
||||
)
|
||||
}
|
||||
|
|
@ -610,30 +658,35 @@ func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
|
|||
/// Resolves a boxed record's paired copy and free functions, ensuring they
|
||||
/// come from a consistent category (refcount: `ref`/`unref`, or copy:
|
||||
/// `copy`/`free`). Returns `(nil, nil)` when no consistent pair exists.
|
||||
private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, free: String?) {
|
||||
///
|
||||
/// `copyReturnsVoid` flags a chosen copy/ref function whose C return type is
|
||||
/// `void` (e.g. `gst_atomic_queue_ref`) rather than the pointer — most
|
||||
/// `ref`/`copy` functions follow the `T *fn(T *)` self-returning convention
|
||||
/// `init(retaining:)` assumes, but some plain refcount bumps don't.
|
||||
private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, copyReturnsVoid: Bool, free: String?) {
|
||||
if let explicitCopy = record.copyFunction, let explicitFree = record.freeFunction {
|
||||
return (explicitCopy, explicitFree)
|
||||
return (explicitCopy, false, explicitFree)
|
||||
}
|
||||
// Prefer refcount semantics (ref + unref together).
|
||||
let ref = record.instanceReleaseMethod(named: ["ref"])
|
||||
let unref = record.instanceReleaseMethod(named: ["unref"])
|
||||
if ref != nil, unref != nil {
|
||||
return (ref, unref)
|
||||
if let ref, let unref {
|
||||
return (ref.cIdentifier, ref.returnsVoid, unref.cIdentifier)
|
||||
}
|
||||
// Fall back to copy/free semantics.
|
||||
let copy = record.instanceReleaseMethod(named: ["copy"])
|
||||
let free = record.instanceReleaseMethod(named: ["free"])
|
||||
if copy != nil, free != nil {
|
||||
return (copy, free)
|
||||
if let copy, let free {
|
||||
return (copy.cIdentifier, copy.returnsVoid, free.cIdentifier)
|
||||
}
|
||||
// No consistent pair found via method scan. If an explicit GIR attribute
|
||||
// was provided (e.g. only `copy-function`), trust it; when both sides come
|
||||
// from auto-detected methods of conflicting categories, return nil for both
|
||||
// to avoid mixing e.g. copy + unref.
|
||||
if record.copyFunction != nil || record.freeFunction != nil {
|
||||
return (record.copyFunction, record.freeFunction)
|
||||
return (record.copyFunction, false, record.freeFunction)
|
||||
}
|
||||
return (nil, nil)
|
||||
return (nil, false, nil)
|
||||
}
|
||||
|
||||
func resolvedFreeFunction(_ record: Record) -> String? {
|
||||
|
|
@ -645,16 +698,18 @@ func resolvedCopyFunction(_ record: Record) -> String? {
|
|||
}
|
||||
|
||||
extension Record {
|
||||
/// The C identifier of the first bindable instance method matching one of
|
||||
/// `names` (in priority order) that takes no arguments beyond the instance
|
||||
/// itself — the shape every canonical `ref`/`unref`/`copy`/`free` shares.
|
||||
func instanceReleaseMethod(named names: [String]) -> String? {
|
||||
/// The first bindable instance method matching one of `names` (in
|
||||
/// priority order) that takes no arguments beyond the instance itself —
|
||||
/// the shape every canonical `ref`/`unref`/`copy`/`free` shares. Returns
|
||||
/// the C identifier alongside whether the method's return type is
|
||||
/// `void` (see `resolvedCopyFreePair`'s `copyReturnsVoid`).
|
||||
func instanceReleaseMethod(named names: [String]) -> (cIdentifier: String, returnsVoid: Bool)? {
|
||||
for wanted in names {
|
||||
if let method = methods.first(where: {
|
||||
$0.name == wanted && $0.symbolInfo.isBindable
|
||||
&& $0.parameters.allSatisfy(\.isInstanceParameter)
|
||||
}) {
|
||||
return method.cIdentifier
|
||||
return (method.cIdentifier, method.returnValue.type == .void)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -814,7 +869,7 @@ private func planProperty(_ property: Property, fullName: String,
|
|||
/// The GValue fallback accessor for this property's mapped type, or nil
|
||||
/// if the type cannot be represented as a GValue.
|
||||
func gvalueAccessor() -> PropertyAccessorPlan? {
|
||||
mapping.gvalue.map { .gvalue(typeMacro: $0.typeMacro, valueSuffix: $0.getterSuffix) }
|
||||
mapping.gvalue.map { .gvalue(typeMacro: $0.typeMacro, valueSuffix: $0.getterSuffix, hasCopyFunction: $0.hasCopyFunction) }
|
||||
}
|
||||
|
||||
let swiftType: String
|
||||
|
|
@ -881,15 +936,19 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
|
||||
// Resolve parent
|
||||
let parentSwiftName: String?
|
||||
let parentGIRNameResolved: String?
|
||||
if let parentGIR = klass.parent {
|
||||
let qualifiedParent = parentGIR.contains(".") ? parentGIR : "\(context.currentNamespace).\(parentGIR)"
|
||||
if let resolved = registry.resolve(girName: qualifiedParent) {
|
||||
parentSwiftName = registry.swiftTypeName(for: resolved, in: context.currentModule)
|
||||
parentGIRNameResolved = resolved.girName
|
||||
} else {
|
||||
parentSwiftName = nil // foreign or unknown — treat as root
|
||||
parentGIRNameResolved = nil
|
||||
}
|
||||
} else {
|
||||
parentSwiftName = nil
|
||||
parentGIRNameResolved = nil
|
||||
}
|
||||
|
||||
let isOpen = registry.subclassedTypes().contains(girName)
|
||||
|
|
@ -935,7 +994,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
|
||||
var methodPlans: [CallablePlan] = []
|
||||
for method in klass.methods where method.symbolInfo.isBindable {
|
||||
collect(planMethod(method, context: context), into: &methodPlans)
|
||||
collect(planMethod(method, declaringGIRName: girName, context: context), into: &methodPlans)
|
||||
}
|
||||
methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips)
|
||||
|
||||
|
|
@ -982,8 +1041,8 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
|
|||
}
|
||||
|
||||
let plan = ClassPlan(
|
||||
name: klass.name, cType: klass.cType,
|
||||
parent: parentSwiftName,
|
||||
name: klass.name, girName: girName, cType: klass.cType,
|
||||
parent: parentSwiftName, parentGIRName: parentGIRNameResolved,
|
||||
isOpen: isOpen,
|
||||
isAbstract: klass.isAbstract,
|
||||
getTypeFunction: klass.getTypeFunction,
|
||||
|
|
@ -1054,8 +1113,23 @@ func planBitfield(_ bitfield: Bitfield, context: MapContext) -> BitfieldPlan {
|
|||
func planConstant(_ constant: Constant, context: MapContext) -> ConstantPlan? {
|
||||
let mappingResult = Result { try map(constant.type, nullable: false, transfer: .none, context: context) }
|
||||
guard case .success(let mapping) = mappingResult else { return nil }
|
||||
// A bitfield/enum-typed constant's GIR value is a bare C integer
|
||||
// literal (e.g. `"15"`), but the Swift type is an `OptionSet` struct or
|
||||
// a raw-value `enum` — neither is integer-literal-expressible, so the
|
||||
// literal must be routed through `Type(rawValue:)`. Every other
|
||||
// constant type (numeric, string, boolean) already carries a
|
||||
// Swift-literal-compatible value verbatim.
|
||||
let value: String
|
||||
switch mapping.marshalIn {
|
||||
case .bitfieldRaw:
|
||||
value = "\(mapping.swiftType)(rawValue: numericCast(\(constant.value)))"
|
||||
case .enumRaw:
|
||||
value = "\(mapping.swiftType)(rawValue: \(constant.value))!"
|
||||
default:
|
||||
value = constant.value
|
||||
}
|
||||
return ConstantPlan(name: swiftConstantName(constant.name), girName: constant.name,
|
||||
value: constant.value, swiftType: mapping.swiftType, doc: constant.doc)
|
||||
value: value, swiftType: mapping.swiftType, doc: constant.doc)
|
||||
}
|
||||
|
||||
/// Outcome of alias planning: success with a plan, or skip with a reason.
|
||||
|
|
@ -1108,7 +1182,7 @@ enum CallablePlanResult {
|
|||
private func planCallable(
|
||||
fullName: String, swiftName: String, cIdentifier: String,
|
||||
parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool,
|
||||
doc: String?, isStatic: Bool, context: MapContext
|
||||
doc: String?, isStatic: Bool, isOverride: Bool = false, context: MapContext
|
||||
) -> CallablePlanResult {
|
||||
// Applies to methods too, not just free functions: some GIR-declared C
|
||||
// symbols (e.g. GSettingsBackend's `g_settings_backend_*` implementor
|
||||
|
|
@ -1188,7 +1262,8 @@ private func planCallable(
|
|||
return .success(CallablePlan(
|
||||
name: swiftName, cIdentifier: cIdentifier,
|
||||
parameters: paramPlans, returnMapping: returnMapping,
|
||||
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError, doc: doc
|
||||
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError,
|
||||
isOverride: isOverride, doc: doc
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -1202,12 +1277,25 @@ func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResu
|
|||
}
|
||||
|
||||
/// Plans an instance method. The instance parameter becomes `self.pointer`.
|
||||
func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
|
||||
planCallable(
|
||||
/// `declaringGIRName`, when given, is the fully qualified GIR name of the
|
||||
/// class the method is planned for — used to detect whether an ancestor
|
||||
/// already declares a same-selector method (see
|
||||
/// `TypeRegistry.overridesAncestorMethod`), requiring Swift's `override`
|
||||
/// keyword. `nil` for interface methods, which have no class ancestry.
|
||||
func planMethod(_ method: Method, declaringGIRName: String? = nil, context: MapContext) -> CallablePlanResult {
|
||||
let isOverride = declaringGIRName.map { girName in
|
||||
context.registry.overridesAncestorMethod(
|
||||
named: method.name,
|
||||
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name),
|
||||
in: girName
|
||||
)
|
||||
} ?? false
|
||||
return planCallable(
|
||||
fullName: "\(context.currentNamespace).\(method.name)",
|
||||
swiftName: swiftFunctionName(method.name), cIdentifier: method.cIdentifier,
|
||||
parameters: method.parameters, returnValue: method.returnValue,
|
||||
throwsGError: method.throwsGError, doc: method.doc, isStatic: false, context: context)
|
||||
throwsGError: method.throwsGError, doc: method.doc, isStatic: false,
|
||||
isOverride: isOverride, context: context)
|
||||
}
|
||||
|
||||
/// Plans a GObject signal for a class or interface.
|
||||
|
|
|
|||
|
|
@ -144,6 +144,16 @@ public struct TypeRegistry: Sendable {
|
|||
/// module `@_exported import`s its full dependency closure. See
|
||||
/// `swiftTypeName(for:in:)`.
|
||||
private var ambiguousSwiftNames: Set<String> = []
|
||||
/// Every class's own (non-inherited) bindable instance-method
|
||||
/// signatures — raw GIR method name plus non-instance parameter count —
|
||||
/// keyed by the class's fully qualified GIR name. Used by `planClass`
|
||||
/// to decide whether a planned method requires Swift's `override`
|
||||
/// keyword: two GIR methods of the same name and arity declared at
|
||||
/// different levels of one class hierarchy (e.g. `Gst.Element.get_bus`
|
||||
/// and `Gst.Pipeline.get_bus`, the latter narrowing the return type to
|
||||
/// non-optional) compile to the same Swift selector, which Swift then
|
||||
/// requires `override` on rather than treating as an overload.
|
||||
private var instanceMethodSignatures: [String: Set<MethodSignature>] = [:]
|
||||
/// Maps a dropped type's fully-qualified GIR name to the dependency
|
||||
/// module that shadows it (the same predicate `Planner.skipIfDuplicate`
|
||||
/// uses at declaration time), computed once so declaration-time and
|
||||
|
|
@ -197,6 +207,33 @@ public struct TypeRegistry: Sendable {
|
|||
}
|
||||
}
|
||||
|
||||
// A type is dropped as a duplicate when a dependency of its own
|
||||
// declaring module already declares a type of the *same simple name
|
||||
// and the same underlying C type* (mirrors `Planner.duplicateDependencyModule`).
|
||||
// The C-type check matters: `Gdk.Rectangle` and `cairo.RectangleInt`-style
|
||||
// forwarding declarations really are the same struct redeclared, but
|
||||
// `Gst.Object` (`GstObject`) and `GObject.Object` (`GObject`) merely
|
||||
// share a post-namespace-stripping Swift *name* — they are distinct
|
||||
// C structs, `GstObject` being a real subclass of `GObject` with its
|
||||
// own fields and methods, and must both be generated, disambiguated
|
||||
// by module qualification instead (see the ambiguity scan below).
|
||||
// Namespace-agnostic: any reference to the dropped GIR name is
|
||||
// caught regardless of which namespace the reference itself was
|
||||
// written in. Computed first so the ambiguity scan below can
|
||||
// exclude these names.
|
||||
var dropped: [String: String] = [:]
|
||||
for (girName, resolved) in types {
|
||||
guard let dot = girName.lastIndex(of: ".") else { continue }
|
||||
let simpleName = String(girName[girName.index(after: dot)...])
|
||||
for dep in directDependencies[resolved.swiftModule] ?? [] {
|
||||
if let depType = resolve(name: simpleName, namespace: dep), depType.cType == resolved.cType {
|
||||
dropped[girName] = dep
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
self.droppedGIRNames = dropped
|
||||
|
||||
// A module's public API is flattened into every downstream consumer
|
||||
// via `@_exported import` (the umbrella "import Gsk gets you GLib,
|
||||
// GObject, Gdk, Graphene, Pango, ..." convenience) — including
|
||||
|
|
@ -205,32 +242,16 @@ public struct TypeRegistry: Sendable {
|
|||
// Pango.Matrix both reach Gsk). The existing declaration-time
|
||||
// `duplicateOfDependency` skip only catches a direct dependency
|
||||
// edge; it can't see this diamond. Any Swift simple name declared
|
||||
// by two or more distinct modules is therefore permanently
|
||||
// ambiguous wherever it's referenced from outside its own
|
||||
// declaring module, and must be module-qualified there.
|
||||
// by two or more distinct *surviving* (non-dropped) modules is
|
||||
// therefore permanently ambiguous wherever it's referenced from
|
||||
// outside its own declaring module, and must be module-qualified
|
||||
// there. Dropped duplicates are excluded — they're never rendered,
|
||||
// so they can't be a real second declarer of the name.
|
||||
var modulesByName: [String: Set<String>] = [:]
|
||||
for resolved in types.values {
|
||||
for (girName, resolved) in types where dropped[girName] == nil {
|
||||
modulesByName[resolved.swiftName, default: []].insert(resolved.swiftModule)
|
||||
}
|
||||
self.ambiguousSwiftNames = Set(modulesByName.filter { $0.value.count > 1 }.keys)
|
||||
|
||||
// A type is dropped as a duplicate when a dependency of its own
|
||||
// declaring module already declares a type of the same simple name
|
||||
// (mirrors `Planner.duplicateDependencyModule`). Namespace-agnostic:
|
||||
// any reference to the dropped GIR name is caught regardless of
|
||||
// which namespace the reference itself was written in.
|
||||
var dropped: [String: String] = [:]
|
||||
for (girName, resolved) in types {
|
||||
guard let dot = girName.lastIndex(of: ".") else { continue }
|
||||
let simpleName = String(girName[girName.index(after: dot)...])
|
||||
for dep in directDependencies[resolved.swiftModule] ?? [] {
|
||||
if resolve(name: simpleName, namespace: dep) != nil {
|
||||
dropped[girName] = dep
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
self.droppedGIRNames = dropped
|
||||
}
|
||||
|
||||
/// Returns the dependency module that shadows `girName`, if the
|
||||
|
|
@ -268,6 +289,16 @@ public struct TypeRegistry: Sendable {
|
|||
unrefFunc: cls.unrefFunc
|
||||
)
|
||||
)
|
||||
instanceMethodSignatures[girName] = Set(
|
||||
cls.methods
|
||||
.filter { $0.symbolInfo.isBindable }
|
||||
.map { method in
|
||||
MethodSignature(
|
||||
name: method.name,
|
||||
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
for iface in ns.interfaces {
|
||||
let girName = "\(ns.name).\(iface.name)"
|
||||
|
|
@ -408,6 +439,62 @@ public struct TypeRegistry: Sendable {
|
|||
return result
|
||||
}
|
||||
|
||||
/// A raw GIR instance-method signature: name plus non-instance
|
||||
/// parameter count. Two methods with the same signature at different
|
||||
/// levels of a class hierarchy compile to the same Swift selector.
|
||||
private struct MethodSignature: Hashable {
|
||||
let name: String
|
||||
/// The non-instance parameters' raw GIR names, in order — these map
|
||||
/// deterministically to Swift argument labels, so a label mismatch
|
||||
/// (e.g. `Gtk.MenuButton.set_direction(direction:)` vs.
|
||||
/// `Gtk.Widget.set_direction(dir:)`) means a genuinely different
|
||||
/// Swift selector despite the same method name and arity, and must
|
||||
/// NOT be treated as an override collision.
|
||||
let paramNames: [String]
|
||||
}
|
||||
|
||||
/// Whether a class's own method of the given raw GIR name and parameter
|
||||
/// names duplicates the Swift selector of a method already declared by
|
||||
/// one of its ancestors *within the same Swift module* — meaning the
|
||||
/// generated Swift method requires the `override` keyword (e.g.
|
||||
/// `Gst.Pipeline.get_bus()` narrows `Gst.Element.get_bus()`'s return
|
||||
/// type from `Bus?` to `Bus`, but both compile to the same
|
||||
/// no-argument `getBus()` selector).
|
||||
///
|
||||
/// Deliberately module-scoped: none of these generated methods are
|
||||
/// marked `open`, so Swift only accepts `override` when the ancestor
|
||||
/// declaration lives in the same module as the overriding class
|
||||
/// (`public`, unlike `open`, permits subclassing/overriding only
|
||||
/// within the defining module). A same-named, same-arity method on a
|
||||
/// cross-module ancestor is coincidence, not a real override — GObject
|
||||
/// C APIs reuse names like `get_name`/`ref` constantly across
|
||||
/// unrelated classes, and treating every such pair as an override
|
||||
/// produced hundreds of illegal "overriding non-open instance method
|
||||
/// outside of its defining module" errors before this scoping.
|
||||
///
|
||||
/// Comparing parameter names (not just arity) matters too: raw GIR
|
||||
/// name + arity alone falsely matched `Gtk.MenuButton.set_direction(direction:)`
|
||||
/// against `Gtk.Widget.set_direction(dir:)` — same C-derived method
|
||||
/// name, same 1-argument arity, but a different argument LABEL means a
|
||||
/// genuinely different Swift selector Swift never asked to override.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - methodName: The raw GIR method name (e.g. `"get_bus"`).
|
||||
/// - paramNames: The non-instance parameters' raw GIR names, in order.
|
||||
/// - girName: The fully qualified GIR name of the declaring class.
|
||||
/// - Returns: `true` when a same-module ancestor declares a method of
|
||||
/// the same signature.
|
||||
public func overridesAncestorMethod(named methodName: String, paramNames: [String], in girName: String) -> Bool {
|
||||
guard let ownModule = resolve(girName: girName)?.swiftModule else { return false }
|
||||
let signature = MethodSignature(name: methodName, paramNames: paramNames)
|
||||
for ancestor in ancestry(of: girName) where ancestor.swiftModule == ownModule {
|
||||
if instanceMethodSignatures[ancestor.girName]?.contains(signature) == true {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether a class descends from `GObject.InitiallyUnowned`.
|
||||
///
|
||||
/// This is the sole basis for deciding that a constructor's result must be
|
||||
|
|
@ -479,9 +566,38 @@ public struct TypeRegistry: Sendable {
|
|||
guard type.swiftModule != module, ambiguousSwiftNames.contains(type.swiftName) else {
|
||||
return type.swiftName
|
||||
}
|
||||
// `GObject` (the Swift module) collides with `GObject` (the raw C
|
||||
// struct every `import C<Module>` transitively vends from
|
||||
// glib-object.h): writing `GObject.Object` as a qualifier makes
|
||||
// Swift resolve the `GObject` component to the C struct in scope
|
||||
// rather than the module, so `.Object` fails to typecheck
|
||||
// ("'Object' is not a member type of struct 'CFoo.GObject'"). This
|
||||
// applies to any GObject-declared type, not just `Object` — e.g.
|
||||
// `GObject.ValueArray` vs. `Gst.ValueArray` hits the identical
|
||||
// failure. `GObject`'s own Support.swift exports a collision-free
|
||||
// bare `GLib`-prefixed alias for each name in this table instead;
|
||||
// every other ambiguous pair (e.g. `Graphene.Matrix`/`Pango.Matrix`)
|
||||
// has no same-spelling C struct in scope and qualifies safely as
|
||||
// normal. Extend `Self.gObjectAliases` (and the matching aliases in
|
||||
// `renderSupport`'s GObject branch) if a future GIR update adds
|
||||
// another GObject-declared name that collides with a sibling
|
||||
// module's type.
|
||||
if type.swiftModule == "GObject", let alias = Self.gObjectAliases[type.swiftName] {
|
||||
return alias
|
||||
}
|
||||
return "\(type.swiftModule).\(type.swiftName)"
|
||||
}
|
||||
|
||||
/// Collision-free bare aliases for `GObject`-declared type names that
|
||||
/// also collide with a sibling module's type of the same simple name
|
||||
/// (see `swiftTypeName`). Keys are the type's Swift name within
|
||||
/// `GObject`; values are the alias `GObject`'s own `Support.swift`
|
||||
/// exports via `public typealias`.
|
||||
private static let gObjectAliases: [String: String] = [
|
||||
"Object": "GLibObject",
|
||||
"ValueArray": "GLibValueArray",
|
||||
]
|
||||
|
||||
/// Every class that is subclassed by some other loaded class.
|
||||
///
|
||||
/// Such classes must be emitted `open` so subclasses — including those in
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ struct NamingTests {
|
|||
#expect(isValidGeneratedFileName("DNDEvent.swift")) // acronym type name
|
||||
#expect(!isValidGeneratedFileName("Align.txt")) // wrong extension
|
||||
#expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // uppercase run, PascalCase start
|
||||
#expect(isValidGeneratedFileName("PluginAPIFlags.swift")) // 4-run acronym, Gst
|
||||
#expect(isValidGeneratedFileName("AuthNTLM.swift")) // 4-run acronym, Soup
|
||||
}
|
||||
|
||||
// MARK: - File merging
|
||||
|
|
|
|||
|
|
@ -203,16 +203,22 @@ struct TypeRegistryTests {
|
|||
@Test("A type dropped as a duplicate of a dependency is detected regardless of the referencing namespace")
|
||||
func detectsCrossModuleDroppedType() {
|
||||
// Dep declares Rect; Mid (which depends on Dep) redeclares the same
|
||||
// simple name — the planner drops Mid.Rect as a duplicate. A
|
||||
// reference into Mid.Rect from any namespace, not just Mid's own,
|
||||
// must be recognized as dangling.
|
||||
// simple name *and the same underlying C struct* — a genuine
|
||||
// redundant forwarding declaration (e.g. a compat header
|
||||
// re-exporting a dependency's type under its own namespace) — the
|
||||
// planner drops Mid.Rect as a duplicate. A reference into Mid.Rect
|
||||
// from any namespace, not just Mid's own, must be recognized as
|
||||
// dangling. A same-name but *different*-cType pair (two genuinely
|
||||
// distinct C structs that merely share a post-namespace-stripping
|
||||
// Swift name, e.g. `GstObject` vs `GObject`) is deliberately NOT
|
||||
// a match here — see `ambiguousCrossModuleNameKeepsBothTypes`.
|
||||
let dep = Repository(namespaces: [
|
||||
Namespace(name: "Dep", version: "1.0",
|
||||
records: [Record(name: "Rect", cType: "DepRect", getTypeFunction: "dep_rect_get_type")])
|
||||
records: [Record(name: "Rect", cType: "Rect", getTypeFunction: "dep_rect_get_type")])
|
||||
])
|
||||
let mid = Repository(namespaces: [
|
||||
Namespace(name: "Mid", version: "1.0",
|
||||
records: [Record(name: "Rect", cType: "MidRect", getTypeFunction: "mid_rect_get_type")])
|
||||
records: [Record(name: "Rect", cType: "Rect", getTypeFunction: "mid_rect_get_type")])
|
||||
])
|
||||
let registry = TypeRegistry(
|
||||
repositories: ["Dep": dep, "Mid": mid],
|
||||
|
|
@ -233,6 +239,32 @@ struct TypeRegistryTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test("A same-name but different-cType type across a dependency is not a duplicate — both are kept and disambiguated")
|
||||
func ambiguousCrossModuleNameKeepsBothTypes() throws {
|
||||
// Mirrors `Gst.Object` (`GstObject`) vs `GObject.Object` (`GObject`):
|
||||
// Base declares `Object`, and Sub (which depends on Base) also
|
||||
// declares its own distinct `Object` with a different cType.
|
||||
// this must NOT be treated as a duplicate: both are real, distinct
|
||||
// C structs that merely share a post-namespace-stripping Swift
|
||||
// name, so both must be generated and cross-references qualified.
|
||||
let base = Repository(namespaces: [
|
||||
Namespace(name: "Base", version: "1.0",
|
||||
classes: [Class(name: "Object", cType: "BaseObject", parent: nil, getTypeFunction: "base_object_get_type")])
|
||||
])
|
||||
let sub = Repository(namespaces: [
|
||||
Namespace(name: "Sub", version: "1.0",
|
||||
classes: [Class(name: "Object", cType: "SubObject", parent: "Base.Object", getTypeFunction: "sub_object_get_type")])
|
||||
])
|
||||
let registry = TypeRegistry(
|
||||
repositories: ["Base": base, "Sub": sub],
|
||||
directDependencies: ["Sub": ["Base"]]
|
||||
)
|
||||
#expect(registry.droppedShadow("Sub.Object") == nil)
|
||||
#expect(registry.droppedShadow("Base.Object") == nil)
|
||||
let subObject = try #require(registry.resolve(girName: "Sub.Object"))
|
||||
#expect(registry.swiftTypeName(for: subObject, in: "Down") == "Sub.Object")
|
||||
}
|
||||
|
||||
@Test("Manually excluded namespaces are treated as foreign")
|
||||
func manualNamespacesAreForeign() {
|
||||
let repo = Repository(namespaces: [
|
||||
|
|
|
|||
29
smoke/tier6/AdwSmoke.swift
Normal file
29
smoke/tier6/AdwSmoke.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// AdwSmoke.swift
|
||||
// Tier-6-only runtime smoke test for Adw (Phase E5). Proves — against the
|
||||
// REAL libadwaita-1, linked at runtime — that the generated bindings reach
|
||||
// real libadwaita C symbols.
|
||||
//
|
||||
// `getMajorVersion()`/`getMinorVersion()` reach `adw_get_major_version`/
|
||||
// `adw_get_minor_version`, plain namespace-level free functions. Version
|
||||
// functions never call `adw_init()`, so no GTK display connection is
|
||||
// required. Uses only plain-public API — no `@_spi` import.
|
||||
//
|
||||
// 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 >= 6 (Adw's module).
|
||||
|
||||
import Testing
|
||||
|
||||
import GLib
|
||||
import Adw
|
||||
|
||||
@Suite("Tier 6 Adw smoke tests")
|
||||
struct AdwSmokeTests {
|
||||
@Test("getMajorVersion()/getMinorVersion() reach the real adw_get_major_version/adw_get_minor_version and report libadwaita 1")
|
||||
func versionFunctionsReachRealAdw() throws {
|
||||
#expect(getMajorVersion() == 1)
|
||||
// libadwaita's minor version is a real, non-placeholder value
|
||||
// reported by the linked library, not a stubbed default.
|
||||
#expect(getMinorVersion() >= 0)
|
||||
}
|
||||
}
|
||||
28
smoke/tier6/GstSmoke.swift
Normal file
28
smoke/tier6/GstSmoke.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// GstSmoke.swift
|
||||
// Tier-6-only runtime smoke test for Gst (Phase E5). Proves — against the
|
||||
// REAL libgstreamer-1.0, linked at runtime — that the generated bindings
|
||||
// reach a real GStreamer C symbol.
|
||||
//
|
||||
// `versionString()` reaches `gst_version_string`, a plain free function that
|
||||
// needs no `gst_init()` call and no pipeline/element construction — the
|
||||
// simplest possible real round-trip through the generated Gst module. Uses
|
||||
// only plain-public API — no `@_spi` import.
|
||||
//
|
||||
// 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 >= 6 (Gst's module).
|
||||
|
||||
import Testing
|
||||
|
||||
import GLib
|
||||
import Gst
|
||||
|
||||
@Suite("Tier 6 Gst smoke tests")
|
||||
struct GstSmokeTests {
|
||||
@Test("versionString() reaches the real gst_version_string and reports a real GStreamer version string")
|
||||
func versionStringReachesRealGst() throws {
|
||||
let version = versionString()
|
||||
#expect(!version.isEmpty)
|
||||
#expect(version.contains("GStreamer"))
|
||||
}
|
||||
}
|
||||
29
smoke/tier6/SoupSmoke.swift
Normal file
29
smoke/tier6/SoupSmoke.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// SoupSmoke.swift
|
||||
// Tier-6-only runtime smoke test for Soup (Phase E5). Proves — against the
|
||||
// REAL libsoup-3.0, linked at runtime — that the generated bindings reach
|
||||
// real Soup C symbols.
|
||||
//
|
||||
// `getMajorVersion()`/`getMinorVersion()` reach `soup_get_major_version`/
|
||||
// `soup_get_minor_version`, plain free functions that need no session,
|
||||
// message, or network connection — display/network-free by construction.
|
||||
// Uses only plain-public API — no `@_spi` import.
|
||||
//
|
||||
// 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 >= 6 (Soup's module).
|
||||
|
||||
import Testing
|
||||
|
||||
import GLib
|
||||
import Soup
|
||||
|
||||
@Suite("Tier 6 Soup smoke tests")
|
||||
struct SoupSmokeTests {
|
||||
@Test("getMajorVersion()/getMinorVersion() reach the real soup_get_major_version/soup_get_minor_version and report libsoup 3")
|
||||
func versionFunctionsReachRealSoup() throws {
|
||||
#expect(getMajorVersion() == 3)
|
||||
// libsoup 3's minor version is a real, non-placeholder value
|
||||
// reported by the linked library, not a stubbed default.
|
||||
#expect(getMinorVersion() >= 0)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue