Complete filename/naming remediation with PascalCase convention
This commit is contained in:
parent
b61512c4eb
commit
321b42f9af
7 changed files with 429 additions and 17 deletions
|
|
@ -230,4 +230,34 @@ struct RealGIRParsingTests {
|
|||
}
|
||||
#expect(!nonIntrospectable.isEmpty, "GTK has non-introspectable methods that must be recognized")
|
||||
}
|
||||
|
||||
/// Every filename rendered from the real tier-1 GIRs must follow the
|
||||
/// PascalCase convention — the end-to-end lock that keeps future codegen
|
||||
/// from reintroducing symbol-named files (`boxedFree.swift`,
|
||||
/// `PARAM_MASK.swift`, `cclosureMarshalBOOLEANFLAGS.swift`).
|
||||
@Test(.enabled(if: hasGIR("GLib-2.0.gir") && hasGIR("GObject-2.0.gir")))
|
||||
func generatedFilenamesArePascalCase() throws {
|
||||
let repositories = [
|
||||
"GLib": try Self.parse("GLib-2.0.gir"),
|
||||
"GObject": try Self.parse("GObject-2.0.gir"),
|
||||
]
|
||||
let analysis = MultiPackageAnalysis(
|
||||
repositories: repositories,
|
||||
directDependencies: [:], transitiveDependencies: [:],
|
||||
implicitImports: [:], packageConfigs: [:]
|
||||
)
|
||||
let registry = TypeRegistry(repositories: repositories)
|
||||
let plans = planModules(analysis: analysis, registry: registry)
|
||||
#expect(plans.count == 2)
|
||||
for (module, plan) in plans {
|
||||
let files = renderModule(plan)
|
||||
let violations = files.keys.filter { !isValidGeneratedFileName($0) }
|
||||
#expect(violations.isEmpty,
|
||||
"\(module) emitted non-PascalCase filenames: \(violations.sorted())")
|
||||
// The merge targets must actually exist — an empty module would
|
||||
// vacuously pass the check above.
|
||||
#expect(files["Functions.swift"] != nil)
|
||||
#expect(files["Constants.swift"] != nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ struct FunctionGenerationTests {
|
|||
}
|
||||
|
||||
/// Renders a single planned callable to Swift source via the public module
|
||||
/// renderer, returning the callable's file body.
|
||||
/// renderer, returning the merged `Functions.swift` body it lands in.
|
||||
func render(_ plan: CallablePlan) -> String {
|
||||
let module = ModulePlan(
|
||||
module: "GLib",
|
||||
|
|
@ -38,7 +38,7 @@ struct FunctionGenerationTests {
|
|||
coverage: CoverageStats()
|
||||
)
|
||||
let files = renderModule(module)
|
||||
return files["\(plan.name).swift"] ?? ""
|
||||
return files["Functions.swift"] ?? ""
|
||||
}
|
||||
|
||||
// MARK: - Plannable callables
|
||||
|
|
|
|||
160
Tests/SwiftGtkGenCoreTests/NamingTests.swift
Normal file
160
Tests/SwiftGtkGenCoreTests/NamingTests.swift
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// 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")) // infra 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
|
||||
#expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // run > 3 caps
|
||||
#expect(!isValidGeneratedFileName("Align.txt")) // wrong extension
|
||||
}
|
||||
|
||||
// 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"])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue