// InheritedMemberTests.swift
// Covers Phase E2's global (cross-module) inherited-member post-pass in
// `planModules`: a class in one Swift module that redeclares a same-name,
// same-arity method already provided by an ancestor class in a DIFFERENT
// module (e.g. `Pango.Coverage.ref()`/`unref()` vs. `GObject.Object.ref()`/
// `unref()`) must have its own copy dropped — Swift rejects the redundant
// redeclaration across module boundaries. A same-name method with a
// DIFFERENT arity (legitimate overload, e.g. `MemoryOutputStream.getData()`
// vs. inherited `Object.getData(key:)`) must NOT be dropped.
import Foundation
import Testing
@testable import GObjectGeneratorCore
@Suite("Cross-module inherited-member dedup")
struct InheritedMemberTests {
/// Builds a two-module `MultiPackageAnalysis` + `TypeRegistry`: module A
/// declares root class `Object` with `ref()` (no args) and `getData(key:)`
/// (one arg); module B declares `Coverage: Object` (redeclaring `ref()`
/// with the SAME arity) and `Stream: Object` (redeclaring `getData()`
/// with a DIFFERENT arity — a legitimate overload).
private func planTwoModules() throws -> [String: ModulePlan] {
let tmpDir = NSTemporaryDirectory() + "inherited_member_test_\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: tmpDir, withIntermediateDirectories: true)
let moduleAGIR = """
"""
let moduleAPath = tmpDir + "/AMod-1.0.gir"
try moduleAGIR.write(toFile: moduleAPath, atomically: true, encoding: .utf8)
let moduleBGIR = """
"""
let moduleBPath = tmpDir + "/BMod-1.0.gir"
try moduleBGIR.write(toFile: moduleBPath, atomically: true, encoding: .utf8)
let packages: [PackageEntry] = [
PackageEntry(name: "AMod", girPath: moduleAPath),
PackageEntry(name: "BMod", girPath: moduleBPath),
]
let config = MonorepoConfig(outputDir: tmpDir, packages: packages)
let analysis = try MultiPackageAnalyzer(config: config).analyze()
let registry = TypeRegistry(repositories: analysis.repositories)
return planModules(analysis: analysis, registry: registry)
}
@Test("A same-arity method redeclared across a cross-module inheritance edge is dropped")
func crossModuleSameArityCollisionIsDropped() throws {
let plans = try planTwoModules()
guard let bPlan = plans["BMod"],
case .class(let coverage)? = bPlan.types.first(where: {
if case .class(let c) = $0 { return c.name == "Coverage" }
return false
}) else {
Issue.record("expected BMod.Coverage to plan"); return
}
#expect(!coverage.methods.contains { $0.name == "ref" })
#expect(bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Coverage.ref") })
}
@Test("A different-arity method with the same base name across modules is kept (legitimate overload)")
func crossModuleDifferentArityOverloadIsKept() throws {
let plans = try planTwoModules()
guard let bPlan = plans["BMod"],
case .class(let stream)? = bPlan.types.first(where: {
if case .class(let c) = $0 { return c.name == "Stream" }
return false
}) else {
Issue.record("expected BMod.Stream to plan"); return
}
// Stream.getData() (0 args) must survive despite Object.getData(key:)
// (1 arg) sharing the base name — different arity, not a collision.
#expect(stream.methods.contains { $0.name == "getData" })
#expect(!bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Stream.getData") })
}
}