1
0
Fork 0

Initial commit: SwiftGtkGen GIR-to-Swift binding generator

Extracted from gtk-swift monorepo. Previous history
contains 38 commits across the full Phase 1 development
including GIR XML parsing, IR model, analysis engine,
code generation, CLI, plugins, and documentation.
This commit is contained in:
Brendan Szymanski 2026-07-01 03:25:08 -04:00
commit 1330f2bd25
24 changed files with 4095 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.build/

79
.swift-format Normal file
View file

@ -0,0 +1,79 @@
{
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"indentBlankLines" : false,
"indentConditionalCompilationBlocks" : true,
"indentSwitchCaseLabels" : false,
"indentation" : {
"spaces" : 4
},
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : false,
"lineBreakBeforeEachGenericRequirement" : false,
"lineBreakBetweenDeclarationAttributes" : false,
"lineLength" : 1000,
"maximumBlankLines" : 1,
"multiElementCollectionTrailingCommas" : true,
"noAssignmentInExpressions" : {
"allowedFunctions" : [
"XCTAssertNoThrow"
]
},
"orderedImports" : {
"includeConditionalImports" : false
},
"prioritizeKeepingFunctionOutputTogether" : false,
"reflowMultilineStringLiterals" : "never",
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLiteralForEmptyCollectionInit" : false,
"AlwaysUseLowerCamelCase" : true,
"AmbiguousTrailingClosureOverload" : true,
"AvoidRetroactiveConformances" : false,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyLinesOpeningClosingBraces" : false,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoPlaygroundLiterals" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OmitExplicitReturns" : false,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReplaceForEachWithForLoop" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"TypeNamesShouldBeCapitalized" : true,
"UseEarlyExits" : false,
"UseExplicitNilCheckInConditions" : true,
"UseLetInEveryBoundCaseVariable" : true,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : true,
"UseSynthesizedInitializer" : true,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
},
"spacesAroundRangeFormationOperators" : false,
"spacesBeforeEndOfLineComments" : 2,
"tabWidth" : 4,
"version" : 1
}

52
Package.swift Normal file
View file

@ -0,0 +1,52 @@
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "gir-generator",
platforms: [.macOS(.v14)],
products: [
.executable(name: "swift-gtk-gen", targets: ["swift-gtk-gen"]),
.plugin(name: "SwiftGtkGenBuildPlugin", targets: ["SwiftGtkGenBuildPlugin"]),
.plugin(name: "SwiftGtkGenCommandPlugin", targets: ["SwiftGtkGenCommandPlugin"]),
],
targets: [
.target(
name: "SwiftGtkGenCore",
dependencies: []
),
.executableTarget(
name: "swift-gtk-gen",
dependencies: ["SwiftGtkGenCore"]
),
.plugin(
name: "SwiftGtkGenBuildPlugin",
capability: .buildTool(),
dependencies: ["swift-gtk-gen"],
path: "Sources/SwiftGtkGenBuildPlugin"
),
.plugin(
name: "SwiftGtkGenCommandPlugin",
capability: .command(
intent: .custom(
verb: "generate-gir-bindings",
description: "Generate Swift bindings from .gir files."
),
permissions: [.writeToPackageDirectory(reason: "Write generated Swift files to source directory.")]
),
dependencies: ["swift-gtk-gen"],
path: "Sources/SwiftGtkGenCommandPlugin"
),
.testTarget(
name: "SwiftGtkGenCoreTests",
dependencies: ["SwiftGtkGenCore"]
),
.testTarget(
name: "SwiftGtkGenCLITests",
dependencies: ["SwiftGtkGenCore"]
),
.testTarget(
name: "IntegrationTests",
dependencies: ["SwiftGtkGenCore"]
),
]
)

View file

@ -0,0 +1,75 @@
import PackagePlugin
import Foundation
/// SwiftPM build tool plugin that automatically generates Swift GIR bindings.
///
/// Inspects each target for a `Config.swift` file and `.gir` files, then
/// invokes the `swift-gtk-gen` executable to produce Swift source before compilation.
/// Targets without a config or GIR files are silently skipped.
@main
struct SwiftGtkGenBuildPlugin: BuildToolPlugin {
/// Creates build commands for GIR code generation for the given target.
///
/// Returns a single `.buildCommand` that runs `swift-gtk-gen` with the target's
/// config and GIR files as inputs. If the target has no `Config.swift` or
/// no `.gir` files, returns an empty array.
///
/// - Parameters:
/// - context: The plugin context providing access to tools and work directories.
/// - target: The target to generate bindings for.
/// - Returns: An array of build commands (empty if no GIR files present).
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
let targetDir = target.directoryURL
let configURL = targetDir.appending(path: "Config.swift")
guard FileManager.default.fileExists(atPath: configURL.path()) else {
return []
}
let executable = try context.tool(named: "swift-gtk-gen")
let girFiles = findGIRFiles(in: targetDir)
guard !girFiles.isEmpty else { return [] }
let outputDir = context.pluginWorkDirectoryURL
return [
.buildCommand(
displayName: "Generating \(target.name) GIR bindings...",
executable: executable.url,
arguments: [
"--config", configURL.path(),
"--library", target.name,
"--version", "4.0",
"--output", outputDir.appending(path: target.name).path(),
],
inputFiles: [configURL] + girFiles,
outputFiles: [
outputDir.appending(path: "\(target.name)/Generated.swift")
]
)
]
}
/// Recursively finds all `.gir` files in the given directory.
///
/// Uses a directory enumerator to walk the file tree and collect URLs
/// whose path extension is `"gir"`.
///
/// - Parameter directory: The root directory to search.
/// - Returns: An array of URLs pointing to discovered `.gir` files.
private func findGIRFiles(in directory: URL) -> [URL] {
guard let enumerator = FileManager.default.enumerator(
at: directory, includingPropertiesForKeys: [.isRegularFileKey]
) else {
return []
}
var girFiles: [URL] = []
for case let fileURL as URL in enumerator {
if fileURL.pathExtension == "gir" {
girFiles.append(fileURL)
}
}
return girFiles
}
}

View file

@ -0,0 +1,92 @@
import PackagePlugin
import Foundation
/// SwiftPM command plugin for manual GIR binding regeneration.
///
/// Invoked via `swift package generate-gir`. Iterates over all package
/// targets, checks for a `Config.swift` and `.gir` files, and runs the
/// `swift-gtk-gen` executable to produce or update generated Swift sources in
/// each target's `Generated/` directory.
@main
struct SwiftGtkGenCommandPlugin: CommandPlugin {
/// Performs GIR code generation for all eligible targets in the package.
///
/// For each target that has a `Config.swift` and at least one `.gir` file,
/// launches `swift-gtk-gen` as a subprocess with arguments derived from the
/// target's directory and metadata. Generated files are written to
/// `<target>/Generated/`. Throws if any subprocess fails.
///
/// - Parameters:
/// - context: The plugin context providing access to tools and package targets.
/// - arguments: Unused command-line arguments passed to the plugin.
func performCommand(context: PluginContext, arguments: [String]) async throws {
let executable = try context.tool(named: "swift-gtk-gen")
for target in context.package.targets {
let targetDir = target.directoryURL
let configURL = targetDir.appending(path: "Config.swift")
guard FileManager.default.fileExists(atPath: configURL.path()) else {
continue
}
let girFiles = findGIRFiles(in: targetDir)
guard !girFiles.isEmpty else { continue }
let outputDir = targetDir.appending(path: "Generated")
let process = Process()
process.executableURL = executable.url
process.arguments = [
"--config", configURL.path(),
"--library", target.name,
"--version", "4.0",
"--output", outputDir.path(),
]
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
throw SwiftGtkGenPluginError.generationFailed(target.name)
}
}
}
/// Recursively finds all `.gir` files in the given directory.
///
/// Uses a directory enumerator to walk the file tree and collect URLs
/// whose path extension is `"gir"`.
///
/// - Parameter directory: The root directory to search.
/// - Returns: An array of URLs pointing to discovered `.gir` files.
private func findGIRFiles(in directory: URL) -> [URL] {
guard let enumerator = FileManager.default.enumerator(
at: directory, includingPropertiesForKeys: [.isRegularFileKey]
) else {
return []
}
var girFiles: [URL] = []
for case let fileURL as URL in enumerator {
if fileURL.pathExtension == "gir" {
girFiles.append(fileURL)
}
}
return girFiles
}
}
/// Errors that can occur during command-plugin GIR generation.
enum SwiftGtkGenPluginError: Error {
/// Generation failed for the specified target name.
case generationFailed(String)
}
extension SwiftGtkGenPluginError: CustomStringConvertible {
/// A human-readable description of the error.
var description: String {
switch self {
case .generationFailed(let target):
return "GIR generation failed for target: \(target)"
}
}
}

View file

