1
0
Fork 0

Add qualifiedTypeToSwift and generateMonorepo for cross-namespace type references

This commit is contained in:
Brendan Szymanski 2026-07-01 20:16:50 -04:00
parent cf9f762155
commit 3dde30dc70

View file

@ -196,6 +196,115 @@ public struct CodeGenerator {
return files
}
/// Generates Swift source for all modules in a monorepo.
///
/// Iterates over all packages in the analysis, producing per-type Swift
/// files for each module. Each file includes the appropriate `import`
/// statements for the module's direct dependencies.
///
/// - Parameter analysis: The resolved multi-package analysis.
/// - Returns: Module name (filename source code) nested dictionary.
/// - Throws: `CodeGenError` if generation fails.
public func generateMonorepo(analysis: MultiPackageAnalysis) throws -> [String: [String: String]] {
var allOutputs: [String: [String: String]] = [:]
for (moduleName, repo) in analysis.repositories {
guard let cfg = analysis.packageConfigs[moduleName] else { continue }
guard let ns = repo.namespaces.first(where: { $0.name == moduleName })
?? repo.namespaces.first else { continue }
let directImports = analysis.directDependencies[moduleName] ?? []
let nsToModule = buildNamespaceToModuleMap(from: analysis)
_ = nsToModule
let rootModuleName = moduleName
let analyzer = Analyzer(config: cfg)
let singleAnalysis = analyzer.analyze(repository: repo)
let classNames = Set(ns.classes.map(\.name))
let shouldGenerateAll = singleAnalysis.generatedTypes.isEmpty
var sourceFiles: [String: String] = [:]
let header = fileHeader(moduleName: rootModuleName, directImports: directImports)
for cls in ns.classes {
let fullName = "\(ns.name).\(cls.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(cls.name).swift"] = header + "\n" + generateClass(cls, namespace: ns.name, analysis: singleAnalysis, classTypeNames: classNames)
}
for enm in ns.enumerations {
let fullName = "\(ns.name).\(enm.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(enm.name).swift"] = header + "\n" + generateEnum(enm)
}
for bf in ns.bitfields {
let fullName = "\(ns.name).\(bf.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(bf.name).swift"] = header + "\n" + generateBitfield(bf)
}
for iface in ns.interfaces {
let fullName = "\(ns.name).\(iface.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(iface.name).swift"] = header + "\n" + generateInterface(iface)
}
for cb in ns.callbacks {
let fullName = "\(ns.name).\(cb.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(cb.name).swift"] = header + "\n" + generateCallback(cb)
}
for fn in ns.functions {
let fullName = "\(ns.name).\(fn.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(Self.pascalCaseName(fn.name)).swift"] = header + "\n" + generateGlobalFunction(fn, namespace: ns.name, classTypeNames: classNames)
}
for rec in ns.records {
let fullName = "\(ns.name).\(rec.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(rec.name).swift"] = header + "\n" + generateRecord(rec, namespace: ns.name, analysis: singleAnalysis, classTypeNames: classNames)
}
for cst in ns.constants {
let fullName = "\(ns.name).\(cst.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(cst.name).swift"] = header + "\n" + generateConstant(cst)
}
for alias in ns.aliases {
let fullName = "\(ns.name).\(alias.name)"
if !shouldGenerateAll && !singleAnalysis.generatedTypes.contains(fullName) { continue }
sourceFiles["\(alias.name).swift"] = header + "\n" + generateAlias(alias)
}
allOutputs[rootModuleName] = sourceFiles
}
return allOutputs
}
/// Generates the standard file header for monorepo source files.
private func fileHeader(moduleName: String, directImports: Set<String>) -> String {
var imports = "import C\(moduleName)\nimport Foundation\n"
for imp in directImports.sorted() {
imports += "import \(imp)\n"
}
return """
// Generated by SwiftGtkGen. DO NOT EDIT.
\(imports)
"""
}
/// Builds a mapping from GIR namespace name to Swift module name.
private func buildNamespaceToModuleMap(from analysis: MultiPackageAnalysis) -> [String: String] {
var map: [String: String] = [:]
for (moduleName, repo) in analysis.repositories {
for ns in repo.namespaces {
map[ns.name] = moduleName
}
}
return map
}
// MARK: - Class Generation
/// Generates a Swift class declaration from a GIR class definition.
@ -954,6 +1063,32 @@ public struct CodeGenerator {
}
}
/// Converts a GIR type to a Swift type string, qualifying cross-namespace
/// type references with their Swift module name prefix.
///
/// Cross-namespace references (`.typeRef(name, namespace: "Gdk")`) produce
/// `Gdk.Rectangle` when the current module is not `Gdk`. Same-namespace
/// references produce the bare `name`.
///
/// - Parameters:
/// - type: The GIR type to map.
/// - currentModule: The Swift module name currently being generated.
/// - nsToModule: Map from GIR namespace name to Swift module name.
/// - Returns: A qualified Swift type name.
static func qualifiedTypeToSwift(_ type: GIRType, currentModule: String, nsToModule: [String: String]) -> String {
switch type {
case .typeRef(let name, let namespace):
if let ns = namespace, let module = nsToModule[ns], module != currentModule {
return "\(module).\(name)"
}
return name
case .array(let inner): return "[\(qualifiedTypeToSwift(inner, currentModule: currentModule, nsToModule: nsToModule))]"
case .cArray(let inner): return "UnsafeBufferPointer<\(qualifiedTypeToSwift(inner, currentModule: currentModule, nsToModule: nsToModule))>"
case .optional(let inner): return "\(qualifiedTypeToSwift(inner, currentModule: currentModule, nsToModule: nsToModule))?"
default: return Self.typeToSwift(type)
}
}
/// Converts a snake_case GIR name to a camelCase Swift property name.
///
/// Splits on underscores and lowercases the first part while capitalizing