a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
212 lines
8.2 KiB
Go
212 lines
8.2 KiB
Go
package resolver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func swiftMethodNode(g graph.Store, id, selector string) {
|
|
g.AddNode(&graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: lastSeg(id), FilePath: "ios/App.swift", StartLine: 10,
|
|
Language: "swift", Meta: map[string]any{"objc_selector": selector},
|
|
})
|
|
}
|
|
|
|
func objcMethodNode(g graph.Store, id, selector string) {
|
|
g.AddNode(&graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: selector, FilePath: "ios/Legacy.m", StartLine: 5,
|
|
Language: "objc",
|
|
})
|
|
}
|
|
|
|
func bridgeEdgeBetween(g graph.Store, from, to string) *graph.Edge {
|
|
for _, e := range g.GetOutEdges(from) {
|
|
if e.To == to && e.Kind == graph.EdgeReferences && e.Meta != nil {
|
|
if v, _ := e.Meta["via"].(string); v == swiftObjCBridgeVia {
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_BindsSelector(t *testing.T) {
|
|
g := graph.New()
|
|
swiftMethodNode(g, "ios/App.swift::Mover.move", "moveFrom:to:")
|
|
objcMethodNode(g, "ios/Legacy.m::moveFrom:to:", "moveFrom:to:")
|
|
|
|
n := ResolveSwiftObjCBridge(g)
|
|
assert.Equal(t, 1, n)
|
|
|
|
fwd := bridgeEdgeBetween(g, "ios/App.swift::Mover.move", "ios/Legacy.m::moveFrom:to:")
|
|
require.NotNil(t, fwd, "swift→objc bridge edge")
|
|
assert.Equal(t, "moveFrom:to:", fwd.Meta["objc_selector"])
|
|
assert.Equal(t, SynthSwiftObjC, fwd.Meta[MetaSynthesizedBy])
|
|
assert.Equal(t, graph.OriginASTInferred, fwd.Origin)
|
|
|
|
rev := bridgeEdgeBetween(g, "ios/Legacy.m::moveFrom:to:", "ios/App.swift::Mover.move")
|
|
require.NotNil(t, rev, "objc→swift bridge edge (bidirectional)")
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_CandidateBridge(t *testing.T) {
|
|
g := graph.New()
|
|
// A Swift method with no @objc selector metadata — only its base name.
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/VC.swift::VC.cellForRow", Kind: graph.KindMethod, Name: "cellForRow",
|
|
FilePath: "ios/VC.swift", StartLine: 20, Language: "swift",
|
|
})
|
|
objcMethodNode(g, "ios/Legacy.m::cellForRowAtIndexPath:", "cellForRowAtIndexPath:")
|
|
|
|
assert.Equal(t, 1, ResolveSwiftObjCBridge(g))
|
|
|
|
fwd := bridgeEdgeBetween(g, "ios/VC.swift::VC.cellForRow", "ios/Legacy.m::cellForRowAtIndexPath:")
|
|
require.NotNil(t, fwd, "candidate swift→objc bridge")
|
|
assert.Equal(t, "cellForRowAtIndexPath:", fwd.Meta["objc_selector"])
|
|
rev := bridgeEdgeBetween(g, "ios/Legacy.m::cellForRowAtIndexPath:", "ios/VC.swift::VC.cellForRow")
|
|
require.NotNil(t, rev, "candidate objc→swift bridge (bidirectional)")
|
|
}
|
|
|
|
func TestSwiftObjCBaseNameCandidates(t *testing.T) {
|
|
cases := map[string][]string{
|
|
"cellForRowAtIndexPath:": {"cellForRowAtIndexPath", "cellForRow"},
|
|
"moveFrom:to:": {"moveFrom", "move"},
|
|
"initWithFrame:": {"initWithFrame"},
|
|
"tableView:numberOfRowsInSection:": {"tableView"},
|
|
"viewDidLoad": {"viewDidLoad"},
|
|
"dataForKey:": {"dataForKey", "data"},
|
|
}
|
|
for sel, want := range cases {
|
|
assert.ElementsMatch(t, want, swiftObjCBaseNameCandidates(sel), "selector %q", sel)
|
|
}
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_PropertyAccessors(t *testing.T) {
|
|
g := graph.New()
|
|
// A Swift @objc property exposes a getter (`title`) and setter (`setTitle:`).
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/W.swift::Widget.title", Kind: graph.KindField, Name: "title",
|
|
FilePath: "ios/W.swift", StartLine: 2, Language: "swift",
|
|
Meta: map[string]any{"objc_selector": "title", "objc_setter_selector": "setTitle:"},
|
|
})
|
|
// Native ObjC accessor methods.
|
|
objcMethodNode(g, "ios/W.m::title", "title")
|
|
objcMethodNode(g, "ios/W.m::setTitle:", "setTitle:")
|
|
|
|
// One Swift declaration bridged (to both accessors).
|
|
assert.Equal(t, 1, ResolveSwiftObjCBridge(g))
|
|
require.NotNil(t, bridgeEdgeBetween(g, "ios/W.swift::Widget.title", "ios/W.m::title"), "getter bridge")
|
|
require.NotNil(t, bridgeEdgeBetween(g, "ios/W.swift::Widget.title", "ios/W.m::setTitle:"), "setter bridge")
|
|
require.NotNil(t, bridgeEdgeBetween(g, "ios/W.m::setTitle:", "ios/W.swift::Widget.title"), "reverse setter bridge")
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_SuppressGenericCandidates(t *testing.T) {
|
|
// Bare NSObject selectors yield no candidates at all.
|
|
for _, sel := range []string{"init", "copy", "description", "isEqual:", "hash"} {
|
|
assert.Empty(t, swiftObjCBaseNameCandidates(sel), "generic selector %q must not produce candidates", sel)
|
|
}
|
|
// A specific selector keeps its verbatim candidate but drops the generic short form.
|
|
assert.ElementsMatch(t, []string{"initWithFrame"}, swiftObjCBaseNameCandidates("initWithFrame:"))
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_SuppressGenericBridge(t *testing.T) {
|
|
g := graph.New()
|
|
// Swift methods named like NSObject selectors must not candidate-bridge.
|
|
g.AddNode(&graph.Node{ID: "ios/A.swift::A.init", Kind: graph.KindMethod, Name: "init", FilePath: "ios/A.swift", StartLine: 3, Language: "swift"})
|
|
g.AddNode(&graph.Node{ID: "ios/A.swift::A.description", Kind: graph.KindMethod, Name: "description", FilePath: "ios/A.swift", StartLine: 6, Language: "swift"})
|
|
objcMethodNode(g, "ios/B.m::init", "init")
|
|
objcMethodNode(g, "ios/B.m::description", "description")
|
|
|
|
assert.Equal(t, 0, ResolveSwiftObjCBridge(g))
|
|
assert.Nil(t, bridgeEdgeBetween(g, "ios/A.swift::A.init", "ios/B.m::init"))
|
|
assert.Nil(t, bridgeEdgeBetween(g, "ios/A.swift::A.description", "ios/B.m::description"))
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_ExplicitSelector(t *testing.T) {
|
|
g := graph.New()
|
|
swiftMethodNode(g, "ios/App.swift::Mover.moveCustom", "customMove:")
|
|
objcMethodNode(g, "ios/Legacy.m::customMove:", "customMove:")
|
|
objcMethodNode(g, "ios/Legacy.m::unrelated:", "unrelated:")
|
|
|
|
assert.Equal(t, 1, ResolveSwiftObjCBridge(g))
|
|
assert.NotNil(t, bridgeEdgeBetween(g, "ios/App.swift::Mover.moveCustom", "ios/Legacy.m::customMove:"))
|
|
assert.Nil(t, bridgeEdgeBetween(g, "ios/App.swift::Mover.moveCustom", "ios/Legacy.m::unrelated:"))
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_NoMatchNoEdge(t *testing.T) {
|
|
g := graph.New()
|
|
swiftMethodNode(g, "ios/App.swift::Mover.move", "moveFrom:to:")
|
|
objcMethodNode(g, "ios/Legacy.m::other:", "other:")
|
|
assert.Equal(t, 0, ResolveSwiftObjCBridge(g))
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_Idempotent(t *testing.T) {
|
|
g := graph.New()
|
|
swiftMethodNode(g, "ios/App.swift::Mover.move", "moveFrom:to:")
|
|
objcMethodNode(g, "ios/Legacy.m::moveFrom:to:", "moveFrom:to:")
|
|
first := ResolveSwiftObjCBridge(g)
|
|
second := ResolveSwiftObjCBridge(g)
|
|
assert.Equal(t, first, second)
|
|
// Exactly two bridge edges (one each direction) survive dedup.
|
|
count := 0
|
|
for e := range g.EdgesByKind(graph.EdgeReferences) {
|
|
if e != nil && e.Meta != nil {
|
|
if v, _ := e.Meta["via"].(string); v == swiftObjCBridgeVia {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, 2, count)
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_ProtocolConformance(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/P.swift::Drawable", Kind: graph.KindInterface, Name: "Drawable",
|
|
FilePath: "ios/P.swift", StartLine: 1, Language: "swift",
|
|
Meta: map[string]any{"objc": true},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/C.m::Widget", Kind: graph.KindType, Name: "Widget",
|
|
FilePath: "ios/C.m", StartLine: 1, Language: "objc",
|
|
Meta: map[string]any{"objc_protocols": "Drawable"},
|
|
})
|
|
|
|
ResolveSwiftObjCBridge(g)
|
|
|
|
var impl *graph.Edge
|
|
for _, e := range g.GetOutEdges("ios/C.m::Widget") {
|
|
if e.To == "ios/P.swift::Drawable" && e.Kind == graph.EdgeImplements {
|
|
impl = e
|
|
}
|
|
}
|
|
require.NotNil(t, impl, "ObjC class adopting a Swift @objc protocol should get EdgeImplements")
|
|
assert.Equal(t, SynthSwiftObjC, impl.Meta[MetaSynthesizedBy])
|
|
assert.Equal(t, "objc", impl.Meta["bridge_from_lang"])
|
|
assert.Equal(t, "swift", impl.Meta["bridge_to_lang"])
|
|
}
|
|
|
|
func TestResolveSwiftObjCBridge_NonObjCProtocolNotBridged(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/P.swift::Plain", Kind: graph.KindInterface, Name: "Plain",
|
|
FilePath: "ios/P.swift", StartLine: 1, Language: "swift",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "ios/C.m::Widget", Kind: graph.KindType, Name: "Widget",
|
|
FilePath: "ios/C.m", StartLine: 1, Language: "objc",
|
|
Meta: map[string]any{"objc_protocols": "Plain"},
|
|
})
|
|
|
|
ResolveSwiftObjCBridge(g)
|
|
|
|
for _, e := range g.GetOutEdges("ios/C.m::Widget") {
|
|
if e.To == "ios/P.swift::Plain" && e.Kind == graph.EdgeImplements {
|
|
t.Errorf("a non-@objc Swift protocol must not be bridged")
|
|
}
|
|
}
|
|
}
|