@ -0,0 +1,127 @@
/// The result of analyzing a GIR repository against a `GenerationConfig`.
///
/// Contains the resolved class hierarchy, the classification of each type
/// (generate, manual, or ignore), and any per-type or per-function overrides
/// from the configuration. This result drives the subsequent code-generation
/// phase.
public struct AnalysisResult {
/// Maps each type name to its parent type name, forming an inheritance tree.
///
/// Keys are fully-qualified type names (e.g. `"Gtk.Widget"`); values are the
/// fully-qualified parent name.
public var classHierarchy: [String: String]
/// The set of fully-qualified type names that should have Swift bindings generated.
public var generatedTypes: Set<String>
/// The set of fully-qualified type names that are implemented manually and
/// should be excluded from generation.
public var manualTypes: Set<String>
/// The set of fully-qualified type names that should be skipped entirely.
public var ignoredTypes: Set<String>
/// Per-type overrides keyed by fully-qualified type name.
public var classOverrides: [String: ObjectOverrides]
/// Per-function overrides keyed by `"ClassName.functionName"`.
public var functionOverrides: [String: FunctionOverrides]
/// Creates an analysis result from the given component values.
///
/// - Parameters:
/// - classHierarchy: Inheritance map from each type to its parent.
/// - generatedTypes: Types that should be generated.
/// - manualTypes: Types that are implemented manually.
/// - ignoredTypes: Types that should be omitted.
/// - classOverrides: Per-type override configuration.
/// - functionOverrides: Per-function override configuration.
public init(classHierarchy: [String: String] = [:], generatedTypes: Set<String> = [],
manualTypes: Set<String> = [], ignoredTypes: Set<String> = [],
classOverrides: [String: ObjectOverrides] = [:],
functionOverrides: [String: FunctionOverrides] = [:]) {
self.classHierarchy = classHierarchy
self.generatedTypes = generatedTypes
self.manualTypes = manualTypes
self.ignoredTypes = ignoredTypes
self.classOverrides = classOverrides
self.functionOverrides = functionOverrides
}
}
/// Analyzes a GIR repository against a `GenerationConfig` to produce an `AnalysisResult`.
///
/// The analyzer walks the repository's type hierarchy and applies the
/// configuration's generate/manual/ignore lists and per-type overrides.
public struct Analyzer {
/// The generation configuration that guides the analysis.
public let config: GenerationConfig
/// Creates an analyzer with the given generation configuration.
///
/// - Parameter config: The configuration specifying which types to
/// generate, handle manually, or ignore, along with any overrides.
public init(config: GenerationConfig) {
self.config = config
}
/// Analyzes the provided GIR repository and returns the classification result.
///
/// The analysis builds a class inheritance hierarchy from the repository,
/// classifies each type according to the configuration's `generate`,
/// `manual`, and `ignore` lists, and extracts per-type and per-function
/// overrides.
///
/// - Parameter repository: The parsed GIR repository to analyze.
/// - Returns: An `AnalysisResult` containing the resolved hierarchy, type
/// classifications, and overrides.
public func analyze(repository: Repository) -> AnalysisResult {
var classHierarchy: [String: String] = [:]
var generatedTypes: Set<String> = []
var manualTypes: Set<String> = []
var ignoredTypes: Set<String> = []
var classOverrides: [String: ObjectOverrides] = [:]
var functionOverrides: [String: FunctionOverrides] = [:]
for ns in repository.namespaces {
for cls in ns.classes {
let fullName = "\(ns.name).\(cls.name)"
if let parent = cls.parent {
let parentFullName = parent.contains(".") ? parent : "\(ns.name).\(parent)"
classHierarchy[fullName] = parentFullName
}
}
}
for fullName in config.generate {
generatedTypes.insert(fullName)
}
for fullName in config.manual {
manualTypes.insert(fullName)
}
for fullName in config.ignore {
ignoredTypes.insert(fullName)
}
for objConfig in config.objects {
switch objConfig {
case .object(let name, let overrides):
classOverrides[name] = overrides
case .function(let className, let functionName, let overrides):
let key = "\(className).\(functionName)"
functionOverrides[key] = overrides
case .functionPattern, .signal, .property:
break
}
}
return AnalysisResult(
classHierarchy: classHierarchy,
generatedTypes: generatedTypes,
manualTypes: manualTypes,
ignoredTypes: ignoredTypes,
classOverrides: classOverrides,
functionOverrides: functionOverrides
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,250 @@
/// Top-level configuration for generating Swift bindings from a GIR repository.
///
/// `GenerationConfig` is the Swift-native equivalent of a config file or TOML
/// approach used by other GIR-based generators. It specifies which `.gir` files
/// to read, where to emit output, which types to generate/manually implement/
/// ignore, and any per-type or per-function overrides.
///
/// ### Example
/// ```swift
/// GenerationConfig(
/// library: "Gtk",
/// version: "4.0",
/// girsDirectories: ["/usr/share/gir-1.0"],
/// targetDirectory: "Sources/Gtk",
/// externalLibraries: ["GLib", "GObject"],
/// generate: ["Gtk.Widget", "Gtk.Window"],
/// manual: ["Gtk.CustomWidget"],
/// ignore: ["Gtk.DeprecatedType"],
/// objects: []
/// )
/// ```
public struct GenerationConfig {
/// The name of the library being wrapped (e.g. `"Gtk"`, `"GLib"`).
public var library: String
/// The version string of the GIR namespace (e.g. `"4.0"`).
public var version: String
/// Directories to search for `.gir` files.
public var girsDirectories: [String]
/// Directory where generated Swift source files should be written.
public var targetDirectory: String
/// Names of external libraries whose types may be referenced (e.g. `"GLib"`, `"GObject"`).
public var externalLibraries: [String]
/// Fully-qualified type names for which Swift bindings should be generated.
public var generate: [String]
/// Fully-qualified type names that are implemented manually and should not be generated.
public var manual: [String]
/// Fully-qualified type names that should be skipped entirely.
public var ignore: [String]
/// Per-type and per-function override entries.
public var objects: [ObjectConfig]
/// Creates a complete generation configuration.
///
/// - Parameters:
/// - library: The library name (e.g. `"Gtk"`).
/// - version: The GIR namespace version (e.g. `"4.0"`).
/// - girsDirectories: Paths to search for `.gir` files.
/// - targetDirectory: Output directory for generated Swift sources.
/// - externalLibraries: Referenced external library names.
/// - generate: Types to generate bindings for.
/// - manual: Types handled by hand-written code.
/// - ignore: Types to skip.
/// - objects: Override entries for types, functions, signals, and properties.
public init(library: String, version: String, girsDirectories: [String],
targetDirectory: String, externalLibraries: [String],
generate: [String], manual: [String], ignore: [String],
objects: [ObjectConfig]) {
self.library = library
self.version = version
self.girsDirectories = girsDirectories
self.targetDirectory = targetDirectory
self.externalLibraries = externalLibraries
self.generate = generate
self.manual = manual
self.ignore = ignore
self.objects = objects
}
}
/// A configuration entry targeting a specific type, function, function pattern,
/// signal, or property in the GIR repository.
///
/// Each case carries the overrides or rename rules that should be applied
/// during code generation for the matched element.
public enum ObjectConfig {
/// Overrides for a specific GObject type identified by its fully-qualified name.
case object(_ name: String, overrides: ObjectOverrides)
/// Overrides for a specific function on a type.
case function(_ type: String, _ name: String, overrides: FunctionOverrides)
/// A regex-based rename rule applied to matching functions on a type.
case functionPattern(_ type: String, pattern: String, rename: RenameRule)
/// Overrides for a specific signal on a type.
case signal(_ type: String, _ name: String, overrides: SignalOverrides)
/// Overrides for a specific property on a type.
case property(_ type: String, _ name: String, overrides: PropertyOverrides)
}
/// Overrides that control how a single GObject type is treated during code generation.
///
/// All properties are optional; only the values that are explicitly set will
/// override the default generation behavior for the matched type.
public struct ObjectOverrides {
/// Whether to generate, mark as manual, or ignore this type.
public var status: ObjectStatus?
/// If `true`, mark the generated class as `final`.
public var finalType: Bool?
/// The concurrency model to apply (e.g. `@MainActor`, `Sendable`).
public var concurrency: ConcurrencyModel?
/// Minimum version string; the type is only generated when targeting this version or later.
public var version: String?
/// An optional `#if` compilation condition to guard the generated code.
public var cfgCondition: String?
/// If `true`, generate a builder pattern struct for constructing this type.
public var generateBuilder: Bool?
}
/// Controls how a generated type should be treated.
public enum ObjectStatus: String {
/// Generate Swift bindings for this type.
case generate
/// This type will be implemented manually; do not generate.
case manual
/// Skip this type entirely.
case ignore
}
/// The concurrency model to apply to a generated type.
public enum ConcurrencyModel: String {
/// Annotate the generated type with `@MainActor`.
case mainActor
/// Mark the generated type as `Sendable`.
case sendable
/// No special concurrency annotation.
case none
}
/// Overrides that control how a single function is treated during code generation.
///
/// All properties are optional; only the values that are explicitly set will
/// override the default generation behavior for the matched function.
public struct FunctionOverrides {
/// If `true`, skip generating this function entirely.
public var ignore: Bool?
/// A rename rule to apply to this function's Swift name.
public var rename: RenameRule?
/// Minimum version string; only generate when targeting this version or later.
public var version: String?
/// An optional `#if` compilation condition to guard the generated code.
public var cfgCondition: String?
/// If `true`, treat this function as a constructor (returns a new instance).
public var constructor: Bool?
/// Override the visibility of the generated method.
public var visibility: Visibility?
/// Per-parameter overrides keyed by the parameter's original name.
public var parameters: [String: ParameterOverride]?
}
/// The visibility level for a generated symbol.
public enum Visibility: String {
/// Visible outside the module.
case `public`
/// Visible only within the same module.
case `internal`
/// Visible to all modules in the same package.
case `package`
}
/// Overrides for a single function parameter in the generated API.
public struct ParameterOverride {
/// If set, overrides whether the parameter is treated as nullable.
public var nullable: Bool?
/// If set, renames this parameter in the generated Swift function signature.
public var newName: String?
}
/// Overrides that control how a GObject signal handler is generated.
///
/// All properties are optional; only the explicitly set values override
/// the default behavior for the matched signal.
public struct SignalOverrides {
/// If `true`, skip generating a handler API for this signal.
public var ignore: Bool?
/// If `true`, the signal can be inhibited (stopped from propagating).
public var inhibit: Bool?
/// Per-parameter overrides for the signal handler's closure parameters.
public var parameters: [String: ParameterOverride]?
}
/// Overrides that control how a GObject property is exposed in the generated API.
public struct PropertyOverrides {
/// The accessor methods to generate for this property.
///
/// If `nil`, the default set of accessors is generated based on the GIR
/// metadata. Provide an explicit array to override which accessors are emitted.
public var generate: [PropertyAccessor]?
}
/// The kind of accessor to generate for a GObject property.
public enum PropertyAccessor: String {
/// Generate a getter method for the property.
case get
/// Generate a setter method for the property.
case set
/// Generate a notification callback/handler for property changes.
case notify
}
/// A regex-based rename rule for transforming GIR symbol names into Swift names.
///
/// Matches the `regex` pattern against the original name and substitutes the
/// `replacement` string, following standard regex capture-group semantics
/// (e.g., `"$1"`, `"$2"`).
///
/// ### Example
/// ```swift
/// RenameRule(regex: "^gtk_", replacement: "")
/// ```
public struct RenameRule {
/// The regular expression pattern to match against the original name.
public var regex: String
/// The replacement string, which may reference capture groups with `$1`, `$2`, etc.
public var replacement: String
/// Creates a rename rule with the given regex pattern and replacement.
///
/// - Parameters:
/// - regex: A regular expression pattern.
/// - replacement: A replacement string (may include capture-group references).
public init(regex: String, replacement: String) {
self.regex = regex
self.replacement = replacement
}
}

View file

@ -0,0 +1,643 @@
/// A complete GIR repository, containing one or more namespaces.
///
/// Corresponds to the root `<repository>` element in a GIR XML file. A single
/// `.gir` file produces one `Repository` holding all namespaces defined within it.
public struct Repository {
/// The namespaces contained in this repository.
public var namespaces: [Namespace]
/// The C header include path from `<c:include>` (e.g. `"gtk/gtk.h"`).
public var cHeaderPath: String
/// Link names for GIR dependencies derived from `<include>` elements.
/// Derived at parse time from the library name and version (e.g. `"Gdk-4.0"` becomes `"gdk-4"`).
public var includedLibraryLinks: [String]
/// Package name from `<package>` element (e.g. `"gtk4"`).
public var packageName: String
/// Creates a new repository.
/// - Parameter namespaces: The namespaces contained in this repository.
public init(namespaces: [Namespace] = [], cHeaderPath: String = "", includedLibraryLinks: [String] = [], packageName: String = "") {
self.namespaces = namespaces
self.cHeaderPath = cHeaderPath
self.includedLibraryLinks = includedLibraryLinks
self.packageName = packageName
}
}
/// A GIR namespace, grouping related type definitions within a repository.
///
/// Corresponds to the `<namespace>` element in a GIR XML file. A namespace
/// holds all type definitions classes, interfaces, records, enumerations,
/// bitfields, callbacks, global functions, constants, and type aliases that
/// belong to a single GIR namespace such as `Gtk` or `GObject`.
public struct Namespace {
/// The namespace name, e.g. `"Gtk"`.
public let name: String
/// The namespace version string, e.g. `"4.0"`.
public let version: String
/// The shared library name from the GIR file (e.g. "libgtk-4.so.1").
public var cSharedLibrary: String
/// The C identifier prefix (e.g. "Gtk", "G").
public var cIdentifierPrefix: String
/// The GObject classes defined in this namespace.
public var classes: [Class]
/// The GObject interfaces defined in this namespace.
public var interfaces: [Interface]
/// The plain C records (structs) defined in this namespace.
public var records: [Record]
/// The enumerations defined in this namespace.
public var enumerations: [Enumeration]
/// The bitfield (flags) types defined in this namespace.
public var bitfields: [Bitfield]
/// The callback function types defined in this namespace.
public var callbacks: [Callback]
/// The global (namespace-level) functions defined in this namespace.
public var functions: [GlobalFunction]
/// The constants defined in this namespace.
public var constants: [Constant]
/// The type aliases defined in this namespace.
public var aliases: [Alias]
/// Creates a new namespace.
/// - Parameters:
/// - name: The namespace name, e.g. `"Gtk"`.
/// - version: The namespace version string, e.g. `"4.0"`.
/// - cSharedLibrary: The shared library name (e.g. "libgtk-4.so.1").
/// - cIdentifierPrefix: The C identifier prefix (e.g. "Gtk", "G").
/// - classes: The GObject classes in the namespace.
/// - interfaces: The GObject interfaces in the namespace.
/// - records: The plain C records in the namespace.
/// - enumerations: The enumerations in the namespace.
/// - bitfields: The bitfield types in the namespace.
/// - callbacks: The callback types in the namespace.
/// - functions: The global functions in the namespace.
/// - constants: The constants in the namespace.
/// - aliases: The type aliases in the namespace.
public init(name: String, version: String, cSharedLibrary: String = "", cIdentifierPrefix: String = "",
classes: [Class] = [], interfaces: [Interface] = [],
records: [Record] = [], enumerations: [Enumeration] = [], bitfields: [Bitfield] = [],
callbacks: [Callback] = [], functions: [GlobalFunction] = [], constants: [Constant] = [],
aliases: [Alias] = []) {
self.name = name; self.version = version
self.cSharedLibrary = cSharedLibrary; self.cIdentifierPrefix = cIdentifierPrefix
self.classes = classes; self.interfaces = interfaces; self.records = records
self.enumerations = enumerations; self.bitfields = bitfields; self.callbacks = callbacks
self.functions = functions; self.constants = constants; self.aliases = aliases
}
}
/// A GObject class definition.
///
/// Corresponds to the `<class>` element in a GIR XML file. Models a GObject
/// class with its parent class, implemented interfaces, constructors, methods,
/// properties, signals, and associated functions.
public struct Class {
/// The class name, e.g. `"Widget"`.
public let name: String
/// The corresponding C type name, e.g. `"GtkWidget"`.
public let cType: String
/// The name of the parent class, or `nil` for the root `GObject` class.
public let parent: String?
/// Whether this class is abstract and cannot be instantiated directly.
public var isAbstract: Bool
/// The names of interfaces this class implements.
public var implements: [String]
/// The constructors for this class.
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
public var constructors: [Constructor]
/// The methods of this class.
public var methods: [Method]
/// The GObject properties of this class.
public var properties: [Property]
/// The signals emitted by this class.
public var signals: [Signal]
/// The functions associated with this class.
public var functions: [GlobalFunction]
/// Creates a new class definition.
/// - Parameters:
/// - name: The class name, e.g. `"Widget"`.
/// - cType: The corresponding C type name, e.g. `"GtkWidget"`.
/// - parent: The name of the parent class, or `nil` if root.
/// - isAbstract: Whether the class is abstract. Defaults to `false`.
/// - implements: The names of implemented interfaces. Defaults to empty.
/// - constructors: The constructors. Defaults to empty.
/// - methods: The methods. Defaults to empty.
/// - properties: The properties. Defaults to empty.
/// - signals: The signals. Defaults to empty.
/// - functions: The associated functions. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, parent: String?, isAbstract: Bool = false,
implements: [String] = [], constructors: [Constructor] = [], methods: [Method] = [],
properties: [Property] = [], signals: [Signal] = [], functions: [GlobalFunction] = [],
doc: String? = nil) {
self.name = name; self.cType = cType; self.parent = parent
self.isAbstract = isAbstract; self.implements = implements
self.constructors = constructors; self.methods = methods
self.properties = properties; self.signals = signals; self.functions = functions
self.doc = doc
}
}
/// A GObject interface definition.
///
/// Corresponds to the `<interface>` element in a GIR XML file. An interface
/// declares methods, properties, and signals that implementing classes must
/// provide, along with prerequisite types that must be satisfied first.
public struct Interface {
/// The interface name, e.g. `"Buildable"`.
public let name: String
/// The corresponding C type name, e.g. `"GtkBuildable"`.
public let cType: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The methods declared by this interface.
public var methods: [Method]
/// The properties declared by this interface.
public var properties: [Property]
/// The signals declared by this interface.
public var signals: [Signal]
/// The prerequisite types a class must satisfy to implement this interface.
public var prereqs: [String]
/// Creates a new interface definition.
/// - Parameters:
/// - name: The interface name.
/// - cType: The corresponding C type name.
/// - methods: The methods declared by the interface. Defaults to empty.
/// - properties: The properties declared by the interface. Defaults to empty.
/// - signals: The signals declared by the interface. Defaults to empty.
/// - prereqs: The prerequisite types. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, methods: [Method] = [], properties: [Property] = [],
signals: [Signal] = [], prereqs: [String] = [], doc: String? = nil) {
self.name = name; self.cType = cType; self.methods = methods
self.properties = properties; self.signals = signals; self.prereqs = prereqs
self.doc = doc
}
}
/// A plain C record (struct) definition.
///
/// Corresponds to the `<record>` element in a GIR XML file. Records are
/// value types in C and may be opaque (no fields exposed), disguised
/// (typedef'd without `struct` keyword), or have fully accessible fields.
public struct Record {
/// The record name.
public let name: String
/// The corresponding C type name.
public let cType: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Whether the record is opaque (fields are not introspectable).
public var isOpaque: Bool
/// Whether the record is disguised (typedef'd without the `struct` keyword).
public var isDisguised: Bool
/// The fields of the record, if introspectable.
public var fields: [Field]
/// The methods operating on this record.
public var methods: [Method]
/// Creates a new record definition.
/// - Parameters:
/// - name: The record name.
/// - cType: The corresponding C type name.
/// - isOpaque: Whether the record is opaque. Defaults to `false`.
/// - isDisguised: Whether the record is disguised. Defaults to `false`.
/// - fields: The fields of the record. Defaults to empty.
/// - methods: The record methods. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, isOpaque: Bool = false, isDisguised: Bool = false,
fields: [Field] = [], methods: [Method] = [], doc: String? = nil) {
self.name = name; self.cType = cType; self.isOpaque = isOpaque
self.isDisguised = isDisguised; self.fields = fields; self.methods = methods; self.doc = doc
}
}
/// A field within a C record.
///
/// Corresponds to the `<field>` element in a GIR XML file. Describes a named
/// member of a C struct, including its type and read/write permissions.
public struct Field {
/// The field name.
public let name: String
/// The GIR type of the field.
public let type: GIRType
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Whether the field is readable (accessible for reading).
public let isReadable: Bool
/// Whether the field is writable (accessible for writing).
public let isWritable: Bool
/// Creates a new field.
/// - Parameters:
/// - name: The field name.
/// - type: The GIR type of the field.
/// - isReadable: Whether the field is readable. Defaults to `true`.
/// - isWritable: Whether the field is writable. Defaults to `false`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, type: GIRType, isReadable: Bool = true, isWritable: Bool = false, doc: String? = nil) {
self.name = name; self.type = type; self.isReadable = isReadable; self.isWritable = isWritable; self.doc = doc
}
}
/// A GObject enumeration type.
///
/// Corresponds to the `<enumeration>` element in a GIR XML file. Defines a
/// set of named integer constants with their C identifiers and numeric values.
public struct Enumeration {
/// The enumeration name.
public let name: String
/// The corresponding C type name.
public let cType: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The members (enum values) of this enumeration.
public var members: [EnumMember]
/// Creates a new enumeration.
/// - Parameters:
/// - name: The enumeration name.
/// - cType: The corresponding C type name.
/// - members: The enum members. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, members: [EnumMember] = [], doc: String? = nil) {
self.name = name; self.cType = cType; self.members = members; self.doc = doc
}
}
/// A single member (value) of an enumeration or bitfield.
///
/// Corresponds to the `<member>` element in a GIR XML file. Each member
/// has a name, its associated numeric value, and the full C identifier.
public struct EnumMember {
/// The member name, e.g. `"visible"`.
public let name: String
/// The numeric value as a string, e.g. `"1"`.
public let value: String
/// The full C identifier, e.g. `"GTK_WIDGET_VISIBLE"`.
public let cIdentifier: String
/// Creates a new enum member.
/// - Parameters:
/// - name: The member name.
/// - value: The numeric value as a string.
/// - cIdentifier: The full C identifier.
public init(name: String, value: String, cIdentifier: String) {
self.name = name; self.value = value; self.cIdentifier = cIdentifier
}
}
/// A GObject bitfield (flags) type.
///
/// Corresponds to the `<bitfield>` element in a GIR XML file. Defines a set
/// of named flags that can be combined with bitwise operations. Each member
/// represents a single bit in the flags value.
public struct Bitfield {
/// The bitfield type name.
public let name: String
/// The corresponding C type name.
public let cType: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The individual flag members.
public var members: [EnumMember]
/// Creates a new bitfield type.
/// - Parameters:
/// - name: The bitfield type name.
/// - cType: The corresponding C type name.
/// - members: The flag members. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, members: [EnumMember] = [], doc: String? = nil) {
self.name = name; self.cType = cType; self.members = members; self.doc = doc
}
}
/// A callback function type.
///
/// Corresponds to the `<callback>` element in a GIR XML file. Describes the
/// function signature parameters and return type for a C callback used
/// in signal handlers, virtual functions, or asynchronous operations.
public struct Callback {
/// The callback type name.
public let name: String
/// The corresponding C type name.
public let cType: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The parameters of the callback function.
public var parameters: [Parameter]
/// The return type of the callback function.
public var returnType: GIRType
/// Creates a new callback type.
/// - Parameters:
/// - name: The callback type name.
/// - cType: The corresponding C type name.
/// - parameters: The callback parameters. Defaults to empty.
/// - returnType: The return type. Defaults to `.void`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
self.name = name; self.cType = cType; self.parameters = parameters; self.returnType = returnType; self.doc = doc
}
}
/// A constructor for a GObject class.
///
/// Corresponds to the `<constructor>` element in a GIR XML file. Constructors
/// are special methods that create new instances of a GObject type, typically
/// wrapping C functions like `gtk_widget_new()`.
public struct Constructor {
/// The constructor name.
public let name: String
/// The corresponding C function identifier.
public let cIdentifier: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The parameters accepted by the constructor.
public var parameters: [Parameter]
/// The return type typically the constructed object type.
public var returnType: GIRType
/// Creates a new constructor definition.
/// - Parameters:
/// - name: The constructor name.
/// - cIdentifier: The corresponding C function identifier.
/// - parameters: The constructor parameters. Defaults to empty.
/// - returnType: The return type. Defaults to `.void`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
self.name = name; self.cIdentifier = cIdentifier
self.parameters = parameters; self.returnType = returnType; self.doc = doc
}
}
/// A method of a GObject class, interface, or record.
///
/// Corresponds to the `<method>` element in a GIR XML file. Methods are
/// instance functions that operate on a particular type, identified by their
/// C function name.
public struct Method {
/// The method name, e.g. `"set_visible"`.
public let name: String
/// The corresponding C function identifier, e.g. `"gtk_widget_set_visible"`.
public let cIdentifier: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The parameters of the method, typically excluding the instance parameter.
public var parameters: [Parameter]
/// The return type of the method.
public var returnType: GIRType
/// Creates a new method definition.
/// - Parameters:
/// - name: The method name.
/// - cIdentifier: The corresponding C function identifier.
/// - parameters: The method parameters. Defaults to empty.
/// - returnType: The return type. Defaults to `.void`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
self.name = name; self.cIdentifier = cIdentifier
self.parameters = parameters; self.returnType = returnType; self.doc = doc
}
}
/// A GObject property definition.
///
/// Corresponds to the `<property>` element in a GIR XML file. Properties are
/// named, typed attributes on GObject classes with configurable read/write
/// access and construct-time-only semantics.
public struct Property {
/// The property name, e.g. `"label"`.
public let name: String
/// The GIR type of the property.
public var type: GIRType
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Whether the property is readable (has a getter).
public var isReadable: Bool
/// Whether the property is writable (has a setter).
public var isWritable: Bool
/// Whether the property can only be set during object construction.
public var isConstructOnly: Bool
/// Creates a new property definition.
/// - Parameters:
/// - name: The property name.
/// - type: The GIR type of the property.
/// - isReadable: Whether the property is readable. Defaults to `true`.
/// - isWritable: Whether the property is writable. Defaults to `false`.
/// - isConstructOnly: Whether the property is construct-only. Defaults to `false`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, type: GIRType, isReadable: Bool = true, isWritable: Bool = false, isConstructOnly: Bool = false, doc: String? = nil) {
self.name = name; self.type = type
self.isReadable = isReadable; self.isWritable = isWritable; self.isConstructOnly = isConstructOnly; self.doc = doc
}
}
/// A GObject signal definition.
///
/// Corresponds to the `<signal>` element in a GIR XML file. Signals are
/// typed event emitters on GObject classes. Each signal has a parameter list,
/// a return value, and may support detailed (string-parameterized) connections.
public struct Signal {
/// The signal name, e.g. `"clicked"`.
public let name: String
/// The parameters emitted with the signal.
public var parameters: [Parameter]
/// The return type of the signal handler.
public var returnType: GIRType
/// Whether the signal supports detail strings (e.g. `"notify::label"`).
public var isDetailed: Bool
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Creates a new signal definition.
/// - Parameters:
/// - name: The signal name.
/// - parameters: The signal parameters. Defaults to empty.
/// - returnType: The handler return type. Defaults to `.void`.
/// - isDetailed: Whether the signal supports detail strings. Defaults to `false`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, parameters: [Parameter] = [], returnType: GIRType = .void, isDetailed: Bool = false, doc: String? = nil) {
self.name = name; self.parameters = parameters; self.returnType = returnType; self.isDetailed = isDetailed; self.doc = doc
}
}
/// A global (namespace-level) function.
///
/// Corresponds to the `<function>` element at the namespace level in a GIR
/// XML file. These are free functions not associated with any particular
/// type, such as utility or factory functions.
public struct GlobalFunction {
/// The function name.
public let name: String
/// The corresponding C function identifier.
public let cIdentifier: String
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The parameters of the function.
public var parameters: [Parameter]
/// The return type of the function.
public var returnType: GIRType
/// Creates a new global function definition.
/// - Parameters:
/// - name: The function name.
/// - cIdentifier: The corresponding C function identifier.
/// - parameters: The function parameters. Defaults to empty.
/// - returnType: The return type. Defaults to `.void`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
self.name = name; self.cIdentifier = cIdentifier
self.parameters = parameters; self.returnType = returnType; self.doc = doc
}
}
/// A constant value definition.
///
/// Corresponds to the `<constant>` element in a GIR XML file. Constants are
/// named immutable values with a specific GIR type, such as enum defaults or
/// version numbers.
public struct Constant {
/// The constant name.
public let name: String
/// The constant value as a string representation.
public let value: String
/// The GIR type of the constant.
public var type: GIRType
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Creates a new constant definition.
/// - Parameters:
/// - name: The constant name.
/// - value: The constant value as a string representation.
/// - type: The GIR type of the constant.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, value: String, type: GIRType, doc: String? = nil) {
self.name = name; self.value = value; self.type = type; self.doc = doc
}
}
/// A type alias definition.
///
/// Corresponds to the `<alias>` element in a GIR XML file. Provides an
/// alternative name (with an optional C type) for an existing GIR type,
/// useful for platform-specific or convenience typedefs.
public struct Alias {
/// The alias name.
public let name: String
/// The corresponding C type name.
public let cType: String
/// The underlying GIR type this alias refers to.
public var target: GIRType
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Creates a new type alias.
/// - Parameters:
/// - name: The alias name.
/// - cType: The corresponding C type name.
/// - target: The underlying GIR type to alias.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, cType: String, target: GIRType, doc: String? = nil) {
self.name = name; self.cType = cType; self.target = target; self.doc = doc
}
}
// MARK: - Shared Types
/// A parameter of a function, method, constructor, callback, or signal.
///
/// Corresponds to the `<parameter>` element in a GIR XML file. Describes the
/// parameter's name, type, nullability, optionality, ownership transfer rules,
/// and whether it is the implicit instance parameter (equivalent to `self`).
public struct Parameter {
/// The parameter name.
public let name: String
/// The GIR type of the parameter.
public var type: GIRType
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// Whether the parameter may be `nil` (NULL).
public var isNullable: Bool
/// Whether the parameter is optional (may be omitted at the call site).
public var isOptional: Bool
/// How ownership is transferred for this parameter.
public var transferOwnership: TransferOwnership
/// Whether this is the implicit instance parameter (self) of a method.
public var isInstanceParameter: Bool
/// Creates a new parameter definition.
/// - Parameters:
/// - name: The parameter name.
/// - type: The GIR type of the parameter.
/// - isNullable: Whether the parameter may be nil. Defaults to `false`.
/// - isOptional: Whether the parameter is optional. Defaults to `false`.
/// - transferOwnership: How ownership is transferred. Defaults to `.none`.
/// - isInstanceParameter: Whether this is the instance parameter. Defaults to `false`.
/// - doc: Documentation comment from the GIR XML.
public init(name: String, type: GIRType, isNullable: Bool = false, isOptional: Bool = false,
transferOwnership: TransferOwnership = .none, isInstanceParameter: Bool = false, doc: String? = nil) {
self.name = name; self.type = type; self.isNullable = isNullable
self.isOptional = isOptional; self.transferOwnership = transferOwnership
self.isInstanceParameter = isInstanceParameter; self.doc = doc
}
}
/// Describes how ownership of a value is transferred between caller and callee.
///
/// Maps to the `transfer-ownership` attribute in GIR XML. Controls memory
/// management semantics: whether the caller must free the returned value
/// (`.full`), whether only the container is owned (`.container`), or whether
/// no ownership transfer occurs (`.none`).
public enum TransferOwnership: String {
/// No transfer; the caller does not own the value and must not free it.
case none
/// Full transfer; the caller owns the value and is responsible for freeing it.
case full
/// Container transfer; the caller owns the container but not its elements.
case container
}
/// A GIR type reference, covering primitives, named type references, arrays, and optionals.
///
/// Corresponds to the `<type>` element in GIR XML. This recursive enum models
/// the full GIR type system: scalar primitives, named type references pointing
/// to other GIR types, arrays (both GArray and C-style fixed arrays), and
/// nullable/optional wrappers.
public indirect enum GIRType: Equatable {
/// No return value (void).
case void
/// A boolean value, mapped from `gboolean`.
case boolean
/// A signed 8-bit integer, mapped from `gint8`.
case int8
/// A signed 16-bit integer, mapped from `gint16`.
case int16
/// A signed 32-bit integer, mapped from `gint32`.
case int32
/// A signed 64-bit integer, mapped from `gint64`.
case int64
/// An unsigned 8-bit integer, mapped from `guint8`.
case uint8
/// An unsigned 16-bit integer, mapped from `guint16`.
case uint16
/// An unsigned 32-bit integer, mapped from `guint32`.
case uint32
/// An unsigned 64-bit integer, mapped from `guint64`.
case uint64
/// A single-precision floating-point value, mapped from `gfloat`.
case float
/// A double-precision floating-point value, mapped from `gdouble`.
case double
/// A null-terminated UTF-8 string, mapped from `utf8`.
case string
/// A filename string (platform-dependent encoding), mapped from `filename`.
case filename
/// An opaque pointer, mapped from `gpointer`.
case pointer
/// A reference to a named type, possibly from another namespace.
/// - Parameters:
/// - String: The type name, e.g. `"Widget"`.
/// - namespace: The namespace qualifier, or `nil` for the current namespace.
case typeRef(String, namespace: String?)
/// A dynamically-sized GArray of the given element type.
case array(GIRType)
/// A C-style fixed-size array of the given element type.
case cArray(GIRType)
/// An optional (nullable) value of the given type.
case `optional`(GIRType)
/// Creates a type reference in the current namespace.
/// - Parameter name: The unqualified type name.
/// - Returns: A `typeRef` with no namespace qualifier.
public static func typeRef(_ name: String) -> GIRType {
.typeRef(name, namespace: nil)
}
}

