Add Tier 4 Gdk/Gsk bindings and E3 review remediation
Generates Gdk and Gsk wrappers (330/417 and 140/183 callables), with runtime smoke tests linked against real libgtk-4. Also fixes a latent cross-module dropped-type reference bug, corrects the init(takingOwnership:) doc for no-free records, restores a precise filename safety check, and adds unit test coverage for four previously compile-gate-only branches.
This commit is contained in:
parent
ea41629a1f
commit
dac435ec89
15 changed files with 446 additions and 41 deletions
|
|
@ -76,7 +76,11 @@ public enum MarshalOut: Equatable, Sendable {
|
||||||
case gbooleanToBool
|
case gbooleanToBool
|
||||||
/// Copy a C string, optionally freeing the source.
|
/// Copy a C string, optionally freeing the source.
|
||||||
/// - Parameter free: When `true`, the caller owns the string and must `g_free` it.
|
/// - Parameter free: When `true`, the caller owns the string and must `g_free` it.
|
||||||
case stringCopy(free: Bool)
|
/// - Parameter constPointee: Whether the C out-param pointee is `const
|
||||||
|
/// char*` (`true`) rather than mutable `char*` (`false`). Only
|
||||||
|
/// meaningful for out-parameters (see `outParamLocalType`); irrelevant
|
||||||
|
/// for in-params (bridged via `withCString`) and return values.
|
||||||
|
case stringCopy(free: Bool, constPointee: Bool = true)
|
||||||
/// Wrap an object pointer, optionally sinking a floating ref.
|
/// Wrap an object pointer, optionally sinking a floating ref.
|
||||||
/// - Parameter sink: When `true`, call `g_object_ref_sink` (for InitiallyUnowned constructors).
|
/// - Parameter sink: When `true`, call `g_object_ref_sink` (for InitiallyUnowned constructors).
|
||||||
case objectWrap(sink: Bool)
|
case objectWrap(sink: Bool)
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,14 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
/// Tests whether a generated filename follows the PascalCase convention — starts
|
/// Tests whether a generated filename follows the PascalCase convention — starts
|
||||||
/// with an uppercase ASCII letter, contains only ASCII letters/digits, and has
|
/// with an uppercase ASCII letter, contains only ASCII letters/digits, and
|
||||||
/// no run of more than 3 consecutive uppercase letters (catches unconverted C
|
/// never has a run of more than 3 consecutive uppercase letters immediately
|
||||||
/// spellings like `cclosureMarshalBOOLEANFLAGS.swift` while allowing legitimate
|
/// preceded by a lowercase letter. That last rule rejects unconverted C
|
||||||
/// 2–3-letter acronym runs like `IOChannel.swift`, `FileIOStream.swift`).
|
/// spellings (`MarshalBOOLEAN`) while still accepting legitimate acronym
|
||||||
|
/// runs, since those never follow a lowercase letter mid-name (`RGBA`,
|
||||||
|
/// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`).
|
||||||
|
/// Per-symbol filenames are only ever derived from authoritative GIR type
|
||||||
|
/// names (constants/functions/callbacks are merged into fixed-name files).
|
||||||
///
|
///
|
||||||
/// - Parameter filename: A relative filename ending in `.swift`.
|
/// - Parameter filename: A relative filename ending in `.swift`.
|
||||||
/// - Returns: `true` when the name is conventional.
|
/// - Returns: `true` when the name is conventional.
|
||||||
|
|
@ -109,9 +113,15 @@ public func isValidGeneratedFileName(_ filename: String) -> Bool {
|
||||||
guard let first = base.first, first.isUppercase else { return false }
|
guard let first = base.first, first.isUppercase else { return false }
|
||||||
guard base.allSatisfy({ ($0.isLetter && $0.isASCII) || $0.isNumber }) else { return false }
|
guard base.allSatisfy({ ($0.isLetter && $0.isASCII) || $0.isNumber }) else { return false }
|
||||||
var run = 0
|
var run = 0
|
||||||
|
var precededByLower = false
|
||||||
for ch in base {
|
for ch in base {
|
||||||
run = ch.isUppercase ? run + 1 : 0
|
if ch.isUppercase && ch.isASCII {
|
||||||
if run > 3 { return false }
|
run += 1
|
||||||
|
if run > 3 && precededByLower { return false }
|
||||||
|
} else {
|
||||||
|
run = 0
|
||||||
|
precededByLower = ch.isLowercase && ch.isASCII
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -461,8 +471,14 @@ private func renderRecord(_ plan: RecordPlan) -> String {
|
||||||
lines.append("public final class \(plan.name) {")
|
lines.append("public final class \(plan.name) {")
|
||||||
lines.append(" public let pointer: UnsafeMutableRawPointer")
|
lines.append(" public let pointer: UnsafeMutableRawPointer")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility")
|
if plan.freeFunction != nil {
|
||||||
lines.append(" /// for freeing it. Use for `transfer-ownership=\"full\"` returns.")
|
lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility")
|
||||||
|
lines.append(" /// for freeing it. Use for `transfer-ownership=\"full\"` returns.")
|
||||||
|
} else {
|
||||||
|
lines.append(" /// Stores a boxed pointer. This wrapper has no known free function, so it")
|
||||||
|
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(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
|
||||||
lines.append(" self.pointer = pointer")
|
lines.append(" self.pointer = pointer")
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
|
|
@ -768,7 +784,20 @@ private func renderWrapperExpr(for p: ParameterPlan, rawName: String, ownerIsInt
|
||||||
return "\(p.mapping.swiftType)(retaining: \(rawName))"
|
return "\(p.mapping.swiftType)(retaining: \(rawName))"
|
||||||
}
|
}
|
||||||
switch p.mapping.marshalIn {
|
switch p.mapping.marshalIn {
|
||||||
case .boxedPointer, .objectPointer:
|
case .boxedPointer:
|
||||||
|
let isOptional = p.mapping.swiftType.hasSuffix("?")
|
||||||
|
let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType
|
||||||
|
// A signal parameter is a borrowed pointer (transfer none); wrapping
|
||||||
|
// it must copy/ref so the wrapper owns an independent instance. Boxed
|
||||||
|
// records without a GIR copy-function (e.g. GdkToplevelSize) have no
|
||||||
|
// `init(retaining:)` (only emitted when one is known — see
|
||||||
|
// renderRecord); adopt the borrowed pointer via `takingOwnership:`
|
||||||
|
// instead, matching the only initializer such records expose.
|
||||||
|
if case .boxedWrap(_, let copyFn) = p.mapping.marshalOut, copyFn == nil {
|
||||||
|
return "\(baseType)(takingOwnership: \(rawName))"
|
||||||
|
}
|
||||||
|
return "\(baseType)(retaining: \(rawName))"
|
||||||
|
case .objectPointer:
|
||||||
let isOptional = p.mapping.swiftType.hasSuffix("?")
|
let isOptional = p.mapping.swiftType.hasSuffix("?")
|
||||||
let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType
|
let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType
|
||||||
return "\(baseType)(retaining: \(rawName))"
|
return "\(baseType)(retaining: \(rawName))"
|
||||||
|
|
@ -926,13 +955,31 @@ private func callbackBoxRelease(_ plan: CallablePlan, indent: String) -> [String
|
||||||
private func outParamLocalType(_ param: ParameterPlan) -> String {
|
private func outParamLocalType(_ param: ParameterPlan) -> String {
|
||||||
switch param.mapping.marshalIn {
|
switch param.mapping.marshalIn {
|
||||||
case .stringToC:
|
case .stringToC:
|
||||||
|
if case .stringCopy(_, let constPointee) = param.mapping.marshalOut, constPointee {
|
||||||
|
// c:type is `const char**`: the Clang importer expects
|
||||||
|
// UnsafePointer<CChar>? as the pointee, not
|
||||||
|
// UnsafeMutablePointer<CChar>? (that's only correct when the
|
||||||
|
// callee hands over a mutable/owned `char**`).
|
||||||
|
return "UnsafePointer<CChar>?"
|
||||||
|
}
|
||||||
return "UnsafeMutablePointer<CChar>?"
|
return "UnsafeMutablePointer<CChar>?"
|
||||||
case .boolToGboolean:
|
case .boolToGboolean:
|
||||||
return "Int32"
|
return "Int32"
|
||||||
case .direct, .numericCast:
|
case .direct, .numericCast:
|
||||||
return param.mapping.cSwiftType
|
// GIR `nullable="1"` on an out-param describes whether the ARGUMENT
|
||||||
|
// itself is omittable (caller may pass NULL to skip it) — this
|
||||||
|
// generator always allocates a local and passes `&local`, so that
|
||||||
|
// never applies. For scalar pointees the type-mapper's blanket
|
||||||
|
// `optionalised(nullable:)` still wraps `Int32` → `Int32?`, which
|
||||||
|
// doesn't match the non-optional `UnsafeMutablePointer<Int32>` the
|
||||||
|
// Clang importer expects. Raw/object/boxed pointees are exempt:
|
||||||
|
// their cSwiftType is `UnsafeMutableRawPointer?`, optional from the
|
||||||
|
// base mapping itself (a real nullable pointee), not from this wrap.
|
||||||
|
let t = param.mapping.cSwiftType
|
||||||
|
return t == "UnsafeMutableRawPointer?" ? t : (t.hasSuffix("?") ? String(t.dropLast()) : t)
|
||||||
case .enumRaw, .bitfieldRaw:
|
case .enumRaw, .bitfieldRaw:
|
||||||
return param.mapping.cSwiftType
|
let t = param.mapping.cSwiftType
|
||||||
|
return t.hasSuffix("?") ? String(t.dropLast()) : t
|
||||||
default:
|
default:
|
||||||
return "UnsafeMutablePointer<Int8>?"
|
return "UnsafeMutablePointer<Int8>?"
|
||||||
}
|
}
|
||||||
|
|
@ -967,11 +1014,12 @@ private func outParamValueExpr(_ param: ParameterPlan, varName: String) -> Strin
|
||||||
return "numericCast(\(varName))"
|
return "numericCast(\(varName))"
|
||||||
case .gbooleanToBool:
|
case .gbooleanToBool:
|
||||||
return "\(varName) != 0"
|
return "\(varName) != 0"
|
||||||
case .stringCopy:
|
case .stringCopy(let free, _):
|
||||||
|
let note = free ? " /* TODO: transfer-full out-string not freed */" : ""
|
||||||
if param.mapping.swiftType.hasSuffix("?") {
|
if param.mapping.swiftType.hasSuffix("?") {
|
||||||
return "\(varName).map { String(cString: $0) } /* TODO: transfer-full out-string not freed */"
|
return "\(varName).map { String(cString: $0) }\(note)"
|
||||||
}
|
}
|
||||||
return "String(cString: \(varName)!) /* TODO: transfer-full out-string not freed */"
|
return "String(cString: \(varName)!)\(note)"
|
||||||
case .enumFromRaw(let swiftType):
|
case .enumFromRaw(let swiftType):
|
||||||
return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))!"
|
return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))!"
|
||||||
case .bitfieldFromRaw(let swiftType):
|
case .bitfieldFromRaw(let swiftType):
|
||||||
|
|
@ -1413,7 +1461,7 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String {
|
||||||
return "numericCast(\(cCall))"
|
return "numericCast(\(cCall))"
|
||||||
case .gbooleanToBool:
|
case .gbooleanToBool:
|
||||||
return "\(cCall) != 0"
|
return "\(cCall) != 0"
|
||||||
case .stringCopy(let free):
|
case .stringCopy(let free, _):
|
||||||
if mapping.swiftType.hasSuffix("?") {
|
if mapping.swiftType.hasSuffix("?") {
|
||||||
let freeStr = free ? "/* TODO: g_free */" : ""
|
let freeStr = free ? "/* TODO: g_free */" : ""
|
||||||
return "\(cCall).map { String(cString: $0) \(freeStr) }"
|
return "\(cCall).map { String(cString: $0) \(freeStr) }"
|
||||||
|
|
|
||||||
|
|
@ -152,12 +152,7 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
|
||||||
// (enums, bitfields, records, classes, interfaces, aliases), never to
|
// (enums, bitfields, records, classes, interfaces, aliases), never to
|
||||||
// constants/functions/callbacks.
|
// constants/functions/callbacks.
|
||||||
func duplicateDependencyModule(_ girSimpleName: String) -> String? {
|
func duplicateDependencyModule(_ girSimpleName: String) -> String? {
|
||||||
for dep in context.dependencyModules {
|
context.registry.droppedShadow("\(ns.name).\(girSimpleName)")
|
||||||
if context.registry.resolve(name: girSimpleName, namespace: dep) != nil {
|
|
||||||
return dep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
func skipIfDuplicate(_ girSimpleName: String) -> Bool {
|
func skipIfDuplicate(_ girSimpleName: String) -> Bool {
|
||||||
guard let dep = duplicateDependencyModule(girSimpleName) else { return false }
|
guard let dep = duplicateDependencyModule(girSimpleName) else { return false }
|
||||||
|
|
@ -569,6 +564,11 @@ private let knownMissingCFunctions: Set<String> = [
|
||||||
// Declared in the GdkPixbuf GIR but not exported through the public
|
// Declared in the GdkPixbuf GIR but not exported through the public
|
||||||
// <gdk-pixbuf/gdk-pixbuf.h> umbrella header.
|
// <gdk-pixbuf/gdk-pixbuf.h> umbrella header.
|
||||||
"gdk_pixbuf_non_anim_new",
|
"gdk_pixbuf_non_anim_new",
|
||||||
|
// Declared in <gsk/broadway/gskbroadwayrenderer.h>, which the public
|
||||||
|
// umbrella (<gsk/gsk.h>) deliberately does not include — Broadway is an
|
||||||
|
// optional backend; unlike the GPU renderers (<gsk/gpu/gskglrenderer.h>,
|
||||||
|
// <gsk/gpu/gskvulkanrenderer.h>, both included), its header is excluded.
|
||||||
|
"gsk_broadway_renderer_new",
|
||||||
]
|
]
|
||||||
|
|
||||||
let knownMisleadingCFunctions: Set<String> = [
|
let knownMisleadingCFunctions: Set<String> = [
|
||||||
|
|
@ -1406,7 +1406,8 @@ func planParameters(
|
||||||
// Out-params are collected and returned as Swift values rather than
|
// Out-params are collected and returned as Swift values rather than
|
||||||
// passed as arguments. Map the VALUE type (not the pointer-to-pointer).
|
// passed as arguments. Map the VALUE type (not the pointer-to-pointer).
|
||||||
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
|
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
|
||||||
transfer: param.transferOwnership, context: context) }
|
transfer: param.transferOwnership, context: context,
|
||||||
|
cType: param.cType) }
|
||||||
switch mappingResult {
|
switch mappingResult {
|
||||||
case .success(let paramMapping):
|
case .success(let paramMapping):
|
||||||
if !paramMapping.isReadyForCallables {
|
if !paramMapping.isReadyForCallables {
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,10 @@ public func map(
|
||||||
_ type: GIRType,
|
_ type: GIRType,
|
||||||
nullable: Bool,
|
nullable: Bool,
|
||||||
transfer: TransferOwnership,
|
transfer: TransferOwnership,
|
||||||
context: MapContext
|
context: MapContext,
|
||||||
|
cType: String = ""
|
||||||
) throws(MapError) -> Mapping {
|
) throws(MapError) -> Mapping {
|
||||||
let result: Mapping = try _mapValue(type: type, transfer: transfer, context: context)
|
let result: Mapping = try _mapValue(type: type, transfer: transfer, context: context, cType: cType)
|
||||||
return result.optionalised(nullable: nullable)
|
return result.optionalised(nullable: nullable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +152,8 @@ public func map(
|
||||||
private func _mapValue(
|
private func _mapValue(
|
||||||
type: GIRType,
|
type: GIRType,
|
||||||
transfer: TransferOwnership,
|
transfer: TransferOwnership,
|
||||||
context: MapContext
|
context: MapContext,
|
||||||
|
cType: String = ""
|
||||||
) throws(MapError) -> Mapping {
|
) throws(MapError) -> Mapping {
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
|
|
@ -169,9 +171,17 @@ private func _mapValue(
|
||||||
case .float, .double:
|
case .float, .double:
|
||||||
return try mapPrimitive(type)
|
return try mapPrimitive(type)
|
||||||
case .string:
|
case .string:
|
||||||
return .stringMapping(free: transfer == .full)
|
// A GIR out-param's c:type spells `const char**` when the pointee
|
||||||
|
// is borrowed (no free needed) but `char**` when it points into
|
||||||
|
// caller/callee-owned storage the generator must not treat as
|
||||||
|
// const (e.g. g_ascii_strtod's endptr, transfer-ownership="none"
|
||||||
|
// yet c:type="gchar**"). transfer-ownership alone is not a
|
||||||
|
// reliable proxy for pointee constness; the raw c:type is.
|
||||||
|
let constOut = cType.isEmpty || cType.hasPrefix("const")
|
||||||
|
return .stringMapping(free: transfer == .full, constOut: constOut)
|
||||||
case .filename:
|
case .filename:
|
||||||
return .filenameMapping(free: transfer == .full)
|
let constOut = cType.isEmpty || cType.hasPrefix("const")
|
||||||
|
return .filenameMapping(free: transfer == .full, constOut: constOut)
|
||||||
case .pointer:
|
case .pointer:
|
||||||
return .pointerMapping
|
return .pointerMapping
|
||||||
case .vaList:
|
case .vaList:
|
||||||
|
|
@ -234,6 +244,20 @@ private func mapTypeRef(
|
||||||
detail: "unresolved type '\(namespace).\(name)'")
|
detail: "unresolved type '\(namespace).\(name)'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A type DECLARED in some module is dropped by the planner
|
||||||
|
// (SkipReason.duplicateOfDependency, see Planner.skipIfDuplicate) when a
|
||||||
|
// dependency of that module already declares a type of the same simple
|
||||||
|
// name (e.g. Gdk.Rectangle vs. Pango.Rectangle) — keeping both would make
|
||||||
|
// unqualified cross-module references ambiguous downstream. Any
|
||||||
|
// callable/property/signal/field that references the dropped type must
|
||||||
|
// skip too, or it emits a reference to a type that was never generated.
|
||||||
|
// `TypeRegistry.droppedShadow` computes this once (namespace-agnostic),
|
||||||
|
// so this catches both same-namespace and cross-module references to a
|
||||||
|
// dropped declaration.
|
||||||
|
if let dep = context.registry.droppedShadow(resolved.girName) {
|
||||||
|
throw MapError(reason: .duplicateOfDependency,
|
||||||
|
detail: "'\(resolved.girName)' was dropped as a duplicate of dependency module '\(dep)'")
|
||||||
|
}
|
||||||
switch resolved.category {
|
switch resolved.category {
|
||||||
case .foreign(let foreignNS):
|
case .foreign(let foreignNS):
|
||||||
throw MapError(reason: .foreignNamespace,
|
throw MapError(reason: .foreignNamespace,
|
||||||
|
|
@ -457,18 +481,18 @@ extension Mapping {
|
||||||
gvalue: GValueOps(typeMacro: "G_TYPE_POINTER",
|
gvalue: GValueOps(typeMacro: "G_TYPE_POINTER",
|
||||||
getterSuffix: "pointer", setterSuffix: "pointer"))
|
getterSuffix: "pointer", setterSuffix: "pointer"))
|
||||||
|
|
||||||
static func stringMapping(free: Bool) -> Mapping {
|
static func stringMapping(free: Bool, constOut: Bool = true) -> Mapping {
|
||||||
Mapping(
|
Mapping(
|
||||||
swiftType: "String", cSwiftType: "UnsafePointer<CChar>?",
|
swiftType: "String", cSwiftType: "UnsafePointer<CChar>?",
|
||||||
marshalIn: .stringToC, marshalOut: .stringCopy(free: free),
|
marshalIn: .stringToC, marshalOut: .stringCopy(free: free, constPointee: constOut),
|
||||||
gvalue: GValueOps(typeMacro: "G_TYPE_STRING",
|
gvalue: GValueOps(typeMacro: "G_TYPE_STRING",
|
||||||
getterSuffix: "string", setterSuffix: "string"))
|
getterSuffix: "string", setterSuffix: "string"))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func filenameMapping(free: Bool) -> Mapping {
|
static func filenameMapping(free: Bool, constOut: Bool = true) -> Mapping {
|
||||||
Mapping(
|
Mapping(
|
||||||
swiftType: "String", cSwiftType: "UnsafePointer<CChar>?",
|
swiftType: "String", cSwiftType: "UnsafePointer<CChar>?",
|
||||||
marshalIn: .stringToC, marshalOut: .stringCopy(free: free))
|
marshalIn: .stringToC, marshalOut: .stringCopy(free: free, constPointee: constOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,18 @@ public struct TypeRegistry: Sendable {
|
||||||
private let foreignNamespaces: Set<String>
|
private let foreignNamespaces: Set<String>
|
||||||
/// Maps a GIR namespace name to its Swift module name.
|
/// Maps a GIR namespace name to its Swift module name.
|
||||||
private let namespaceToModule: [String: String]
|
private let namespaceToModule: [String: String]
|
||||||
|
/// Swift simple type names declared by two or more distinct modules
|
||||||
|
/// (e.g. `Matrix` in both `Graphene` and `Pango`) — ambiguous wherever
|
||||||
|
/// referenced from a module other than the declaring one, since every
|
||||||
|
/// module `@_exported import`s its full dependency closure. See
|
||||||
|
/// `swiftTypeName(for:in:)`.
|
||||||
|
private var ambiguousSwiftNames: Set<String> = []
|
||||||
|
/// 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
|
||||||
|
/// reference-time drop checks share a single source of truth. See
|
||||||
|
/// ``droppedShadow(_:)``.
|
||||||
|
private var droppedGIRNames: [String: String] = [:]
|
||||||
|
|
||||||
/// The fully qualified name of the GObject root class.
|
/// The fully qualified name of the GObject root class.
|
||||||
public static let objectGIRName = "GObject.Object"
|
public static let objectGIRName = "GObject.Object"
|
||||||
|
|
@ -150,12 +162,18 @@ public struct TypeRegistry: Sendable {
|
||||||
/// Namespaces referenced via `<include>` but absent from `repositories`
|
/// Namespaces referenced via `<include>` but absent from `repositories`
|
||||||
/// are recorded as foreign, so references into them resolve to
|
/// are recorded as foreign, so references into them resolve to
|
||||||
/// ``TypeCategory/foreign(namespace:)`` rather than failing.
|
/// ``TypeCategory/foreign(namespace:)`` rather than failing.
|
||||||
///
|
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - repositories: Parsed repositories keyed by Swift module name.
|
/// - repositories: Parsed repositories keyed by Swift module name.
|
||||||
|
/// - directDependencies: Each module's direct dependency module
|
||||||
|
/// names, used to compute ``droppedShadow(_:)``. Defaults to empty
|
||||||
|
/// (no drop detection) for callers that don't need it.
|
||||||
/// - manualNamespaces: Namespaces deliberately excluded from generation
|
/// - manualNamespaces: Namespaces deliberately excluded from generation
|
||||||
/// and treated as foreign even when a GIR is present. Defaults to empty.
|
/// and treated as foreign even when a GIR is present. Defaults to empty.
|
||||||
public init(repositories: [String: Repository], manualNamespaces: Set<String> = []) {
|
public init(
|
||||||
|
repositories: [String: Repository],
|
||||||
|
directDependencies: [String: Set<String>] = [:],
|
||||||
|
manualNamespaces: Set<String> = []
|
||||||
|
) {
|
||||||
var namespaceToModule: [String: String] = [:]
|
var namespaceToModule: [String: String] = [:]
|
||||||
for (module, repo) in repositories {
|
for (module, repo) in repositories {
|
||||||
for ns in repo.namespaces {
|
for ns in repo.namespaces {
|
||||||
|
|
@ -178,6 +196,51 @@ public struct TypeRegistry: Sendable {
|
||||||
register(namespace: ns, module: module)
|
register(namespace: ns, module: module)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// sideways, between two modules that don't depend on each other but
|
||||||
|
// share a downstream dependent (e.g. Graphene.Matrix and
|
||||||
|
// 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.
|
||||||
|
var modulesByName: [String: Set<String>] = [:]
|
||||||
|
for resolved in types.values {
|
||||||
|
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
|
||||||
|
/// declaration was dropped as a duplicate (see ``init(repositories:directDependencies:manualNamespaces:)``).
|
||||||
|
///
|
||||||
|
/// - Parameter girName: A fully qualified GIR name, e.g. `"Gdk.Rectangle"`.
|
||||||
|
/// - Returns: The shadowing dependency module name, or `nil` if `girName`
|
||||||
|
/// was not dropped.
|
||||||
|
public func droppedShadow(_ girName: String) -> String? {
|
||||||
|
droppedGIRNames[girName]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers every type in one namespace.
|
/// Registers every type in one namespace.
|
||||||
|
|
@ -395,15 +458,28 @@ public struct TypeRegistry: Sendable {
|
||||||
|
|
||||||
/// The Swift spelling of a resolved type as written from `module`.
|
/// The Swift spelling of a resolved type as written from `module`.
|
||||||
///
|
///
|
||||||
/// Types from another module are qualified (`GObject.Object`); types from
|
/// Bare (`Widget`) in the overwhelmingly common case. Every module
|
||||||
/// the current module are bare (`Widget`).
|
/// `@_exported import`s its full dependency closure (so `import Gsk`
|
||||||
|
/// alone reaches `GLib`/`GObject`/`Gdk`/`Graphene`/`Pango`/...), which
|
||||||
|
/// flattens all those modules' public API into one lookup scope for any
|
||||||
|
/// file that imports one of them — including sideways, between two
|
||||||
|
/// modules that don't depend on each other but share a downstream
|
||||||
|
/// dependent (`Graphene.Matrix` and `Pango.Matrix` both reach `Gsk`).
|
||||||
|
/// A Swift simple name declared by more than one loaded module is
|
||||||
|
/// therefore genuinely ambiguous outside its own declaring module, and
|
||||||
|
/// is qualified (`Graphene.Matrix`) to disambiguate; same-module
|
||||||
|
/// self-references stay bare regardless (Swift shadowing rules mean a
|
||||||
|
/// module's own declarations are never ambiguous with themselves).
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - type: The resolved type to spell.
|
/// - type: The resolved type to spell.
|
||||||
/// - module: The Swift module the reference is being written in.
|
/// - module: The Swift module the reference is being written in.
|
||||||
/// - Returns: The Swift type name to emit.
|
/// - Returns: The Swift type name to emit.
|
||||||
public func swiftTypeName(for type: ResolvedType, in module: String) -> String {
|
public func swiftTypeName(for type: ResolvedType, in module: String) -> String {
|
||||||
type.swiftName
|
guard type.swiftModule != module, ambiguousSwiftNames.contains(type.swiftName) else {
|
||||||
|
return type.swiftName
|
||||||
|
}
|
||||||
|
return "\(type.swiftModule).\(type.swiftName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every class that is subclassed by some other loaded class.
|
/// Every class that is subclassed by some other loaded class.
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,14 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
||||||
stack.append(.ignored(elementName))
|
stack.append(.ignored(elementName))
|
||||||
|
|
||||||
case "record":
|
case "record":
|
||||||
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
guard let name = attributeDict["name"] else {
|
||||||
|
// Anonymous nested <record> (a C struct inside a <union>, e.g.
|
||||||
|
// GskPathPoint). Not a namespace-level type; discard it and its
|
||||||
|
// fields — record fields are deferred and the parent union is
|
||||||
|
// already .ignored.
|
||||||
|
stack.append(.ignored(elementName))
|
||||||
|
return
|
||||||
|
}
|
||||||
stack.append(.record(Record(
|
stack.append(.record(Record(
|
||||||
name: name,
|
name: name,
|
||||||
cType: attributeDict["c:type"] ?? "",
|
cType: attributeDict["c:type"] ?? "",
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ struct SwiftGtkGenCLI {
|
||||||
let outputRoot = URL(fileURLWithPath: monorepoConfig.outputDir)
|
let outputRoot = URL(fileURLWithPath: monorepoConfig.outputDir)
|
||||||
|
|
||||||
// Plan engine (the only engine): build registry, plan, render.
|
// Plan engine (the only engine): build registry, plan, render.
|
||||||
let registry = TypeRegistry(repositories: analysis.repositories)
|
let registry = TypeRegistry(repositories: analysis.repositories, directDependencies: analysis.directDependencies)
|
||||||
let modulePlans = planModules(analysis: analysis, registry: registry)
|
let modulePlans = planModules(analysis: analysis, registry: registry)
|
||||||
|
|
||||||
var outputs: [String: [String: String]] = [:]
|
var outputs: [String: [String: String]] = [:]
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,12 @@ struct NamingTests {
|
||||||
#expect(!isValidGeneratedFileName("boxedFree.swift")) // lowerCamelCase
|
#expect(!isValidGeneratedFileName("boxedFree.swift")) // lowerCamelCase
|
||||||
#expect(!isValidGeneratedFileName("PARAM_MASK.swift")) // SCREAMING_SNAKE
|
#expect(!isValidGeneratedFileName("PARAM_MASK.swift")) // SCREAMING_SNAKE
|
||||||
#expect(!isValidGeneratedFileName("CSET_a_2_z.swift")) // underscores
|
#expect(!isValidGeneratedFileName("CSET_a_2_z.swift")) // underscores
|
||||||
#expect(!isValidGeneratedFileName("cclosureMarshalBOOLEANFLAGS.swift")) // uppercase run
|
#expect(!isValidGeneratedFileName("cclosureMarshalBOOLEANFLAGS.swift")) // uppercase run, lowercase start
|
||||||
#expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // run > 3 caps
|
#expect(isValidGeneratedFileName("RGBA.swift")) // acronym type name
|
||||||
|
#expect(isValidGeneratedFileName("GLAPI.swift")) // acronym type name
|
||||||
|
#expect(isValidGeneratedFileName("DNDEvent.swift")) // acronym type name
|
||||||
#expect(!isValidGeneratedFileName("Align.txt")) // wrong extension
|
#expect(!isValidGeneratedFileName("Align.txt")) // wrong extension
|
||||||
|
#expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // uppercase run, PascalCase start
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - File merging
|
// MARK: - File merging
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ struct RecordGenerationTests {
|
||||||
#expect(source.contains("g_variant_type_copy(_instancePointer(pointer))"))
|
#expect(source.contains("g_variant_type_copy(_instancePointer(pointer))"))
|
||||||
#expect(source.contains("isolated deinit {"))
|
#expect(source.contains("isolated deinit {"))
|
||||||
#expect(source.contains("g_variant_type_free(_instancePointer(pointer))"))
|
#expect(source.contains("g_variant_type_free(_instancePointer(pointer))"))
|
||||||
|
// Records with a free function claim to take responsibility for freeing.
|
||||||
|
#expect(source.contains("takes responsibility"))
|
||||||
|
#expect(!source.contains("never frees"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("A record with no resolvable free renders no deinit")
|
@Test("A record with no resolvable free renders no deinit")
|
||||||
|
|
@ -91,6 +94,9 @@ struct RecordGenerationTests {
|
||||||
#expect(!source.contains("retaining"))
|
#expect(!source.contains("retaining"))
|
||||||
// The takingOwnership init is always present.
|
// The takingOwnership init is always present.
|
||||||
#expect(source.contains("takingOwnership"))
|
#expect(source.contains("takingOwnership"))
|
||||||
|
// No-free records must not claim to free the pointee.
|
||||||
|
#expect(source.contains("never frees"))
|
||||||
|
#expect(!source.contains("takes responsibility"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - C4 copy/free pairing
|
// MARK: - C4 copy/free pairing
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,36 @@ struct RendererCallableTests {
|
||||||
#expect(body.contains("func getCoords(id: Int32) -> (x: Int32, y: Int32)"))
|
#expect(body.contains("func getCoords(id: Int32) -> (x: Int32, y: Int32)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("Out-param local type follows the c:type: mutable char** vs const char**")
|
||||||
|
func outParamLocalTypeFollowsCType() throws {
|
||||||
|
let mutableFn = GlobalFunction(
|
||||||
|
name: "next_token", cIdentifier: "g_next_token",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "input", type: .string, cType: "const char*"),
|
||||||
|
Parameter(name: "endptr", type: .string, cType: "char**", direction: .out),
|
||||||
|
],
|
||||||
|
returnValue: ReturnValue(type: .void)
|
||||||
|
)
|
||||||
|
guard case .success(let mutablePlan) = planFunction(mutableFn, context: makeContext()) else {
|
||||||
|
Issue.record("expected next_token to plan successfully"); return
|
||||||
|
}
|
||||||
|
#expect(renderCallable(mutablePlan).contains("UnsafeMutablePointer<CChar>?"))
|
||||||
|
|
||||||
|
let constFn = GlobalFunction(
|
||||||
|
name: "peek_token", cIdentifier: "g_peek_token",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "input", type: .string, cType: "const char*"),
|
||||||
|
Parameter(name: "endptr", type: .string, cType: "const char**", direction: .out),
|
||||||
|
],
|
||||||
|
returnValue: ReturnValue(type: .void)
|
||||||
|
)
|
||||||
|
guard case .success(let constPlan) = planFunction(constFn, context: makeContext()) else {
|
||||||
|
Issue.record("expected peek_token to plan successfully"); return
|
||||||
|
}
|
||||||
|
#expect(renderCallable(constPlan).contains("UnsafePointer<CChar>?"))
|
||||||
|
#expect(!renderCallable(constPlan).contains("UnsafeMutablePointer<CChar>?"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test("Swift return with out-params produces a labeled tuple")
|
@Test("Swift return with out-params produces a labeled tuple")
|
||||||
func returnWithOutParamsLabeledTuple() throws {
|
func returnWithOutParamsLabeledTuple() throws {
|
||||||
let fn = GlobalFunction(
|
let fn = GlobalFunction(
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,34 @@ struct SignalGenerationTests {
|
||||||
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A context whose registry additionally declares two boxed records: one
|
||||||
|
/// with a copy function (wraps via `init(retaining:)`), one without
|
||||||
|
/// (wraps via `init(takingOwnership:)`, mirroring `GdkToplevelSize`).
|
||||||
|
func makeBoxedContext() -> MapContext {
|
||||||
|
let gobject = Repository(namespaces: [
|
||||||
|
Namespace(
|
||||||
|
name: "GObject", version: "2.0",
|
||||||
|
records: [
|
||||||
|
Record(name: "Value", cType: "GValue", getTypeFunction: "g_value_get_type",
|
||||||
|
copyFunction: "g_value_copy", freeFunction: "g_value_free"),
|
||||||
|
Record(name: "NoCopyBox", cType: "GNoCopyBox", getTypeFunction: "g_no_copy_box_get_type"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
])
|
||||||
|
let registry = TypeRegistry(repositories: ["GObject": gobject])
|
||||||
|
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderClass(named name: String, signals: [Signal], context: MapContext) -> (source: String, plan: ClassPlan, skips: [SkipEntry]) {
|
||||||
|
let klass = Class(name: name, cType: "G\(name)", parent: nil,
|
||||||
|
getTypeFunction: "g_\(name.lowercased())_get_type",
|
||||||
|
signals: signals)
|
||||||
|
let (plan, skips) = planClass(klass, context: context)
|
||||||
|
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: skips,
|
||||||
|
coverage: CoverageStats())
|
||||||
|
return (renderModule(module)["\(name).swift"] ?? "", plan, skips)
|
||||||
|
}
|
||||||
|
|
||||||
/// Plans a one-off class carrying `signals` and renders it, returning the
|
/// Plans a one-off class carrying `signals` and renders it, returning the
|
||||||
/// class file body plus the plan and skips.
|
/// class file body plus the plan and skips.
|
||||||
func renderClass(named name: String, signals: [Signal]) -> (source: String, plan: ClassPlan, skips: [SkipEntry]) {
|
func renderClass(named name: String, signals: [Signal]) -> (source: String, plan: ClassPlan, skips: [SkipEntry]) {
|
||||||
|
|
@ -102,4 +130,23 @@ struct SignalGenerationTests {
|
||||||
#expect(source.contains("_sgtk_signal_connect_data("))
|
#expect(source.contains("_sgtk_signal_connect_data("))
|
||||||
#expect(!source.contains("_sgtk_signal_connect_data(ptr, cName, unsafeBitCast(\\(plan.trampolineCName)"))
|
#expect(!source.contains("_sgtk_signal_connect_data(ptr, cName, unsafeBitCast(\\(plan.trampolineCName)"))
|
||||||
}
|
}
|
||||||
|
@Test("Boxed signal param wraps via retaining: when a copy function exists, takingOwnership: when it doesn't")
|
||||||
|
func boxedSignalParamWrapperSelection() throws {
|
||||||
|
let withCopy = Signal(
|
||||||
|
name: "value-changed",
|
||||||
|
parameters: [Parameter(name: "value", type: .typeRef("Value", namespace: "GObject"),
|
||||||
|
cType: "GValue*")]
|
||||||
|
)
|
||||||
|
let withoutCopy = Signal(
|
||||||
|
name: "box-changed",
|
||||||
|
parameters: [Parameter(name: "box", type: .typeRef("NoCopyBox", namespace: "GObject"),
|
||||||
|
cType: "GNoCopyBox*")]
|
||||||
|
)
|
||||||
|
let (source, plan, skips) = renderClass(named: "Emitter", signals: [withCopy, withoutCopy], context: makeBoxedContext())
|
||||||
|
#expect(skips.isEmpty)
|
||||||
|
#expect(plan.signals.count == 2)
|
||||||
|
#expect(source.contains("Value(retaining:"))
|
||||||
|
#expect(source.contains("NoCopyBox(takingOwnership:"))
|
||||||
|
#expect(!source.contains("NoCopyBox(retaining:"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,27 @@ struct TypeMapperTests {
|
||||||
#expect(mapping.marshalOut == .stringCopy(free: false))
|
#expect(mapping.marshalOut == .stringCopy(free: false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("String out-param constPointee follows the c:type, not transfer")
|
||||||
|
func stringConstPointeeFollowsCType() throws {
|
||||||
|
let ctx = makeContext()
|
||||||
|
// `const gchar**`: pointee is borrowed, marshal must not free through
|
||||||
|
// a mutable pointer.
|
||||||
|
let constMapping = try map(.string, nullable: false, transfer: .none,
|
||||||
|
context: ctx, cType: "const gchar**")
|
||||||
|
#expect(constMapping.marshalOut == .stringCopy(free: false, constPointee: true))
|
||||||
|
// `gchar**`: pointee is mutable/owned.
|
||||||
|
let mutableMapping = try map(.string, nullable: false, transfer: .none,
|
||||||
|
context: ctx, cType: "gchar**")
|
||||||
|
#expect(mutableMapping.marshalOut == .stringCopy(free: false, constPointee: false))
|
||||||
|
// No c:type given: defaults to const (the conservative choice).
|
||||||
|
let defaultMapping = try map(.string, nullable: false, transfer: .none, context: ctx)
|
||||||
|
#expect(defaultMapping.marshalOut == .stringCopy(free: false, constPointee: true))
|
||||||
|
// .filename follows the same rule.
|
||||||
|
let filenameMapping = try map(.filename, nullable: false, transfer: .none,
|
||||||
|
context: ctx, cType: "gchar**")
|
||||||
|
#expect(filenameMapping.marshalOut == .stringCopy(free: false, constPointee: false))
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Nullable wrapping
|
// MARK: - Nullable wrapping
|
||||||
|
|
||||||
@Test("Nullable string becomes Optional")
|
@Test("Nullable string becomes Optional")
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,67 @@ struct TypeRegistryTests {
|
||||||
#expect(registry.swiftTypeName(for: widget, in: "Gtk") == "Widget")
|
#expect(registry.swiftTypeName(for: widget, in: "Gtk") == "Widget")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test("A simple name declared by two sibling modules is qualified outside its own module")
|
||||||
|
func qualifiesAmbiguousSwiftNames() throws {
|
||||||
|
// Graphene and Pango both declare `Matrix`; neither depends on the
|
||||||
|
// other, but Gsk (downstream) reaches both via @_exported import.
|
||||||
|
let graphene = Repository(namespaces: [
|
||||||
|
Namespace(name: "Graphene", version: "1.0",
|
||||||
|
records: [Record(name: "Matrix", cType: "graphene_matrix_t",
|
||||||
|
getTypeFunction: "graphene_matrix_get_type")])
|
||||||
|
])
|
||||||
|
let pango = Repository(namespaces: [
|
||||||
|
Namespace(name: "Pango", version: "1.0",
|
||||||
|
records: [
|
||||||
|
Record(name: "Matrix", cType: "PangoMatrix",
|
||||||
|
getTypeFunction: "pango_matrix_get_type"),
|
||||||
|
Record(name: "Rectangle", cType: "PangoRectangle",
|
||||||
|
getTypeFunction: "pango_rectangle_get_type"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let registry = TypeRegistry(repositories: ["Graphene": graphene, "Pango": pango])
|
||||||
|
let grapheneMatrix = try #require(registry.resolve(girName: "Graphene.Matrix"))
|
||||||
|
let uniqueType = try #require(registry.resolve(girName: "Pango.Rectangle"))
|
||||||
|
#expect(registry.swiftTypeName(for: grapheneMatrix, in: "Gsk") == "Graphene.Matrix")
|
||||||
|
#expect(registry.swiftTypeName(for: uniqueType, in: "Gsk") == "Rectangle")
|
||||||
|
// Same-module self-reference always stays bare, even though the
|
||||||
|
// name is ambiguous elsewhere.
|
||||||
|
#expect(registry.swiftTypeName(for: grapheneMatrix, in: "Graphene") == "Matrix")
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.
|
||||||
|
let dep = Repository(namespaces: [
|
||||||
|
Namespace(name: "Dep", version: "1.0",
|
||||||
|
records: [Record(name: "Rect", cType: "DepRect", 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")])
|
||||||
|
])
|
||||||
|
let registry = TypeRegistry(
|
||||||
|
repositories: ["Dep": dep, "Mid": mid],
|
||||||
|
directDependencies: ["Mid": ["Dep"]]
|
||||||
|
)
|
||||||
|
#expect(registry.droppedShadow("Mid.Rect") == "Dep")
|
||||||
|
#expect(registry.droppedShadow("Dep.Rect") == nil)
|
||||||
|
|
||||||
|
let context = MapContext(registry: registry, currentModule: "Down", currentNamespace: "Down",
|
||||||
|
dependencyModules: ["Dep", "Mid"])
|
||||||
|
do {
|
||||||
|
_ = try map(.typeRef("Rect", namespace: "Mid"), nullable: false, transfer: .none, context: context)
|
||||||
|
Issue.record("expected reference to dropped Mid.Rect to throw")
|
||||||
|
} catch let err as MapError {
|
||||||
|
#expect(err.reason == .duplicateOfDependency)
|
||||||
|
} catch {
|
||||||
|
Issue.record("unexpected error type: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test("Manually excluded namespaces are treated as foreign")
|
@Test("Manually excluded namespaces are treated as foreign")
|
||||||
func manualNamespacesAreForeign() {
|
func manualNamespacesAreForeign() {
|
||||||
let repo = Repository(namespaces: [
|
let repo = Repository(namespaces: [
|
||||||
|
|
|
||||||
39
smoke/tier4/GdkSmoke.swift
Normal file
39
smoke/tier4/GdkSmoke.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// GdkSmoke.swift
|
||||||
|
// Tier-4-only runtime smoke test for Gdk (Phase E3). Proves — against the
|
||||||
|
// REAL libgtk-4, linked at runtime — that:
|
||||||
|
// 1. `keyvalFromName(keyvalName:)` reaches the real `gdk_keyval_from_name`
|
||||||
|
// C call, marshalling a Swift `String` through `withCString` and the
|
||||||
|
// returned `guint` keyval back as `UInt32`.
|
||||||
|
// 2. `keyvalName(keyval:)` reaches the real `gdk_keyval_name`, marshalling
|
||||||
|
// the `guint` argument and the returned `const gchar*` back into a
|
||||||
|
// Swift `String?` — a round trip through both keyval accessors.
|
||||||
|
//
|
||||||
|
// Both are display-free: no `GdkDisplay`/surface is created, so this runs
|
||||||
|
// without a windowing backend.
|
||||||
|
//
|
||||||
|
// 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 >= 4 (Gdk's module).
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
import GLib
|
||||||
|
import GObject
|
||||||
|
import Gdk
|
||||||
|
|
||||||
|
@Suite("Tier 4 Gdk smoke tests")
|
||||||
|
struct GdkSmokeTests {
|
||||||
|
@Test("keyvalFromName(keyvalName:) reaches the real gdk_keyval_from_name and reports the known GDK_KEY_a value")
|
||||||
|
func keyvalFromNameReturnsKnownKeyval() throws {
|
||||||
|
let keyval = keyvalFromName(keyvalName: "a")
|
||||||
|
// GDK_KEY_a is a fixed constant (0x61, matching ASCII 'a').
|
||||||
|
#expect(keyval == 0x61)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("keyvalName(keyval:) round-trips the value keyvalFromName reports back to the original name")
|
||||||
|
func keyvalRoundTrips() throws {
|
||||||
|
let keyval = keyvalFromName(keyvalName: "a")
|
||||||
|
let name = keyvalName(keyval: keyval)
|
||||||
|
#expect(name == "a")
|
||||||
|
}
|
||||||
|
}
|
||||||
38
smoke/tier4/GskSmoke.swift
Normal file
38
smoke/tier4/GskSmoke.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// GskSmoke.swift
|
||||||
|
// Tier-4-only runtime smoke test for Gsk (Phase E3). Proves — against the
|
||||||
|
// REAL libgtk-4, linked at runtime — that `CairoRenderer()` reaches the real
|
||||||
|
// `gsk_cairo_renderer_new` C constructor and adopts the returned pointer
|
||||||
|
// through `init(takingOwnership:)`, exercising the Gsk GObject-class
|
||||||
|
// construction path end to end.
|
||||||
|
//
|
||||||
|
// `CairoRenderer` is display-free to construct (it only needs a `GdkSurface`
|
||||||
|
// at `realize()` time, which this test does not call), so this runs without
|
||||||
|
// a windowing backend.
|
||||||
|
//
|
||||||
|
// `gsk_serialization_error_quark` — the other display-free candidate this
|
||||||
|
// tier considered — is deprecated/moved-to `SerializationError.quark` in the
|
||||||
|
// GIR and the nested enum-scoped function is not yet planned by the
|
||||||
|
// generator (absent from docs/skip-baseline/tier4/Gsk.json's bound set), so
|
||||||
|
// `CairoRenderer()` alone carries the Gsk runtime-coverage requirement here.
|
||||||
|
//
|
||||||
|
// 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 >= 4 (Gsk's module).
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
import GLib
|
||||||
|
import GObject
|
||||||
|
import Gdk
|
||||||
|
import Gsk
|
||||||
|
|
||||||
|
@Suite("Tier 4 Gsk smoke tests")
|
||||||
|
struct GskSmokeTests {
|
||||||
|
@Test("CairoRenderer() reaches the real gsk_cairo_renderer_new and constructs a live GObject")
|
||||||
|
func cairoRendererConstructs() throws {
|
||||||
|
let renderer = CairoRenderer()
|
||||||
|
// `isRealized()` is a plain GObject property read (no surface needed):
|
||||||
|
// a freshly constructed renderer must never report itself realized.
|
||||||
|
#expect(renderer.isRealized() == false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue