133 lines
6.6 KiB
Swift
133 lines
6.6 KiB
Swift
// 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 = """
|
|
<?xml version="1.0"?>
|
|
<repository version="1.2"
|
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
|
<c:include name="gobject.h"/>
|
|
<package name="gobject-2.0"/>
|
|
<namespace name="AMod" version="1.0"
|
|
shared-library="libamod.so.0" c:identifier-prefixes="A">
|
|
<class name="Object" c:type="AObject" parent="" glib:type-name="AObject"
|
|
glib:get-type="a_object_get_type">
|
|
<method name="ref" c:identifier="a_object_ref">
|
|
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
|
|
</method>
|
|
<method name="get_data" c:identifier="a_object_get_data">
|
|
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
|
|
<parameters>
|
|
<instance-parameter name="self" transfer-ownership="none">
|
|
<type name="Object" c:type="AObject*"/>
|
|
</instance-parameter>
|
|
<parameter name="key" transfer-ownership="none">
|
|
<type name="utf8" c:type="const char*"/>
|
|
</parameter>
|
|
</parameters>
|
|
</method>
|
|
</class>
|
|
</namespace>
|
|
</repository>
|
|
"""
|
|
let moduleAPath = tmpDir + "/AMod-1.0.gir"
|
|
try moduleAGIR.write(toFile: moduleAPath, atomically: true, encoding: .utf8)
|
|
|
|
let moduleBGIR = """
|
|
<?xml version="1.0"?>
|
|
<repository version="1.2"
|
|
xmlns="http://www.gtk.org/introspection/core/1.0"
|
|
xmlns:c="http://www.gtk.org/introspection/c/1.0"
|
|
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
|
<include name="AMod" version="1.0"/>
|
|
<c:include name="bmod.h"/>
|
|
<package name="bmod-1.0"/>
|
|
<namespace name="BMod" version="1.0"
|
|
shared-library="libbmod.so.0" c:identifier-prefixes="B">
|
|
<class name="Coverage" c:type="BCoverage" parent="AMod.Object"
|
|
glib:type-name="BCoverage" glib:get-type="b_coverage_get_type">
|
|
<method name="ref" c:identifier="b_coverage_ref">
|
|
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
|
|
</method>
|
|
</class>
|
|
<class name="Stream" c:type="BStream" parent="AMod.Object"
|
|
glib:type-name="BStream" glib:get-type="b_stream_get_type">
|
|
<method name="get_data" c:identifier="b_stream_get_data">
|
|
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
|
|
<parameters>
|
|
<instance-parameter name="self" transfer-ownership="none">
|
|
<type name="Stream" c:type="BStream*"/>
|
|
</instance-parameter>
|
|
</parameters>
|
|
</method>
|
|
</class>
|
|
</namespace>
|
|
</repository>
|
|
"""
|
|
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") })
|
|
}
|
|
}
|