View file

@ -0,0 +1,489 @@
import Foundation
#if canImport(FoundationXML)
import FoundationXML
#endif
/// Errors that can occur during GIR XML parsing.
public enum GIRParserError: Error {
/// The XML document is malformed or could not be parsed.
/// - Parameter description: A human-readable error description.
case invalidXML(String)
/// An unexpected XML element was encountered.
/// - Parameter description: Details about the unexpected element.
case unexpectedElement(String)
/// A required attribute is missing from an XML element.
/// - Parameter description: Description of the missing attribute.
case missingAttribute(String)
}
/// Parses GIR XML files into the intermediate representation model.
///
/// Uses Foundation's `XMLParser` in a SAX-style delegate pattern. The parser
/// handles all GIR element types including classes, interfaces, records,
/// enumerations, bitfields, callbacks, methods, properties, signals, functions,
/// constants, and aliases.
public struct GIRParser {
/// Creates a new GIR parser.
public init() {}
/// Parses a GIR XML document from a string.
/// - Parameter xmlString: The XML content to parse.
/// - Returns: A `Repository` containing all parsed namespaces and types.
/// - Throws: `GIRParserError` if the XML cannot be encoded as UTF-8 or parsing fails.
public func parse(xmlString: String) throws -> Repository {
guard let data = xmlString.data(using: .utf8) else {
throw GIRParserError.invalidXML("Failed to encode XML string as UTF-8")
}
return try parse(data: data)
}
/// Parses a GIR XML document from a file URL.
/// - Parameter fileURL: The URL of the .gir file to parse.
/// - Returns: A `Repository` containing all parsed namespaces and types.
/// - Throws: `GIRParserError` if the file cannot be read or parsing fails.
public func parse(fileURL: URL) throws -> Repository {
let data = try Data(contentsOf: fileURL)
return try parse(data: data)
}
/// Parses raw XML data using a SAX-style delegate.
/// - Parameter data: The XML data to parse.
/// - Returns: A `Repository` with the parsed content.
/// - Throws: `GIRParserError` if parsing fails.
private func parse(data: Data) throws -> Repository {
let delegate = GIRXMLDelegate()
let parser = XMLParser(data: data)
parser.delegate = delegate
if parser.parse(), delegate.parseError == nil {
return delegate.repository
}
throw delegate.parseError ?? GIRParserError.invalidXML(parser.parserError?.localizedDescription ?? "unknown error")
}
}
// MARK: - XMLParser Delegate
/// SAX-style delegate for `XMLParser` that builds a `Repository` from GIR XML.
///
/// Tracks a stack of currently-open GIR elements via `current*` properties.
/// On `didStartElement` it creates the corresponding model object and populates
/// attributes. On `didEndElement` it appends the completed object to its parent.
/// The final `repository` property contains the fully parsed GIR document.
final class GIRXMLDelegate: NSObject, XMLParserDelegate {
/// The repository being populated during parsing.
var repository = Repository()
/// Set to the first error encountered, or `nil` if parsing succeeds.
var parseError: GIRParserError?
var currentNamespace: Namespace?
var currentClass: Class?
var currentInterface: Interface?
var currentRecord: Record?
var currentEnum: Enumeration?
var currentBitfield: Bitfield?
var currentMethod: Method?
var currentConstructor: Constructor?
var currentSignal: Signal?
var currentProperty: Property?
var currentFunction: GlobalFunction?
var currentParameter: Parameter?
var currentCallback: Callback?
var currentConstant: Constant?
var currentAlias: Alias?
var currentReturnType: GIRType?
var currentText: String = ""
/// Counter for nested untracked elements that contain child elements.
/// When > 0, `<doc>` elements belong to untracked parents and should be discarded.
var untrackedDepth: Int = 0
/// Called when the XML parser encounters an opening element tag.
///
/// Creates the corresponding model object for the element and populates it
/// from XML attributes. Maintains a stack of `current*` properties so that
/// nested elements can attach themselves to their parent on close.
/// - Parameters:
/// - parser: The XML parser.
/// - elementName: The name of the XML element.
/// - namespaceURI: The namespace URI of the element.
/// - qualifiedName: The qualified name of the element.
/// - attributeDict: The element's attributes.
func parser(_ parser: XMLParser, didStartElement elementName: String,
namespaceURI: String?, qualifiedName: String?,
attributes attributeDict: [String: String] = [:]) {
switch elementName {
case "c:include":
guard let name = attributeDict["name"] else { return }
repository.cHeaderPath = name
case "package":
guard let name = attributeDict["name"] else { return }
repository.packageName = name
case "include":
guard let name = attributeDict["name"], let version = attributeDict["version"] else { return }
// GIR dependency includes appear before the <namespace> element
// Derive link name: lowercase(library) + "-" + major version (e.g. "Gdk-4.0" "gdk-4")
let majorVersion = version.split(separator: ".").first.map(String.init) ?? version
let linkName = "\(name.lowercased())-\(majorVersion)"
repository.includedLibraryLinks.append(linkName)
case "namespace":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let version = requireAttribute("version", from: attributeDict, for: elementName, parser: parser) else { return }
let sharedLibrary = attributeDict["shared-library"] ?? ""
let cIdentifierPrefix = attributeDict["c:identifier-prefixes"] ?? ""
currentNamespace = Namespace(name: name, version: version, cSharedLibrary: sharedLibrary, cIdentifierPrefix: cIdentifierPrefix)
case "class":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
let parent = attributeDict["parent"]
let isAbstract = attributeDict["abstract"] == "1"
currentClass = Class(name: name, cType: cType, parent: parent, isAbstract: isAbstract)
case "interface":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
let prereqs = attributeDict["prerequisite"]?.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } ?? []
currentInterface = Interface(name: name, cType: cType, prereqs: prereqs)
case "record":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentRecord = Record(name: name, cType: cType, isOpaque: attributeDict["opaque"] == "1",
isDisguised: attributeDict["disguised"] == "1")
case "enumeration":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentEnum = Enumeration(name: name, cType: cType)
case "bitfield":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentBitfield = Bitfield(name: name, cType: cType)
case "member":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser),
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
let member = EnumMember(name: name, value: value, cIdentifier: cid)
currentEnum?.members.append(member)
currentBitfield?.members.append(member)
case "callback":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let cType = attributeDict["c:type"] ?? ""
currentCallback = Callback(name: name, cType: cType)
case "constructor":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
currentConstructor = Constructor(name: name, cIdentifier: cid)
case "method":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
currentMethod = Method(name: name, cIdentifier: cid)
case "function":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
currentFunction = GlobalFunction(name: name, cIdentifier: cid)
case "signal":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
case "glib:signal":
guard let name = attributeDict["name"] ?? attributeDict["glib:name"] else { return }
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
case "property":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
currentProperty = Property(name: name, type: .void,
isReadable: attributeDict["readable"] != "0",
isWritable: attributeDict["writable"] == "1",
isConstructOnly: attributeDict["construct-only"] == "1")
case "parameter", "instance-parameter":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
let transfer = TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none
currentParameter = Parameter(name: name, type: .void, isNullable: attributeDict["nullable"] == "1",
isOptional: attributeDict["optional"] == "1",
transferOwnership: transfer,
isInstanceParameter: elementName == "instance-parameter")
case "return-value":
currentReturnType = .void
case "type":
let typeName = attributeDict["name"] ?? "none"
let resolvedType = parseGIRType(typeName)
if currentParameter != nil {
currentParameter?.type = resolvedType
} else if currentReturnType != nil {
currentReturnType = resolvedType
} else if currentProperty != nil {
currentProperty?.type = resolvedType
} else if currentConstant != nil {
currentConstant?.type = resolvedType
} else if currentAlias != nil {
currentAlias?.target = resolvedType
}
case "array":
if let param = currentParameter {
currentParameter?.type = .array(param.type)
}
case "constant":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser) else { return }
currentConstant = Constant(name: name, value: value, type: .void)
case "alias":
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
let cType = requireAttribute("c:type", from: attributeDict, for: elementName, parser: parser) else { return }
currentAlias = Alias(name: name, cType: cType, target: .void)
case "implements":
if let name = attributeDict["name"] {
currentClass?.implements.append(name)
}
case "doc-version", "doc-deprecated", "doc-stability", "source-position":
break
case "doc":
currentText = ""
break
case "virtual-method", "field", "parameters":
untrackedDepth += 1
default:
break
}
}
/// Called when the XML parser encounters a closing element tag.
///
/// Finalizes the current model object, resolves the return type if one was
/// collected, appends the object to its parent container, and clears the
/// corresponding `current*` property.
/// - Parameters:
/// - parser: The XML parser.
/// - elementName: The name of the XML element.
/// - namespaceURI: The namespace URI of the element.
/// - qualifiedName: The qualified name of the element.
func parser(_ parser: XMLParser, didEndElement elementName: String,
namespaceURI: String?, qualifiedName: String?) {
switch elementName {
case "namespace":
if let ns = currentNamespace { repository.namespaces.append(ns) }
currentNamespace = nil
case "class":
if let cls = currentClass { currentNamespace?.classes.append(cls) }
currentClass = nil
case "interface":
if let iface = currentInterface { currentNamespace?.interfaces.append(iface) }
currentInterface = nil
case "record":
if let record = currentRecord { currentNamespace?.records.append(record) }
currentRecord = nil
case "enumeration":
if let enm = currentEnum { currentNamespace?.enumerations.append(enm) }
currentEnum = nil
case "bitfield":
if let bf = currentBitfield { currentNamespace?.bitfields.append(bf) }
currentBitfield = nil
case "callback":
if let cb = currentCallback {
// Only add to namespace when at the top level (not inside a class/interface/record)
if currentClass == nil && currentInterface == nil && currentRecord == nil {
currentNamespace?.callbacks.append(cb)
}
}
currentCallback = nil
case "constructor":
if var ctor = currentConstructor {
if let rt = currentReturnType, rt != .void {
ctor.returnType = rt
}
currentClass?.constructors.append(ctor)
}
currentConstructor = nil
currentReturnType = nil
case "method":
if var method = currentMethod {
if let rt = currentReturnType, rt != .void {
method.returnType = rt
}
currentClass?.methods.append(method)
currentInterface?.methods.append(method)
currentRecord?.methods.append(method)
}
currentMethod = nil
currentReturnType = nil
case "function":
if var fn = currentFunction {
if let rt = currentReturnType, rt != .void {
fn.returnType = rt
}
// Class/interface/record-level functions are not globals
if currentClass != nil {
currentClass?.functions.append(fn)
} else if currentRecord != nil {
currentRecord?.methods.append(Method(name: fn.name, cIdentifier: fn.cIdentifier,
parameters: fn.parameters, returnType: fn.returnType))
} else if currentInterface != nil {
// Interface functions handled via interface methods
} else {
currentNamespace?.functions.append(fn)
}
}
currentFunction = nil
currentReturnType = nil
case "signal", "glib:signal":
if var sig = currentSignal {
if let rt = currentReturnType, rt != .void {
sig.returnType = rt
}
currentClass?.signals.append(sig)
currentInterface?.signals.append(sig)
}
currentSignal = nil
currentReturnType = nil
case "property":
if let prop = currentProperty {
currentClass?.properties.append(prop)
currentInterface?.properties.append(prop)
}
currentProperty = nil
case "parameter", "instance-parameter":
if let param = currentParameter {
currentMethod?.parameters.append(param)
currentConstructor?.parameters.append(param)
currentSignal?.parameters.append(param)
currentFunction?.parameters.append(param)
currentCallback?.parameters.append(param)
}
currentParameter = nil
case "return-value":
break
case "constant":
if let c = currentConstant { currentNamespace?.constants.append(c) }
currentConstant = nil
case "alias":
if let a = currentAlias { currentNamespace?.aliases.append(a) }
currentAlias = nil
case "virtual-method", "field", "parameters":
untrackedDepth -= 1
case "doc":
let text = currentText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
// Skip doc inside untracked elements (virtual-method, field, etc.)
guard untrackedDepth == 0 else { return }
// Check innermost (most nested) elements first to handle hierarchy
// e.g. a signal inside a class check currentSignal before currentClass
if currentParameter != nil { currentParameter?.doc = text }
else if currentMethod != nil { currentMethod?.doc = text }
else if currentConstructor != nil { currentConstructor?.doc = text }
else if currentSignal != nil { currentSignal?.doc = text }
else if currentProperty != nil { currentProperty?.doc = text }
else if currentCallback != nil { currentCallback?.doc = text }
else if currentFunction != nil { currentFunction?.doc = text }
else if currentInterface != nil { currentInterface?.doc = text }
else if currentEnum != nil { currentEnum?.doc = text }
else if currentBitfield != nil { currentBitfield?.doc = text }
else if currentRecord != nil { currentRecord?.doc = text }
else if currentClass != nil { currentClass?.doc = text }
else if currentConstant != nil { currentConstant?.doc = text }
else if currentAlias != nil { currentAlias?.doc = text }
default:
break
}
}
/// Called when the XML parser encounters character data between elements.
///
/// Accumulates text content for the current element, used primarily for
/// capturing `<doc>` element text content.
/// - Parameters:
/// - parser: The XML parser.
/// - string: The character data found.
func parser(_ parser: XMLParser, foundCharacters string: String) {
currentText += string
}
// MARK: - Helpers
/// Requires that an attribute exists in the given dictionary, or aborts parsing.
/// - Parameters:
/// - key: The attribute name to look up.
/// - dict: The attribute dictionary from the current XML element.
/// - element: The name of the XML element (for error messages).
/// - parser: The XML parser to abort on failure.
/// - Returns: The attribute value, or `nil` if the attribute is missing.
private func requireAttribute(_ key: String, from dict: [String: String], for element: String, parser: XMLParser) -> String? {
guard let value = dict[key] else {
parseError = .missingAttribute("Missing '\(key)' on <\(element)>")
parser.abortParsing()
return nil
}
return value
}
/// Maps a GIR type name string to the corresponding `GIRType` enum case.
///
/// Recognizes primitive GLib types (`gboolean`, `gint32`, `utf8`, etc.) and
/// dotted namespace-qualified type references (e.g. `Gtk.Widget`). Unknown
/// names are returned as an unqualified `.typeRef`.
/// - Parameter name: The GIR type name (e.g. `"gint32"`, `"utf8"`, `"Gtk.Widget"`).
/// - Returns: The corresponding `GIRType` value.
private func parseGIRType(_ name: String) -> GIRType {
switch name {
case "none": return .void
case "gboolean": return .boolean
case "gint8": return .int8
case "gint16": return .int16
case "gint", "gint32": return .int32
case "gint64": return .int64
case "guint8": return .uint8
case "guint16": return .uint16
case "guint", "guint32": return .uint32
case "guint64": return .uint64
case "gfloat": return .float
case "gdouble": return .double
case "utf8": return .string
case "filename": return .filename
case "gpointer", "gconstpointer": return .pointer
default:
if let dotIndex = name.firstIndex(of: ".") {
let ns = String(name[..<dotIndex])
let type = String(name[name.index(after: dotIndex)...])
return .typeRef(type, namespace: ns)
}
return .typeRef(name)
}
}
}

