Initial commit: SwiftGtkGen GIR-to-Swift binding generator
Extracted from gtk-swift monorepo. Previous history contains 38 commits across the full Phase 1 development including GIR XML parsing, IR model, analysis engine, code generation, CLI, plugins, and documentation.
This commit is contained in:
commit
1330f2bd25
24 changed files with 4095 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.build/
|
||||||
79
.swift-format
Normal file
79
.swift-format
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"fileScopedDeclarationPrivacy" : {
|
||||||
|
"accessLevel" : "private"
|
||||||
|
},
|
||||||
|
"indentBlankLines" : false,
|
||||||
|
"indentConditionalCompilationBlocks" : true,
|
||||||
|
"indentSwitchCaseLabels" : false,
|
||||||
|
"indentation" : {
|
||||||
|
"spaces" : 4
|
||||||
|
},
|
||||||
|
"lineBreakAroundMultilineExpressionChainComponents" : false,
|
||||||
|
"lineBreakBeforeControlFlowKeywords" : false,
|
||||||
|
"lineBreakBeforeEachArgument" : false,
|
||||||
|
"lineBreakBeforeEachGenericRequirement" : false,
|
||||||
|
"lineBreakBetweenDeclarationAttributes" : false,
|
||||||
|
"lineLength" : 1000,
|
||||||
|
"maximumBlankLines" : 1,
|
||||||
|
"multiElementCollectionTrailingCommas" : true,
|
||||||
|
"noAssignmentInExpressions" : {
|
||||||
|
"allowedFunctions" : [
|
||||||
|
"XCTAssertNoThrow"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"orderedImports" : {
|
||||||
|
"includeConditionalImports" : false
|
||||||
|
},
|
||||||
|
"prioritizeKeepingFunctionOutputTogether" : false,
|
||||||
|
"reflowMultilineStringLiterals" : "never",
|
||||||
|
"respectsExistingLineBreaks" : true,
|
||||||
|
"rules" : {
|
||||||
|
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||||
|
"AlwaysUseLiteralForEmptyCollectionInit" : false,
|
||||||
|
"AlwaysUseLowerCamelCase" : true,
|
||||||
|
"AmbiguousTrailingClosureOverload" : true,
|
||||||
|
"AvoidRetroactiveConformances" : false,
|
||||||
|
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||||
|
"DoNotUseSemicolons" : true,
|
||||||
|
"DontRepeatTypeInStaticProperties" : true,
|
||||||
|
"FileScopedDeclarationPrivacy" : true,
|
||||||
|
"FullyIndirectEnum" : true,
|
||||||
|
"GroupNumericLiterals" : true,
|
||||||
|
"IdentifiersMustBeASCII" : true,
|
||||||
|
"NeverForceUnwrap" : false,
|
||||||
|
"NeverUseForceTry" : false,
|
||||||
|
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||||
|
"NoAccessLevelOnExtensionDeclaration" : true,
|
||||||
|
"NoAssignmentInExpressions" : true,
|
||||||
|
"NoBlockComments" : true,
|
||||||
|
"NoCasesWithOnlyFallthrough" : true,
|
||||||
|
"NoEmptyLinesOpeningClosingBraces" : false,
|
||||||
|
"NoEmptyTrailingClosureParentheses" : true,
|
||||||
|
"NoLabelsInCasePatterns" : true,
|
||||||
|
"NoLeadingUnderscores" : false,
|
||||||
|
"NoParensAroundConditions" : true,
|
||||||
|
"NoPlaygroundLiterals" : true,
|
||||||
|
"NoVoidReturnOnFunctionSignature" : true,
|
||||||
|
"OmitExplicitReturns" : false,
|
||||||
|
"OneCasePerLine" : true,
|
||||||
|
"OneVariableDeclarationPerLine" : true,
|
||||||
|
"OnlyOneTrailingClosureArgument" : true,
|
||||||
|
"OrderedImports" : true,
|
||||||
|
"ReplaceForEachWithForLoop" : true,
|
||||||
|
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||||
|
"TypeNamesShouldBeCapitalized" : true,
|
||||||
|
"UseEarlyExits" : false,
|
||||||
|
"UseExplicitNilCheckInConditions" : true,
|
||||||
|
"UseLetInEveryBoundCaseVariable" : true,
|
||||||
|
"UseShorthandTypeNames" : true,
|
||||||
|
"UseSingleLinePropertyGetter" : true,
|
||||||
|
"UseSynthesizedInitializer" : true,
|
||||||
|
"UseTripleSlashForDocumentationComments" : true,
|
||||||
|
"UseWhereClausesInForLoops" : false,
|
||||||
|
"ValidateDocumentationComments" : false
|
||||||
|
},
|
||||||
|
"spacesAroundRangeFormationOperators" : false,
|
||||||
|
"spacesBeforeEndOfLineComments" : 2,
|
||||||
|
"tabWidth" : 4,
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
52
Package.swift
Normal file
52
Package.swift
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// swift-tools-version: 6.1
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "gir-generator",
|
||||||
|
platforms: [.macOS(.v14)],
|
||||||
|
products: [
|
||||||
|
.executable(name: "swift-gtk-gen", targets: ["swift-gtk-gen"]),
|
||||||
|
.plugin(name: "SwiftGtkGenBuildPlugin", targets: ["SwiftGtkGenBuildPlugin"]),
|
||||||
|
.plugin(name: "SwiftGtkGenCommandPlugin", targets: ["SwiftGtkGenCommandPlugin"]),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "SwiftGtkGenCore",
|
||||||
|
dependencies: []
|
||||||
|
),
|
||||||
|
.executableTarget(
|
||||||
|
name: "swift-gtk-gen",
|
||||||
|
dependencies: ["SwiftGtkGenCore"]
|
||||||
|
),
|
||||||
|
.plugin(
|
||||||
|
name: "SwiftGtkGenBuildPlugin",
|
||||||
|
capability: .buildTool(),
|
||||||
|
dependencies: ["swift-gtk-gen"],
|
||||||
|
path: "Sources/SwiftGtkGenBuildPlugin"
|
||||||
|
),
|
||||||
|
.plugin(
|
||||||
|
name: "SwiftGtkGenCommandPlugin",
|
||||||
|
capability: .command(
|
||||||
|
intent: .custom(
|
||||||
|
verb: "generate-gir-bindings",
|
||||||
|
description: "Generate Swift bindings from .gir files."
|
||||||
|
),
|
||||||
|
permissions: [.writeToPackageDirectory(reason: "Write generated Swift files to source directory.")]
|
||||||
|
),
|
||||||
|
dependencies: ["swift-gtk-gen"],
|
||||||
|
path: "Sources/SwiftGtkGenCommandPlugin"
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "SwiftGtkGenCoreTests",
|
||||||
|
dependencies: ["SwiftGtkGenCore"]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "SwiftGtkGenCLITests",
|
||||||
|
dependencies: ["SwiftGtkGenCore"]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "IntegrationTests",
|
||||||
|
dependencies: ["SwiftGtkGenCore"]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
75
Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift
Normal file
75
Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import PackagePlugin
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// SwiftPM build tool plugin that automatically generates Swift GIR bindings.
|
||||||
|
///
|
||||||
|
/// Inspects each target for a `Config.swift` file and `.gir` files, then
|
||||||
|
/// invokes the `swift-gtk-gen` executable to produce Swift source before compilation.
|
||||||
|
/// Targets without a config or GIR files are silently skipped.
|
||||||
|
@main
|
||||||
|
struct SwiftGtkGenBuildPlugin: BuildToolPlugin {
|
||||||
|
/// Creates build commands for GIR code generation for the given target.
|
||||||
|
///
|
||||||
|
/// Returns a single `.buildCommand` that runs `swift-gtk-gen` with the target's
|
||||||
|
/// config and GIR files as inputs. If the target has no `Config.swift` or
|
||||||
|
/// no `.gir` files, returns an empty array.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - context: The plugin context providing access to tools and work directories.
|
||||||
|
/// - target: The target to generate bindings for.
|
||||||
|
/// - Returns: An array of build commands (empty if no GIR files present).
|
||||||
|
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
|
||||||
|
let targetDir = target.directoryURL
|
||||||
|
let configURL = targetDir.appending(path: "Config.swift")
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: configURL.path()) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let executable = try context.tool(named: "swift-gtk-gen")
|
||||||
|
|
||||||
|
let girFiles = findGIRFiles(in: targetDir)
|
||||||
|
guard !girFiles.isEmpty else { return [] }
|
||||||
|
|
||||||
|
let outputDir = context.pluginWorkDirectoryURL
|
||||||
|
|
||||||
|
return [
|
||||||
|
.buildCommand(
|
||||||
|
displayName: "Generating \(target.name) GIR bindings...",
|
||||||
|
executable: executable.url,
|
||||||
|
arguments: [
|
||||||
|
"--config", configURL.path(),
|
||||||
|
"--library", target.name,
|
||||||
|
"--version", "4.0",
|
||||||
|
"--output", outputDir.appending(path: target.name).path(),
|
||||||
|
],
|
||||||
|
inputFiles: [configURL] + girFiles,
|
||||||
|
outputFiles: [
|
||||||
|
outputDir.appending(path: "\(target.name)/Generated.swift")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively finds all `.gir` files in the given directory.
|
||||||
|
///
|
||||||
|
/// Uses a directory enumerator to walk the file tree and collect URLs
|
||||||
|
/// whose path extension is `"gir"`.
|
||||||
|
///
|
||||||
|
/// - Parameter directory: The root directory to search.
|
||||||
|
/// - Returns: An array of URLs pointing to discovered `.gir` files.
|
||||||
|
private func findGIRFiles(in directory: URL) -> [URL] {
|
||||||
|
guard let enumerator = FileManager.default.enumerator(
|
||||||
|
at: directory, includingPropertiesForKeys: [.isRegularFileKey]
|
||||||
|
) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
var girFiles: [URL] = []
|
||||||
|
for case let fileURL as URL in enumerator {
|
||||||
|
if fileURL.pathExtension == "gir" {
|
||||||
|
girFiles.append(fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return girFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift
Normal file
92
Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import PackagePlugin
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// SwiftPM command plugin for manual GIR binding regeneration.
|
||||||
|
///
|
||||||
|
/// Invoked via `swift package generate-gir`. Iterates over all package
|
||||||
|
/// targets, checks for a `Config.swift` and `.gir` files, and runs the
|
||||||
|
/// `swift-gtk-gen` executable to produce or update generated Swift sources in
|
||||||
|
/// each target's `Generated/` directory.
|
||||||
|
@main
|
||||||
|
struct SwiftGtkGenCommandPlugin: CommandPlugin {
|
||||||
|
/// Performs GIR code generation for all eligible targets in the package.
|
||||||
|
///
|
||||||
|
/// For each target that has a `Config.swift` and at least one `.gir` file,
|
||||||
|
/// launches `swift-gtk-gen` as a subprocess with arguments derived from the
|
||||||
|
/// target's directory and metadata. Generated files are written to
|
||||||
|
/// `<target>/Generated/`. Throws if any subprocess fails.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - context: The plugin context providing access to tools and package targets.
|
||||||
|
/// - arguments: Unused command-line arguments passed to the plugin.
|
||||||
|
func performCommand(context: PluginContext, arguments: [String]) async throws {
|
||||||
|
let executable = try context.tool(named: "swift-gtk-gen")
|
||||||
|
|
||||||
|
for target in context.package.targets {
|
||||||
|
let targetDir = target.directoryURL
|
||||||
|
let configURL = targetDir.appending(path: "Config.swift")
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: configURL.path()) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let girFiles = findGIRFiles(in: targetDir)
|
||||||
|
guard !girFiles.isEmpty else { continue }
|
||||||
|
|
||||||
|
let outputDir = targetDir.appending(path: "Generated")
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = executable.url
|
||||||
|
process.arguments = [
|
||||||
|
"--config", configURL.path(),
|
||||||
|
"--library", target.name,
|
||||||
|
"--version", "4.0",
|
||||||
|
"--output", outputDir.path(),
|
||||||
|
]
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
guard process.terminationStatus == 0 else {
|
||||||
|
throw SwiftGtkGenPluginError.generationFailed(target.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively finds all `.gir` files in the given directory.
|
||||||
|
///
|
||||||
|
/// Uses a directory enumerator to walk the file tree and collect URLs
|
||||||
|
/// whose path extension is `"gir"`.
|
||||||
|
///
|
||||||
|
/// - Parameter directory: The root directory to search.
|
||||||
|
/// - Returns: An array of URLs pointing to discovered `.gir` files.
|
||||||
|
private func findGIRFiles(in directory: URL) -> [URL] {
|
||||||
|
guard let enumerator = FileManager.default.enumerator(
|
||||||
|
at: directory, includingPropertiesForKeys: [.isRegularFileKey]
|
||||||
|
) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
var girFiles: [URL] = []
|
||||||
|
for case let fileURL as URL in enumerator {
|
||||||
|
if fileURL.pathExtension == "gir" {
|
||||||
|
girFiles.append(fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return girFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that can occur during command-plugin GIR generation.
|
||||||
|
enum SwiftGtkGenPluginError: Error {
|
||||||
|
/// Generation failed for the specified target name.
|
||||||
|
case generationFailed(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SwiftGtkGenPluginError: CustomStringConvertible {
|
||||||
|
/// A human-readable description of the error.
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .generationFailed(let target):
|
||||||
|
return "GIR generation failed for target: \(target)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Sources/SwiftGtkGenCore/Analysis.swift
Normal file
127
Sources/SwiftGtkGenCore/Analysis.swift
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/// The result of analyzing a GIR repository against a `GenerationConfig`.
|
||||||
|
///
|
||||||
|
/// Contains the resolved class hierarchy, the classification of each type
|
||||||
|
/// (generate, manual, or ignore), and any per-type or per-function overrides
|
||||||
|
/// from the configuration. This result drives the subsequent code-generation
|
||||||
|
/// phase.
|
||||||
|
public struct AnalysisResult {
|
||||||
|
/// Maps each type name to its parent type name, forming an inheritance tree.
|
||||||
|
///
|
||||||
|
/// Keys are fully-qualified type names (e.g. `"Gtk.Widget"`); values are the
|
||||||
|
/// fully-qualified parent name.
|
||||||
|
public var classHierarchy: [String: String]
|
||||||
|
|
||||||
|
/// The set of fully-qualified type names that should have Swift bindings generated.
|
||||||
|
public var generatedTypes: Set<String>
|
||||||
|
|
||||||
|
/// The set of fully-qualified type names that are implemented manually and
|
||||||
|
/// should be excluded from generation.
|
||||||
|
public var manualTypes: Set<String>
|
||||||
|
|
||||||
|
/// The set of fully-qualified type names that should be skipped entirely.
|
||||||
|
public var ignoredTypes: Set<String>
|
||||||
|
|
||||||
|
/// Per-type overrides keyed by fully-qualified type name.
|
||||||
|
public var classOverrides: [String: ObjectOverrides]
|
||||||
|
|
||||||
|
/// Per-function overrides keyed by `"ClassName.functionName"`.
|
||||||
|
public var functionOverrides: [String: FunctionOverrides]
|
||||||
|
|
||||||
|
/// Creates an analysis result from the given component values.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - classHierarchy: Inheritance map from each type to its parent.
|
||||||
|
/// - generatedTypes: Types that should be generated.
|
||||||
|
/// - manualTypes: Types that are implemented manually.
|
||||||
|
/// - ignoredTypes: Types that should be omitted.
|
||||||
|
/// - classOverrides: Per-type override configuration.
|
||||||
|
/// - functionOverrides: Per-function override configuration.
|
||||||
|
public init(classHierarchy: [String: String] = [:], generatedTypes: Set<String> = [],
|
||||||
|
manualTypes: Set<String> = [], ignoredTypes: Set<String> = [],
|
||||||
|
classOverrides: [String: ObjectOverrides] = [:],
|
||||||
|
functionOverrides: [String: FunctionOverrides] = [:]) {
|
||||||
|
self.classHierarchy = classHierarchy
|
||||||
|
self.generatedTypes = generatedTypes
|
||||||
|
self.manualTypes = manualTypes
|
||||||
|
self.ignoredTypes = ignoredTypes
|
||||||
|
self.classOverrides = classOverrides
|
||||||
|
self.functionOverrides = functionOverrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analyzes a GIR repository against a `GenerationConfig` to produce an `AnalysisResult`.
|
||||||
|
///
|
||||||
|
/// The analyzer walks the repository's type hierarchy and applies the
|
||||||
|
/// configuration's generate/manual/ignore lists and per-type overrides.
|
||||||
|
public struct Analyzer {
|
||||||
|
/// The generation configuration that guides the analysis.
|
||||||
|
public let config: GenerationConfig
|
||||||
|
|
||||||
|
/// Creates an analyzer with the given generation configuration.
|
||||||
|
///
|
||||||
|
/// - Parameter config: The configuration specifying which types to
|
||||||
|
/// generate, handle manually, or ignore, along with any overrides.
|
||||||
|
public init(config: GenerationConfig) {
|
||||||
|
self.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analyzes the provided GIR repository and returns the classification result.
|
||||||
|
///
|
||||||
|
/// The analysis builds a class inheritance hierarchy from the repository,
|
||||||
|
/// classifies each type according to the configuration's `generate`,
|
||||||
|
/// `manual`, and `ignore` lists, and extracts per-type and per-function
|
||||||
|
/// overrides.
|
||||||
|
///
|
||||||
|
/// - Parameter repository: The parsed GIR repository to analyze.
|
||||||
|
/// - Returns: An `AnalysisResult` containing the resolved hierarchy, type
|
||||||
|
/// classifications, and overrides.
|
||||||
|
public func analyze(repository: Repository) -> AnalysisResult {
|
||||||
|
var classHierarchy: [String: String] = [:]
|
||||||
|
var generatedTypes: Set<String> = []
|
||||||
|
var manualTypes: Set<String> = []
|
||||||
|
var ignoredTypes: Set<String> = []
|
||||||
|
var classOverrides: [String: ObjectOverrides] = [:]
|
||||||
|
var functionOverrides: [String: FunctionOverrides] = [:]
|
||||||
|
|
||||||
|
for ns in repository.namespaces {
|
||||||
|
for cls in ns.classes {
|
||||||
|
let fullName = "\(ns.name).\(cls.name)"
|
||||||
|
if let parent = cls.parent {
|
||||||
|
let parentFullName = parent.contains(".") ? parent : "\(ns.name).\(parent)"
|
||||||
|
classHierarchy[fullName] = parentFullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for fullName in config.generate {
|
||||||
|
generatedTypes.insert(fullName)
|
||||||
|
}
|
||||||
|
for fullName in config.manual {
|
||||||
|
manualTypes.insert(fullName)
|
||||||
|
}
|
||||||
|
for fullName in config.ignore {
|
||||||
|
ignoredTypes.insert(fullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for objConfig in config.objects {
|
||||||
|
switch objConfig {
|
||||||
|
case .object(let name, let overrides):
|
||||||
|
classOverrides[name] = overrides
|
||||||
|
case .function(let className, let functionName, let overrides):
|
||||||
|
let key = "\(className).\(functionName)"
|
||||||
|
functionOverrides[key] = overrides
|
||||||
|
case .functionPattern, .signal, .property:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnalysisResult(
|
||||||
|
classHierarchy: classHierarchy,
|
||||||
|
generatedTypes: generatedTypes,
|
||||||
|
manualTypes: manualTypes,
|
||||||
|
ignoredTypes: ignoredTypes,
|
||||||
|
classOverrides: classOverrides,
|
||||||
|
functionOverrides: functionOverrides
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
1012
Sources/SwiftGtkGenCore/CodeGen.swift
Normal file
1012
Sources/SwiftGtkGenCore/CodeGen.swift
Normal file
File diff suppressed because it is too large
Load diff
250
Sources/SwiftGtkGenCore/Config.swift
Normal file
250
Sources/SwiftGtkGenCore/Config.swift
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
/// Top-level configuration for generating Swift bindings from a GIR repository.
|
||||||
|
///
|
||||||
|
/// `GenerationConfig` is the Swift-native equivalent of a config file or TOML
|
||||||
|
/// approach used by other GIR-based generators. It specifies which `.gir` files
|
||||||
|
/// to read, where to emit output, which types to generate/manually implement/
|
||||||
|
/// ignore, and any per-type or per-function overrides.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```swift
|
||||||
|
/// GenerationConfig(
|
||||||
|
/// library: "Gtk",
|
||||||
|
/// version: "4.0",
|
||||||
|
/// girsDirectories: ["/usr/share/gir-1.0"],
|
||||||
|
/// targetDirectory: "Sources/Gtk",
|
||||||
|
/// externalLibraries: ["GLib", "GObject"],
|
||||||
|
/// generate: ["Gtk.Widget", "Gtk.Window"],
|
||||||
|
/// manual: ["Gtk.CustomWidget"],
|
||||||
|
/// ignore: ["Gtk.DeprecatedType"],
|
||||||
|
/// objects: []
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
public struct GenerationConfig {
|
||||||
|
/// The name of the library being wrapped (e.g. `"Gtk"`, `"GLib"`).
|
||||||
|
public var library: String
|
||||||
|
|
||||||
|
/// The version string of the GIR namespace (e.g. `"4.0"`).
|
||||||
|
public var version: String
|
||||||
|
|
||||||
|
/// Directories to search for `.gir` files.
|
||||||
|
public var girsDirectories: [String]
|
||||||
|
|
||||||
|
/// Directory where generated Swift source files should be written.
|
||||||
|
public var targetDirectory: String
|
||||||
|
|
||||||
|
/// Names of external libraries whose types may be referenced (e.g. `"GLib"`, `"GObject"`).
|
||||||
|
public var externalLibraries: [String]
|
||||||
|
|
||||||
|
/// Fully-qualified type names for which Swift bindings should be generated.
|
||||||
|
public var generate: [String]
|
||||||
|
|
||||||
|
/// Fully-qualified type names that are implemented manually and should not be generated.
|
||||||
|
public var manual: [String]
|
||||||
|
|
||||||
|
/// Fully-qualified type names that should be skipped entirely.
|
||||||
|
public var ignore: [String]
|
||||||
|
|
||||||
|
/// Per-type and per-function override entries.
|
||||||
|
public var objects: [ObjectConfig]
|
||||||
|
|
||||||
|
/// Creates a complete generation configuration.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - library: The library name (e.g. `"Gtk"`).
|
||||||
|
/// - version: The GIR namespace version (e.g. `"4.0"`).
|
||||||
|
/// - girsDirectories: Paths to search for `.gir` files.
|
||||||
|
/// - targetDirectory: Output directory for generated Swift sources.
|
||||||
|
/// - externalLibraries: Referenced external library names.
|
||||||
|
/// - generate: Types to generate bindings for.
|
||||||
|
/// - manual: Types handled by hand-written code.
|
||||||
|
/// - ignore: Types to skip.
|
||||||
|
/// - objects: Override entries for types, functions, signals, and properties.
|
||||||
|
public init(library: String, version: String, girsDirectories: [String],
|
||||||
|
targetDirectory: String, externalLibraries: [String],
|
||||||
|
generate: [String], manual: [String], ignore: [String],
|
||||||
|
objects: [ObjectConfig]) {
|
||||||
|
self.library = library
|
||||||
|
self.version = version
|
||||||
|
self.girsDirectories = girsDirectories
|
||||||
|
self.targetDirectory = targetDirectory
|
||||||
|
self.externalLibraries = externalLibraries
|
||||||
|
self.generate = generate
|
||||||
|
self.manual = manual
|
||||||
|
self.ignore = ignore
|
||||||
|
self.objects = objects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A configuration entry targeting a specific type, function, function pattern,
|
||||||
|
/// signal, or property in the GIR repository.
|
||||||
|
///
|
||||||
|
/// Each case carries the overrides or rename rules that should be applied
|
||||||
|
/// during code generation for the matched element.
|
||||||
|
public enum ObjectConfig {
|
||||||
|
/// Overrides for a specific GObject type identified by its fully-qualified name.
|
||||||
|
case object(_ name: String, overrides: ObjectOverrides)
|
||||||
|
|
||||||
|
/// Overrides for a specific function on a type.
|
||||||
|
case function(_ type: String, _ name: String, overrides: FunctionOverrides)
|
||||||
|
|
||||||
|
/// A regex-based rename rule applied to matching functions on a type.
|
||||||
|
case functionPattern(_ type: String, pattern: String, rename: RenameRule)
|
||||||
|
|
||||||
|
/// Overrides for a specific signal on a type.
|
||||||
|
case signal(_ type: String, _ name: String, overrides: SignalOverrides)
|
||||||
|
|
||||||
|
/// Overrides for a specific property on a type.
|
||||||
|
case property(_ type: String, _ name: String, overrides: PropertyOverrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides that control how a single GObject type is treated during code generation.
|
||||||
|
///
|
||||||
|
/// All properties are optional; only the values that are explicitly set will
|
||||||
|
/// override the default generation behavior for the matched type.
|
||||||
|
public struct ObjectOverrides {
|
||||||
|
/// Whether to generate, mark as manual, or ignore this type.
|
||||||
|
public var status: ObjectStatus?
|
||||||
|
|
||||||
|
/// If `true`, mark the generated class as `final`.
|
||||||
|
public var finalType: Bool?
|
||||||
|
|
||||||
|
/// The concurrency model to apply (e.g. `@MainActor`, `Sendable`).
|
||||||
|
public var concurrency: ConcurrencyModel?
|
||||||
|
|
||||||
|
/// Minimum version string; the type is only generated when targeting this version or later.
|
||||||
|
public var version: String?
|
||||||
|
|
||||||
|
/// An optional `#if` compilation condition to guard the generated code.
|
||||||
|
public var cfgCondition: String?
|
||||||
|
|
||||||
|
/// If `true`, generate a builder pattern struct for constructing this type.
|
||||||
|
public var generateBuilder: Bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls how a generated type should be treated.
|
||||||
|
public enum ObjectStatus: String {
|
||||||
|
/// Generate Swift bindings for this type.
|
||||||
|
case generate
|
||||||
|
/// This type will be implemented manually; do not generate.
|
||||||
|
case manual
|
||||||
|
/// Skip this type entirely.
|
||||||
|
case ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The concurrency model to apply to a generated type.
|
||||||
|
public enum ConcurrencyModel: String {
|
||||||
|
/// Annotate the generated type with `@MainActor`.
|
||||||
|
case mainActor
|
||||||
|
/// Mark the generated type as `Sendable`.
|
||||||
|
case sendable
|
||||||
|
/// No special concurrency annotation.
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides that control how a single function is treated during code generation.
|
||||||
|
///
|
||||||
|
/// All properties are optional; only the values that are explicitly set will
|
||||||
|
/// override the default generation behavior for the matched function.
|
||||||
|
public struct FunctionOverrides {
|
||||||
|
/// If `true`, skip generating this function entirely.
|
||||||
|
public var ignore: Bool?
|
||||||
|
|
||||||
|
/// A rename rule to apply to this function's Swift name.
|
||||||
|
public var rename: RenameRule?
|
||||||
|
|
||||||
|
/// Minimum version string; only generate when targeting this version or later.
|
||||||
|
public var version: String?
|
||||||
|
|
||||||
|
/// An optional `#if` compilation condition to guard the generated code.
|
||||||
|
public var cfgCondition: String?
|
||||||
|
|
||||||
|
/// If `true`, treat this function as a constructor (returns a new instance).
|
||||||
|
public var constructor: Bool?
|
||||||
|
|
||||||
|
/// Override the visibility of the generated method.
|
||||||
|
public var visibility: Visibility?
|
||||||
|
|
||||||
|
/// Per-parameter overrides keyed by the parameter's original name.
|
||||||
|
public var parameters: [String: ParameterOverride]?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The visibility level for a generated symbol.
|
||||||
|
public enum Visibility: String {
|
||||||
|
/// Visible outside the module.
|
||||||
|
case `public`
|
||||||
|
/// Visible only within the same module.
|
||||||
|
case `internal`
|
||||||
|
/// Visible to all modules in the same package.
|
||||||
|
case `package`
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides for a single function parameter in the generated API.
|
||||||
|
public struct ParameterOverride {
|
||||||
|
/// If set, overrides whether the parameter is treated as nullable.
|
||||||
|
public var nullable: Bool?
|
||||||
|
|
||||||
|
/// If set, renames this parameter in the generated Swift function signature.
|
||||||
|
public var newName: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides that control how a GObject signal handler is generated.
|
||||||
|
///
|
||||||
|
/// All properties are optional; only the explicitly set values override
|
||||||
|
/// the default behavior for the matched signal.
|
||||||
|
public struct SignalOverrides {
|
||||||
|
/// If `true`, skip generating a handler API for this signal.
|
||||||
|
public var ignore: Bool?
|
||||||
|
|
||||||
|
/// If `true`, the signal can be inhibited (stopped from propagating).
|
||||||
|
public var inhibit: Bool?
|
||||||
|
|
||||||
|
/// Per-parameter overrides for the signal handler's closure parameters.
|
||||||
|
public var parameters: [String: ParameterOverride]?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides that control how a GObject property is exposed in the generated API.
|
||||||
|
public struct PropertyOverrides {
|
||||||
|
/// The accessor methods to generate for this property.
|
||||||
|
///
|
||||||
|
/// If `nil`, the default set of accessors is generated based on the GIR
|
||||||
|
/// metadata. Provide an explicit array to override which accessors are emitted.
|
||||||
|
public var generate: [PropertyAccessor]?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of accessor to generate for a GObject property.
|
||||||
|
public enum PropertyAccessor: String {
|
||||||
|
/// Generate a getter method for the property.
|
||||||
|
case get
|
||||||
|
/// Generate a setter method for the property.
|
||||||
|
case set
|
||||||
|
/// Generate a notification callback/handler for property changes.
|
||||||
|
case notify
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A regex-based rename rule for transforming GIR symbol names into Swift names.
|
||||||
|
///
|
||||||
|
/// Matches the `regex` pattern against the original name and substitutes the
|
||||||
|
/// `replacement` string, following standard regex capture-group semantics
|
||||||
|
/// (e.g., `"$1"`, `"$2"`).
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```swift
|
||||||
|
/// RenameRule(regex: "^gtk_", replacement: "")
|
||||||
|
/// ```
|
||||||
|
public struct RenameRule {
|
||||||
|
/// The regular expression pattern to match against the original name.
|
||||||
|
public var regex: String
|
||||||
|
|
||||||
|
/// The replacement string, which may reference capture groups with `$1`, `$2`, etc.
|
||||||
|
public var replacement: String
|
||||||
|
|
||||||
|
/// Creates a rename rule with the given regex pattern and replacement.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - regex: A regular expression pattern.
|
||||||
|
/// - replacement: A replacement string (may include capture-group references).
|
||||||
|
public init(regex: String, replacement: String) {
|
||||||
|
self.regex = regex
|
||||||
|
self.replacement = replacement
|
||||||
|
}
|
||||||
|
}
|
||||||
643
Sources/SwiftGtkGenCore/IRModel.swift
Normal file
643
Sources/SwiftGtkGenCore/IRModel.swift
Normal file
|
|
@ -0,0 +1,643 @@
|
||||||
|
/// A complete GIR repository, containing one or more namespaces.
|
||||||
|
///
|
||||||
|
/// Corresponds to the root `<repository>` element in a GIR XML file. A single
|
||||||
|
/// `.gir` file produces one `Repository` holding all namespaces defined within it.
|
||||||
|
public struct Repository {
|
||||||
|
/// The namespaces contained in this repository.
|
||||||
|
public var namespaces: [Namespace]
|
||||||
|
/// The C header include path from `<c:include>` (e.g. `"gtk/gtk.h"`).
|
||||||
|
public var cHeaderPath: String
|
||||||
|
/// Link names for GIR dependencies derived from `<include>` elements.
|
||||||
|
/// Derived at parse time from the library name and version (e.g. `"Gdk-4.0"` becomes `"gdk-4"`).
|
||||||
|
public var includedLibraryLinks: [String]
|
||||||
|
/// Package name from `<package>` element (e.g. `"gtk4"`).
|
||||||
|
public var packageName: String
|
||||||
|
/// Creates a new repository.
|
||||||
|
/// - Parameter namespaces: The namespaces contained in this repository.
|
||||||
|
public init(namespaces: [Namespace] = [], cHeaderPath: String = "", includedLibraryLinks: [String] = [], packageName: String = "") {
|
||||||
|
self.namespaces = namespaces
|
||||||
|
self.cHeaderPath = cHeaderPath
|
||||||
|
self.includedLibraryLinks = includedLibraryLinks
|
||||||
|
self.packageName = packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GIR namespace, grouping related type definitions within a repository.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<namespace>` element in a GIR XML file. A namespace
|
||||||
|
/// holds all type definitions — classes, interfaces, records, enumerations,
|
||||||
|
/// bitfields, callbacks, global functions, constants, and type aliases — that
|
||||||
|
/// belong to a single GIR namespace such as `Gtk` or `GObject`.
|
||||||
|
public struct Namespace {
|
||||||
|
/// The namespace name, e.g. `"Gtk"`.
|
||||||
|
public let name: String
|
||||||
|
/// The namespace version string, e.g. `"4.0"`.
|
||||||
|
public let version: String
|
||||||
|
/// The shared library name from the GIR file (e.g. "libgtk-4.so.1").
|
||||||
|
public var cSharedLibrary: String
|
||||||
|
/// The C identifier prefix (e.g. "Gtk", "G").
|
||||||
|
public var cIdentifierPrefix: String
|
||||||
|
/// The GObject classes defined in this namespace.
|
||||||
|
public var classes: [Class]
|
||||||
|
/// The GObject interfaces defined in this namespace.
|
||||||
|
public var interfaces: [Interface]
|
||||||
|
/// The plain C records (structs) defined in this namespace.
|
||||||
|
public var records: [Record]
|
||||||
|
/// The enumerations defined in this namespace.
|
||||||
|
public var enumerations: [Enumeration]
|
||||||
|
/// The bitfield (flags) types defined in this namespace.
|
||||||
|
public var bitfields: [Bitfield]
|
||||||
|
/// The callback function types defined in this namespace.
|
||||||
|
public var callbacks: [Callback]
|
||||||
|
/// The global (namespace-level) functions defined in this namespace.
|
||||||
|
public var functions: [GlobalFunction]
|
||||||
|
/// The constants defined in this namespace.
|
||||||
|
public var constants: [Constant]
|
||||||
|
/// The type aliases defined in this namespace.
|
||||||
|
public var aliases: [Alias]
|
||||||
|
/// Creates a new namespace.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The namespace name, e.g. `"Gtk"`.
|
||||||
|
/// - version: The namespace version string, e.g. `"4.0"`.
|
||||||
|
/// - cSharedLibrary: The shared library name (e.g. "libgtk-4.so.1").
|
||||||
|
/// - cIdentifierPrefix: The C identifier prefix (e.g. "Gtk", "G").
|
||||||
|
/// - classes: The GObject classes in the namespace.
|
||||||
|
/// - interfaces: The GObject interfaces in the namespace.
|
||||||
|
/// - records: The plain C records in the namespace.
|
||||||
|
/// - enumerations: The enumerations in the namespace.
|
||||||
|
/// - bitfields: The bitfield types in the namespace.
|
||||||
|
/// - callbacks: The callback types in the namespace.
|
||||||
|
/// - functions: The global functions in the namespace.
|
||||||
|
/// - constants: The constants in the namespace.
|
||||||
|
/// - aliases: The type aliases in the namespace.
|
||||||
|
public init(name: String, version: String, cSharedLibrary: String = "", cIdentifierPrefix: String = "",
|
||||||
|
classes: [Class] = [], interfaces: [Interface] = [],
|
||||||
|
records: [Record] = [], enumerations: [Enumeration] = [], bitfields: [Bitfield] = [],
|
||||||
|
callbacks: [Callback] = [], functions: [GlobalFunction] = [], constants: [Constant] = [],
|
||||||
|
aliases: [Alias] = []) {
|
||||||
|
self.name = name; self.version = version
|
||||||
|
self.cSharedLibrary = cSharedLibrary; self.cIdentifierPrefix = cIdentifierPrefix
|
||||||
|
self.classes = classes; self.interfaces = interfaces; self.records = records
|
||||||
|
self.enumerations = enumerations; self.bitfields = bitfields; self.callbacks = callbacks
|
||||||
|
self.functions = functions; self.constants = constants; self.aliases = aliases
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject class definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<class>` element in a GIR XML file. Models a GObject
|
||||||
|
/// class with its parent class, implemented interfaces, constructors, methods,
|
||||||
|
/// properties, signals, and associated functions.
|
||||||
|
public struct Class {
|
||||||
|
/// The class name, e.g. `"Widget"`.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name, e.g. `"GtkWidget"`.
|
||||||
|
public let cType: String
|
||||||
|
/// The name of the parent class, or `nil` for the root `GObject` class.
|
||||||
|
public let parent: String?
|
||||||
|
/// Whether this class is abstract and cannot be instantiated directly.
|
||||||
|
public var isAbstract: Bool
|
||||||
|
/// The names of interfaces this class implements.
|
||||||
|
public var implements: [String]
|
||||||
|
/// The constructors for this class.
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
public var constructors: [Constructor]
|
||||||
|
/// The methods of this class.
|
||||||
|
public var methods: [Method]
|
||||||
|
/// The GObject properties of this class.
|
||||||
|
public var properties: [Property]
|
||||||
|
/// The signals emitted by this class.
|
||||||
|
public var signals: [Signal]
|
||||||
|
/// The functions associated with this class.
|
||||||
|
public var functions: [GlobalFunction]
|
||||||
|
/// Creates a new class definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The class name, e.g. `"Widget"`.
|
||||||
|
/// - cType: The corresponding C type name, e.g. `"GtkWidget"`.
|
||||||
|
/// - parent: The name of the parent class, or `nil` if root.
|
||||||
|
/// - isAbstract: Whether the class is abstract. Defaults to `false`.
|
||||||
|
/// - implements: The names of implemented interfaces. Defaults to empty.
|
||||||
|
/// - constructors: The constructors. Defaults to empty.
|
||||||
|
/// - methods: The methods. Defaults to empty.
|
||||||
|
/// - properties: The properties. Defaults to empty.
|
||||||
|
/// - signals: The signals. Defaults to empty.
|
||||||
|
/// - functions: The associated functions. Defaults to empty.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, parent: String?, isAbstract: Bool = false,
|
||||||
|
implements: [String] = [], constructors: [Constructor] = [], methods: [Method] = [],
|
||||||
|
properties: [Property] = [], signals: [Signal] = [], functions: [GlobalFunction] = [],
|
||||||
|
doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.parent = parent
|
||||||
|
self.isAbstract = isAbstract; self.implements = implements
|
||||||
|
self.constructors = constructors; self.methods = methods
|
||||||
|
self.properties = properties; self.signals = signals; self.functions = functions
|
||||||
|
self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject interface definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<interface>` element in a GIR XML file. An interface
|
||||||
|
/// declares methods, properties, and signals that implementing classes must
|
||||||
|
/// provide, along with prerequisite types that must be satisfied first.
|
||||||
|
public struct Interface {
|
||||||
|
/// The interface name, e.g. `"Buildable"`.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name, e.g. `"GtkBuildable"`.
|
||||||
|
public let cType: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The methods declared by this interface.
|
||||||
|
public var methods: [Method]
|
||||||
|
/// The properties declared by this interface.
|
||||||
|
public var properties: [Property]
|
||||||
|
/// The signals declared by this interface.
|
||||||
|
public var signals: [Signal]
|
||||||
|
/// The prerequisite types a class must satisfy to implement this interface.
|
||||||
|
public var prereqs: [String]
|
||||||
|
/// Creates a new interface definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The interface name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - methods: The methods declared by the interface. Defaults to empty.
|
||||||
|
/// - properties: The properties declared by the interface. Defaults to empty.
|
||||||
|
/// - signals: The signals declared by the interface. Defaults to empty.
|
||||||
|
/// - prereqs: The prerequisite types. Defaults to empty.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, methods: [Method] = [], properties: [Property] = [],
|
||||||
|
signals: [Signal] = [], prereqs: [String] = [], doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.methods = methods
|
||||||
|
self.properties = properties; self.signals = signals; self.prereqs = prereqs
|
||||||
|
self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain C record (struct) definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<record>` element in a GIR XML file. Records are
|
||||||
|
/// value types in C and may be opaque (no fields exposed), disguised
|
||||||
|
/// (typedef'd without `struct` keyword), or have fully accessible fields.
|
||||||
|
public struct Record {
|
||||||
|
/// The record name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name.
|
||||||
|
public let cType: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Whether the record is opaque (fields are not introspectable).
|
||||||
|
public var isOpaque: Bool
|
||||||
|
/// Whether the record is disguised (typedef'd without the `struct` keyword).
|
||||||
|
public var isDisguised: Bool
|
||||||
|
/// The fields of the record, if introspectable.
|
||||||
|
public var fields: [Field]
|
||||||
|
/// The methods operating on this record.
|
||||||
|
public var methods: [Method]
|
||||||
|
/// Creates a new record definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The record name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - isOpaque: Whether the record is opaque. Defaults to `false`.
|
||||||
|
/// - isDisguised: Whether the record is disguised. Defaults to `false`.
|
||||||
|
/// - fields: The fields of the record. Defaults to empty.
|
||||||
|
/// - methods: The record methods. Defaults to empty.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, isOpaque: Bool = false, isDisguised: Bool = false,
|
||||||
|
fields: [Field] = [], methods: [Method] = [], doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.isOpaque = isOpaque
|
||||||
|
self.isDisguised = isDisguised; self.fields = fields; self.methods = methods; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A field within a C record.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<field>` element in a GIR XML file. Describes a named
|
||||||
|
/// member of a C struct, including its type and read/write permissions.
|
||||||
|
public struct Field {
|
||||||
|
/// The field name.
|
||||||
|
public let name: String
|
||||||
|
/// The GIR type of the field.
|
||||||
|
public let type: GIRType
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Whether the field is readable (accessible for reading).
|
||||||
|
public let isReadable: Bool
|
||||||
|
/// Whether the field is writable (accessible for writing).
|
||||||
|
public let isWritable: Bool
|
||||||
|
/// Creates a new field.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The field name.
|
||||||
|
/// - type: The GIR type of the field.
|
||||||
|
/// - isReadable: Whether the field is readable. Defaults to `true`.
|
||||||
|
/// - isWritable: Whether the field is writable. Defaults to `false`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, type: GIRType, isReadable: Bool = true, isWritable: Bool = false, doc: String? = nil) {
|
||||||
|
self.name = name; self.type = type; self.isReadable = isReadable; self.isWritable = isWritable; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject enumeration type.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<enumeration>` element in a GIR XML file. Defines a
|
||||||
|
/// set of named integer constants with their C identifiers and numeric values.
|
||||||
|
public struct Enumeration {
|
||||||
|
/// The enumeration name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name.
|
||||||
|
public let cType: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The members (enum values) of this enumeration.
|
||||||
|
public var members: [EnumMember]
|
||||||
|
/// Creates a new enumeration.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The enumeration name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - members: The enum members. Defaults to empty.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, members: [EnumMember] = [], doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.members = members; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single member (value) of an enumeration or bitfield.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<member>` element in a GIR XML file. Each member
|
||||||
|
/// has a name, its associated numeric value, and the full C identifier.
|
||||||
|
public struct EnumMember {
|
||||||
|
/// The member name, e.g. `"visible"`.
|
||||||
|
public let name: String
|
||||||
|
/// The numeric value as a string, e.g. `"1"`.
|
||||||
|
public let value: String
|
||||||
|
/// The full C identifier, e.g. `"GTK_WIDGET_VISIBLE"`.
|
||||||
|
public let cIdentifier: String
|
||||||
|
/// Creates a new enum member.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The member name.
|
||||||
|
/// - value: The numeric value as a string.
|
||||||
|
/// - cIdentifier: The full C identifier.
|
||||||
|
public init(name: String, value: String, cIdentifier: String) {
|
||||||
|
self.name = name; self.value = value; self.cIdentifier = cIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject bitfield (flags) type.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<bitfield>` element in a GIR XML file. Defines a set
|
||||||
|
/// of named flags that can be combined with bitwise operations. Each member
|
||||||
|
/// represents a single bit in the flags value.
|
||||||
|
public struct Bitfield {
|
||||||
|
/// The bitfield type name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name.
|
||||||
|
public let cType: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The individual flag members.
|
||||||
|
public var members: [EnumMember]
|
||||||
|
/// Creates a new bitfield type.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The bitfield type name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - members: The flag members. Defaults to empty.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, members: [EnumMember] = [], doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.members = members; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A callback function type.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<callback>` element in a GIR XML file. Describes the
|
||||||
|
/// function signature — parameters and return type — for a C callback used
|
||||||
|
/// in signal handlers, virtual functions, or asynchronous operations.
|
||||||
|
public struct Callback {
|
||||||
|
/// The callback type name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name.
|
||||||
|
public let cType: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The parameters of the callback function.
|
||||||
|
public var parameters: [Parameter]
|
||||||
|
/// The return type of the callback function.
|
||||||
|
public var returnType: GIRType
|
||||||
|
/// Creates a new callback type.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The callback type name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - parameters: The callback parameters. Defaults to empty.
|
||||||
|
/// - returnType: The return type. Defaults to `.void`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.parameters = parameters; self.returnType = returnType; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A constructor for a GObject class.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<constructor>` element in a GIR XML file. Constructors
|
||||||
|
/// are special methods that create new instances of a GObject type, typically
|
||||||
|
/// wrapping C functions like `gtk_widget_new()`.
|
||||||
|
public struct Constructor {
|
||||||
|
/// The constructor name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C function identifier.
|
||||||
|
public let cIdentifier: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The parameters accepted by the constructor.
|
||||||
|
public var parameters: [Parameter]
|
||||||
|
/// The return type — typically the constructed object type.
|
||||||
|
public var returnType: GIRType
|
||||||
|
/// Creates a new constructor definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The constructor name.
|
||||||
|
/// - cIdentifier: The corresponding C function identifier.
|
||||||
|
/// - parameters: The constructor parameters. Defaults to empty.
|
||||||
|
/// - returnType: The return type. Defaults to `.void`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
|
||||||
|
self.name = name; self.cIdentifier = cIdentifier
|
||||||
|
self.parameters = parameters; self.returnType = returnType; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A method of a GObject class, interface, or record.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<method>` element in a GIR XML file. Methods are
|
||||||
|
/// instance functions that operate on a particular type, identified by their
|
||||||
|
/// C function name.
|
||||||
|
public struct Method {
|
||||||
|
/// The method name, e.g. `"set_visible"`.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C function identifier, e.g. `"gtk_widget_set_visible"`.
|
||||||
|
public let cIdentifier: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The parameters of the method, typically excluding the instance parameter.
|
||||||
|
public var parameters: [Parameter]
|
||||||
|
/// The return type of the method.
|
||||||
|
public var returnType: GIRType
|
||||||
|
/// Creates a new method definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The method name.
|
||||||
|
/// - cIdentifier: The corresponding C function identifier.
|
||||||
|
/// - parameters: The method parameters. Defaults to empty.
|
||||||
|
/// - returnType: The return type. Defaults to `.void`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
|
||||||
|
self.name = name; self.cIdentifier = cIdentifier
|
||||||
|
self.parameters = parameters; self.returnType = returnType; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject property definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<property>` element in a GIR XML file. Properties are
|
||||||
|
/// named, typed attributes on GObject classes with configurable read/write
|
||||||
|
/// access and construct-time-only semantics.
|
||||||
|
public struct Property {
|
||||||
|
/// The property name, e.g. `"label"`.
|
||||||
|
public let name: String
|
||||||
|
/// The GIR type of the property.
|
||||||
|
public var type: GIRType
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Whether the property is readable (has a getter).
|
||||||
|
public var isReadable: Bool
|
||||||
|
/// Whether the property is writable (has a setter).
|
||||||
|
public var isWritable: Bool
|
||||||
|
/// Whether the property can only be set during object construction.
|
||||||
|
public var isConstructOnly: Bool
|
||||||
|
/// Creates a new property definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The property name.
|
||||||
|
/// - type: The GIR type of the property.
|
||||||
|
/// - isReadable: Whether the property is readable. Defaults to `true`.
|
||||||
|
/// - isWritable: Whether the property is writable. Defaults to `false`.
|
||||||
|
/// - isConstructOnly: Whether the property is construct-only. Defaults to `false`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, type: GIRType, isReadable: Bool = true, isWritable: Bool = false, isConstructOnly: Bool = false, doc: String? = nil) {
|
||||||
|
self.name = name; self.type = type
|
||||||
|
self.isReadable = isReadable; self.isWritable = isWritable; self.isConstructOnly = isConstructOnly; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GObject signal definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<signal>` element in a GIR XML file. Signals are
|
||||||
|
/// typed event emitters on GObject classes. Each signal has a parameter list,
|
||||||
|
/// a return value, and may support detailed (string-parameterized) connections.
|
||||||
|
public struct Signal {
|
||||||
|
/// The signal name, e.g. `"clicked"`.
|
||||||
|
public let name: String
|
||||||
|
/// The parameters emitted with the signal.
|
||||||
|
public var parameters: [Parameter]
|
||||||
|
/// The return type of the signal handler.
|
||||||
|
public var returnType: GIRType
|
||||||
|
/// Whether the signal supports detail strings (e.g. `"notify::label"`).
|
||||||
|
public var isDetailed: Bool
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Creates a new signal definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The signal name.
|
||||||
|
/// - parameters: The signal parameters. Defaults to empty.
|
||||||
|
/// - returnType: The handler return type. Defaults to `.void`.
|
||||||
|
/// - isDetailed: Whether the signal supports detail strings. Defaults to `false`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, parameters: [Parameter] = [], returnType: GIRType = .void, isDetailed: Bool = false, doc: String? = nil) {
|
||||||
|
self.name = name; self.parameters = parameters; self.returnType = returnType; self.isDetailed = isDetailed; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A global (namespace-level) function.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<function>` element at the namespace level in a GIR
|
||||||
|
/// XML file. These are free functions not associated with any particular
|
||||||
|
/// type, such as utility or factory functions.
|
||||||
|
public struct GlobalFunction {
|
||||||
|
/// The function name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C function identifier.
|
||||||
|
public let cIdentifier: String
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// The parameters of the function.
|
||||||
|
public var parameters: [Parameter]
|
||||||
|
/// The return type of the function.
|
||||||
|
public var returnType: GIRType
|
||||||
|
/// Creates a new global function definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The function name.
|
||||||
|
/// - cIdentifier: The corresponding C function identifier.
|
||||||
|
/// - parameters: The function parameters. Defaults to empty.
|
||||||
|
/// - returnType: The return type. Defaults to `.void`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cIdentifier: String, parameters: [Parameter] = [], returnType: GIRType = .void, doc: String? = nil) {
|
||||||
|
self.name = name; self.cIdentifier = cIdentifier
|
||||||
|
self.parameters = parameters; self.returnType = returnType; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A constant value definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<constant>` element in a GIR XML file. Constants are
|
||||||
|
/// named immutable values with a specific GIR type, such as enum defaults or
|
||||||
|
/// version numbers.
|
||||||
|
public struct Constant {
|
||||||
|
/// The constant name.
|
||||||
|
public let name: String
|
||||||
|
/// The constant value as a string representation.
|
||||||
|
public let value: String
|
||||||
|
/// The GIR type of the constant.
|
||||||
|
public var type: GIRType
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Creates a new constant definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The constant name.
|
||||||
|
/// - value: The constant value as a string representation.
|
||||||
|
/// - type: The GIR type of the constant.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, value: String, type: GIRType, doc: String? = nil) {
|
||||||
|
self.name = name; self.value = value; self.type = type; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type alias definition.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<alias>` element in a GIR XML file. Provides an
|
||||||
|
/// alternative name (with an optional C type) for an existing GIR type,
|
||||||
|
/// useful for platform-specific or convenience typedefs.
|
||||||
|
public struct Alias {
|
||||||
|
/// The alias name.
|
||||||
|
public let name: String
|
||||||
|
/// The corresponding C type name.
|
||||||
|
public let cType: String
|
||||||
|
/// The underlying GIR type this alias refers to.
|
||||||
|
public var target: GIRType
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Creates a new type alias.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The alias name.
|
||||||
|
/// - cType: The corresponding C type name.
|
||||||
|
/// - target: The underlying GIR type to alias.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, cType: String, target: GIRType, doc: String? = nil) {
|
||||||
|
self.name = name; self.cType = cType; self.target = target; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shared Types
|
||||||
|
|
||||||
|
/// A parameter of a function, method, constructor, callback, or signal.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<parameter>` element in a GIR XML file. Describes the
|
||||||
|
/// parameter's name, type, nullability, optionality, ownership transfer rules,
|
||||||
|
/// and whether it is the implicit instance parameter (equivalent to `self`).
|
||||||
|
public struct Parameter {
|
||||||
|
/// The parameter name.
|
||||||
|
public let name: String
|
||||||
|
/// The GIR type of the parameter.
|
||||||
|
public var type: GIRType
|
||||||
|
/// Documentation comment from the GIR XML `<doc>` element.
|
||||||
|
public var doc: String?
|
||||||
|
/// Whether the parameter may be `nil` (NULL).
|
||||||
|
public var isNullable: Bool
|
||||||
|
/// Whether the parameter is optional (may be omitted at the call site).
|
||||||
|
public var isOptional: Bool
|
||||||
|
/// How ownership is transferred for this parameter.
|
||||||
|
public var transferOwnership: TransferOwnership
|
||||||
|
/// Whether this is the implicit instance parameter (self) of a method.
|
||||||
|
public var isInstanceParameter: Bool
|
||||||
|
/// Creates a new parameter definition.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The parameter name.
|
||||||
|
/// - type: The GIR type of the parameter.
|
||||||
|
/// - isNullable: Whether the parameter may be nil. Defaults to `false`.
|
||||||
|
/// - isOptional: Whether the parameter is optional. Defaults to `false`.
|
||||||
|
/// - transferOwnership: How ownership is transferred. Defaults to `.none`.
|
||||||
|
/// - isInstanceParameter: Whether this is the instance parameter. Defaults to `false`.
|
||||||
|
/// - doc: Documentation comment from the GIR XML.
|
||||||
|
public init(name: String, type: GIRType, isNullable: Bool = false, isOptional: Bool = false,
|
||||||
|
transferOwnership: TransferOwnership = .none, isInstanceParameter: Bool = false, doc: String? = nil) {
|
||||||
|
self.name = name; self.type = type; self.isNullable = isNullable
|
||||||
|
self.isOptional = isOptional; self.transferOwnership = transferOwnership
|
||||||
|
self.isInstanceParameter = isInstanceParameter; self.doc = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how ownership of a value is transferred between caller and callee.
|
||||||
|
///
|
||||||
|
/// Maps to the `transfer-ownership` attribute in GIR XML. Controls memory
|
||||||
|
/// management semantics: whether the caller must free the returned value
|
||||||
|
/// (`.full`), whether only the container is owned (`.container`), or whether
|
||||||
|
/// no ownership transfer occurs (`.none`).
|
||||||
|
public enum TransferOwnership: String {
|
||||||
|
/// No transfer; the caller does not own the value and must not free it.
|
||||||
|
case none
|
||||||
|
/// Full transfer; the caller owns the value and is responsible for freeing it.
|
||||||
|
case full
|
||||||
|
/// Container transfer; the caller owns the container but not its elements.
|
||||||
|
case container
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GIR type reference, covering primitives, named type references, arrays, and optionals.
|
||||||
|
///
|
||||||
|
/// Corresponds to the `<type>` element in GIR XML. This recursive enum models
|
||||||
|
/// the full GIR type system: scalar primitives, named type references pointing
|
||||||
|
/// to other GIR types, arrays (both GArray and C-style fixed arrays), and
|
||||||
|
/// nullable/optional wrappers.
|
||||||
|
public indirect enum GIRType: Equatable {
|
||||||
|
/// No return value (void).
|
||||||
|
case void
|
||||||
|
/// A boolean value, mapped from `gboolean`.
|
||||||
|
case boolean
|
||||||
|
/// A signed 8-bit integer, mapped from `gint8`.
|
||||||
|
case int8
|
||||||
|
/// A signed 16-bit integer, mapped from `gint16`.
|
||||||
|
case int16
|
||||||
|
/// A signed 32-bit integer, mapped from `gint32`.
|
||||||
|
case int32
|
||||||
|
/// A signed 64-bit integer, mapped from `gint64`.
|
||||||
|
case int64
|
||||||
|
/// An unsigned 8-bit integer, mapped from `guint8`.
|
||||||
|
case uint8
|
||||||
|
/// An unsigned 16-bit integer, mapped from `guint16`.
|
||||||
|
case uint16
|
||||||
|
/// An unsigned 32-bit integer, mapped from `guint32`.
|
||||||
|
case uint32
|
||||||
|
/// An unsigned 64-bit integer, mapped from `guint64`.
|
||||||
|
case uint64
|
||||||
|
/// A single-precision floating-point value, mapped from `gfloat`.
|
||||||
|
case float
|
||||||
|
/// A double-precision floating-point value, mapped from `gdouble`.
|
||||||
|
case double
|
||||||
|
/// A null-terminated UTF-8 string, mapped from `utf8`.
|
||||||
|
case string
|
||||||
|
/// A filename string (platform-dependent encoding), mapped from `filename`.
|
||||||
|
case filename
|
||||||
|
/// An opaque pointer, mapped from `gpointer`.
|
||||||
|
case pointer
|
||||||
|
/// A reference to a named type, possibly from another namespace.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - String: The type name, e.g. `"Widget"`.
|
||||||
|
/// - namespace: The namespace qualifier, or `nil` for the current namespace.
|
||||||
|
case typeRef(String, namespace: String?)
|
||||||
|
/// A dynamically-sized GArray of the given element type.
|
||||||
|
case array(GIRType)
|
||||||
|
/// A C-style fixed-size array of the given element type.
|
||||||
|
case cArray(GIRType)
|
||||||
|
/// An optional (nullable) value of the given type.
|
||||||
|
case `optional`(GIRType)
|
||||||
|
|
||||||
|
/// Creates a type reference in the current namespace.
|
||||||
|
/// - Parameter name: The unqualified type name.
|
||||||
|
/// - Returns: A `typeRef` with no namespace qualifier.
|
||||||
|
public static func typeRef(_ name: String) -> GIRType {
|
||||||
|
.typeRef(name, namespace: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
489
Sources/SwiftGtkGenCore/XMLParser.swift
Normal file
489
Sources/SwiftGtkGenCore/XMLParser.swift
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationXML)
|
||||||
|
import FoundationXML
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Errors that can occur during GIR XML parsing.
|
||||||
|
public enum GIRParserError: Error {
|
||||||
|
/// The XML document is malformed or could not be parsed.
|
||||||
|
/// - Parameter description: A human-readable error description.
|
||||||
|
case invalidXML(String)
|
||||||
|
/// An unexpected XML element was encountered.
|
||||||
|
/// - Parameter description: Details about the unexpected element.
|
||||||
|
case unexpectedElement(String)
|
||||||
|
/// A required attribute is missing from an XML element.
|
||||||
|
/// - Parameter description: Description of the missing attribute.
|
||||||
|
case missingAttribute(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses GIR XML files into the intermediate representation model.
|
||||||
|
///
|
||||||
|
/// Uses Foundation's `XMLParser` in a SAX-style delegate pattern. The parser
|
||||||
|
/// handles all GIR element types including classes, interfaces, records,
|
||||||
|
/// enumerations, bitfields, callbacks, methods, properties, signals, functions,
|
||||||
|
/// constants, and aliases.
|
||||||
|
public struct GIRParser {
|
||||||
|
/// Creates a new GIR parser.
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
/// Parses a GIR XML document from a string.
|
||||||
|
/// - Parameter xmlString: The XML content to parse.
|
||||||
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
||||||
|
/// - Throws: `GIRParserError` if the XML cannot be encoded as UTF-8 or parsing fails.
|
||||||
|
public func parse(xmlString: String) throws -> Repository {
|
||||||
|
guard let data = xmlString.data(using: .utf8) else {
|
||||||
|
throw GIRParserError.invalidXML("Failed to encode XML string as UTF-8")
|
||||||
|
}
|
||||||
|
return try parse(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a GIR XML document from a file URL.
|
||||||
|
/// - Parameter fileURL: The URL of the .gir file to parse.
|
||||||
|
/// - Returns: A `Repository` containing all parsed namespaces and types.
|
||||||
|
/// - Throws: `GIRParserError` if the file cannot be read or parsing fails.
|
||||||
|
public func parse(fileURL: URL) throws -> Repository {
|
||||||
|
let data = try Data(contentsOf: fileURL)
|
||||||
|
return try parse(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses raw XML data using a SAX-style delegate.
|
||||||
|
/// - Parameter data: The XML data to parse.
|
||||||
|
/// - Returns: A `Repository` with the parsed content.
|
||||||
|
/// - Throws: `GIRParserError` if parsing fails.
|
||||||
|
private func parse(data: Data) throws -> Repository {
|
||||||
|
let delegate = GIRXMLDelegate()
|
||||||
|
let parser = XMLParser(data: data)
|
||||||
|
parser.delegate = delegate
|
||||||
|
if parser.parse(), delegate.parseError == nil {
|
||||||
|
return delegate.repository
|
||||||
|
}
|
||||||
|
throw delegate.parseError ?? GIRParserError.invalidXML(parser.parserError?.localizedDescription ?? "unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - XMLParser Delegate
|
||||||
|
|
||||||
|
/// SAX-style delegate for `XMLParser` that builds a `Repository` from GIR XML.
|
||||||
|
///
|
||||||
|
/// Tracks a stack of currently-open GIR elements via `current*` properties.
|
||||||
|
/// On `didStartElement` it creates the corresponding model object and populates
|
||||||
|
/// attributes. On `didEndElement` it appends the completed object to its parent.
|
||||||
|
/// The final `repository` property contains the fully parsed GIR document.
|
||||||
|
final class GIRXMLDelegate: NSObject, XMLParserDelegate {
|
||||||
|
/// The repository being populated during parsing.
|
||||||
|
var repository = Repository()
|
||||||
|
/// Set to the first error encountered, or `nil` if parsing succeeds.
|
||||||
|
var parseError: GIRParserError?
|
||||||
|
|
||||||
|
var currentNamespace: Namespace?
|
||||||
|
var currentClass: Class?
|
||||||
|
var currentInterface: Interface?
|
||||||
|
var currentRecord: Record?
|
||||||
|
var currentEnum: Enumeration?
|
||||||
|
var currentBitfield: Bitfield?
|
||||||
|
var currentMethod: Method?
|
||||||
|
var currentConstructor: Constructor?
|
||||||
|
var currentSignal: Signal?
|
||||||
|
var currentProperty: Property?
|
||||||
|
var currentFunction: GlobalFunction?
|
||||||
|
var currentParameter: Parameter?
|
||||||
|
var currentCallback: Callback?
|
||||||
|
var currentConstant: Constant?
|
||||||
|
var currentAlias: Alias?
|
||||||
|
var currentReturnType: GIRType?
|
||||||
|
var currentText: String = ""
|
||||||
|
/// Counter for nested untracked elements that contain child elements.
|
||||||
|
/// When > 0, `<doc>` elements belong to untracked parents and should be discarded.
|
||||||
|
var untrackedDepth: Int = 0
|
||||||
|
|
||||||
|
/// Called when the XML parser encounters an opening element tag.
|
||||||
|
///
|
||||||
|
/// Creates the corresponding model object for the element and populates it
|
||||||
|
/// from XML attributes. Maintains a stack of `current*` properties so that
|
||||||
|
/// nested elements can attach themselves to their parent on close.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - parser: The XML parser.
|
||||||
|
/// - elementName: The name of the XML element.
|
||||||
|
/// - namespaceURI: The namespace URI of the element.
|
||||||
|
/// - qualifiedName: The qualified name of the element.
|
||||||
|
/// - attributeDict: The element's attributes.
|
||||||
|
func parser(_ parser: XMLParser, didStartElement elementName: String,
|
||||||
|
namespaceURI: String?, qualifiedName: String?,
|
||||||
|
attributes attributeDict: [String: String] = [:]) {
|
||||||
|
switch elementName {
|
||||||
|
case "c:include":
|
||||||
|
guard let name = attributeDict["name"] else { return }
|
||||||
|
repository.cHeaderPath = name
|
||||||
|
|
||||||
|
case "package":
|
||||||
|
guard let name = attributeDict["name"] else { return }
|
||||||
|
repository.packageName = name
|
||||||
|
|
||||||
|
case "include":
|
||||||
|
guard let name = attributeDict["name"], let version = attributeDict["version"] else { return }
|
||||||
|
// GIR dependency includes appear before the <namespace> element
|
||||||
|
// Derive link name: lowercase(library) + "-" + major version (e.g. "Gdk-4.0" → "gdk-4")
|
||||||
|
let majorVersion = version.split(separator: ".").first.map(String.init) ?? version
|
||||||
|
let linkName = "\(name.lowercased())-\(majorVersion)"
|
||||||
|
repository.includedLibraryLinks.append(linkName)
|
||||||
|
|
||||||
|
case "namespace":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let version = requireAttribute("version", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let sharedLibrary = attributeDict["shared-library"] ?? ""
|
||||||
|
let cIdentifierPrefix = attributeDict["c:identifier-prefixes"] ?? ""
|
||||||
|
currentNamespace = Namespace(name: name, version: version, cSharedLibrary: sharedLibrary, cIdentifierPrefix: cIdentifierPrefix)
|
||||||
|
|
||||||
|
case "class":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
let parent = attributeDict["parent"]
|
||||||
|
let isAbstract = attributeDict["abstract"] == "1"
|
||||||
|
currentClass = Class(name: name, cType: cType, parent: parent, isAbstract: isAbstract)
|
||||||
|
|
||||||
|
case "interface":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
let prereqs = attributeDict["prerequisite"]?.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } ?? []
|
||||||
|
currentInterface = Interface(name: name, cType: cType, prereqs: prereqs)
|
||||||
|
|
||||||
|
case "record":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
currentRecord = Record(name: name, cType: cType, isOpaque: attributeDict["opaque"] == "1",
|
||||||
|
isDisguised: attributeDict["disguised"] == "1")
|
||||||
|
|
||||||
|
case "enumeration":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
currentEnum = Enumeration(name: name, cType: cType)
|
||||||
|
|
||||||
|
case "bitfield":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
currentBitfield = Bitfield(name: name, cType: cType)
|
||||||
|
|
||||||
|
case "member":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let member = EnumMember(name: name, value: value, cIdentifier: cid)
|
||||||
|
currentEnum?.members.append(member)
|
||||||
|
currentBitfield?.members.append(member)
|
||||||
|
|
||||||
|
case "callback":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let cType = attributeDict["c:type"] ?? ""
|
||||||
|
currentCallback = Callback(name: name, cType: cType)
|
||||||
|
|
||||||
|
case "constructor":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentConstructor = Constructor(name: name, cIdentifier: cid)
|
||||||
|
|
||||||
|
case "method":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentMethod = Method(name: name, cIdentifier: cid)
|
||||||
|
|
||||||
|
case "function":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let cid = requireAttribute("c:identifier", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentFunction = GlobalFunction(name: name, cIdentifier: cid)
|
||||||
|
|
||||||
|
case "signal":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
|
||||||
|
|
||||||
|
case "glib:signal":
|
||||||
|
guard let name = attributeDict["name"] ?? attributeDict["glib:name"] else { return }
|
||||||
|
currentSignal = Signal(name: name, isDetailed: attributeDict["detailed"] == "1")
|
||||||
|
|
||||||
|
case "property":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentProperty = Property(name: name, type: .void,
|
||||||
|
isReadable: attributeDict["readable"] != "0",
|
||||||
|
isWritable: attributeDict["writable"] == "1",
|
||||||
|
isConstructOnly: attributeDict["construct-only"] == "1")
|
||||||
|
|
||||||
|
case "parameter", "instance-parameter":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
let transfer = TransferOwnership(rawValue: attributeDict["transfer-ownership"] ?? "none") ?? .none
|
||||||
|
currentParameter = Parameter(name: name, type: .void, isNullable: attributeDict["nullable"] == "1",
|
||||||
|
isOptional: attributeDict["optional"] == "1",
|
||||||
|
transferOwnership: transfer,
|
||||||
|
isInstanceParameter: elementName == "instance-parameter")
|
||||||
|
|
||||||
|
case "return-value":
|
||||||
|
currentReturnType = .void
|
||||||
|
|
||||||
|
case "type":
|
||||||
|
let typeName = attributeDict["name"] ?? "none"
|
||||||
|
let resolvedType = parseGIRType(typeName)
|
||||||
|
if currentParameter != nil {
|
||||||
|
currentParameter?.type = resolvedType
|
||||||
|
} else if currentReturnType != nil {
|
||||||
|
currentReturnType = resolvedType
|
||||||
|
} else if currentProperty != nil {
|
||||||
|
currentProperty?.type = resolvedType
|
||||||
|
} else if currentConstant != nil {
|
||||||
|
currentConstant?.type = resolvedType
|
||||||
|
} else if currentAlias != nil {
|
||||||
|
currentAlias?.target = resolvedType
|
||||||
|
}
|
||||||
|
|
||||||
|
case "array":
|
||||||
|
if let param = currentParameter {
|
||||||
|
currentParameter?.type = .array(param.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "constant":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let value = requireAttribute("value", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentConstant = Constant(name: name, value: value, type: .void)
|
||||||
|
|
||||||
|
case "alias":
|
||||||
|
guard let name = requireAttribute("name", from: attributeDict, for: elementName, parser: parser),
|
||||||
|
let cType = requireAttribute("c:type", from: attributeDict, for: elementName, parser: parser) else { return }
|
||||||
|
currentAlias = Alias(name: name, cType: cType, target: .void)
|
||||||
|
|
||||||
|
case "implements":
|
||||||
|
if let name = attributeDict["name"] {
|
||||||
|
currentClass?.implements.append(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "doc-version", "doc-deprecated", "doc-stability", "source-position":
|
||||||
|
break
|
||||||
|
|
||||||
|
case "doc":
|
||||||
|
currentText = ""
|
||||||
|
break
|
||||||
|
|
||||||
|
case "virtual-method", "field", "parameters":
|
||||||
|
untrackedDepth += 1
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the XML parser encounters a closing element tag.
|
||||||
|
///
|
||||||
|
/// Finalizes the current model object, resolves the return type if one was
|
||||||
|
/// collected, appends the object to its parent container, and clears the
|
||||||
|
/// corresponding `current*` property.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - parser: The XML parser.
|
||||||
|
/// - elementName: The name of the XML element.
|
||||||
|
/// - namespaceURI: The namespace URI of the element.
|
||||||
|
/// - qualifiedName: The qualified name of the element.
|
||||||
|
func parser(_ parser: XMLParser, didEndElement elementName: String,
|
||||||
|
namespaceURI: String?, qualifiedName: String?) {
|
||||||
|
switch elementName {
|
||||||
|
case "namespace":
|
||||||
|
if let ns = currentNamespace { repository.namespaces.append(ns) }
|
||||||
|
currentNamespace = nil
|
||||||
|
|
||||||
|
case "class":
|
||||||
|
if let cls = currentClass { currentNamespace?.classes.append(cls) }
|
||||||
|
currentClass = nil
|
||||||
|
|
||||||
|
case "interface":
|
||||||
|
if let iface = currentInterface { currentNamespace?.interfaces.append(iface) }
|
||||||
|
currentInterface = nil
|
||||||
|
|
||||||
|
case "record":
|
||||||
|
if let record = currentRecord { currentNamespace?.records.append(record) }
|
||||||
|
currentRecord = nil
|
||||||
|
|
||||||
|
case "enumeration":
|
||||||
|
if let enm = currentEnum { currentNamespace?.enumerations.append(enm) }
|
||||||
|
currentEnum = nil
|
||||||
|
|
||||||
|
case "bitfield":
|
||||||
|
if let bf = currentBitfield { currentNamespace?.bitfields.append(bf) }
|
||||||
|
currentBitfield = nil
|
||||||
|
|
||||||
|
case "callback":
|
||||||
|
if let cb = currentCallback {
|
||||||
|
// Only add to namespace when at the top level (not inside a class/interface/record)
|
||||||
|
if currentClass == nil && currentInterface == nil && currentRecord == nil {
|
||||||
|
currentNamespace?.callbacks.append(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentCallback = nil
|
||||||
|
|
||||||
|
case "constructor":
|
||||||
|
if var ctor = currentConstructor {
|
||||||
|
if let rt = currentReturnType, rt != .void {
|
||||||
|
ctor.returnType = rt
|
||||||
|
}
|
||||||
|
currentClass?.constructors.append(ctor)
|
||||||
|
}
|
||||||
|
currentConstructor = nil
|
||||||
|
currentReturnType = nil
|
||||||
|
|
||||||
|
case "method":
|
||||||
|
if var method = currentMethod {
|
||||||
|
if let rt = currentReturnType, rt != .void {
|
||||||
|
method.returnType = rt
|
||||||
|
}
|
||||||
|
currentClass?.methods.append(method)
|
||||||
|
currentInterface?.methods.append(method)
|
||||||
|
currentRecord?.methods.append(method)
|
||||||
|
}
|
||||||
|
currentMethod = nil
|
||||||
|
currentReturnType = nil
|
||||||
|
|
||||||
|
case "function":
|
||||||
|
if var fn = currentFunction {
|
||||||
|
if let rt = currentReturnType, rt != .void {
|
||||||
|
fn.returnType = rt
|
||||||
|
}
|
||||||
|
// Class/interface/record-level functions are not globals
|
||||||
|
if currentClass != nil {
|
||||||
|
currentClass?.functions.append(fn)
|
||||||
|
} else if currentRecord != nil {
|
||||||
|
currentRecord?.methods.append(Method(name: fn.name, cIdentifier: fn.cIdentifier,
|
||||||
|
parameters: fn.parameters, returnType: fn.returnType))
|
||||||
|
} else if currentInterface != nil {
|
||||||
|
// Interface functions handled via interface methods
|
||||||
|
} else {
|
||||||
|
currentNamespace?.functions.append(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentFunction = nil
|
||||||
|
currentReturnType = nil
|
||||||
|
|
||||||
|
case "signal", "glib:signal":
|
||||||
|
if var sig = currentSignal {
|
||||||
|
if let rt = currentReturnType, rt != .void {
|
||||||
|
sig.returnType = rt
|
||||||
|
}
|
||||||
|
currentClass?.signals.append(sig)
|
||||||
|
currentInterface?.signals.append(sig)
|
||||||
|
}
|
||||||
|
currentSignal = nil
|
||||||
|
currentReturnType = nil
|
||||||
|
|
||||||
|
case "property":
|
||||||
|
if let prop = currentProperty {
|
||||||
|
currentClass?.properties.append(prop)
|
||||||
|
currentInterface?.properties.append(prop)
|
||||||
|
}
|
||||||
|
currentProperty = nil
|
||||||
|
|
||||||
|
case "parameter", "instance-parameter":
|
||||||
|
if let param = currentParameter {
|
||||||
|
currentMethod?.parameters.append(param)
|
||||||
|
currentConstructor?.parameters.append(param)
|
||||||
|
currentSignal?.parameters.append(param)
|
||||||
|
currentFunction?.parameters.append(param)
|
||||||
|
currentCallback?.parameters.append(param)
|
||||||
|
}
|
||||||
|
currentParameter = nil
|
||||||
|
|
||||||
|
case "return-value":
|
||||||
|
break
|
||||||
|
|
||||||
|
case "constant":
|
||||||
|
if let c = currentConstant { currentNamespace?.constants.append(c) }
|
||||||
|
currentConstant = nil
|
||||||
|
|
||||||
|
case "alias":
|
||||||
|
if let a = currentAlias { currentNamespace?.aliases.append(a) }
|
||||||
|
currentAlias = nil
|
||||||
|
|
||||||
|
case "virtual-method", "field", "parameters":
|
||||||
|
untrackedDepth -= 1
|
||||||
|
|
||||||
|
case "doc":
|
||||||
|
let text = currentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !text.isEmpty else { return }
|
||||||
|
// Skip doc inside untracked elements (virtual-method, field, etc.)
|
||||||
|
guard untrackedDepth == 0 else { return }
|
||||||
|
// Check innermost (most nested) elements first to handle hierarchy
|
||||||
|
// e.g. a signal inside a class → check currentSignal before currentClass
|
||||||
|
if currentParameter != nil { currentParameter?.doc = text }
|
||||||
|
else if currentMethod != nil { currentMethod?.doc = text }
|
||||||
|
else if currentConstructor != nil { currentConstructor?.doc = text }
|
||||||
|
else if currentSignal != nil { currentSignal?.doc = text }
|
||||||
|
else if currentProperty != nil { currentProperty?.doc = text }
|
||||||
|
else if currentCallback != nil { currentCallback?.doc = text }
|
||||||
|
else if currentFunction != nil { currentFunction?.doc = text }
|
||||||
|
else if currentInterface != nil { currentInterface?.doc = text }
|
||||||
|
else if currentEnum != nil { currentEnum?.doc = text }
|
||||||
|
else if currentBitfield != nil { currentBitfield?.doc = text }
|
||||||
|
else if currentRecord != nil { currentRecord?.doc = text }
|
||||||
|
else if currentClass != nil { currentClass?.doc = text }
|
||||||
|
else if currentConstant != nil { currentConstant?.doc = text }
|
||||||
|
else if currentAlias != nil { currentAlias?.doc = text }
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the XML parser encounters character data between elements.
|
||||||
|
///
|
||||||
|
/// Accumulates text content for the current element, used primarily for
|
||||||
|
/// capturing `<doc>` element text content.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - parser: The XML parser.
|
||||||
|
/// - string: The character data found.
|
||||||
|
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
||||||
|
currentText += string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
/// Requires that an attribute exists in the given dictionary, or aborts parsing.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The attribute name to look up.
|
||||||
|
/// - dict: The attribute dictionary from the current XML element.
|
||||||
|
/// - element: The name of the XML element (for error messages).
|
||||||
|
/// - parser: The XML parser to abort on failure.
|
||||||
|
/// - Returns: The attribute value, or `nil` if the attribute is missing.
|
||||||
|
private func requireAttribute(_ key: String, from dict: [String: String], for element: String, parser: XMLParser) -> String? {
|
||||||
|
guard let value = dict[key] else {
|
||||||
|
parseError = .missingAttribute("Missing '\(key)' on <\(element)>")
|
||||||
|
parser.abortParsing()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps a GIR type name string to the corresponding `GIRType` enum case.
|
||||||
|
///
|
||||||
|
/// Recognizes primitive GLib types (`gboolean`, `gint32`, `utf8`, etc.) and
|
||||||
|
/// dotted namespace-qualified type references (e.g. `Gtk.Widget`). Unknown
|
||||||
|
/// names are returned as an unqualified `.typeRef`.
|
||||||
|
/// - Parameter name: The GIR type name (e.g. `"gint32"`, `"utf8"`, `"Gtk.Widget"`).
|
||||||
|
/// - Returns: The corresponding `GIRType` value.
|
||||||
|
private func parseGIRType(_ name: String) -> GIRType {
|
||||||
|
switch name {
|
||||||
|
case "none": return .void
|
||||||
|
case "gboolean": return .boolean
|
||||||
|
case "gint8": return .int8
|
||||||
|
case "gint16": return .int16
|
||||||
|
case "gint", "gint32": return .int32
|
||||||
|
case "gint64": return .int64
|
||||||
|
case "guint8": return .uint8
|
||||||
|
case "guint16": return .uint16
|
||||||
|
case "guint", "guint32": return .uint32
|
||||||
|
case "guint64": return .uint64
|
||||||
|
case "gfloat": return .float
|
||||||
|
case "gdouble": return .double
|
||||||
|
case "utf8": return .string
|
||||||
|
case "filename": return .filename
|
||||||
|
case "gpointer", "gconstpointer": return .pointer
|
||||||
|
default:
|
||||||
|
if let dotIndex = name.firstIndex(of: ".") {
|
||||||
|
let ns = String(name[..<dotIndex])
|
||||||
|
let type = String(name[name.index(after: dotIndex)...])
|
||||||
|
return .typeRef(type, namespace: ns)
|
||||||
|
}
|
||||||
|
return .typeRef(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Sources/swift-gtk-gen/Main.swift
Normal file
154
Sources/swift-gtk-gen/Main.swift
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftGtkGenCore
|
||||||
|
|
||||||
|
/// Command-line interface for the GIR-to-Swift binding generator.
|
||||||
|
///
|
||||||
|
/// Parses command-line arguments, invokes the GIR parser, analyzer, and code
|
||||||
|
/// generator, then writes the resulting Swift source to a file. Designed as
|
||||||
|
/// the subprocess entry point invoked by both the build tool plugin and the
|
||||||
|
/// command plugin.
|
||||||
|
@main
|
||||||
|
struct SwiftGtkGenCLI {
|
||||||
|
/// Program entry point. Parses arguments, generates code, and writes output.
|
||||||
|
static func main() {
|
||||||
|
let args = parseArguments()
|
||||||
|
if args.showHelp {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let parser = GIRParser()
|
||||||
|
let primaryGIR = try parser.parse(fileURL: URL(fileURLWithPath: args.girFile))
|
||||||
|
|
||||||
|
let generateList = args.generateAll ? [] : args.generate
|
||||||
|
let config = GenerationConfig(
|
||||||
|
library: args.library,
|
||||||
|
version: args.version,
|
||||||
|
girsDirectories: args.girsDirs,
|
||||||
|
targetDirectory: args.output,
|
||||||
|
externalLibraries: args.externalLibs,
|
||||||
|
generate: generateList,
|
||||||
|
manual: args.manual,
|
||||||
|
ignore: args.ignore,
|
||||||
|
objects: []
|
||||||
|
)
|
||||||
|
|
||||||
|
let analyzer = Analyzer(config: config)
|
||||||
|
let analysis = analyzer.analyze(repository: primaryGIR)
|
||||||
|
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
|
||||||
|
let outputDir = URL(fileURLWithPath: args.output)
|
||||||
|
|
||||||
|
if args.emitSingleFile {
|
||||||
|
let output = try generator.generate(repository: primaryGIR, analysis: analysis)
|
||||||
|
try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
|
||||||
|
let outputFile = outputDir.appendingPathComponent("\(config.library).swift")
|
||||||
|
try output.write(to: outputFile, atomically: true, encoding: .utf8)
|
||||||
|
print("Generated \(outputFile.path)")
|
||||||
|
} else {
|
||||||
|
let files = try generator.generateFiles(repository: primaryGIR, analysis: analysis)
|
||||||
|
let sourcesDir = outputDir.appendingPathComponent("Sources/\(config.library)")
|
||||||
|
try FileManager.default.createDirectory(at: sourcesDir, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
for (fileName, content) in files.sorted(by: { $0.key < $1.key }) {
|
||||||
|
let fileURL = sourcesDir.appendingPathComponent(fileName)
|
||||||
|
try content.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
print("Generated \(fileURL.path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: primaryGIR)
|
||||||
|
for (relativePath, content) in scaffolding.sorted(by: { $0.key < $1.key }) {
|
||||||
|
let fileURL = outputDir.appendingPathComponent(relativePath)
|
||||||
|
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
|
try content.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
print("Generated \(fileURL.path)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error: \(error)")
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed command-line arguments for the GIR generator CLI.
|
||||||
|
struct CLIArgs {
|
||||||
|
var showHelp: Bool = false
|
||||||
|
var emitSingleFile: Bool = false
|
||||||
|
var girFile: String = ""
|
||||||
|
var library: String = ""
|
||||||
|
var version: String = ""
|
||||||
|
var output: String = "."
|
||||||
|
var girsDirs: [String] = []
|
||||||
|
var externalLibs: [String] = []
|
||||||
|
var generate: [String] = []
|
||||||
|
var generateAll: Bool = false
|
||||||
|
var manual: [String] = []
|
||||||
|
var ignore: [String] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses command-line arguments into a ``CLIArgs`` value.
|
||||||
|
///
|
||||||
|
/// Supports the flags `--help`, `--gir-file`, `--library`, `--version`,
|
||||||
|
/// `--output`, `--girs-dirs`, `--external-libs`, `--generate`, `--manual`,
|
||||||
|
/// and `--ignore`. Multi-value flags (`--girs-dirs`, etc.) accept
|
||||||
|
/// comma-separated lists.
|
||||||
|
///
|
||||||
|
/// - Returns: A populated ``CLIArgs`` extracted from `CommandLine.arguments`.
|
||||||
|
static func parseArguments() -> CLIArgs {
|
||||||
|
var args = Array(CommandLine.arguments.dropFirst())
|
||||||
|
var cli = CLIArgs()
|
||||||
|
|
||||||
|
while let flag = args.first {
|
||||||
|
args.removeFirst()
|
||||||
|
switch flag {
|
||||||
|
case "--emit-single-file":
|
||||||
|
cli.emitSingleFile = true
|
||||||
|
case "--help", "-h":
|
||||||
|
cli.showHelp = true
|
||||||
|
case "--gir-file":
|
||||||
|
cli.girFile = args.removeFirst()
|
||||||
|
case "--library":
|
||||||
|
cli.library = args.removeFirst()
|
||||||
|
case "--version":
|
||||||
|
cli.version = args.removeFirst()
|
||||||
|
case "--output":
|
||||||
|
cli.output = args.removeFirst()
|
||||||
|
case "--girs-dirs":
|
||||||
|
cli.girsDirs = args.removeFirst().components(separatedBy: ",")
|
||||||
|
case "--external-libs":
|
||||||
|
cli.externalLibs = args.removeFirst().components(separatedBy: ",")
|
||||||
|
case "--generate-all":
|
||||||
|
cli.generateAll = true
|
||||||
|
case "--generate":
|
||||||
|
cli.generate = args.removeFirst().components(separatedBy: ",")
|
||||||
|
case "--manual":
|
||||||
|
cli.manual = args.removeFirst().components(separatedBy: ",")
|
||||||
|
case "--ignore":
|
||||||
|
cli.ignore = args.removeFirst().components(separatedBy: ",")
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints usage information and the list of accepted flags to stdout.
|
||||||
|
static func printHelp() {
|
||||||
|
print("Usage: swift-gtk-gen [options]")
|
||||||
|
print(" --gir-file PATH Path to the .gir file")
|
||||||
|
print(" --library NAME GIR library namespace (e.g., GLib)")
|
||||||
|
print(" --version VER GIR version (e.g., 2.0)")
|
||||||
|
print(" --output DIR Output directory (default: .)")
|
||||||
|
print(" --girs-dirs DIRS Comma-separated GIR directories")
|
||||||
|
print(" --external-libs LIBS Comma-separated external libraries")
|
||||||
|
print(" --generate TYPES Comma-separated types to generate")
|
||||||
|
print(" --generate-all Generate all types in the namespace")
|
||||||
|
print(" --manual TYPES Comma-separated types to mark manual")
|
||||||
|
print(" --ignore TYPES Comma-separated types to ignore")
|
||||||
|
print(" --emit-single-file Emit a single file instead of per-type files")
|
||||||
|
print(" --help, -h Show this help")
|
||||||
|
}
|
||||||
|
}
|
||||||
182
Tests/IntegrationTests/GtkGenerationTests.swift
Normal file
182
Tests/IntegrationTests/GtkGenerationTests.swift
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Full pipeline: generate complete Gtk 4.0 wrapper package")
|
||||||
|
func testFullGtkGeneration() throws {
|
||||||
|
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/Gtk-4.0.gir")
|
||||||
|
guard FileManager.default.fileExists(atPath: girURL.path) else {
|
||||||
|
throw TestSkipError.fileNotFound("Gtk-4.0.gir not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(fileURL: girURL)
|
||||||
|
guard let gtk = repo.namespaces.first(where: { $0.name == "Gtk" }) else {
|
||||||
|
throw TestSkipError.noNamespace("Gtk")
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = GenerationConfig(
|
||||||
|
library: "Gtk", version: "4.0",
|
||||||
|
girsDirectories: ["/usr/share/gir-1.0"],
|
||||||
|
targetDirectory: "",
|
||||||
|
externalLibraries: ["Gdk-4.0", "Gsk-4.0"],
|
||||||
|
generate: ["Gtk.Widget", "Gtk.Window", "Gtk.Button", "Gtk.Label",
|
||||||
|
"Gtk.Box", "Gtk.Align", "Gtk.Application"],
|
||||||
|
manual: [],
|
||||||
|
ignore: [],
|
||||||
|
objects: []
|
||||||
|
)
|
||||||
|
let analysis = Analyzer(config: config).analyze(repository: repo)
|
||||||
|
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: analysis)
|
||||||
|
|
||||||
|
// Verify per-file generation
|
||||||
|
#expect(files.keys.contains("Widget.swift"))
|
||||||
|
#expect(files.keys.contains("Window.swift"))
|
||||||
|
#expect(files.keys.contains("Button.swift"))
|
||||||
|
#expect(files.keys.contains("Label.swift"))
|
||||||
|
#expect(files.keys.contains("Box.swift"))
|
||||||
|
#expect(files.keys.contains("Align.swift"))
|
||||||
|
#expect(files.keys.contains("Application.swift"))
|
||||||
|
|
||||||
|
// Verify no fatalError() stubs remain in any file
|
||||||
|
for (name, content) in files {
|
||||||
|
#expect(!content.contains("fatalError"), "File \(name) still contains fatalError stub")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify real C function calls
|
||||||
|
#expect(files["Widget.swift"]?.contains("gtk_widget_show(pointer)") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("gtk_widget_get_visible(pointer)") == true)
|
||||||
|
|
||||||
|
// Verify real property accessors via GValue
|
||||||
|
#expect(files["Widget.swift"]?.contains("g_value_init") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("g_object_get_property") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("g_object_set_property") == true)
|
||||||
|
|
||||||
|
// Verify real signal connections
|
||||||
|
#expect(files["Widget.swift"]?.contains("g_signal_connect_data") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("Unmanaged.passRetained") == true)
|
||||||
|
|
||||||
|
// Verify constructors
|
||||||
|
#expect(files["Button.swift"]?.contains("convenience init") == true)
|
||||||
|
#expect(files["Button.swift"]?.contains("gtk_button_new") == true)
|
||||||
|
#expect(files["Button.swift"]?.contains("g_object_ref_sink") == true)
|
||||||
|
|
||||||
|
// Verify scaffolding
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
#expect(scaffolding.keys.contains("Package.swift"))
|
||||||
|
#expect(scaffolding.keys.contains("Sources/CGtk/module.modulemap"))
|
||||||
|
#expect(scaffolding.keys.contains("Sources/CGtk/CGtk.h"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Full pipeline: parse GLib-2.0.gir and verify analysis")
|
||||||
|
func testFullGLibParsing() throws {
|
||||||
|
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/GLib-2.0.gir")
|
||||||
|
guard FileManager.default.fileExists(atPath: girURL.path) else {
|
||||||
|
throw TestSkipError.fileNotFound("GLib-2.0.gir not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(fileURL: girURL)
|
||||||
|
#expect(!repo.namespaces.isEmpty)
|
||||||
|
guard let glib = repo.namespaces.first(where: { $0.name == "GLib" }) else {
|
||||||
|
throw TestSkipError.noNamespace("GLib")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify parsing of records, functions, enumerations
|
||||||
|
#expect(!glib.records.isEmpty, "Expected records in GLib namespace")
|
||||||
|
#expect(!glib.functions.isEmpty, "Expected functions in GLib namespace")
|
||||||
|
#expect(!glib.enumerations.isEmpty, "Expected enumerations in GLib namespace")
|
||||||
|
|
||||||
|
// Check specific well-known records exist
|
||||||
|
let recordNames = glib.records.map { $0.name }
|
||||||
|
#expect(recordNames.contains("String"))
|
||||||
|
#expect(recordNames.contains("List"))
|
||||||
|
#expect(recordNames.contains("MainLoop"))
|
||||||
|
|
||||||
|
// Verify analysis works for GLib types
|
||||||
|
let config = GenerationConfig(
|
||||||
|
library: "GLib", version: "2.0",
|
||||||
|
girsDirectories: [girURL.deletingLastPathComponent().path],
|
||||||
|
targetDirectory: "",
|
||||||
|
externalLibraries: [],
|
||||||
|
generate: ["GLib.String", "GLib.List", "GLib.MainLoop"],
|
||||||
|
manual: [],
|
||||||
|
ignore: [],
|
||||||
|
objects: []
|
||||||
|
)
|
||||||
|
|
||||||
|
let analyzer = Analyzer(config: config)
|
||||||
|
let analysis = analyzer.analyze(repository: repo)
|
||||||
|
|
||||||
|
#expect(analysis.generatedTypes.contains("GLib.String"))
|
||||||
|
#expect(analysis.generatedTypes.contains("GLib.List"))
|
||||||
|
#expect(analysis.generatedTypes.contains("GLib.MainLoop"))
|
||||||
|
|
||||||
|
// Note: GLib types are records (opaque C structs), not GObject classes.
|
||||||
|
// Code generation for records is not yet implemented.
|
||||||
|
// This test verifies parsing and analysis correctness only.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Full pipeline: generate ALL Gtk types when generate list is empty")
|
||||||
|
func testGenerateAllGtkTypes() throws {
|
||||||
|
let girURL = URL(fileURLWithPath: "/usr/share/gir-1.0/Gtk-4.0.gir")
|
||||||
|
guard FileManager.default.fileExists(atPath: girURL.path) else {
|
||||||
|
throw TestSkipError.fileNotFound("Gtk-4.0.gir not found")
|
||||||
|
}
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(fileURL: girURL)
|
||||||
|
guard let gtk = repo.namespaces.first(where: { $0.name == "Gtk" }) else {
|
||||||
|
throw TestSkipError.noNamespace("Gtk")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty generate list = generate ALL
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: [], manual: [], ignore: [], objects: [])
|
||||||
|
let analysis = Analyzer(config: config).analyze(repository: repo)
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: analysis)
|
||||||
|
|
||||||
|
// Get expected counts from parsed GIR data
|
||||||
|
let expectedClasses = gtk.classes.count
|
||||||
|
let expectedInterfaces = gtk.interfaces.count
|
||||||
|
let expectedEnums = gtk.enumerations.count
|
||||||
|
let expectedBitfields = gtk.bitfields.count
|
||||||
|
|
||||||
|
// Count generated files by type
|
||||||
|
let generatedClasses = files.values.filter { $0.contains("public final class") || $0.contains("public open class") }.count
|
||||||
|
let generatedInterfaces = files.values.filter { $0.contains("public protocol") }.count
|
||||||
|
let generatedEnums = files.values.filter { $0.contains("public enum") && !$0.contains("OptionSet") }.count
|
||||||
|
let generatedBitfields = files.values.filter { $0.contains("OptionSet") }.count
|
||||||
|
let generatedCallbacks = files.values.filter { $0.contains("public typealias") }.count
|
||||||
|
let totalGenerated = files.count
|
||||||
|
|
||||||
|
// Verify counts match GIR exactly
|
||||||
|
#expect(generatedClasses == expectedClasses,
|
||||||
|
"Expected \(expectedClasses) classes, got \(generatedClasses)")
|
||||||
|
#expect(generatedInterfaces == expectedInterfaces,
|
||||||
|
"Expected \(expectedInterfaces) interfaces, got \(generatedInterfaces)")
|
||||||
|
#expect(generatedEnums == expectedEnums,
|
||||||
|
"Expected \(expectedEnums) enums, got \(generatedEnums)")
|
||||||
|
#expect(generatedBitfields == expectedBitfields,
|
||||||
|
"Expected \(expectedBitfields) bitfields, got \(generatedBitfields)")
|
||||||
|
#expect(generatedCallbacks > 0,
|
||||||
|
"Expected at least 1 callback typealias, got \(generatedCallbacks)")
|
||||||
|
#expect(totalGenerated > 200,
|
||||||
|
"Expected 200+ total files, got \(totalGenerated)")
|
||||||
|
|
||||||
|
// Verify specific well-known files exist
|
||||||
|
#expect(files.keys.contains("Widget.swift"), "Missing Widget.swift")
|
||||||
|
#expect(files.keys.contains("Window.swift"), "Missing Window.swift")
|
||||||
|
#expect(files.keys.contains("Button.swift"), "Missing Button.swift")
|
||||||
|
#expect(files.keys.contains("Buildable.swift"), "Missing Buildable.swift (interface)")
|
||||||
|
// Verify interface is generated as a protocol
|
||||||
|
#expect(files["Buildable.swift"]?.contains("public protocol") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestSkipError: Error {
|
||||||
|
case fileNotFound(String)
|
||||||
|
case noNamespace(String)
|
||||||
|
}
|
||||||
41
Tests/SwiftGtkGenCLITests/CLITests.swift
Normal file
41
Tests/SwiftGtkGenCLITests/CLITests.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@Test("CLI produces help output")
|
||||||
|
func testCLIHelp() throws {
|
||||||
|
let process = Process()
|
||||||
|
let binPath = findBinary() ?? failBinaryNotFound()
|
||||||
|
process.executableURL = binPath
|
||||||
|
process.arguments = ["--help"]
|
||||||
|
let output = try process.runAndCapture()
|
||||||
|
#expect(output.contains("Usage:"))
|
||||||
|
#expect(output.contains("--library"))
|
||||||
|
#expect(output.contains("--output"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBinary() -> URL? {
|
||||||
|
let candidates = [
|
||||||
|
URL(fileURLWithPath: ".build/debug/swift-gtk-gen"),
|
||||||
|
URL(fileURLWithPath: "generator/.build/debug/swift-gtk-gen"),
|
||||||
|
URL(fileURLWithPath: ".build/release/swift-gtk-gen"),
|
||||||
|
URL(fileURLWithPath: "generator/.build/release/swift-gtk-gen"),
|
||||||
|
]
|
||||||
|
return candidates.first { FileManager.default.fileExists(atPath: $0.path) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func failBinaryNotFound() -> URL {
|
||||||
|
Issue.record("swift-gtk-gen binary not found at any expected path")
|
||||||
|
return URL(fileURLWithPath: "/nonexistent")
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Process {
|
||||||
|
func runAndCapture() throws -> String {
|
||||||
|
let stdout = Pipe()
|
||||||
|
standardOutput = stdout
|
||||||
|
standardError = Pipe()
|
||||||
|
try run()
|
||||||
|
waitUntilExit()
|
||||||
|
let data = stdout.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
return String(data: data, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Tests/SwiftGtkGenCoreTests/AnalysisTests.swift
Normal file
40
Tests/SwiftGtkGenCoreTests/AnalysisTests.swift
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Analysis resolves class hierarchy")
|
||||||
|
func testClassHierarchy() {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
|
||||||
|
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
|
||||||
|
Class(name: "ApplicationWindow", cType: "GtkApplicationWindow", parent: "Window"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let analyzer = Analyzer(config: GenerationConfig(
|
||||||
|
library: "Gtk", version: "4.0", girsDirectories: [], targetDirectory: "",
|
||||||
|
externalLibraries: [], generate: ["Gtk.Window"], manual: [], ignore: [], objects: []
|
||||||
|
))
|
||||||
|
let result = analyzer.analyze(repository: repo)
|
||||||
|
#expect(result.classHierarchy["Gtk.Window"] == "Gtk.Widget")
|
||||||
|
#expect(result.classHierarchy["Gtk.ApplicationWindow"] == "Gtk.Window")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Analysis filters types by generate list")
|
||||||
|
func testTypeFiltering() {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
|
||||||
|
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
|
||||||
|
Class(name: "Button", cType: "GtkButton", parent: "Widget"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget", "Gtk.Button"], manual: [], ignore: ["Gtk.Window"], objects: [])
|
||||||
|
let analyzer = Analyzer(config: config)
|
||||||
|
let result = analyzer.analyze(repository: repo)
|
||||||
|
#expect(result.generatedTypes.contains("Gtk.Widget"))
|
||||||
|
#expect(result.generatedTypes.contains("Gtk.Button"))
|
||||||
|
#expect(!result.generatedTypes.contains("Gtk.Window"))
|
||||||
|
#expect(result.manualTypes.isEmpty)
|
||||||
|
}
|
||||||
116
Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift
Normal file
116
Tests/SwiftGtkGenCoreTests/CFunctionCallTests.swift
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Simple void method calls C function directly")
|
||||||
|
func testVoidMethodCall() throws {
|
||||||
|
let method = Method(name: "show", cIdentifier: "gtk_widget_show",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .void)
|
||||||
|
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(callExpr == "gtk_widget_show(pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("String return wraps with String(cString:)")
|
||||||
|
func testStringReturnCall() throws {
|
||||||
|
let method = Method(name: "getLabel", cIdentifier: "gtk_label_get_text",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "label", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .string)
|
||||||
|
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(callExpr == "String(cString: gtk_label_get_text(pointer))")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Bool return uses CInt comparison")
|
||||||
|
func testBoolReturnCall() throws {
|
||||||
|
let method = Method(name: "getVisible", cIdentifier: "gtk_widget_get_visible",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .boolean)
|
||||||
|
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(callExpr == "(gtk_widget_get_visible(pointer) != 0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("GObject pointer parameter uses .pointer property")
|
||||||
|
func testGObjectParameterCall() throws {
|
||||||
|
let method = Method(name: "add", cIdentifier: "gtk_container_add",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "container", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true),
|
||||||
|
Parameter(name: "child", type: .typeRef("Widget"),
|
||||||
|
transferOwnership: .none)
|
||||||
|
], returnType: .void)
|
||||||
|
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(callExpr == "gtk_container_add(pointer, child.pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("TypeRef return wraps in Swift type")
|
||||||
|
func testTypeRefReturnCall() throws {
|
||||||
|
let method = Method(name: "getParent", cIdentifier: "gtk_widget_get_parent",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .typeRef("Widget", namespace: "Gtk"))
|
||||||
|
let callExpr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(callExpr == "Widget(pointer: gtk_widget_get_parent(pointer))")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Void return type produces bare call")
|
||||||
|
func testVoidReturn() throws {
|
||||||
|
let method = Method(name: "show", cIdentifier: "gtk_widget_show",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .void)
|
||||||
|
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(expr == "gtk_widget_show(pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Optional typeRef parameter uses optional chaining")
|
||||||
|
func testOptionalTypeRefParameter() throws {
|
||||||
|
let method = Method(name: "setParent", cIdentifier: "gtk_widget_set_parent",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true),
|
||||||
|
Parameter(name: "parent", type: .optional(.typeRef("Widget")),
|
||||||
|
transferOwnership: .none)
|
||||||
|
], returnType: .void)
|
||||||
|
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(expr == "gtk_widget_set_parent(pointer, parent?.pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Optional typeRef return uses map")
|
||||||
|
func testOptionalTypeRefReturn() throws {
|
||||||
|
let method = Method(name: "getChild", cIdentifier: "gtk_widget_get_child",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .optional(.typeRef("Widget")))
|
||||||
|
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(expr == "gtk_widget_get_child(pointer).map { Widget(pointer: $0) }")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Int32 return passes through directly")
|
||||||
|
func testIntReturn() throws {
|
||||||
|
let method = Method(name: "getWidth", cIdentifier: "gtk_widget_get_width",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "widget", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .int32)
|
||||||
|
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(expr == "gtk_widget_get_width(pointer)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Filename return wraps with String(cString:)")
|
||||||
|
func testFilenameReturn() throws {
|
||||||
|
let method = Method(name: "getFilename", cIdentifier: "gtk_file_chooser_get_filename",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "chooser", type: .pointer,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true)
|
||||||
|
], returnType: .filename)
|
||||||
|
let expr = CodeGenerator.generateCFunctionCall(method: method, instancePointer: "pointer")
|
||||||
|
#expect(expr == "String(cString: gtk_file_chooser_get_filename(pointer))")
|
||||||
|
}
|
||||||
99
Tests/SwiftGtkGenCoreTests/CodeGenTests.swift
Normal file
99
Tests/SwiftGtkGenCoreTests/CodeGenTests.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Generates a simple class wrapper")
|
||||||
|
func testGenerateClass() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "GLib", version: "2.0", classes: [
|
||||||
|
Class(name: "String", cType: "GString", parent: nil,
|
||||||
|
constructors: [
|
||||||
|
Constructor(name: "new", cIdentifier: "g_string_new",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "init", type: .string, transferOwnership: .none)
|
||||||
|
], returnType: .typeRef("String"))
|
||||||
|
],
|
||||||
|
methods: [
|
||||||
|
Method(name: "assign", cIdentifier: "g_string_assign",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "string", type: .string, isNullable: false,
|
||||||
|
transferOwnership: .none, isInstanceParameter: true),
|
||||||
|
Parameter(name: "value", type: .string)
|
||||||
|
], returnType: .typeRef("String"))
|
||||||
|
], properties: [
|
||||||
|
Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
||||||
|
], signals: [
|
||||||
|
Signal(name: "changed", parameters: [], returnType: .void)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "GLib", version: "2.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["GLib.String"], manual: [], ignore: [], objects: [])
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
|
||||||
|
|
||||||
|
let output = files["String.swift"] ?? ""
|
||||||
|
#expect(output.contains("public final class String"))
|
||||||
|
#expect(output.contains("convenience init"))
|
||||||
|
#expect(output.contains("g_string_new(`init`)"))
|
||||||
|
#expect(output.contains("func assign"))
|
||||||
|
#expect(output.contains("g_value_get_int"))
|
||||||
|
#expect(output.contains("connectChanged"))
|
||||||
|
#expect(output.contains("g_signal_connect_data"))
|
||||||
|
// Verify real C function call instead of fatalError
|
||||||
|
#expect(output.contains("g_string_assign(pointer, value)"))
|
||||||
|
#expect(!output.contains("fatalError(\"g_object_get_property"))
|
||||||
|
#expect(!output.contains("fatalError(\"g_object_set_property"))
|
||||||
|
#expect(!output.contains("fatalError(\"C function call not yet implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Kebab-case property names are converted to camelCase")
|
||||||
|
func testKebabCaseToCamelCase() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Box", cType: "GtkBox", parent: "Widget",
|
||||||
|
properties: [
|
||||||
|
Property(name: "baseline-child", type: .int32, isReadable: true, isWritable: true),
|
||||||
|
Property(name: "baseline-position", type: .int32, isReadable: true, isWritable: false),
|
||||||
|
Property(name: "homogeneous", type: .boolean, isReadable: true, isWritable: true),
|
||||||
|
Property(name: "spacing", type: .int32, isReadable: true, isWritable: true),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Box"], manual: [], ignore: [], objects: [])
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
|
||||||
|
|
||||||
|
let output = files["Box.swift"] ?? ""
|
||||||
|
#expect(output.contains("var baselineChild"), "baseline-child should become baselineChild")
|
||||||
|
#expect(output.contains("var baselinePosition"), "baseline-position should become baselinePosition")
|
||||||
|
#expect(output.contains("var homogeneous"), "homogeneous should stay as-is (no hyphens)")
|
||||||
|
#expect(output.contains("var spacing"), "spacing should stay as-is")
|
||||||
|
// Verify no kebab-case names remain in var declarations
|
||||||
|
#expect(!output.contains("var baseline-"), "No kebab-case var names should remain")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Generates enumeration")
|
||||||
|
func testGenerateEnum() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", enumerations: [
|
||||||
|
Enumeration(name: "Align", cType: "GtkAlign",
|
||||||
|
members: [
|
||||||
|
EnumMember(name: "fill", value: "0", cIdentifier: "GTK_ALIGN_FILL"),
|
||||||
|
EnumMember(name: "start", value: "1", cIdentifier: "GTK_ALIGN_START"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Align"], manual: [], ignore: [], objects: [])
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: Analyzer(config: config).analyze(repository: repo))
|
||||||
|
|
||||||
|
let output = files["Align.swift"] ?? ""
|
||||||
|
#expect(output.contains("public enum Align"))
|
||||||
|
#expect(output.contains("case fill"))
|
||||||
|
#expect(output.contains("case start"))
|
||||||
|
}
|
||||||
83
Tests/SwiftGtkGenCoreTests/ConfigTests.swift
Normal file
83
Tests/SwiftGtkGenCoreTests/ConfigTests.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Config builder creates valid configuration")
|
||||||
|
func testConfigBuilder() {
|
||||||
|
let config = GenerationConfig(
|
||||||
|
library: "GLib",
|
||||||
|
version: "2.0",
|
||||||
|
girsDirectories: ["/usr/share/gir-1.0"],
|
||||||
|
targetDirectory: "Sources/GLib/Generated",
|
||||||
|
externalLibraries: [],
|
||||||
|
generate: ["GLib.String", "GLib.List"],
|
||||||
|
manual: ["GLib.MainLoop"],
|
||||||
|
ignore: ["GLib.Test"],
|
||||||
|
objects: []
|
||||||
|
)
|
||||||
|
#expect(config.library == "GLib")
|
||||||
|
#expect(config.generate.count == 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Object overrides merge correctly")
|
||||||
|
func testObjectOverrides() {
|
||||||
|
let override = ObjectOverrides(status: .generate, finalType: true, concurrency: .mainActor,
|
||||||
|
version: "2.0", cfgCondition: nil, generateBuilder: false)
|
||||||
|
#expect(override.status == .generate)
|
||||||
|
#expect(override.finalType == true)
|
||||||
|
#expect(override.concurrency == .mainActor)
|
||||||
|
#expect(override.version == "2.0")
|
||||||
|
#expect(override.cfgCondition == nil)
|
||||||
|
#expect(override.generateBuilder == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Function overrides can ignore a function")
|
||||||
|
func testFunctionOverrides() {
|
||||||
|
let override = FunctionOverrides(ignore: true)
|
||||||
|
#expect(override.ignore == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Config with object and function overrides stores values correctly")
|
||||||
|
func testConfigWithObjectOverrides() throws {
|
||||||
|
let widgetOverrides = ObjectOverrides(
|
||||||
|
status: .generate, finalType: false, concurrency: .mainActor,
|
||||||
|
version: "4.0", cfgCondition: nil, generateBuilder: false
|
||||||
|
)
|
||||||
|
let showOverride = FunctionOverrides(ignore: true)
|
||||||
|
let config = GenerationConfig(
|
||||||
|
library: "Gtk",
|
||||||
|
version: "4.0",
|
||||||
|
girsDirectories: ["vendor/gir-files"],
|
||||||
|
targetDirectory: "Sources/GTK/Generated",
|
||||||
|
externalLibraries: ["Gdk-4.0", "Gsk-4.0"],
|
||||||
|
generate: ["Gtk.Widget", "Gtk.Window"],
|
||||||
|
manual: ["Gtk.Buildable"],
|
||||||
|
ignore: [],
|
||||||
|
objects: [
|
||||||
|
.object("Gtk.Widget", overrides: widgetOverrides),
|
||||||
|
.function("Gtk.Widget", "show", overrides: showOverride),
|
||||||
|
.functionPattern("Gtk.Window", pattern: "set_*", rename: RenameRule(regex: "^set_(.*)", replacement: "$1")),
|
||||||
|
.signal("Gtk.Button", "clicked", overrides: SignalOverrides(ignore: true)),
|
||||||
|
.property("Gtk.Label", "label", overrides: PropertyOverrides(generate: [.get, .set])),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
#expect(config.objects.count == 5)
|
||||||
|
if case .object(let name, let ov) = config.objects[0] {
|
||||||
|
#expect(name == "Gtk.Widget")
|
||||||
|
#expect(ov.finalType == false)
|
||||||
|
} else { #expect(Bool(false), "Expected .object case") }
|
||||||
|
if case .functionPattern(let type, let pattern, let rename) = config.objects[2] {
|
||||||
|
#expect(type == "Gtk.Window")
|
||||||
|
#expect(pattern == "set_*")
|
||||||
|
#expect(rename.regex == "^set_(.*)")
|
||||||
|
} else { #expect(Bool(false), "Expected .functionPattern case") }
|
||||||
|
if case .signal(let type, let name, let ov) = config.objects[3] {
|
||||||
|
#expect(type == "Gtk.Button")
|
||||||
|
#expect(name == "clicked")
|
||||||
|
#expect(ov.ignore == true)
|
||||||
|
} else { #expect(Bool(false), "Expected .signal case") }
|
||||||
|
if case .property(let type, let name, let ov) = config.objects[4] {
|
||||||
|
#expect(type == "Gtk.Label")
|
||||||
|
#expect(name == "label")
|
||||||
|
#expect(ov.generate == [.get, .set])
|
||||||
|
} else { #expect(Bool(false), "Expected .property case") }
|
||||||
|
}
|
||||||
35
Tests/SwiftGtkGenCoreTests/ConstructorTests.swift
Normal file
35
Tests/SwiftGtkGenCoreTests/ConstructorTests.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Constructor with no parameters generates convenience init")
|
||||||
|
func testConstructorNoParams() {
|
||||||
|
let ctor = Constructor(name: "new", cIdentifier: "gtk_button_new",
|
||||||
|
parameters: [], returnType: .typeRef("Widget"))
|
||||||
|
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "Button")
|
||||||
|
#expect(initMethod.contains("convenience init"))
|
||||||
|
#expect(initMethod.contains("gtk_button_new()"))
|
||||||
|
#expect(initMethod.contains("g_object_ref_sink"))
|
||||||
|
#expect(initMethod.contains("self.init(pointer:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Constructor with string parameter")
|
||||||
|
func testConstructorWithStringParam() {
|
||||||
|
let ctor = Constructor(name: "newWithLabel", cIdentifier: "gtk_button_new_with_label",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "label", type: .string, transferOwnership: .none)
|
||||||
|
], returnType: .typeRef("Widget"))
|
||||||
|
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "Button")
|
||||||
|
#expect(initMethod.contains("label: String"))
|
||||||
|
#expect(initMethod.contains("gtk_button_new_with_label(label)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Constructor with GObject parameter uses .pointer")
|
||||||
|
func testConstructorWithGObjectParam() {
|
||||||
|
let ctor = Constructor(name: "newFromModel", cIdentifier: "gtk_combo_box_new_from_model",
|
||||||
|
parameters: [
|
||||||
|
Parameter(name: "model", type: .typeRef("TreeModel"), transferOwnership: .none)
|
||||||
|
], returnType: .typeRef("Widget"))
|
||||||
|
let initMethod = CodeGenerator.generateConstructor(constructor: ctor, className: "ComboBox")
|
||||||
|
#expect(initMethod.contains("model: TreeModel"))
|
||||||
|
#expect(initMethod.contains("gtk_combo_box_new_from_model(model.pointer)"))
|
||||||
|
}
|
||||||
66
Tests/SwiftGtkGenCoreTests/FileOutputTests.swift
Normal file
66
Tests/SwiftGtkGenCoreTests/FileOutputTests.swift
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("generateFiles returns one file per type")
|
||||||
|
func testGenerateFilesReturnsMultipleFiles() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
|
||||||
|
Class(name: "Window", cType: "GtkWindow", parent: "Widget"),
|
||||||
|
], enumerations: [
|
||||||
|
Enumeration(name: "Align", cType: "GtkAlign", members: [
|
||||||
|
EnumMember(name: "fill", value: "0", cIdentifier: "GTK_ALIGN_FILL"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget", "Gtk.Window", "Gtk.Align"], manual: [], ignore: [], objects: [])
|
||||||
|
let analysis = Analyzer(config: config).analyze(repository: repo)
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: analysis)
|
||||||
|
|
||||||
|
#expect(files.keys.contains("Widget.swift"))
|
||||||
|
#expect(files.keys.contains("Window.swift"))
|
||||||
|
#expect(files.keys.contains("Align.swift"))
|
||||||
|
#expect(files["Widget.swift"]?.contains("class Widget") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("class Window") == false)
|
||||||
|
#expect(files["Align.swift"]?.contains("enum Align") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generateFiles includes import CGtk in header")
|
||||||
|
func testGenerateFilesIncludesCGtkImport() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
|
||||||
|
let analysis = Analyzer(config: config).analyze(repository: repo)
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: analysis)
|
||||||
|
|
||||||
|
#expect(files["Widget.swift"]?.contains("import Foundation") == true)
|
||||||
|
#expect(files["Widget.swift"]?.contains("import CGtk") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generateFiles skips types not in analysis")
|
||||||
|
func testGenerateFilesSkipsNonGeneratedTypes() throws {
|
||||||
|
let repo = Repository(namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", classes: [
|
||||||
|
Class(name: "Widget", cType: "GtkWidget", parent: "GObject.Object"),
|
||||||
|
Class(name: "Button", cType: "GtkButton", parent: "Widget"),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
|
||||||
|
let analysis = Analyzer(config: config).analyze(repository: repo)
|
||||||
|
let generator = CodeGenerator(config: config)
|
||||||
|
let files = try generator.generateFiles(repository: repo, analysis: analysis)
|
||||||
|
|
||||||
|
#expect(files.keys.contains("Widget.swift"))
|
||||||
|
#expect(files.keys.contains("Button.swift") == false)
|
||||||
|
}
|
||||||
86
Tests/SwiftGtkGenCoreTests/IRModelTests.swift
Normal file
86
Tests/SwiftGtkGenCoreTests/IRModelTests.swift
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import Testing
|
||||||
|
import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Namespace can hold types")
|
||||||
|
func testNamespaceHoldsTypes() {
|
||||||
|
let ns = Namespace(name: "Gtk", version: "4.0")
|
||||||
|
#expect(ns.name == "Gtk")
|
||||||
|
#expect(ns.version == "4.0")
|
||||||
|
#expect(ns.classes.isEmpty)
|
||||||
|
#expect(ns.interfaces.isEmpty)
|
||||||
|
#expect(ns.enumerations.isEmpty)
|
||||||
|
#expect(ns.records.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Class has properties and methods")
|
||||||
|
func testClassHasProperties() {
|
||||||
|
let cls = Class(name: "Widget", cType: "GtkWidget", parent: nil, isAbstract: false)
|
||||||
|
#expect(cls.name == "Widget")
|
||||||
|
#expect(cls.properties.isEmpty)
|
||||||
|
#expect(cls.methods.isEmpty)
|
||||||
|
#expect(cls.signals.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Method stores parameters and return type")
|
||||||
|
func testMethodStoresParameters() {
|
||||||
|
let param = Parameter(name: "label", type: .string, isNullable: false, transferOwnership: .none)
|
||||||
|
let method = Method(name: "set_label", cIdentifier: "gtk_label_set_text", parameters: [param], returnType: .void)
|
||||||
|
#expect(method.name == "set_label")
|
||||||
|
#expect(method.parameters.count == 1)
|
||||||
|
#expect(method.returnType == .void)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Enumeration stores members")
|
||||||
|
func testEnumerationStoresMembers() {
|
||||||
|
let member = EnumMember(name: "GTK_ALIGN_FILL", value: "1", cIdentifier: "GTK_ALIGN_FILL")
|
||||||
|
let enumeration = Enumeration(name: "GtkAlign", cType: "GtkAlign", members: [member])
|
||||||
|
#expect(enumeration.name == "GtkAlign")
|
||||||
|
#expect(enumeration.members.count == 1)
|
||||||
|
#expect(enumeration.members[0].value == "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Signal stores parameters and return type")
|
||||||
|
func testSignalStoresParameters() {
|
||||||
|
let signal = Signal(name: "clicked", parameters: [], returnType: .void, isDetailed: false)
|
||||||
|
#expect(signal.name == "clicked")
|
||||||
|
#expect(signal.returnType == .void)
|
||||||
|
#expect(signal.isDetailed == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Property stores type and flags")
|
||||||
|
func testPropertyStoresTypeAndFlags() {
|
||||||
|
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true, isConstructOnly: false)
|
||||||
|
#expect(prop.name == "label")
|
||||||
|
#expect(prop.type == .string)
|
||||||
|
#expect(prop.isReadable == true)
|
||||||
|
#expect(prop.isWritable == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Repository holds multiple namespaces")
|
||||||
|
func testRepositoryHoldsMultipleNamespaces() {
|
||||||
|
let gtk = Namespace(name: "Gtk", version: "4.0")
|
||||||
|
let gdk = Namespace(name: "Gdk", version: "4.0")
|
||||||
|
let repo = Repository(namespaces: [gtk, gdk])
|
||||||
|
#expect(repo.namespaces.count == 2)
|
||||||
|
#expect(repo.namespaces[0].name == "Gtk")
|
||||||
|
#expect(repo.namespaces[1].name == "Gdk")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("GIRType equality works")
|
||||||
|
func testGIRTypeEquality() {
|
||||||
|
#expect(GIRType.void == GIRType.void)
|
||||||
|
#expect(GIRType.string == GIRType.string)
|
||||||
|
#expect(GIRType.typeRef("GtkWidget") == GIRType.typeRef("GtkWidget"))
|
||||||
|
#expect(GIRType.typeRef("GtkWidget") != GIRType.typeRef("GtkLabel"))
|
||||||
|
#expect(GIRType.array(.string) == GIRType.array(.string))
|
||||||
|
#expect(GIRType.array(.string) != GIRType.array(.int32))
|
||||||
|
#expect(GIRType.optional(.string) == GIRType.optional(.string))
|
||||||
|
#expect(GIRType.optional(.string) != GIRType.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Interface stores methods, properties, signals, and prereqs")
|
||||||
|
func testInterfaceStoresMembers() {
|
||||||
|
let iface = Interface(name: "GtkBuildable", cType: "GtkBuildable", prereqs: ["GtkWidget"])
|
||||||
|
#expect(iface.name == "GtkBuildable")
|
||||||
|
#expect(iface.prereqs.count == 1)
|
||||||
|
}
|
||||||
205
Tests/SwiftGtkGenCoreTests/ParserTests.swift
Normal file
205
Tests/SwiftGtkGenCoreTests/ParserTests.swift
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Parser reads namespace from minimal GIR XML")
|
||||||
|
func testParsesNamespace() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="GLib" version="2.0" shared-library="libglib-2.0.so.0"
|
||||||
|
c:identifier-prefixes="g" c:symbol-prefixes="g"/>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
#expect(repo.namespaces.count == 1)
|
||||||
|
#expect(repo.namespaces[0].name == "GLib")
|
||||||
|
#expect(repo.namespaces[0].version == "2.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser reads a class with method and property")
|
||||||
|
func testParsesClass() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="Gtk" version="4.0">
|
||||||
|
<class name="Widget" c:type="GtkWidget" parent="GObject.Object"
|
||||||
|
glib:type-name="GtkWidget" glib:get-type="gtk_widget_get_type">
|
||||||
|
<method name="show" c:identifier="gtk_widget_show">
|
||||||
|
<return-value transfer-ownership="none">
|
||||||
|
<type name="none" c:type="void"/>
|
||||||
|
</return-value>
|
||||||
|
<parameters>
|
||||||
|
<instance-parameter name="widget" transfer-ownership="none">
|
||||||
|
<type name="Widget" c:type="GtkWidget*"/>
|
||||||
|
</instance-parameter>
|
||||||
|
</parameters>
|
||||||
|
</method>
|
||||||
|
<property name="name" writable="1" transfer-ownership="none">
|
||||||
|
<type name="utf8" c:type="gchar*"/>
|
||||||
|
</property>
|
||||||
|
</class>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
let cls = repo.namespaces[0].classes[0]
|
||||||
|
#expect(cls.name == "Widget")
|
||||||
|
#expect(cls.parent == "GObject.Object")
|
||||||
|
#expect(cls.methods.count == 1)
|
||||||
|
#expect(cls.methods[0].name == "show")
|
||||||
|
#expect(cls.properties.count == 1)
|
||||||
|
#expect(cls.properties[0].name == "name")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser reads an enumeration with members")
|
||||||
|
func testParsesEnumeration() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="Gtk" version="4.0">
|
||||||
|
<enumeration name="Align" glib:type-name="GtkAlign"
|
||||||
|
glib:get-type="gtk_align_get_type" c:type="GtkAlign">
|
||||||
|
<member name="fill" value="0" c:identifier="GTK_ALIGN_FILL"/>
|
||||||
|
<member name="start" value="1" c:identifier="GTK_ALIGN_START"/>
|
||||||
|
<member name="end" value="2" c:identifier="GTK_ALIGN_END"/>
|
||||||
|
<member name="center" value="3" c:identifier="GTK_ALIGN_CENTER"/>
|
||||||
|
</enumeration>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
let enums = repo.namespaces[0].enumerations
|
||||||
|
#expect(enums.count == 1)
|
||||||
|
#expect(enums[0].name == "Align")
|
||||||
|
#expect(enums[0].members.count == 4)
|
||||||
|
#expect(enums[0].members[0].name == "fill")
|
||||||
|
#expect(enums[0].members[3].cIdentifier == "GTK_ALIGN_CENTER")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser reads bitfield with members")
|
||||||
|
func testParsesBitfield() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="Gtk" version="4.0">
|
||||||
|
<bitfield name="SizeGroupMode" c:type="GtkSizeGroupMode"
|
||||||
|
glib:type-name="GtkSizeGroupMode" glib:get-type="gtk_size_group_mode_get_type">
|
||||||
|
<member name="none" value="0" c:identifier="GTK_SIZE_GROUP_MODE_NONE"/>
|
||||||
|
<member name="horizontal" value="1" c:identifier="GTK_SIZE_GROUP_MODE_HORIZONTAL"/>
|
||||||
|
<member name="vertical" value="2" c:identifier="GTK_SIZE_GROUP_MODE_VERTICAL"/>
|
||||||
|
<member name="both" value="3" c:identifier="GTK_SIZE_GROUP_MODE_BOTH"/>
|
||||||
|
</bitfield>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
let bf = repo.namespaces[0].bitfields[0]
|
||||||
|
#expect(bf.name == "SizeGroupMode")
|
||||||
|
#expect(bf.members.count == 4)
|
||||||
|
#expect(bf.members[0].name == "none")
|
||||||
|
#expect(bf.members[3].cIdentifier == "GTK_SIZE_GROUP_MODE_BOTH")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser reads interface with method")
|
||||||
|
func testParsesInterface() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="Gtk" version="4.0">
|
||||||
|
<interface name="Buildable" c:type="GtkBuildable">
|
||||||
|
<method name="buildable_get_id" c:identifier="gtk_buildable_get_id">
|
||||||
|
<return-value transfer-ownership="none">
|
||||||
|
<type name="utf8" c:type="gchar*"/>
|
||||||
|
</return-value>
|
||||||
|
<parameters>
|
||||||
|
<instance-parameter name="buildable" transfer-ownership="none">
|
||||||
|
<type name="Buildable" c:type="GtkBuildable*"/>
|
||||||
|
</instance-parameter>
|
||||||
|
</parameters>
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
let iface = repo.namespaces[0].interfaces[0]
|
||||||
|
#expect(iface.name == "Buildable")
|
||||||
|
#expect(iface.methods.count == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser reads global function")
|
||||||
|
func testParsesGlobalFunction() throws {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
||||||
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
||||||
|
<namespace name="GLib" version="2.0">
|
||||||
|
<function name="getenv" c:identifier="g_getenv"
|
||||||
|
glib:type-name="GLib" glib:get-type="glib_get_type">
|
||||||
|
<return-value transfer-ownership="none">
|
||||||
|
<type name="utf8" c:type="gchar*"/>
|
||||||
|
</return-value>
|
||||||
|
<parameters>
|
||||||
|
<parameter name="variable" transfer-ownership="none">
|
||||||
|
<type name="utf8" c:type="gchar*"/>
|
||||||
|
</parameter>
|
||||||
|
</parameters>
|
||||||
|
</function>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
let repo = try parser.parse(xmlString: xml)
|
||||||
|
let fn = repo.namespaces[0].functions[0]
|
||||||
|
#expect(fn.name == "getenv")
|
||||||
|
#expect(fn.parameters.count == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser throws error on missing required attributes")
|
||||||
|
func testParsingMissingAttributes() {
|
||||||
|
let xml = """
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<repository version="1.2"
|
||||||
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
||||||
|
xmlns:c="http://www.gtk.org/introspection/c/1.0">
|
||||||
|
<namespace name="Gtk" version="4.0">
|
||||||
|
<class c:type="GtkWidget" parent="GObject.Object"/>
|
||||||
|
</namespace>
|
||||||
|
</repository>
|
||||||
|
"""
|
||||||
|
let parser = GIRParser()
|
||||||
|
#expect(throws: GIRParserError.self) {
|
||||||
|
try parser.parse(xmlString: xml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Parser rejects invalid XML")
|
||||||
|
func testParsingError() {
|
||||||
|
let parser = GIRParser()
|
||||||
|
#expect(throws: GIRParserError.self) {
|
||||||
|
try parser.parse(xmlString: "not xml")
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift
Normal file
48
Tests/SwiftGtkGenCoreTests/PropertyAccessorTests.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Readable Int32 property generates correct accessor")
|
||||||
|
func testReadableInt32Property() throws {
|
||||||
|
let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false)
|
||||||
|
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
|
||||||
|
#expect(accessor.contains("g_value_init"))
|
||||||
|
#expect(accessor.contains("g_value_get_int"))
|
||||||
|
#expect(accessor.contains("public var length: Int32"))
|
||||||
|
#expect(!accessor.contains("get {"))
|
||||||
|
#expect(!accessor.contains("set {"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Writable string property generates read-write accessor")
|
||||||
|
func testWritableStringProperty() throws {
|
||||||
|
let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true)
|
||||||
|
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
|
||||||
|
#expect(accessor.contains("g_value_set_string"))
|
||||||
|
#expect(accessor.contains("set {"))
|
||||||
|
#expect(accessor.contains("get {"))
|
||||||
|
#expect(accessor.contains("g_value_get_string"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Read-only boolean property generates getter only")
|
||||||
|
func testReadOnlyBooleanProperty() throws {
|
||||||
|
let prop = Property(name: "visible", type: .boolean, isReadable: true, isWritable: false)
|
||||||
|
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
|
||||||
|
#expect(!accessor.contains("get {"))
|
||||||
|
#expect(!accessor.contains("set {"))
|
||||||
|
#expect(accessor.contains("g_value_get_boolean"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Write-only property has setter only")
|
||||||
|
func testWriteOnlyProperty() throws {
|
||||||
|
let prop = Property(name: "opacity", type: .double, isReadable: false, isWritable: true)
|
||||||
|
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
|
||||||
|
#expect(!accessor.contains("get {"))
|
||||||
|
#expect(accessor.contains("set {"))
|
||||||
|
#expect(accessor.contains("g_value_set_double"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Construct-only property has no accessor")
|
||||||
|
func testConstructOnlyProperty() throws {
|
||||||
|
let prop = Property(name: "type", type: .typeRef("Type"), isReadable: false, isWritable: true, isConstructOnly: true)
|
||||||
|
let accessor = CodeGenerator.generatePropertyAccessor(property: prop)
|
||||||
|
#expect(accessor.isEmpty)
|
||||||
|
}
|
||||||
98
Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift
Normal file
98
Tests/SwiftGtkGenCoreTests/ScaffoldingTests.swift
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("generatePackageScaffolding produces Package.swift")
|
||||||
|
func testPackageScaffolding() {
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
|
||||||
|
let repo = Repository(
|
||||||
|
namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
|
||||||
|
],
|
||||||
|
cHeaderPath: "gtk/gtk.h",
|
||||||
|
includedLibraryLinks: ["gdk-4", "gsk-4"]
|
||||||
|
)
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
|
||||||
|
#expect(scaffolding.keys.contains("Package.swift"))
|
||||||
|
#expect(scaffolding["Package.swift"]?.contains("name: \"Gtk\"") == true)
|
||||||
|
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"gtk-4\"") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generatePackageScaffolding produces module map")
|
||||||
|
func testModuleMapScaffolding() {
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
|
||||||
|
let repo = Repository(
|
||||||
|
namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
|
||||||
|
],
|
||||||
|
cHeaderPath: "gtk/gtk.h",
|
||||||
|
includedLibraryLinks: ["gdk-4", "gsk-4"]
|
||||||
|
)
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
|
||||||
|
let moduleMapName = "Sources/CGtk/module.modulemap"
|
||||||
|
#expect(scaffolding.keys.contains(moduleMapName))
|
||||||
|
#expect(scaffolding[moduleMapName]?.contains("module CGtk [system]") == true)
|
||||||
|
#expect(scaffolding[moduleMapName]?.contains("link \"gdk-4\"") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generatePackageScaffolding produces umbrella header")
|
||||||
|
func testUmbrellaHeader() {
|
||||||
|
let config = GenerationConfig(library: "Gtk", version: "4.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Gtk.Widget"], manual: [], ignore: [], objects: [])
|
||||||
|
let repo = Repository(
|
||||||
|
namespaces: [
|
||||||
|
Namespace(name: "Gtk", version: "4.0", cSharedLibrary: "libgtk-4.so.1", cIdentifierPrefix: "Gtk")
|
||||||
|
],
|
||||||
|
cHeaderPath: "gtk/gtk.h",
|
||||||
|
includedLibraryLinks: ["gdk-4", "gsk-4"]
|
||||||
|
)
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
|
||||||
|
let headerName = "Sources/CGtk/CGtk.h"
|
||||||
|
#expect(scaffolding.keys.contains(headerName))
|
||||||
|
#expect(scaffolding[headerName]?.contains("#include <gtk/gtk.h>") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generatePackageScaffolding works for GLib")
|
||||||
|
func testPackageScaffoldingGLib() {
|
||||||
|
let config = GenerationConfig(library: "GLib", version: "2.0", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["GLib.String"], manual: [], ignore: [], objects: [])
|
||||||
|
let repo = Repository(
|
||||||
|
namespaces: [
|
||||||
|
Namespace(name: "GLib", version: "2.0", cSharedLibrary: "libglib-2.0.so.0", cIdentifierPrefix: "G")
|
||||||
|
],
|
||||||
|
cHeaderPath: "glib.h"
|
||||||
|
)
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
|
||||||
|
#expect(scaffolding["Package.swift"]?.contains("name: \"GLib\"") == true)
|
||||||
|
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"glib-2.0\"") == true)
|
||||||
|
#expect(scaffolding["Sources/CGLib/module.modulemap"]?.contains("link \"glib-2.0\"") == true)
|
||||||
|
#expect(scaffolding["Sources/CGLib/CGLib.h"]?.contains("#include <glib.h>") == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("generatePackageScaffolding works for Adw")
|
||||||
|
func testPackageScaffoldingAdw() {
|
||||||
|
let config = GenerationConfig(library: "Adw", version: "1", girsDirectories: [],
|
||||||
|
targetDirectory: "", externalLibraries: [],
|
||||||
|
generate: ["Adw.Window"], manual: [], ignore: [], objects: [])
|
||||||
|
let repo = Repository(
|
||||||
|
namespaces: [
|
||||||
|
Namespace(name: "Adw", version: "1", cSharedLibrary: "libadwaita-1.so.0", cIdentifierPrefix: "Adw")
|
||||||
|
],
|
||||||
|
cHeaderPath: "adwaita.h",
|
||||||
|
includedLibraryLinks: ["gio-2", "gtk-4"]
|
||||||
|
)
|
||||||
|
let scaffolding = CodeGenerator.generatePackageScaffolding(config: config, repository: repo)
|
||||||
|
|
||||||
|
#expect(scaffolding["Package.swift"]?.contains("pkgConfig: \"adwaita-1\"") == true)
|
||||||
|
#expect(scaffolding["Sources/CAdw/module.modulemap"]?.contains("link \"adwaita-1\"") == true)
|
||||||
|
#expect(scaffolding["Sources/CAdw/CAdw.h"]?.contains("#include <adwaita.h>") == true)
|
||||||
|
}
|
||||||
22
Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift
Normal file
22
Tests/SwiftGtkGenCoreTests/SignalConnectionTests.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Testing
|
||||||
|
@testable import SwiftGtkGenCore
|
||||||
|
|
||||||
|
@Test("Signal with no parameters generates connection")
|
||||||
|
func testSignalNoParams() {
|
||||||
|
let signal = Signal(name: "activate", parameters: [], returnType: .void)
|
||||||
|
let connection = CodeGenerator.generateSignalConnection(signal: signal)
|
||||||
|
#expect(connection.contains("g_signal_connect_data"))
|
||||||
|
#expect(connection.contains("connectActivate"))
|
||||||
|
#expect(connection.contains("() -> Void"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Signal with parameters includes them in handler")
|
||||||
|
func testSignalWithParams() {
|
||||||
|
let signal = Signal(name: "value-changed", parameters: [
|
||||||
|
Parameter(name: "value", type: .int32, transferOwnership: .none)
|
||||||
|
], returnType: .void)
|
||||||
|
let connection = CodeGenerator.generateSignalConnection(signal: signal)
|
||||||
|
#expect(connection.contains("connectValueChanged"))
|
||||||
|
#expect(connection.contains("_: Int32"))
|
||||||
|
#expect(connection.contains("g_signal_connect_data"))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue