1
0
Fork 0
gobject-generator/Tests/SwiftGtkGenCoreTests/NamingTests.swift
Brendan Szymanski 525aefa7aa Complete Phase E5 tier-6 Adw/Soup/Gst compile-gate compliance
Relaxes the generated-filename PascalCase validator's uppercase-run cap
(>3 -> >4) so legitimate acronym type names (PluginAPIFlags, AuthNTLM)
stop crashing generation.

Fixes four compounding cross-module correctness bugs the larger tier-6
GIR set exposed at scale:
- The duplicate-of-dependency drop only checked simple-name collision,
  wrongly dropping genuinely distinct C types that merely share a
  post-namespace-stripping Swift name (Gst.Object/GstObject vs
  GObject.Object/GObject, plus three tier-4/5 cases: Gdk.AppLaunchContext,
  Gdk.Gravity, Gdk.Rectangle). Now also requires matching cType.
- Qualifying a cross-module reference as "GObject.X" broke wherever a
  raw C struct also named GObject was in scope (every C target's import),
  since Swift resolved the module name to the shadowing struct. GObject's
  Support.swift now exports collision-free GLibObject/GLibValueArray
  aliases used instead.
- Missing `override` keyword: added ancestor-method-selector detection
  (name + parameter labels, same-module only, since none of these members
  are open) so a subclass narrowing an ancestor's return type compiles.
- The pre-existing cross-module inherited-member dedup pass keyed
  ancestors by bare Swift name; its cycle guard falsely self-terminated
  once two classes shared a name, missing real inherited members (e.g.
  GstObject's own ref()/unref() were never recognized as duplicating
  GObject.Object's, producing an illegal redeclaration). Rewired to walk
  by unambiguous GIR name via new ClassPlan.girName/parentGIRName fields.

Also fixes bitfield/enum-typed global constants (wraps the raw literal
in Type(rawValue:)) and a void-returning ref function
(gst_atomic_queue_ref, unlike GstBuffer/GObject's T*-returning
convention) via new RecordPlan.copyReturnsVoid.

Adds tier-6 smoke tests (Gst/Soup/Adw version calls against the real
linked libraries) and refreshes the tier-4/5 skip baselines for the
duplicate-detection fix's legitimate coverage growth. Zero skip-baseline
drift on tiers 1-6; 220/220 unit tests and all tier smoke suites pass.
2026-07-20 21:59:10 -04:00

165 lines
8.2 KiB
Swift

// 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 SwiftGtkGenCore
@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"])
}
}