View file

@ -0,0 +1,154 @@
import Foundation
import SwiftGtkGenCore
/// Command-line interface for the GIR-to-Swift binding generator.
///
/// Parses command-line arguments, invokes the GIR parser, analyzer, and code
/// generator, then writes the resulting Swift source to a file. Designed as
/// the subprocess entry point invoked by both the build tool plugin and the
/// command plugin.
@main
struct SwiftGtkGenCLI {
/// Program entry point. Parses arguments, generates code, and writes output.
static func main() {
let args = parseArguments()
if args.showHelp {
printHelp()
return
}
do {
let parser = GIRParser()
let primaryGIR = try parser.parse(fileURL: URL(fileURLWithPath: args.girFile))
let generateList = args.generateAll ? [] : args.generate
let config = GenerationConfig(
library: args.library,
version: args.version,
girsDirectories: args.girsDirs,
targetDirectory: args.output,
externalLibraries: args.externalLibs,
generate: generateList,
manual: args.manual,
ignore: args.ignore,
objects: []
)
let analyzer = Analyzer(config: config)
let analysis = analyzer.analyze(repository: primaryGIR)
let generator = CodeGenerator(config: config)
let outputDir = URL(fileURLWithPath: args.output)
if args.emitSingleFile {
let output = try generator.generate(repository: primaryGIR, analysis: analysis)
try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
let outputFile = outputDir.appendingPathComponent("\(config.library).swift")
try output.write(to: outputFile, atomically: true, encoding: .utf8)
print("Generated \(outputFile.path)")
} else {
let files = try generator.generateFiles(repository: primaryGIR, analysis: analysis)
let sourcesDir = outputDir.appendingPathComponent("Sources/\(config.library)")
try FileManager.default.createDirectory(at: sourcesDir, withIntermediateDirectories: true)
for (fileName, content) in files.sorted(by: { $0.key < $1.key }) {
let fileURL = sourcesDir.appendingPathComponent(fileName)
try content.write(to: fileURL, atomically: true, encoding: .utf8)
print("Generated \(fileURL.path)")
}
}
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: primaryGIR)
for (relativePath, content) in scaffolding.sorted(by: { $0.key < $1.key }) {
let fileURL = outputDir.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try content.write(to: fileURL, atomically: true, encoding: .utf8)
print("Generated \(fileURL.path)")
}
} catch {
print("Error: \(error)")
exit(1)
}
}
/// Parsed command-line arguments for the GIR generator CLI.
struct CLIArgs {
var showHelp: Bool = false
var emitSingleFile: Bool = false
var girFile: String = ""
var library: String = ""
var version: String = ""
var output: String = "."
var girsDirs: [String] = []
var externalLibs: [String] = []
var generate: [String] = []
var generateAll: Bool = false
var manual: [String] = []
var ignore: [String] = []
}
/// Parses command-line arguments into a ``CLIArgs`` value.
///
/// Supports the flags `--help`, `--gir-file`, `--library`, `--version`,
/// `--output`, `--girs-dirs`, `--external-libs`, `--generate`, `--manual`,
/// and `--ignore`. Multi-value flags (`--girs-dirs`, etc.) accept
/// comma-separated lists.
///
/// - Returns: A populated ``CLIArgs`` extracted from `CommandLine.arguments`.
static func parseArguments() -> CLIArgs {
var args = Array(CommandLine.arguments.dropFirst())
var cli = CLIArgs()
while let flag = args.first {
args.removeFirst()
switch flag {
case "--emit-single-file":
cli.emitSingleFile = true
case "--help", "-h":
cli.showHelp = true
case "--gir-file":
cli.girFile = args.removeFirst()
case "--library":
cli.library = args.removeFirst()
case "--version":
cli.version = args.removeFirst()
case "--output":
cli.output = args.removeFirst()
case "--girs-dirs":
cli.girsDirs = args.removeFirst().components(separatedBy: ",")
case "--external-libs":
cli.externalLibs = args.removeFirst().components(separatedBy: ",")
case "--generate-all":
cli.generateAll = true
case "--generate":
cli.generate = args.removeFirst().components(separatedBy: ",")
case "--manual":
cli.manual = args.removeFirst().components(separatedBy: ",")
case "--ignore":
cli.ignore = args.removeFirst().components(separatedBy: ",")
default:
break
}
}
return cli
}
/// Prints usage information and the list of accepted flags to stdout.
static func printHelp() {
print("Usage: swift-gtk-gen [options]")
print(" --gir-file PATH Path to the .gir file")
print(" --library NAME GIR library namespace (e.g., GLib)")
print(" --version VER GIR version (e.g., 2.0)")
print(" --output DIR Output directory (default: .)")
print(" --girs-dirs DIRS Comma-separated GIR directories")
print(" --external-libs LIBS Comma-separated external libraries")
print(" --generate TYPES Comma-separated types to generate")
print(" --generate-all Generate all types in the namespace")
print(" --manual TYPES Comma-separated types to mark manual")
print(" --ignore TYPES Comma-separated types to ignore")
print(" --emit-single-file Emit a single file instead of per-type files")
print(" --help, -h Show this help")
}
}

View file

@ -0,0 +1,182 @@
import Testing
import Foundation
@testable import SwiftGtkGenCore
@Test("Full pipeline: generate complete Gtk 4.0 wrapper package")
func testFullGtkGeneration() throws {
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/Gtk-4.0.gir")
guard FileManager.default.fileExists(atPath: girURL.path) else {
throw TestSkipError.fileNotFound("Gtk-4.0.gir not found")
}
let parser = GIRParser()
let repo = try parser.parse(fileURL: girURL)
guard let gtk = repo.namespaces.first(where: { $0.name == "Gtk" }) else {
throw TestSkipError.noNamespace("Gtk")
}
let config = GenerationConfig(
library: "Gtk", version: "4.0",
girsDirectories: ["/usr/share/gir-1.0"],
targetDirectory: "",
externalLibraries: ["Gdk-4.0", "Gsk-4.0"],
generate: ["Gtk.Widget", "Gtk.Window", "Gtk.Button", "Gtk.Label",
"Gtk.Box", "Gtk.Align", "Gtk.Application"],
manual: [],
ignore: [],
objects: []
)
let analysis = Analyzer(config: config).analyze(repository: repo)
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: analysis)
// Verify per-file generation
#expect(files.keys.contains("Widget.swift"))
#expect(files.keys.contains("Window.swift"))
#expect(files.keys.contains("Button.swift"))
#expect(files.keys.contains("Label.swift"))
#expect(files.keys.contains("Box.swift"))
#expect(files.keys.contains("Align.swift"))
#expect(files.keys.contains("Application.swift"))
// Verify no fatalError() stubs remain in any file
for (name, content) in files {
#expect(!content.contains("fatalError"), "File \(name) still contains fatalError stub")
}
// Verify real C function calls
#expect(files["Widget.swift"]?.contains("gtk_widget_show(pointer)") == true)
#expect(files["Widget.swift"]?.contains("gtk_widget_get_visible(pointer)") == true)
// Verify real property accessors via GValue
#expect(files["Widget.swift"]?.contains("g_value_init") == true)
#expect(files["Widget.swift"]?.contains("g_object_get_property") == true)
#expect(files["Widget.swift"]?.contains("g_object_set_property") == true)
// Verify real signal connections
#expect(files["Widget.swift"]?.contains("g_signal_connect_data") == true)
#expect(files["Widget.swift"]?.contains("Unmanaged.passRetained") == true)
// Verify constructors
#expect(files["Button.swift"]?.contains("convenience init") == true)
#expect(files["Button.swift"]?.contains("gtk_button_new") == true)
#expect(files["Button.swift"]?.contains("g_object_ref_sink") == true)
// Verify scaffolding
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
#expect(scaffolding.keys.contains("Package.swift"))
#expect(scaffolding.keys.contains("Sources/CGtk/module.modulemap"))
#expect(scaffolding.keys.contains("Sources/CGtk/CGtk.h"))
}
@Test("Full pipeline: parse GLib-2.0.gir and verify analysis")
func testFullGLibParsing() throws {
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/GLib-2.0.gir")
guard FileManager.default.fileExists(atPath: girURL.path) else {
throw TestSkipError.fileNotFound("GLib-2.0.gir not found")
}
let parser = GIRParser()
let repo = try parser.parse(fileURL: girURL)
#expect(!repo.namespaces.isEmpty)
guard let glib = repo.namespaces.first(where: { $0.name == "GLib" }) else {
throw TestSkipError.noNamespace("GLib")
}
// Verify parsing of records, functions, enumerations
#expect(!glib.records.isEmpty, "Expected records in GLib namespace")
#expect(!glib.functions.isEmpty, "Expected functions in GLib namespace")
#expect(!glib.enumerations.isEmpty, "Expected enumerations in GLib namespace")
// Check specific well-known records exist
let recordNames = glib.records.map { $0.name }
#expect(recordNames.contains("String"))
#expect(recordNames.contains("List"))
#expect(recordNames.contains("MainLoop"))
// Verify analysis works for GLib types
let config = GenerationConfig(
library: "GLib", version: "2.0",
girsDirectories: [girURL.deletingLastPathComponent().path],
targetDirectory: "",
externalLibraries: [],
generate: ["GLib.String", "GLib.List", "GLib.MainLoop"],
manual: [],
ignore: [],
objects: []
)
let analyzer = Analyzer(config: config)
let analysis = analyzer.analyze(repository: repo)
#expect(analysis.generatedTypes.contains("GLib.String"))
#expect(analysis.generatedTypes.contains("GLib.List"))
#expect(analysis.generatedTypes.contains("GLib.MainLoop"))
// Note: GLib types are records (opaque C structs), not GObject classes.
// Code generation for records is not yet implemented.
// This test verifies parsing and analysis correctness only.
}
@Test("Full pipeline: generate ALL Gtk types when generate list is empty")
func testGenerateAllGtkTypes() throws {
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/Gtk-4.0.gir")
guard FileManager.default.fileExists(atPath: girURL.path) else {
throw TestSkipError.fileNotFound("Gtk-4.0.gir not found")
}
let parser = GIRParser()
let repo = try parser.parse(fileURL: girURL)
guard let gtk = repo.namespaces.first(where: { $0.name == "Gtk" }) else {
throw TestSkipError.noNamespace("Gtk")
}
// Empty generate list = generate ALL
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: [], manual: [], ignore: [], objects: [])
let analysis = Analyzer(config: config).analyze(repository: repo)
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: analysis)
// Get expected counts from parsed GIR data
let expectedClasses = gtk.classes.count
let expectedInterfaces = gtk.interfaces.count
let expectedEnums = gtk.enumerations.count
let expectedBitfields = gtk.bitfields.count
// Count generated files by type
let generatedClasses = files.values.filter { $0.contains("public final class") || $0.contains("public open class") }.count
let generatedInterfaces = files.values.filter { $0.contains("public protocol") }.count
let generatedEnums = files.values.filter { $0.contains("public enum") && !$0.contains("OptionSet") }.count
let generatedBitfields = files.values.filter { $0.contains("OptionSet") }.count
let generatedCallbacks = files.values.filter { $0.contains("public typealias") }.count
let totalGenerated = files.count
// Verify counts match GIR exactly
#expect(generatedClasses == expectedClasses,
"Expected \(expectedClasses) classes, got \(generatedClasses)")
#expect(generatedInterfaces == expectedInterfaces,
"Expected \(expectedInterfaces) interfaces, got \(generatedInterfaces)")
#expect(generatedEnums == expectedEnums,
"Expected \(expectedEnums) enums, got \(generatedEnums)")
#expect(generatedBitfields == expectedBitfields,
"Expected \(expectedBitfields) bitfields, got \(generatedBitfields)")
#expect(generatedCallbacks > 0,
"Expected at least 1 callback typealias, got \(generatedCallbacks)")
#expect(totalGenerated > 200,
"Expected 200+ total files, got \(totalGenerated)")
// Verify specific well-known files exist
#expect(files.keys.contains("Widget.swift"), "Missing Widget.swift")
#expect(files.keys.contains("Window.swift"), "Missing Window.swift")
#expect(files.keys.contains("Button.swift"), "Missing Button.swift")
#expect(files.keys.contains("Buildable.swift"), "Missing Buildable.swift (interface)")
// Verify interface is generated as a protocol
#expect(files["Buildable.swift"]?.contains("public protocol") == true)
}
enum TestSkipError: Error {
case fileNotFound(String)
case noNamespace(String)
}

View file

@ -0,0 +1,41 @@
import Testing
import Foundation
@Test("CLI produces help output")
func testCLIHelp() throws {
let process = Process()
let binPath = findBinary() ?? failBinaryNotFound()
process.executableURL = binPath
process.arguments = ["--help"]
let output = try process.runAndCapture()
#expect(output.contains("Usage:"))
#expect(output.contains("--library"))
#expect(output.contains("--output"))
}
private func findBinary() -> URL? {
let candidates = [
URL(fileURLWithPath: ".build/debug/swift-gtk-gen"),
URL(fileURLWithPath: "generator/.build/debug/swift-gtk-gen"),
URL(fileURLWithPath: ".build/release/swift-gtk-gen"),
URL(fileURLWithPath: "generator/.build/release/swift-gtk-gen"),
]
return candidates.first { FileManager.default.fileExists(atPath: $0.path) }
}
private func failBinaryNotFound() -> URL {
Issue.record("swift-gtk-gen binary not found at any expected path")
return URL(fileURLWithPath: "/nonexistent")
}
extension Process {
func runAndCapture() throws -> String {
let stdout = Pipe()
standardOutput = stdout
standardError = Pipe()
try run()
waitUntilExit()
let data = stdout.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8) ?? ""
}
}

View file

@ -0,0 +1,40 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Analysis resolves class hierarchy")
func testClassHierarchy() {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
Class(name: "ApplicationWindow", cType: "GtkApplicationWindow", parent: "Window"),
])
])
let analyzer = Analyzer(config: GenerationConfig(
library: "Gtk", version: "4.0", girsDirectories: [], targetDirectory: "",
externalLibraries: [], generate: ["Gtk.Window"], manual: [], ignore: [], objects: []
))
let result = analyzer.analyze(repository: repo)
#expect(result.classHierarchy["Gtk.Window"] == "Gtk.Widget")
#expect(result.classHierarchy["Gtk.ApplicationWindow"] == "Gtk.Window")
}
@Test("Analysis filters types by generate list")
func testTypeFiltering() {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
Class(name: "Button", cType: "GtkButton", parent: "Widget"),
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget", "Gtk.Button"], manual: [], ignore: ["Gtk.Window"], objects: [])
let analyzer = Analyzer(config: config)
let result = analyzer.analyze(repository: repo)
#expect(result.generatedTypes.contains("Gtk.Widget"))
#expect(result.generatedTypes.contains("Gtk.Button"))
#expect(!result.generatedTypes.contains("Gtk.Window"))
#expect(result.manualTypes.isEmpty)
}

View file

