From 1330f2bd251ce1d10ad10b520bfe11524c88389b Mon Sep 17 00:00:00 2001 From: Brendan Szymanski Date: Wed, 1 Jul 2026 03:25:08 -0400 Subject: [PATCH] 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. --- .gitignore | 1 + .swift-format | 79 ++ Package.swift | 52 + .../BuildToolPlugin.swift | 75 ++ .../CommandPlugin.swift | 92 ++ Sources/SwiftGtkGenCore/Analysis.swift | 127 +++ Sources/SwiftGtkGenCore/CodeGen.swift | 1012 +++++++++++++++++ Sources/SwiftGtkGenCore/Config.swift | 250 ++++ Sources/SwiftGtkGenCore/IRModel.swift | 643 +++++++++++ Sources/SwiftGtkGenCore/XMLParser.swift | 489 ++++++++ Sources/swift-gtk-gen/Main.swift | 154 +++ .../IntegrationTests/GtkGenerationTests.swift | 182 +++ Tests/SwiftGtkGenCLITests/CLITests.swift | 41 + .../SwiftGtkGenCoreTests/AnalysisTests.swift | 40 + .../CFunctionCallTests.swift | 116 ++ Tests/SwiftGtkGenCoreTests/CodeGenTests.swift | 99 ++ Tests/SwiftGtkGenCoreTests/ConfigTests.swift | 83 ++ .../ConstructorTests.swift | 35 + .../FileOutputTests.swift | 66 ++ Tests/SwiftGtkGenCoreTests/IRModelTests.swift | 86 ++ Tests/SwiftGtkGenCoreTests/ParserTests.swift | 205 ++++ .../PropertyAccessorTests.swift | 48 + .../ScaffoldingTests.swift | 98 ++ .../SignalConnectionTests.swift | 22 + 24 files changed, 4095 insertions(+) create mode 100644 .gitignore create mode 100644 .swift-format create mode 100644 Package.swift create mode 100644 Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift create mode 100644 Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift create mode 100644 Sources/SwiftGtkGenCore/Analysis.swift create mode 100644 Sources/SwiftGtkGenCore/CodeGen.swift create mode 100644 Sources/SwiftGtkGenCore/Config.swift create mode 100644 Sources/SwiftGtkGenCore/IRModel.swift create mode 100644 Sources/SwiftGtkGenCore/XMLParser.swift create mode 100644 Sources/swift-gtk-gen/Main.swift create mode 100644 Tests/IntegrationTests/GtkGenerationTests.swift create mode 100644 Tests/SwiftGtkGenCLITests/CLITests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/AnalysisTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/CodeGenTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/ConfigTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/ConstructorTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/FileOutputTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/IRModelTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/ParserTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift create mode 100644 Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..dd49168 --- /dev/null +++ b/.swift-format @@ -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 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..b8052f8 --- /dev/null +++ b/Package.swift @@ -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"] + ), + ] +) diff --git a/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift b/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift new file mode 100644 index 0000000..1a484ba --- /dev/null +++ b/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift @@ -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 + } +} diff --git a/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift b/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift new file mode 100644 index 0000000..e638333 --- /dev/null +++ b/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift @@ -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 + /// `/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)" + } + } +} diff --git a/Sources/SwiftGtkGenCore/Analysis.swift b/Sources/SwiftGtkGenCore/Analysis.swift new file mode 100644 index 0000000..1432ddf --- /dev/null +++ b/Sources/SwiftGtkGenCore/Analysis.swift @@ -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 + + /// The set of fully-qualified type names that are implemented manually and + /// should be excluded from generation. + public var manualTypes: Set + + /// The set of fully-qualified type names that should be skipped entirely. + public var ignoredTypes: Set + + /// 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 = [], + manualTypes: Set = [], ignoredTypes: Set = [], + 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 = [] + var manualTypes: Set = [] + var ignoredTypes: Set = [] + 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 + ) + } +} diff --git a/Sources/SwiftGtkGenCore/CodeGen.swift b/Sources/SwiftGtkGenCore/CodeGen.swift new file mode 100644 index 0000000..1e6cc45 --- /dev/null +++ b/Sources/SwiftGtkGenCore/CodeGen.swift @@ -0,0 +1,1012 @@ +import Foundation + +/// Errors that can occur during Swift source code generation. +public enum CodeGenError: Error, Equatable { + /// Generation failed with a descriptive message. + case generationFailed(String) +} + +/// Generates Swift source code from a parsed GIR repository and analysis results. +/// +/// Takes the intermediate representation produced by the XML parser, filtered +/// through an ``Analyzer``/``AnalysisResult``, and emits Swift source text +/// containing class wrappers (with GObject reference counting), enumerations, +/// and option sets for bitfields. Generated output targets a single Swift file +/// per invocation. +public struct CodeGenerator { + /// The generation configuration controlling output behavior. + public let config: GenerationConfig + + /// Creates a new code generator with the given configuration. + /// - Parameter config: The generation configuration. + public init(config: GenerationConfig) { + self.config = config + } + + /// Generates Swift source code for the specified repository. + /// + /// Iterates over all namespaces, classes, enumerations, and bitfields in + /// the repository, producing Swift declarations only for types included + /// in the analysis result. Generated classes include pointer-based storage + /// with `g_object_ref_sink`/`g_object_unref` lifetime management, computed + /// property accessors, method stubs, and signal connection methods. + /// + /// - Parameters: + /// - repository: The parsed GIR repository to generate code from. + /// - analysis: The analysis results specifying which types to generate + /// and any per-type overrides. + /// - Returns: A string containing the generated Swift source code. + /// - Throws: `CodeGenError` if generation fails. + public func generate(repository: Repository, analysis: AnalysisResult) throws -> String { + var output = """ + // Generated by SwiftGtkGen. DO NOT EDIT. + // Source: \(config.library) \(config.version) + + import C\(config.library) + import Foundation + + """ + + for ns in repository.namespaces where ns.name == config.library { + let shouldGenerateAll = analysis.generatedTypes.isEmpty + + for cls in ns.classes { + let fullName = "\(ns.name).\(cls.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateClass(cls, namespace: ns.name, analysis: analysis) + } + for enm in ns.enumerations { + let fullName = "\(ns.name).\(enm.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateEnum(enm) + } + for bf in ns.bitfields { + let fullName = "\(ns.name).\(bf.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateBitfield(bf) + } + for iface in ns.interfaces { + let fullName = "\(ns.name).\(iface.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateInterface(iface) + } + for cb in ns.callbacks { + let fullName = "\(ns.name).\(cb.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateCallback(cb) + } + for fn in ns.functions { + let fullName = "\(ns.name).\(fn.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + output += generateGlobalFunction(fn, namespace: ns.name) + } + } + + return output + } + + /// Generates Swift source code split into one file per type. + /// + /// Returns a dictionary mapping filenames (e.g. `"Widget.swift"`) to their + /// generated Swift source code. Each file includes a standard header with + /// `import Foundation` and the generated C interop import, followed by the + /// declaration for that single type. Only types present in the analysis + /// result are included. + /// + /// - Parameters: + /// - repository: The parsed GIR repository to generate code from. + /// - analysis: The analysis results specifying which types to generate. + /// - Returns: A dictionary of filename to generated source code. + /// - Throws: `CodeGenError` if generation fails. + public func generateFiles(repository: Repository, analysis: AnalysisResult) throws -> [String: String] { + var bodies: [String: String] = [:] + + for ns in repository.namespaces where ns.name == config.library { + let shouldGenerateAll = analysis.generatedTypes.isEmpty + + for cls in ns.classes { + let fullName = "\(ns.name).\(cls.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + bodies["\(cls.name).swift"] = generateClass(cls, namespace: ns.name, analysis: analysis) + } + for enm in ns.enumerations { + let fullName = "\(ns.name).\(enm.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + bodies["\(enm.name).swift"] = generateEnum(enm) + } + for bf in ns.bitfields { + let fullName = "\(ns.name).\(bf.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + bodies["\(bf.name).swift"] = generateBitfield(bf) + } + for iface in ns.interfaces { + let fullName = "\(ns.name).\(iface.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + bodies["\(iface.name).swift"] = generateInterface(iface) + } + for cb in ns.callbacks { + let fullName = "\(ns.name).\(cb.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + bodies["\(cb.name).swift"] = generateCallback(cb) + } + for fn in ns.functions { + let fullName = "\(ns.name).\(fn.name)" + if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue } + let fnFilename = "\(Self.pascalCaseName(fn.name)).swift" + if !bodies.keys.contains(fnFilename) { + bodies[fnFilename] = generateGlobalFunction(fn, namespace: ns.name) + } + } + } + + let header = """ + // Generated by SwiftGtkGen. DO NOT EDIT. + // Source: \(config.library) \(config.version) + + import C\(config.library) + import Foundation + + """ + "\n" + + var files: [String: String] = [:] + for (name, body) in bodies { + files[name] = header + body + } + + return files + } + + /// Generates the package scaffolding files (Package.swift, module map, umbrella header) + /// for a self-contained SwiftPM wrapper package. + /// + /// Returns a dictionary mapping relative file paths (e.g. `"Package.swift"`, + /// `"Sources/CGtk/module.modulemap"`) to their textual content. The library + /// name in `config.library` determines the package name, the C interop target + /// name (prefixed with `C`). Link names and pkg-config names are derived from + /// the repository's namespace `shared-library` attribute, the umbrella header + /// from the repository's `` element, and external library link flags + /// from `` elements and `config.externalLibraries`. + /// + /// - Parameters: + /// - config: The generation configuration (uses `library` and + /// `externalLibraries` fields). + /// - repository: The parsed GIR repository containing namespace metadata, + /// ``, ``, and `` values. + /// - Returns: A dictionary of relative path → file content. + public static func generatePackageScaffolding(config: GenerationConfig, repository: Repository) -> [String: String] { + var files: [String: String] = [:] + let cName = "C\(config.library)" + let lowerName = config.library.lowercased() + + // Extract namespace data from the repository + let ns = repository.namespaces.first { $0.name == config.library } + let sharedLibrary = ns?.cSharedLibrary ?? "" + + // Parse shared-library to derive all link names + // shared-library format: "libgtk-4.so.1" or "libgio-2.0.so.0,libglib-2.0.so.0" + let libraries = sharedLibrary.split(separator: ",").map(String.init) + let allLinkNames = libraries.compactMap { extractLinkName(from: $0) } + let lowerLibName = config.library.lowercased() + + // Pick the best primary link: prefer the one matching the library name + let linkName = allLinkNames.first { $0.lowercased().contains(lowerLibName) } + ?? allLinkNames.first + ?? lowerName + let pkgConfigName = linkName + + // Derive umbrella header from repository + let cHeader = repository.cHeaderPath + let umbrellaHeader = cHeader.isEmpty + ? "#include <\(lowerName)/\(lowerName).h>" + : "#include <\(cHeader)>" + + // Build external link names from GIR dependencies + config + var externalLinks: [String] = [] + for linkName in repository.includedLibraryLinks { + if !externalLinks.contains(linkName) { + externalLinks.append(linkName) + } + } + for ext in config.externalLibraries { + let el = ext.lowercased() + if !externalLinks.contains(el) { + externalLinks.append(el) + } + } + + // Package.swift + let packageSwift = """ + // swift-tools-version: 6.0 + import PackageDescription + + let package = Package( + name: "\(config.library)", + products: [ + .library( + name: "\(config.library)", + targets: ["\(config.library)"] + ), + ], + targets: [ + .target( + name: "\(config.library)", + dependencies: ["\(cName)"] + ), + .systemLibrary( + name: "\(cName)", + path: "Sources/\(cName)", + pkgConfig: "\(pkgConfigName)" + ), + ] + ) + """ + files["Package.swift"] = packageSwift + + // Module map: include all shared libraries as links + let linkLines = (allLinkNames + externalLinks).map { " link \"\($0)\"" }.joined(separator: "\n") + let moduleMap = """ + module \(cName) [system] { + umbrella "\(lowerName)" + \(linkLines) + } + + """ + files["Sources/\(cName)/module.modulemap"] = moduleMap + + files["Sources/\(cName)/\(cName).h"] = umbrellaHeader + "\n" + + return files + } + + /// Extracts link name from a shared-library string like "libgtk-4.so.1" -> "gtk-4". + private static func extractLinkName(from sharedLib: String) -> String? { + guard sharedLib.hasPrefix("lib") else { return nil } + let withoutLib = String(sharedLib.dropFirst(3)) + guard let soRange = withoutLib.range(of: ".so") else { return withoutLib } + return String(withoutLib[.. String { + var swift = "" + let override = analysis.classOverrides["\(namespace).\(cls.name)"] + + swift += Self.formatDocComment(cls.doc) + let isFinal = override?.finalType ?? true + let classKeyword = isFinal ? "final class" : "open class" + let parentClause = cls.parent.map { ": \($0)" } ?? "" + swift += "public \(classKeyword) \(cls.name)\(parentClause) {\n" + + swift += """ + let pointer: UnsafeMutableRawPointer + + public init(pointer: UnsafeMutableRawPointer) { + g_object_ref_sink(pointer) + self.pointer = pointer + } + + deinit { + g_object_unref(pointer) + } + + + """ + + for ctor in cls.constructors { + swift += Self.formatDocComment(ctor.doc, indentation: 4) + swift += Self.generateConstructor(constructor: ctor, className: cls.name) + } + + for prop in cls.properties { + swift += Self.formatDocComment(prop.doc, indentation: 4) + swift += Self.generatePropertyAccessor(property: prop) + } + + // Class-level functions (static methods) + for fn in cls.functions { + swift += Self.formatDocComment(fn.doc, indentation: 4) + let params = Self.nonVarargParameters(fn.parameters) + let paramList = params.map { p -> String in + let type = Self.typeToSwift(p.type) + return "\(Self.swiftifyParameterName(p.name)): \(type)" + }.joined(separator: ", ") + let returnTypeStr = fn.returnType == .void ? "" : " -> \(Self.typeToSwift(fn.returnType))" + let returnStmt = fn.returnType == .void ? "" : "return " + let args = params.map { p -> String in + Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type) + }.joined(separator: ", ") + let cCall = "\(fn.cIdentifier)(\(args))" + let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType) + + swift += """ + public static func \(Self.swiftifyMethodName(fn.name))(\(paramList))\(returnTypeStr) { + \(returnStmt)\(wrappedReturn) + } + + + """ + } + + for method in cls.methods { + swift += Self.formatDocComment(method.doc, indentation: 4) + let params = Self.nonVarargParameters(method.parameters.filter { !$0.isInstanceParameter }) + let paramList = params.map { p -> String in + "\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))" + }.joined(separator: ", ") + let returnTypeStr = method.returnType == .void ? "" : " -> \(Self.typeToSwift(method.returnType))" + let returnStmt = method.returnType == .void ? "" : "return " + let cCall = Self.generateCFunctionCall(method: method, instancePointer: "pointer") + + swift += """ + public func \(Self.swiftifyMethodName(method.name))(\(paramList))\(returnTypeStr) { + \(returnStmt)\(cCall) + } + + + """ + } + + for signal in cls.signals { + swift += Self.formatDocComment(signal.doc, indentation: 4) + swift += Self.generateSignalConnection(signal: signal) + } + + while swift.hasSuffix("\n") { + swift = String(swift.dropLast()) + } + swift += "\n}\n" + return swift + } + + // MARK: - Enum Generation + + /// Generates a Swift `enum` with `Int` raw values from a GIR enumeration. + /// + /// Each GIR member is mapped to a camelCase Swift case. The enum adopts + /// `Int` as its raw value type, matching the underlying C enum layout. + /// + /// - Parameter enm: The GIR enumeration to generate. + /// - Returns: A Swift enum declaration as a string. + private func generateEnum(_ enm: Enumeration) -> String { + var swift = "" + swift += Self.formatDocComment(enm.doc) + swift += "public enum \(enm.name): Int {\n" + for member in enm.members { + let caseName = Self.swiftifyEnumCaseName(member.name) + let formattedValue = Self.formatNumericLiteral(member.value) + swift += " case \(caseName) = \(formattedValue)\n" + } + while swift.hasSuffix("\n") { swift = String(swift.dropLast()) } + swift += "\n}\n" + return swift + } + + // MARK: - Bitfield Generation + + /// Generates a Swift `OptionSet` conformance from a GIR bitfield definition. + /// + /// Produces a struct with an `Int` raw value and static constants using + /// bit-shifted literals (`1 << N`), matching the C bitmask semantics. + /// + /// - Parameter bf: The GIR bitfield to generate. + /// - Returns: A Swift `OptionSet` struct declaration as a string. + private func generateBitfield(_ bf: Bitfield) -> String { + var swift = "" + swift += Self.formatDocComment(bf.doc) + swift += "public struct \(bf.name): OptionSet {\n" + swift += " public let rawValue: Int\n" + swift += " public init(rawValue: Int) { self.rawValue = rawValue }\n\n" + for member in bf.members { + let caseName = Self.swiftifyEnumCaseName(member.name) + let formattedValue = Self.formatNumericLiteral(member.value) + swift += " public static let \(caseName) = \(bf.name)(rawValue: 1 << \(formattedValue))\n" + } + while swift.hasSuffix("\n") { swift = String(swift.dropLast()) } + swift += "\n}\n" + return swift + } + + // MARK: - Interface Generation + + /// Generates a Swift protocol declaration from a GIR interface definition. + /// + /// Produces a `protocol` with method requirements (excluding the instance + /// parameter) and computed property requirements for readable/writable + /// properties. Signals are not included since protocol-based signal + /// connection is not directly expressible. + /// + /// - Parameter iface: The GIR interface to generate. + /// - Returns: A Swift protocol declaration as a string. + private func generateInterface(_ iface: Interface) -> String { + var swift = "" + swift += Self.formatDocComment(iface.doc) + swift += "public protocol \(iface.name) {\n" + + for method in iface.methods { + let params = Self.nonVarargParameters(method.parameters.filter { !$0.isInstanceParameter }) + let paramList = params.map { p -> String in + "\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))" + }.joined(separator: ", ") + let returnTypeStr = method.returnType == .void ? "" : " -> \(Self.typeToSwift(method.returnType))" + swift += " func \(Self.swiftifyMethodName(method.name))(\(paramList))\(returnTypeStr)\n" + } + + for prop in iface.properties { + let propName = Self.swiftifyPropertyName(prop.name) + let swiftType = Self.typeToSwift(prop.type) + if prop.isReadable && prop.isWritable { + swift += " var \(propName): \(swiftType) { get set }\n" + } else if prop.isReadable { + swift += " var \(propName): \(swiftType) { get }\n" + } + } + + while swift.hasSuffix("\n") { swift = String(swift.dropLast()) } + swift += "\n}\n" + return swift + } + + // MARK: - Callback Generation + + /// Generates a Swift `typealias` with `@convention(c)` from a GIR callback + /// definition. + /// + /// Produces a public typealias suitable for use as a C function pointer in + /// the generated GObject wrapper APIs. Each callback parameter is mapped + /// to its Swift type, and the return type is preserved. + /// + /// - Parameter cb: The GIR callback to generate. + /// - Returns: A Swift typealias declaration as a string. + private func generateCallback(_ cb: Callback) -> String { + var result = Self.formatDocComment(cb.doc) + let paramList = cb.parameters.map { p -> String in + let type = Self.typeToSwift(p.type) + return "\(Self.swiftifyParameterName(p.name)): \(type)" + }.joined(separator: ", ") + let returnType = cb.returnType == .void ? "Void" : Self.typeToSwift(cb.returnType) + result += "public typealias \(cb.name) = @convention(c) (\(paramList)) -> \(returnType)\n" + return result + } + + // MARK: - Global Function Generation + + /// Generates a Swift free function from a GIR global function definition. + /// + /// Produces a `public func` that wraps the underlying C function call, + /// including parameter passing (extracting `.pointer` from GObject types) + /// and return value conversion (e.g. `String(cString:)` for strings, + /// `!= 0` for booleans). + /// + /// - Parameters: + /// - fn: The GIR global function to generate. + /// - namespace: The GIR namespace the function belongs to. + /// - Returns: A Swift function declaration as a string. + private func generateGlobalFunction(_ fn: GlobalFunction, namespace: String) -> String { + let params = Self.nonVarargParameters(fn.parameters) + let paramList = params.map { p -> String in + "\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))" + }.joined(separator: ", ") + let returnTypeStr = fn.returnType == .void ? "" : " -> \(Self.typeToSwift(fn.returnType))" + let returnStmt = fn.returnType == .void ? "" : "return " + + let args = params.map { p -> String in + Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type) + }.joined(separator: ", ") + let cCall = "\(fn.cIdentifier)(\(args))" + let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType) + + var result = Self.formatDocComment(fn.doc) + result += "public func \(Self.swiftifyMethodName(fn.name))(\(paramList))\(returnTypeStr) {\n" + result += " \(returnStmt)\(wrappedReturn)\n" + result += "}\n" + return result + } + + // MARK: - C Function Call Generation + + /// Generates a Swift expression that calls the C function corresponding + /// to a GIR method, including return-type wrapping. + /// + /// Builds the argument list from the method's parameters (replacing the + /// instance parameter with the given pointer expression), then wraps + /// the call result according to the return type (e.g. `String(cString:)` + /// for strings, `!= 0` for booleans). + /// + /// - Parameters: + /// - method: The GIR method to generate a call for. + /// - instancePointer: The Swift expression for the instance pointer + /// (typically `"pointer"`). + /// - Returns: A Swift expression string for the C function call. + // MARK: - Property Accessor Generation + + /// Generates a Swift computed property declaration for a GObject property + /// using the `GValue` API (`g_object_get_property`/`g_object_set_property`). + /// + /// Construct-only properties return an empty string since they are handled + /// via constructor parameters rather than runtime setters. + /// + /// - Parameter property: The GIR property to generate an accessor for. + /// - Returns: A Swift computed property declaration string, or empty if + /// the property is construct-only. + public static func generatePropertyAccessor(property: Property) -> String { + guard !property.isConstructOnly else { return "" } + + let propName = swiftifyPropertyName(property.name) + let swiftType = typeToSwift(property.type) + let cTypeName = gValueTypeName(for: property.type) + let getterFunc = gValueGetterFunc(for: property.type) + let setterFunc = gValueSetterFunc(for: property.type) + let girPropName = property.name + + var swift = "" + + if property.isReadable && property.isWritable { + swift += """ + public var \(propName): \(swiftType) { + get { + var value = GValue() + g_value_init(&value, \(cTypeName)) + g_object_get_property(pointer, "\(girPropName)", &value) + let result = \(getterFunc)(&value) + g_value_unset(&value) + return result + } + set { + var value = GValue() + g_value_init(&value, \(cTypeName)) + \(setterFunc)(&value, newValue) + g_object_set_property(pointer, "\(girPropName)", &value) + g_value_unset(&value) + } + } + + + """ + } else if property.isReadable { + swift += """ + public var \(propName): \(swiftType) { + var value = GValue() + g_value_init(&value, \(cTypeName)) + g_object_get_property(pointer, "\(girPropName)", &value) + let result = \(getterFunc)(&value) + g_value_unset(&value) + return result + } + + + """ + } else if property.isWritable { + swift += """ + public var \(propName): \(swiftType) { + set { + var value = GValue() + g_value_init(&value, \(cTypeName)) + \(setterFunc)(&value, newValue) + g_object_set_property(pointer, "\(girPropName)", &value) + g_value_unset(&value) + } + } + + + """ + } + + return swift + } + + /// Returns the GType name constant for the given GIR type. + private static func gValueTypeName(for type: GIRType) -> String { + switch type { + case .boolean: return "G_TYPE_BOOLEAN" + case .int8: return "G_TYPE_INT8" + case .int16: return "G_TYPE_INT16" + case .int32: return "G_TYPE_INT" + case .int64: return "G_TYPE_INT64" + case .uint8: return "G_TYPE_UINT8" + case .uint16: return "G_TYPE_UINT16" + case .uint32: return "G_TYPE_UINT" + case .uint64: return "G_TYPE_UINT64" + case .float: return "G_TYPE_FLOAT" + case .double: return "G_TYPE_DOUBLE" + case .string, .filename: return "G_TYPE_STRING" + case .typeRef: return "G_TYPE_OBJECT" + case .pointer: return "G_TYPE_POINTER" + case .array: return "G_TYPE_ARRAY" + case .cArray: return "G_TYPE_ARRAY" + case .void: return "G_TYPE_NONE" + case .optional: return "G_TYPE_NONE" + } + } + + /// Returns the GValue getter function name for the given GIR type. + private static func gValueGetterFunc(for type: GIRType) -> String { + switch type { + case .boolean: return "g_value_get_boolean" + case .int8: return "g_value_get_schar" + case .int16: return "g_value_get_int16" + case .int32: return "g_value_get_int" + case .int64: return "g_value_get_int64" + case .uint8: return "g_value_get_uchar" + case .uint16: return "g_value_get_uint16" + case .uint32: return "g_value_get_uint" + case .uint64: return "g_value_get_uint64" + case .float: return "g_value_get_float" + case .double: return "g_value_get_double" + case .string, .filename: return "g_value_get_string" + case .typeRef: return "g_value_get_object" + case .pointer: return "g_value_get_pointer" + case .array: return "g_value_get_boxed" + case .cArray: return "g_value_get_boxed" + default: return "g_value_get_pointer" + } + } + + /// Returns the GValue setter function name for the given GIR type. + private static func gValueSetterFunc(for type: GIRType) -> String { + switch type { + case .boolean: return "g_value_set_boolean" + case .int8: return "g_value_set_schar" + case .int16: return "g_value_set_int16" + case .int32: return "g_value_set_int" + case .int64: return "g_value_set_int64" + case .uint8: return "g_value_set_uchar" + case .uint16: return "g_value_set_uint16" + case .uint32: return "g_value_set_uint" + case .uint64: return "g_value_set_uint64" + case .float: return "g_value_set_float" + case .double: return "g_value_set_double" + case .string, .filename: return "g_value_set_string" + case .typeRef: return "g_value_set_object" + case .pointer: return "g_value_set_pointer" + default: return "g_value_set_pointer" + } + } + + // MARK: - Signal Connection Generation + + /// Generates a signal connection method using `g_signal_connect_data`. + /// + /// The method takes a Swift closure and bridges it to a C function pointer + /// via `Unmanaged.passRetained`. The retain/release is managed by + /// `GClosureNotify` (the destroy handler passed to `g_signal_connect_data`). + /// + /// For signals with zero parameters, a complete bridging trampoline is + /// generated. For signals with parameters, the generated code uses + /// `g_signal_connect_data` with a nil callback, noting manual implementation. + /// + /// - Parameter signal: The GIR signal to generate. + /// - Returns: A signal connection method declaration as a string. + public static func generateSignalConnection(signal: Signal) -> String { + let signalName = swiftifySignalName(signal.name) + let handlerArgs = signal.parameters.map { p -> String in + let swiftType = typeToSwift(p.type) + return "_: \(swiftType)" + }.joined(separator: ", ") + + let detailClause = signal.isDetailed ? ", \"\(signal.name)\"" : "" + + if signal.parameters.isEmpty { + return """ + public func connect\(signalName)(_ handler: @escaping () -> Void) -> Int { + let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque() + let callback: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void = { (_, data) in + let stored = Unmanaged.fromOpaque(data!).takeUnretainedValue() as! () -> Void + stored() + } + let destroy: @convention(c) (UnsafeMutableRawPointer?) -> Void = { Unmanaged.fromOpaque($0!).release() } + return Int(g_signal_connect_data(pointer, "\(signal.name)", callback, boxed, destroy, 0)) + } + + + """ + } else { + return """ + public func connect\(signalName)(_ handler: @escaping (\(handlerArgs)) -> Void) -> Int { + // Manual: Signal connection with parameters needs manual implementation + return Int(g_signal_connect_data(pointer, "\(signal.name)"\(detailClause), nil, nil, nil, 0)) + } + + + """ + } + } + + // MARK: - Constructor Generation + + /// Generates a Swift convenience initializer for a GObject constructor. + /// + /// Calls the C constructor function (e.g. `gtk_button_new_with_label(label)`) + /// and handles the floating reference pattern by calling `g_object_ref_sink` + /// on the returned pointer before passing it to `self.init(pointer:)`. + /// This is correct for all `GtkWidget` subclasses and is harmless for + /// non-floating objects (where `g_object_ref_sink` simply refs once). + /// + /// - Parameters: + /// - constructor: The GIR constructor definition. + /// - className: The Swift class name. + /// - Returns: A convenience initializer declaration as a string. + public static func generateConstructor(constructor: Constructor, className: String) -> String { + let params = Self.nonVarargParameters(constructor.parameters) + let paramList = params.map { p -> String in + "\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))" + }.joined(separator: ", ") + let args = params.map { p -> String in + Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type) + }.joined(separator: ", ") + + return """ + public convenience init(\(paramList)) { + let ptr = \(constructor.cIdentifier)(\(args)) + g_object_ref_sink(ptr) + self.init(pointer: ptr!) + } + + + """ + } + + // MARK: - C Function Call Generation + + public static func generateCFunctionCall(method: Method, instancePointer: String) -> String { + let sortedParams = Self.nonVarargParameters(method.parameters) + let args = sortedParams.map { p -> String in + if p.isInstanceParameter { return instancePointer } + return cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type) + }.joined(separator: ", ") + + let cFuncCall = "\(method.cIdentifier)(\(args))" + return wrapCReturnValue(callExpression: cFuncCall, returnType: method.returnType) + } + + /// Returns the Swift argument expression for a parameter of the given type. + /// + /// Extracts `.pointer` from GObject wrapper types and handles optional + /// GObject pointers with optional chaining. All other types pass through + /// as-is (Swift C interop handles the conversion natively). + /// + /// - Parameters: + /// - name: The Swift parameter name. + /// - type: The GIR type of the parameter. + /// - Returns: A Swift expression string for the C function argument. + private static func cParameterExpression(name: String, type: GIRType) -> String { + switch type { + case .typeRef: + return "\(name).pointer" + case .optional(let inner): + if case .typeRef = inner { + return "\(name)?.pointer" + } + return name + default: + return name + } + } + + /// Wraps a C function call expression to convert its return value to the + /// corresponding Swift type. + /// + /// - `.void` returns the bare call expression (no `return` statement). + /// - `.boolean` compares the result to `0` via `cFunc() != 0`. + /// - `.string` / `.filename` wraps with `String(cString:)`. + /// - `.typeRef` wraps in the Swift wrapper type's initializer. + /// - `.optional(.typeRef)` uses `Optional.map` to wrap non-nil results. + /// - All other types pass through directly. + /// + /// - Parameters: + /// - callExpression: The raw C function call expression. + /// - returnType: The GIR return type. + /// - Returns: A Swift expression string with appropriate type wrapping. + private static func wrapCReturnValue(callExpression: String, returnType: GIRType) -> String { + switch returnType { + case .void: + return callExpression + case .boolean: + return "(\(callExpression) != 0)" + case .string, .filename: + return "String(cString: \(callExpression))" + case .typeRef(let name, _): + return "\(name)(pointer: \(callExpression))" + case .optional(let inner): + if case .typeRef(let name, _) = inner { + return "\(callExpression).map { \(name)(pointer: $0) }" + } + if case .string = inner { + return "\(callExpression).map { String(cString: $0) }" + } + if case .filename = inner { + return "\(callExpression).map { String(cString: $0) }" + } + return callExpression + default: + return callExpression + } + } + + // MARK: - Documentation Comment Formatting + + /// Formats a documentation string as DocC-compatible `///` comments. + /// - Parameter doc: The raw documentation text from the GIR file. + /// - Returns: A string of `///`-prefixed lines, or empty string if nil/empty. + private static func formatDocComment(_ doc: String?, indentation: Int = 0) -> String { + guard let doc = doc, !doc.isEmpty else { return "" } + let indent = String(repeating: " ", count: indentation) + let lines = doc.split(separator: "\n", omittingEmptySubsequences: false) + var result = "" + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { + result += "\(indent)///\n" + } else { + result += "\(indent)/// \(trimmed)\n" + } + } + return result + } + + // MARK: - Formatting Helpers + + /// Formats a numeric literal string with underscore separators for readability. + /// For example, `"4294967295"` becomes `"4_294_967_295"`. + private static func formatNumericLiteral(_ value: String) -> String { + guard value.count > 4, let _ = Int(value) else { return value } + var result = "" + var remaining = value + while remaining.count > 3 { + let chunk = remaining.suffix(3) + remaining = String(remaining.dropLast(3)) + result = "_\(chunk)" + result + } + return remaining + result + } + + // MARK: - Naming Helpers + + /// Maps a GIR type to its corresponding Swift type name string. + /// + /// Handles primitive types (`boolean` → `Bool`, `int32` → `Int32`, etc.), + /// type references (looked up by name), and compound types (arrays, + /// C-style arrays, optionals). Pointers and filenames map to + /// `UnsafeMutableRawPointer` and `String` respectively. + /// + /// - Parameter type: The GIR type to map. + /// - Returns: The Swift type name as a string. + private static func typeToSwift(_ type: GIRType) -> String { + switch type { + case .void: return "Void" + case .boolean: return "Bool" + case .int8: return "Int8" + case .int16: return "Int16" + case .int32: return "Int32" + case .int64: return "Int64" + case .uint8: return "UInt8" + case .uint16: return "UInt16" + case .uint32: return "UInt32" + case .uint64: return "UInt64" + case .float: return "Float" + case .double: return "Double" + case .string: return "String" + case .filename: return "String" + case .pointer: return "UnsafeMutableRawPointer" + case .typeRef(let name, _): return name + case .array(let inner): return "[\(typeToSwift(inner))]" + case .cArray(let inner): return "UnsafeBufferPointer<\(typeToSwift(inner))>" + case .optional(let inner): return "\(typeToSwift(inner))?" + } + } + + /// Converts a snake_case GIR name to a camelCase Swift property name. + /// + /// Splits on underscores and lowercases the first part while capitalizing + /// each subsequent part. For example, `"current_page"` becomes + /// `"currentPage"`. + /// + /// - Parameter name: The snake_case GIR property name. + /// - Returns: A camelCase Swift property name. + private static func swiftifyPropertyName(_ name: String) -> String { + name.split { $0 == "_" || $0 == "-" }.enumerated().map { i, part in + i == 0 ? String(part).lowercased() : String(part).capitalized + }.joined() + } + + /// The set of Swift reserved keywords that cannot be used as identifiers + /// without backtick escaping. + private static let reservedKeywords: Set = [ + "self", "type", "class", "default", "in", "for", "repeat", "while", + "switch", "case", "break", "continue", "return", "if", "else", + "guard", "defer", "do", "try", "throw", "catch", "import", "let", + "var", "func", "static", "struct", "enum", "protocol", "extension", + "init", "deinit", "subscript", "where", "operator", "Protocol", + "rethrows", "associatedtype", "precedencegroup", + "true", "false", "nil", "Self", "Type", + "private", "fileprivate", "internal", "public", "open", + "is", "as", "async", "await", "nonisolated", "throws", + ] + + /// Converts a GIR parameter name to a valid Swift identifier. + /// + /// Applies camelCase conversion (via ``swiftifyPropertyName(_:)``) and + /// backtick-escapes the result if it collides with a Swift reserved + /// keyword such as `self`, `class`, `default`, or `return`. + /// + /// - Parameter name: The raw GIR parameter name. + /// - Returns: A valid Swift parameter label, backtick-escaped if needed. + private static func swiftifyParameterName(_ name: String) -> String { + let swiftName = swiftifyPropertyName(name) + return reservedKeywords.contains(swiftName) ? "`\(swiftName)`" : swiftName + } + + private static func swiftifyMethodName(_ name: String) -> String { + let swiftName = Self.swiftifyPropertyName(name) + return reservedKeywords.contains(swiftName) ? "`\(swiftName)`" : swiftName + } + + /// Converts a GIR signal name (kebab-case) to a PascalCase Swift identifier. + /// + /// Splits on hyphens and capitalizes each component. For example, + /// `"page-changed"` becomes `"PageChanged"`, suitable for use in + /// `connectPageChanged(_:)`. + /// + /// - Parameter name: The kebab-case GIR signal name. + /// - Returns: A PascalCase Swift method name component. + private static func swiftifySignalName(_ name: String) -> String { + name.split(separator: "-").map { $0.capitalized }.joined() + } + + /// Converts a GIR enum case name (SCREAMING_SNAKE_CASE) to a camelCase Swift case. + /// + /// Lowercases the entire string, splits on underscores, and lowercases the + /// first word while capitalizing subsequent words. For example, + /// `"GTK_WIDGET_HELP"` becomes `"gtkWidgetHelp"`. Handles reserved keyword + /// collisions (backtick-escapes) and identifiers starting with digits (prefixes + /// with underscore). + /// + /// - Parameter name: The GIR enum case name, typically in SCREAMING_SNAKE_CASE. + /// - Returns: A valid camelCase Swift enum case name. + private static func swiftifyEnumCaseName(_ name: String) -> String { + let lower = name.lowercased() + let parts = lower.split { $0 == "_" || $0 == "-" } + var result = parts.enumerated().map { i, p in + i == 0 ? String(p) : String(p).capitalized + }.joined() + + // Prefix with underscore if starts with a digit + if result.first?.isNumber == true { + result = "_" + result + } + + // Backtick-escape Swift keywords + if reservedKeywords.contains(result) { + result = "`\(result)`" + } + + return result + } + + /// Converts a snake_case or kebab-case name to PascalCase. + /// + /// Splits on underscores and hyphens and capitalizes each component. + /// For example, `"gtk_init"` becomes `"GtkInit"`. + private static func pascalCaseName(_ name: String) -> String { + name.split { $0 == "_" || $0 == "-" }.map { $0.capitalized }.joined() + } + + /// Filters out variadic C parameters (those named `"..."` or empty) from a parameter list. + private static func nonVarargParameters(_ params: [Parameter]) -> [Parameter] { + params.filter { $0.name != "..." && !$0.name.isEmpty } + } +} diff --git a/Sources/SwiftGtkGenCore/Config.swift b/Sources/SwiftGtkGenCore/Config.swift new file mode 100644 index 0000000..eb42d9c --- /dev/null +++ b/Sources/SwiftGtkGenCore/Config.swift @@ -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 + } +} diff --git a/Sources/SwiftGtkGenCore/IRModel.swift b/Sources/SwiftGtkGenCore/IRModel.swift new file mode 100644 index 0000000..0e28946 --- /dev/null +++ b/Sources/SwiftGtkGenCore/IRModel.swift @@ -0,0 +1,643 @@ +/// A complete GIR repository, containing one or more namespaces. +/// +/// Corresponds to the root `` 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 `` (e.g. `"gtk/gtk.h"`). + public var cHeaderPath: String + /// Link names for GIR dependencies derived from `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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 `` 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) + } +} diff --git a/Sources/SwiftGtkGenCore/XMLParser.swift b/Sources/SwiftGtkGenCore/XMLParser.swift new file mode 100644 index 0000000..611bc53 --- /dev/null +++ b/Sources/SwiftGtkGenCore/XMLParser.swift @@ -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, `` 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 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 `` 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[.. 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") + } +} diff --git a/Tests/IntegrationTests/GtkGenerationTests.swift b/Tests/IntegrationTests/GtkGenerationTests.swift new file mode 100644 index 0000000..435e592 --- /dev/null +++ b/Tests/IntegrationTests/GtkGenerationTests.swift @@ -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) +} diff --git a/Tests/SwiftGtkGenCLITests/CLITests.swift b/Tests/SwiftGtkGenCLITests/CLITests.swift new file mode 100644 index 0000000..76aa3d6 --- /dev/null +++ b/Tests/SwiftGtkGenCLITests/CLITests.swift @@ -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) ?? "" + } +} diff --git a/Tests/SwiftGtkGenCoreTests/AnalysisTests.swift b/Tests/SwiftGtkGenCoreTests/AnalysisTests.swift new file mode 100644 index 0000000..4f3ef00 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/AnalysisTests.swift @@ -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) +} diff --git a/Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift b/Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift new file mode 100644 index 0000000..216f047 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift @@ -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))") +} diff --git a/Tests/SwiftGtkGenCoreTests/CodeGenTests.swift b/Tests/SwiftGtkGenCoreTests/CodeGenTests.swift new file mode 100644 index 0000000..4ae22e8 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/CodeGenTests.swift @@ -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")) +} diff --git a/Tests/SwiftGtkGenCoreTests/ConfigTests.swift b/Tests/SwiftGtkGenCoreTests/ConfigTests.swift new file mode 100644 index 0000000..58095bd --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/ConfigTests.swift @@ -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") } +} diff --git a/Tests/SwiftGtkGenCoreTests/ConstructorTests.swift b/Tests/SwiftGtkGenCoreTests/ConstructorTests.swift new file mode 100644 index 0000000..6157173 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/ConstructorTests.swift @@ -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)")) +} diff --git a/Tests/SwiftGtkGenCoreTests/FileOutputTests.swift b/Tests/SwiftGtkGenCoreTests/FileOutputTests.swift new file mode 100644 index 0000000..221568e --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/FileOutputTests.swift @@ -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) +} diff --git a/Tests/SwiftGtkGenCoreTests/IRModelTests.swift b/Tests/SwiftGtkGenCoreTests/IRModelTests.swift new file mode 100644 index 0000000..e50a6c4 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/IRModelTests.swift @@ -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) +} diff --git a/Tests/SwiftGtkGenCoreTests/ParserTests.swift b/Tests/SwiftGtkGenCoreTests/ParserTests.swift new file mode 100644 index 0000000..fc9ca26 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/ParserTests.swift @@ -0,0 +1,205 @@ +import Testing +import Foundation +@testable import SwiftGtkGenCore + +@Test("Parser reads namespace from minimal GIR XML") +func testParsesNamespace() throws { + let xml = """ + + + + + """ + 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 = """ + + + + + + + + + + + + + + + + + + + + + """ + 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 = """ + + + + + + + + + + + + """ + 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 = """ + + + + + + + + + + + + """ + 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 = """ + + + + + + + + + + + + + + + + + + """ + 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 = """ + + + + + + + + + + + + + + + + """ + 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 = """ + + + + + + + """ + 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") + } +} diff --git a/Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift b/Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift new file mode 100644 index 0000000..0415f7a --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift @@ -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) +} diff --git a/Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift b/Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift new file mode 100644 index 0000000..be650c7 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift @@ -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 ") == 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 ") == 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 ") == true) +} diff --git a/Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift b/Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift new file mode 100644 index 0000000..6bb0929 --- /dev/null +++ b/Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift @@ -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")) +}