// NamingTests.swift // Locks in the generated naming conventions: symbol case conversion // (uppercase GIR segments fold to words, constants become lowerCamelCase), // the PascalCase filename rule with merged Functions.swift/Constants.swift // files, and collision handling when case conversion merges distinct GIR // names. A regression here means generated packages stop following Swift // naming conventions. import Testing @testable import GObjectGeneratorCore @Suite("Naming conventions") struct NamingTests { // MARK: - Symbol case conversion @Test("Uppercase GIR segments fold to single words") func uppercaseSegmentsNormalize() { // The marshal symbols are the canonical offenders: uppercase segments // and a double underscore that must not glue words together. #expect(camelCased("cclosure_marshal_BOOLEAN__BOXED_BOXED") == "cclosureMarshalBooleanBoxedBoxed") #expect(camelCased("cclosure_marshal_VOID__UINT_POINTER") == "cclosureMarshalVoidUintPointer") // Ordinary snake_case is unaffected. #expect(camelCased("set_application_name") == "setApplicationName") // Mixed-case segments pass through untouched. #expect(camelCased("show") == "show") } @Test("Constants convert to lowerCamelCase") func constantNames() { #expect(swiftConstantName("PARAM_MASK") == "paramMask") #expect(swiftConstantName("TYPE_FLAG_RESERVED_ID_BIT") == "typeFlagReservedIdBit") #expect(swiftConstantName("PI_2") == "pi2") #expect(swiftConstantName("E") == "e") } // MARK: - Filename convention @Test("Filename validator accepts PascalCase and infra names only") func filenameValidation() { #expect(isValidGeneratedFileName("Align.swift")) #expect(isValidGeneratedFileName("IOChannel.swift")) // 3-cap acronym run #expect(isValidGeneratedFileName("FileIOStream.swift")) #expect(!isValidGeneratedFileName("_Support.swift")) // underscore prefix #expect(isValidGeneratedFileName("ParamSpecInt64.swift")) // digits #expect(!isValidGeneratedFileName("boxedFree.swift")) // lowerCamelCase #expect(!isValidGeneratedFileName("PARAM_MASK.swift")) // SCREAMING_SNAKE #expect(!isValidGeneratedFileName("CSET_a_2_z.swift")) // underscores #expect(!isValidGeneratedFileName("cclosureMarshalBOOLEANFLAGS.swift")) // uppercase run, lowercase start #expect(isValidGeneratedFileName("RGBA.swift")) // acronym type name #expect(isValidGeneratedFileName("GLAPI.swift")) // acronym type name #expect(isValidGeneratedFileName("DNDEvent.swift")) // acronym type name #expect(!isValidGeneratedFileName("Align.txt")) // wrong extension #expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // uppercase run, PascalCase start #expect(isValidGeneratedFileName("PluginAPIFlags.swift")) // 4-run acronym, Gst #expect(isValidGeneratedFileName("AuthNTLM.swift")) // 4-run acronym, Soup } // MARK: - File merging /// A context whose registry contains only the GLib namespace under test. func makeContext(_ ns: Namespace) -> MapContext { let repo = Repository(namespaces: [ns]) let registry = TypeRegistry(repositories: ["GLib": repo]) return MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib") } /// Plans a synthetic namespace through the public `planModules` entry /// point and returns its module plan. func plan(_ ns: Namespace) -> ModulePlan { let repo = Repository(namespaces: [ns]) let analysis = MultiPackageAnalysis( repositories: ["GLib": repo], directDependencies: [:], transitiveDependencies: [:], implicitImports: [:], packageConfigs: [:] ) let registry = TypeRegistry(repositories: ["GLib": repo]) return planModules(analysis: analysis, registry: registry)["GLib"]! } @Test("Free functions merge into Functions.swift with MARK sections") func functionsMerge() { let ns = Namespace( name: "GLib", version: "2.0", functions: [ GlobalFunction(name: "ascii_strup", cIdentifier: "g_ascii_strup", parameters: [Parameter(name: "str", type: .string, cType: "const gchar*")], returnValue: ReturnValue(type: .string, transferOwnership: .full)), GlobalFunction(name: "unichar_istitle", cIdentifier: "g_unichar_istitle", parameters: [Parameter(name: "c", type: .unichar, cType: "gunichar")], returnValue: ReturnValue(type: .boolean)), ] ) let files = renderModule(plan(ns)) let source = files["Functions.swift"] ?? "" #expect(files["asciiStrup.swift"] == nil) #expect(source.contains("public func asciiStrup")) #expect(source.contains("public func unicharIstitle")) #expect(source.contains("// MARK: - Ascii")) #expect(source.contains("// MARK: - Unichar")) } @Test("Constants merge into Constants.swift as lowerCamelCase with C name in docs") func constantsMerge() { let ns = Namespace( name: "GLib", version: "2.0", constants: [Constant(name: "PARAM_MASK", value: "255", type: .int32)] ) let files = renderModule(plan(ns)) let source = files["Constants.swift"] ?? "" #expect(files["PARAM_MASK.swift"] == nil) #expect(source.contains("public nonisolated let paramMask: Int32 = 255")) #expect(source.contains("/// Binds the GIR constant `PARAM_MASK`.")) } @Test("Case-conversion collisions keep the first constant and skip the rest") func constantCollision() { // GLib really has this pair: both convert to `csetA2Z`. let ns = Namespace( name: "GLib", version: "2.0", constants: [ Constant(name: "CSET_A_2_Z", value: "ABC", type: .string), Constant(name: "CSET_a_2_z", value: "abc", type: .string), ] ) let modulePlan = plan(ns) let constants = modulePlan.types.compactMap { plan -> ConstantPlan? in if case .constant(let c) = plan { return c } else { return nil } } #expect(constants.map(\.name) == ["csetA2Z"]) #expect(constants.first?.girName == "CSET_A_2_Z") let collisions = modulePlan.skips.filter { $0.reason == .nameCollision } #expect(collisions.map(\.cIdentifier) == ["CSET_a_2_z"]) } @Test("Cross-category collision: constant and function folding to same Swift name") func constantFunctionCollision() { // GLib has ATOMIC_REF_COUNT_INIT (constant) and // atomic_ref_count_init (function) — both map to atomicRefCountInit. // Functions win over constants, so the constant is skipped. let ns = Namespace( name: "GLib", version: "2.0", functions: [ GlobalFunction(name: "atomic_ref_count_init", cIdentifier: "g_atomic_ref_count_init", parameters: [], returnValue: ReturnValue(type: .void)), ], constants: [ Constant(name: "ATOMIC_REF_COUNT_INIT", value: "1", type: .int32), ] ) let modulePlan = plan(ns) let constants = modulePlan.types.compactMap { plan -> ConstantPlan? in if case .constant(let c) = plan { return c } else { return nil } } let callables = modulePlan.types.compactMap { plan -> CallablePlan? in if case .callable(let fn) = plan { return fn } else { return nil } } #expect(callables.map(\.name) == ["atomicRefCountInit"]) #expect(callables.first?.cIdentifier == "g_atomic_ref_count_init") #expect(constants.isEmpty) let collisions = modulePlan.skips.filter { $0.reason == .nameCollision } #expect(collisions.map(\.cIdentifier) == ["ATOMIC_REF_COUNT_INIT"]) } }