@ -0,0 +1,116 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Simple void method calls C function directly")
func testVoidMethodCall() throws {
let method = Method(name: "show", cIdentifier: "gtk_widget_show",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .void)
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(callExpr == "gtk_widget_show(pointer)")
}
@Test("String return wraps with String(cString:)")
func testStringReturnCall() throws {
let method = Method(name: "getLabel", cIdentifier: "gtk_label_get_text",
parameters: [
Parameter(name: "label", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .string)
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(callExpr == "String(cString: gtk_label_get_text(pointer))")
}
@Test("Bool return uses CInt comparison")
func testBoolReturnCall() throws {
let method = Method(name: "getVisible", cIdentifier: "gtk_widget_get_visible",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .boolean)
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(callExpr == "(gtk_widget_get_visible(pointer) != 0)")
}
@Test("GObject pointer parameter uses .pointer property")
func testGObjectParameterCall() throws {
let method = Method(name: "add", cIdentifier: "gtk_container_add",
parameters: [
Parameter(name: "container", type: .pointer,
transferOwnership: .none, isInstanceParameter: true),
Parameter(name: "child", type: .typeRef("Widget"),
transferOwnership: .none)
], returnType: .void)
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(callExpr == "gtk_container_add(pointer, child.pointer)")
}
@Test("TypeRef return wraps in Swift type")
func testTypeRefReturnCall() throws {
let method = Method(name: "getParent", cIdentifier: "gtk_widget_get_parent",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .typeRef("Widget", namespace: "Gtk"))
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(callExpr == "Widget(pointer: gtk_widget_get_parent(pointer))")
}
@Test("Void return type produces bare call")
func testVoidReturn() throws {
let method = Method(name: "show", cIdentifier: "gtk_widget_show",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .void)
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(expr == "gtk_widget_show(pointer)")
}
@Test("Optional typeRef parameter uses optional chaining")
func testOptionalTypeRefParameter() throws {
let method = Method(name: "setParent", cIdentifier: "gtk_widget_set_parent",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true),
Parameter(name: "parent", type: .optional(.typeRef("Widget")),
transferOwnership: .none)
], returnType: .void)
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(expr == "gtk_widget_set_parent(pointer, parent?.pointer)")
}
@Test("Optional typeRef return uses map")
func testOptionalTypeRefReturn() throws {
let method = Method(name: "getChild", cIdentifier: "gtk_widget_get_child",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .optional(.typeRef("Widget")))
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(expr == "gtk_widget_get_child(pointer).map { Widget(pointer: $0) }")
}
@Test("Int32 return passes through directly")
func testIntReturn() throws {
let method = Method(name: "getWidth", cIdentifier: "gtk_widget_get_width",
parameters: [
Parameter(name: "widget", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .int32)
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(expr == "gtk_widget_get_width(pointer)")
}
@Test("Filename return wraps with String(cString:)")
func testFilenameReturn() throws {
let method = Method(name: "getFilename", cIdentifier: "gtk_file_chooser_get_filename",
parameters: [
Parameter(name: "chooser", type: .pointer,
transferOwnership: .none, isInstanceParameter: true)
], returnType: .filename)
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
#expect(expr == "String(cString: gtk_file_chooser_get_filename(pointer))")
}

View file

@ -0,0 +1,99 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Generates a simple class wrapper")
func testGenerateClass() throws {
let repo = Repository(namespaces: [
Namespace(name: "GLib", version: "2.0", classes: [
Class(name: "String", cType: "GString", parent: nil,
constructors: [
Constructor(name: "new", cIdentifier: "g_string_new",
parameters: [
Parameter(name: "init", type: .string, transferOwnership: .none)
], returnType: .typeRef("String"))
],
methods: [
Method(name: "assign", cIdentifier: "g_string_assign",
parameters: [
Parameter(name: "string", type: .string, isNullable: false,
transferOwnership: .none, isInstanceParameter: true),
Parameter(name: "value", type: .string)
], returnType: .typeRef("String"))
], properties: [
Property(name: "length", type: .int32, isReadable: true, isWritable: false)
], signals: [
Signal(name: "changed", parameters: [], returnType: .void)
])
])
])
let config = GenerationConfig(library: "GLib", version: "2.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["GLib.String"], manual: [], ignore: [], objects: [])
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
let output = files["String.swift"] ?? ""
#expect(output.contains("public final class String"))
#expect(output.contains("convenience init"))
#expect(output.contains("g_string_new(`init`)"))
#expect(output.contains("func assign"))
#expect(output.contains("g_value_get_int"))
#expect(output.contains("connectChanged"))
#expect(output.contains("g_signal_connect_data"))
// Verify real C function call instead of fatalError
#expect(output.contains("g_string_assign(pointer, value)"))
#expect(!output.contains("fatalError(\"g_object_get_property"))
#expect(!output.contains("fatalError(\"g_object_set_property"))
#expect(!output.contains("fatalError(\"C function call not yet implemented"))
}
@Test("Kebab-case property names are converted to camelCase")
func testKebabCaseToCamelCase() throws {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Box", cType: "GtkBox", parent: "Widget",
properties: [
Property(name: "baseline-child", type: .int32, isReadable: true, isWritable: true),
Property(name: "baseline-position", type: .int32, isReadable: true, isWritable: false),
Property(name: "homogeneous", type: .boolean, isReadable: true, isWritable: true),
Property(name: "spacing", type: .int32, isReadable: true, isWritable: true),
])
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Box"], manual: [], ignore: [], objects: [])
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
let output = files["Box.swift"] ?? ""
#expect(output.contains("var baselineChild"), "baseline-child should become baselineChild")
#expect(output.contains("var baselinePosition"), "baseline-position should become baselinePosition")
#expect(output.contains("var homogeneous"), "homogeneous should stay as-is (no hyphens)")
#expect(output.contains("var spacing"), "spacing should stay as-is")
// Verify no kebab-case names remain in var declarations
#expect(!output.contains("var baseline-"), "No kebab-case var names should remain")
}
@Test("Generates enumeration")
func testGenerateEnum() throws {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", enumerations: [
Enumeration(name: "Align", cType: "GtkAlign",
members: [
EnumMember(name: "fill", value: "0", cIdentifier: "GTK_ALIGN_FILL"),
EnumMember(name: "start", value: "1", cIdentifier: "GTK_ALIGN_START"),
])
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Align"], manual: [], ignore: [], objects: [])
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
let output = files["Align.swift"] ?? ""
#expect(output.contains("public enum Align"))
#expect(output.contains("case fill"))
#expect(output.contains("case start"))
}

View file

@ -0,0 +1,83 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Config builder creates valid configuration")
func testConfigBuilder() {
let config = GenerationConfig(
library: "GLib",
version: "2.0",
girsDirectories: ["/usr/share/gir-1.0"],
targetDirectory: "Sources/GLib/Generated",
externalLibraries: [],
generate: ["GLib.String", "GLib.List"],
manual: ["GLib.MainLoop"],
ignore: ["GLib.Test"],
objects: []
)
#expect(config.library == "GLib")
#expect(config.generate.count == 2)
}
@Test("Object overrides merge correctly")
func testObjectOverrides() {
let override = ObjectOverrides(status: .generate, finalType: true, concurrency: .mainActor,
version: "2.0", cfgCondition: nil, generateBuilder: false)
#expect(override.status == .generate)
#expect(override.finalType == true)
#expect(override.concurrency == .mainActor)
#expect(override.version == "2.0")
#expect(override.cfgCondition == nil)
#expect(override.generateBuilder == false)
}
@Test("Function overrides can ignore a function")
func testFunctionOverrides() {
let override = FunctionOverrides(ignore: true)
#expect(override.ignore == true)
}
@Test("Config with object and function overrides stores values correctly")
func testConfigWithObjectOverrides() throws {
let widgetOverrides = ObjectOverrides(
status: .generate, finalType: false, concurrency: .mainActor,
version: "4.0", cfgCondition: nil, generateBuilder: false
)
let showOverride = FunctionOverrides(ignore: true)
let config = GenerationConfig(
library: "Gtk",
version: "4.0",
girsDirectories: ["vendor/gir-files"],
targetDirectory: "Sources/GTK/Generated",
externalLibraries: ["Gdk-4.0", "Gsk-4.0"],
generate: ["Gtk.Widget", "Gtk.Window"],
manual: ["Gtk.Buildable"],
ignore: [],
objects: [
.object("Gtk.Widget", overrides: widgetOverrides),
.function("Gtk.Widget", "show", overrides: showOverride),
.functionPattern("Gtk.Window", pattern: "set_*", rename: RenameRule(regex: "^set_(.*)", replacement: "$1")),
.signal("Gtk.Button", "clicked", overrides: SignalOverrides(ignore: true)),
.property("Gtk.Label", "label", overrides: PropertyOverrides(generate: [.get, .set])),
]
)
#expect(config.objects.count == 5)
if case .object(let name, let ov) = config.objects[0] {
#expect(name == "Gtk.Widget")
#expect(ov.finalType == false)
} else { #expect(Bool(false), "Expected .object case") }
if case .functionPattern(let type, let pattern, let rename) = config.objects[2] {
#expect(type == "Gtk.Window")
#expect(pattern == "set_*")
#expect(rename.regex == "^set_(.*)")
} else { #expect(Bool(false), "Expected .functionPattern case") }
if case .signal(let type, let name, let ov) = config.objects[3] {
#expect(type == "Gtk.Button")
#expect(name == "clicked")
#expect(ov.ignore == true)
} else { #expect(Bool(false), "Expected .signal case") }
if case .property(let type, let name, let ov) = config.objects[4] {
#expect(type == "Gtk.Label")
#expect(name == "label")
#expect(ov.generate == [.get, .set])
} else { #expect(Bool(false), "Expected .property case") }
}

View file

@ -0,0 +1,35 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Constructor with no parameters generates convenience init")
func testConstructorNoParams() {
let ctor = Constructor(name: "new", cIdentifier: "gtk_button_new",
parameters: [], returnType: .typeRef("Widget"))
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "Button")
#expect(initMethod.contains("convenience init"))
#expect(initMethod.contains("gtk_button_new()"))
#expect(initMethod.contains("g_object_ref_sink"))
#expect(initMethod.contains("self.init(pointer:"))
}
@Test("Constructor with string parameter")
func testConstructorWithStringParam() {
let ctor = Constructor(name: "newWithLabel", cIdentifier: "gtk_button_new_with_label",
parameters: [
Parameter(name: "label", type: .string, transferOwnership: .none)
], returnType: .typeRef("Widget"))
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "Button")
#expect(initMethod.contains("label: String"))
#expect(initMethod.contains("gtk_button_new_with_label(label)"))
}
@Test("Constructor with GObject parameter uses .pointer")
func testConstructorWithGObjectParam() {
let ctor = Constructor(name: "newFromModel", cIdentifier: "gtk_combo_box_new_from_model",
parameters: [
Parameter(name: "model", type: .typeRef("TreeModel"), transferOwnership: .none)
], returnType: .typeRef("Widget"))
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "ComboBox")
#expect(initMethod.contains("model: TreeModel"))
#expect(initMethod.contains("gtk_combo_box_new_from_model(model.pointer)"))
}

View file

@ -0,0 +1,66 @@
import Testing
@testable import SwiftGtkGenCore
@Test("generateFiles returns one file per type")
func testGenerateFilesReturnsMultipleFiles() throws {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
], enumerations: [
Enumeration(name: "Align", cType: "GtkAlign", members: [
EnumMember(name: "fill", value: "0", cIdentifier: "GTK_ALIGN_FILL"),
])
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget", "Gtk.Window", "Gtk.Align"], manual: [], ignore: [], objects: [])
let analysis = Analyzer(config: config).analyze(repository: repo)
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: analysis)
#expect(files.keys.contains("Widget.swift"))
#expect(files.keys.contains("Window.swift"))
#expect(files.keys.contains("Align.swift"))
#expect(files["Widget.swift"]?.contains("class Widget") == true)
#expect(files["Widget.swift"]?.contains("class Window") == false)
#expect(files["Align.swift"]?.contains("enum Align") == true)
}
@Test("generateFiles includes import CGtk in header")
func testGenerateFilesIncludesCGtkImport() throws {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
let analysis = Analyzer(config: config).analyze(repository: repo)
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: analysis)
#expect(files["Widget.swift"]?.contains("import Foundation") == true)
#expect(files["Widget.swift"]?.contains("import CGtk") == true)
}
@Test("generateFiles skips types not in analysis")
func testGenerateFilesSkipsNonGeneratedTypes() throws {
let repo = Repository(namespaces: [
Namespace(name: "Gtk", version: "4.0", classes: [
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
Class(name: "Button", cType: "GtkButton", parent: "Widget"),
])
])
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
let analysis = Analyzer(config: config).analyze(repository: repo)
let generator = CodeGenerator(config: config)
let files = try generator.generateFiles(repository: repo, analysis: analysis)
#expect(files.keys.contains("Widget.swift"))
#expect(files.keys.contains("Button.swift") == false)
}

View file

@ -0,0 +1,86 @@
import Testing
import SwiftGtkGenCore
@Test("Namespace can hold types")
func testNamespaceHoldsTypes() {
let ns = Namespace(name: "Gtk", version: "4.0")
#expect(ns.name == "Gtk")
#expect(ns.version == "4.0")
#expect(ns.classes.isEmpty)
#expect(ns.interfaces.isEmpty)
#expect(ns.enumerations.isEmpty)
#expect(ns.records.isEmpty)
}
@Test("Class has properties and methods")
func testClassHasProperties() {
let cls = Class(name: "Widget", cType: "GtkWidget", parent: nil, isAbstract: false)
#expect(cls.name == "Widget")
#expect(cls.properties.isEmpty)
#expect(cls.methods.isEmpty)
#expect(cls.signals.isEmpty)
}
@Test("Method stores parameters and return type")
func testMethodStoresParameters() {
let param = Parameter(name: "label", type: .string, isNullable: false, transferOwnership: .none)
let method = Method(name: "set_label", cIdentifier: "gtk_label_set_text", parameters: [param], returnType: .void)
#expect(method.name == "set_label")
#expect(method.parameters.count == 1)
#expect(method.returnType == .void)
}
@Test("Enumeration stores members")
func testEnumerationStoresMembers() {
let member = EnumMember(name: "GTK_ALIGN_FILL", value: "1", cIdentifier: "GTK_ALIGN_FILL")
let enumeration = Enumeration(name: "GtkAlign", cType: "GtkAlign", members: [member])
#expect(enumeration.name == "GtkAlign")
#expect(enumeration.members.count == 1)
#expect(enumeration.members[0].value == "1")
}
@Test("Signal stores parameters and return type")
func testSignalStoresParameters() {
let signal = Signal(name: "clicked", parameters: [], returnType: .void, isDetailed: false)
#expect(signal.name == "clicked")
#expect(signal.returnType == .void)
#expect(signal.isDetailed == false)
}
@Test("Property stores type and flags")
func testPropertyStoresTypeAndFlags() {
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true, isConstructOnly: false)
#expect(prop.name == "label")
#expect(prop.type == .string)
#expect(prop.isReadable == true)
#expect(prop.isWritable == true)
}
@Test("Repository holds multiple namespaces")
func testRepositoryHoldsMultipleNamespaces() {
let gtk = Namespace(name: "Gtk", version: "4.0")
let gdk = Namespace(name: "Gdk", version: "4.0")
let repo = Repository(namespaces: [gtk, gdk])
#expect(repo.namespaces.count == 2)
#expect(repo.namespaces[0].name == "Gtk")
#expect(repo.namespaces[1].name == "Gdk")
}
@Test("GIRType equality works")
func testGIRTypeEquality() {
#expect(GIRType.void == GIRType.void)
#expect(GIRType.string == GIRType.string)
#expect(GIRType.typeRef("GtkWidget") == GIRType.typeRef("GtkWidget"))
#expect(GIRType.typeRef("GtkWidget") != GIRType.typeRef("GtkLabel"))
#expect(GIRType.array(.string) == GIRType.array(.string))
#expect(GIRType.array(.string) != GIRType.array(.int32))
#expect(GIRType.optional(.string) == GIRType.optional(.string))
#expect(GIRType.optional(.string) != GIRType.string)
}
@Test("Interface stores methods, properties, signals, and prereqs")
func testInterfaceStoresMembers() {
let iface = Interface(name: "GtkBuildable", cType: "GtkBuildable", prereqs: ["GtkWidget"])
#expect(iface.name == "GtkBuildable")
#expect(iface.prereqs.count == 1)
}

View file

@ -0,0 +1,205 @@
import Testing
import Foundation
@testable import SwiftGtkGenCore
@Test("Parser reads namespace from minimal GIR XML")
func testParsesNamespace() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="GLib" version="2.0" shared-library="libglib-2.0.so.0"
c:identifier-prefixes="g" c:symbol-prefixes="g"/>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
#expect(repo.namespaces.count == 1)
#expect(repo.namespaces[0].name == "GLib")
#expect(repo.namespaces[0].version == "2.0")
}
@Test("Parser reads a class with method and property")
func testParsesClass() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="Gtk" version="4.0">
<class name="Widget" c:type="GtkWidget" parent="GObject.Object"
glib:type-name="GtkWidget" glib:get-type="gtk_widget_get_type">
<method name="show" c:identifier="gtk_widget_show">
<return-value transfer-ownership="none">
<type name="none" c:type="void"/>
</return-value>
<parameters>
<instance-parameter name="widget" transfer-ownership="none">
<type name="Widget" c:type="GtkWidget*"/>
</instance-parameter>
</parameters>
</method>
<property name="name" writable="1" transfer-ownership="none">
<type name="utf8" c:type="gchar*"/>
</property>
</class>
</namespace>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
let cls = repo.namespaces[0].classes[0]
#expect(cls.name == "Widget")
#expect(cls.parent == "GObject.Object")
#expect(cls.methods.count == 1)
#expect(cls.methods[0].name == "show")
#expect(cls.properties.count == 1)
#expect(cls.properties[0].name == "name")
}
@Test("Parser reads an enumeration with members")
func testParsesEnumeration() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="Gtk" version="4.0">
<enumeration name="Align" glib:type-name="GtkAlign"
glib:get-type="gtk_align_get_type" c:type="GtkAlign">
<member name="fill" value="0" c:identifier="GTK_ALIGN_FILL"/>
<member name="start" value="1" c:identifier="GTK_ALIGN_START"/>
<member name="end" value="2" c:identifier="GTK_ALIGN_END"/>
<member name="center" value="3" c:identifier="GTK_ALIGN_CENTER"/>
</enumeration>
</namespace>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
let enums = repo.namespaces[0].enumerations
#expect(enums.count == 1)
#expect(enums[0].name == "Align")
#expect(enums[0].members.count == 4)
#expect(enums[0].members[0].name == "fill")
#expect(enums[0].members[3].cIdentifier == "GTK_ALIGN_CENTER")
}
@Test("Parser reads bitfield with members")
func testParsesBitfield() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="Gtk" version="4.0">
<bitfield name="SizeGroupMode" c:type="GtkSizeGroupMode"
glib:type-name="GtkSizeGroupMode" glib:get-type="gtk_size_group_mode_get_type">
<member name="none" value="0" c:identifier="GTK_SIZE_GROUP_MODE_NONE"/>
<member name="horizontal" value="1" c:identifier="GTK_SIZE_GROUP_MODE_HORIZONTAL"/>
<member name="vertical" value="2" c:identifier="GTK_SIZE_GROUP_MODE_VERTICAL"/>
<member name="both" value="3" c:identifier="GTK_SIZE_GROUP_MODE_BOTH"/>
</bitfield>
</namespace>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
let bf = repo.namespaces[0].bitfields[0]
#expect(bf.name == "SizeGroupMode")
#expect(bf.members.count == 4)
#expect(bf.members[0].name == "none")
#expect(bf.members[3].cIdentifier == "GTK_SIZE_GROUP_MODE_BOTH")
}
@Test("Parser reads interface with method")
func testParsesInterface() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="Gtk" version="4.0">
<interface name="Buildable" c:type="GtkBuildable">
<method name="buildable_get_id" c:identifier="gtk_buildable_get_id">
<return-value transfer-ownership="none">
<type name="utf8" c:type="gchar*"/>
</return-value>
<parameters>
<instance-parameter name="buildable" transfer-ownership="none">
<type name="Buildable" c:type="GtkBuildable*"/>
</instance-parameter>
</parameters>
</method>
</interface>
</namespace>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
let iface = repo.namespaces[0].interfaces[0]
#expect(iface.name == "Buildable")
#expect(iface.methods.count == 1)
}
@Test("Parser reads global function")
func testParsesGlobalFunction() throws {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<namespace name="GLib" version="2.0">
<function name="getenv" c:identifier="g_getenv"
glib:type-name="GLib" glib:get-type="glib_get_type">
<return-value transfer-ownership="none">
<type name="utf8" c:type="gchar*"/>
</return-value>
<parameters>
<parameter name="variable" transfer-ownership="none">
<type name="utf8" c:type="gchar*"/>
</parameter>
</parameters>
</function>
</namespace>
</repository>
"""
let parser = GIRParser()
let repo = try parser.parse(xmlString: xml)
let fn = repo.namespaces[0].functions[0]
#expect(fn.name == "getenv")
#expect(fn.parameters.count == 1)
}
@Test("Parser throws error on missing required attributes")
func testParsingMissingAttributes() {
let xml = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0">
<namespace name="Gtk" version="4.0">
<class c:type="GtkWidget" parent="GObject.Object"/>
</namespace>
</repository>
"""
let parser = GIRParser()
#expect(throws: GIRParserError.self) {
try parser.parse(xmlString: xml)
}
}
@Test("Parser rejects invalid XML")
func testParsingError() {
let parser = GIRParser()
#expect(throws: GIRParserError.self) {
try parser.parse(xmlString: "not xml")
}
}

View file

@ -0,0 +1,48 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Readable Int32 property generates correct accessor")
func testReadableInt32Property() throws {
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
#expect(accessor.contains("g_value_init"))
#expect(accessor.contains("g_value_get_int"))
#expect(accessor.contains("public var length: Int32"))
#expect(!accessor.contains("get {"))
#expect(!accessor.contains("set {"))
}
@Test("Writable string property generates read-write accessor")
func testWritableStringProperty() throws {
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
#expect(accessor.contains("g_value_set_string"))
#expect(accessor.contains("set {"))
#expect(accessor.contains("get {"))
#expect(accessor.contains("g_value_get_string"))
}
@Test("Read-only boolean property generates getter only")
func testReadOnlyBooleanProperty() throws {
let prop = Property(name: "visible", type: .boolean, isReadable: true, isWritable: false)
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
#expect(!accessor.contains("get {"))
#expect(!accessor.contains("set {"))
#expect(accessor.contains("g_value_get_boolean"))
}
@Test("Write-only property has setter only")
func testWriteOnlyProperty() throws {
let prop = Property(name: "opacity", type: .double, isReadable: false, isWritable: true)
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
#expect(!accessor.contains("get {"))
#expect(accessor.contains("set {"))
#expect(accessor.contains("g_value_set_double"))
}
@Test("Construct-only property has no accessor")
func testConstructOnlyProperty() throws {
let prop = Property(name: "type", type: .typeRef("Type"), isReadable: false, isWritable: true, isConstructOnly: true)
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
#expect(accessor.isEmpty)
}

View file

@ -0,0 +1,98 @@
import Testing
@testable import SwiftGtkGenCore
@Test("generatePackageScaffolding produces Package.swift")
func testPackageScaffolding() {
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
let repo = Repository(
namespaces: [
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
],
cHeaderPath: "gtk/gtk.h",
includedLibraryLinks: ["gdk-4", "gsk-4"]
)
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
#expect(scaffolding.keys.contains("Package.swift"))
#expect(scaffolding["Package.swift"]?.contains("name: \"Gtk\"") == true)
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"gtk-4\"") == true)
}
@Test("generatePackageScaffolding produces module map")
func testModuleMapScaffolding() {
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
let repo = Repository(
namespaces: [
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
],
cHeaderPath: "gtk/gtk.h",
includedLibraryLinks: ["gdk-4", "gsk-4"]
)
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
let moduleMapName = "Sources/CGtk/module.modulemap"
#expect(scaffolding.keys.contains(moduleMapName))
#expect(scaffolding[moduleMapName]?.contains("module CGtk [system]") == true)
#expect(scaffolding[moduleMapName]?.contains("link \"gdk-4\"") == true)
}
@Test("generatePackageScaffolding produces umbrella header")
func testUmbrellaHeader() {
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
let repo = Repository(
namespaces: [
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
],
cHeaderPath: "gtk/gtk.h",
includedLibraryLinks: ["gdk-4", "gsk-4"]
)
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
let headerName = "Sources/CGtk/CGtk.h"
#expect(scaffolding.keys.contains(headerName))
#expect(scaffolding[headerName]?.contains("#include <gtk/gtk.h>") == true)
}
@Test("generatePackageScaffolding works for GLib")
func testPackageScaffoldingGLib() {
let config = GenerationConfig(library: "GLib", version: "2.0", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["GLib.String"], manual: [], ignore: [], objects: [])
let repo = Repository(
namespaces: [
Namespace(name: "GLib", version: "2.0", cSharedLibrary: "libglib-2.0.so.0", cIdentifierPrefix: "G")
],
cHeaderPath: "glib.h"
)
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
#expect(scaffolding["Package.swift"]?.contains("name: \"GLib\"") == true)
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"glib-2.0\"") == true)
#expect(scaffolding["Sources/CGLib/module.modulemap"]?.contains("link \"glib-2.0\"") == true)
#expect(scaffolding["Sources/CGLib/CGLib.h"]?.contains("#include <glib.h>") == true)
}
@Test("generatePackageScaffolding works for Adw")
func testPackageScaffoldingAdw() {
let config = GenerationConfig(library: "Adw", version: "1", girsDirectories: [],
targetDirectory: "", externalLibraries: [],
generate: ["Adw.Window"], manual: [], ignore: [], objects: [])
let repo = Repository(
namespaces: [
Namespace(name: "Adw", version: "1", cSharedLibrary: "libadwaita-1.so.0", cIdentifierPrefix: "Adw")
],
cHeaderPath: "adwaita.h",
includedLibraryLinks: ["gio-2", "gtk-4"]
)
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"adwaita-1\"") == true)
#expect(scaffolding["Sources/CAdw/module.modulemap"]?.contains("link \"adwaita-1\"") == true)
#expect(scaffolding["Sources/CAdw/CAdw.h"]?.contains("#include <adwaita.h>") == true)
}

View file

@ -0,0 +1,22 @@
import Testing
@testable import SwiftGtkGenCore
@Test("Signal with no parameters generates connection")
func testSignalNoParams() {
let signal = Signal(name: "activate", parameters: [], returnType: .void)
let connection = CodeGenerator.generateSignalConnection(signal: signal)
#expect(connection.contains("g_signal_connect_data"))
#expect(connection.contains("connectActivate"))
#expect(connection.contains("() -> Void"))
}
@Test("Signal with parameters includes them in handler")
func testSignalWithParams() {
let signal = Signal(name: "value-changed", parameters: [
Parameter(name: "value", type: .int32, transferOwnership: .none)
], returnType: .void)
let connection = CodeGenerator.generateSignalConnection(signal: signal)
#expect(connection.contains("connectValueChanged"))
#expect(connection.contains("_: Int32"))
#expect(connection.contains("g_signal_connect_data"))
}