Files
zzet--gortex/internal/resolver/csharp_receiver_gate_test.go
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

202 lines
6.9 KiB
Go

package resolver
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
func findCallEdge(g graph.Store, from, to string) *graph.Edge {
for _, e := range g.GetOutEdges(from) {
if e.Kind == graph.EdgeCalls && e.To == to {
return e
}
}
return nil
}
// TestReceiverGate_DemotesUnrelatedAttribution: a `g.Convert()` on a Gadget
// (which has no Convert) locality-falls-back to an unrelated Ukrainian.Convert;
// the receiver-type gate demotes that misattribution to the speculative tier so
// find_usages on Ukrainian.Convert no longer returns the Gadget site.
func TestReceiverGate_DemotesUnrelatedAttribution(t *testing.T) {
g := buildCSharpResolverGraph(t, map[string]string{
"Widget.cs": `namespace App {
public class Gadget {}
public class Widget {
public void DoThing() {
Gadget g = new Gadget();
g.Convert();
}
}
}`,
"Ukr.cs": `namespace App {
public class Ukrainian {
public string Convert() { return "uk"; }
}
}`,
})
New(g).ResolveAll()
edge := findCallEdge(g, "Widget.cs::Widget.DoThing", "Ukr.cs::Ukrainian.Convert")
require.NotNil(t, edge, "locality fallback should have bound the misattribution")
require.Equal(t, "Gadget", edge.Meta["receiver_type"])
require.False(t, edge.IsSpeculative(), "edge is visible before the gate")
n := demoteCSharpMisattributedMemberCalls(g)
require.Equal(t, 1, n)
// Re-fetch: the demotion removes and re-adds the edge, so query the graph
// for its current state rather than trusting the pre-demote pointer.
edge = findCallEdge(g, "Widget.cs::Widget.DoThing", "Ukr.cs::Ukrainian.Convert")
require.NotNil(t, edge)
assert.True(t, edge.IsSpeculative(), "unrelated-type attribution must be demoted")
assert.Equal(t, graph.OriginSpeculative, edge.Origin)
assert.Equal(t, "receiver_type_mismatch", edge.Meta["demoted"])
}
// TestReceiverGate_PreservesValidExtensionBinding: the extension rule binds
// `w.Foo()` to a `static Foo(this Widget)` extension whose target receiver is
// the static host class (unrelated to Widget). The gate must NOT demote that
// valid binding.
func TestReceiverGate_PreservesValidExtensionBinding(t *testing.T) {
g := buildCSharpResolverGraph(t, map[string]string{
"Types.cs": `namespace App {
public class Widget {}
public static class E {
public static int Foo(this Widget w) { return 1; }
}
}`,
"Caller.cs": `namespace App {
public class Runner {
public void Run() {
Widget w = new Widget();
w.Foo();
}
}
}`,
})
New(g).ResolveAll()
edge := findCallEdge(g, "Caller.cs::Runner.Run", "Types.cs::E.Foo")
require.NotNil(t, edge, "w.Foo() should bind to the extension E.Foo")
require.Equal(t, "Widget", edge.Meta["receiver_type"])
require.Equal(t, "extension_method", edge.Meta["resolution"])
n := demoteCSharpMisattributedMemberCalls(g)
assert.Equal(t, 0, n, "a valid extension binding must not be demoted")
edge = findCallEdge(g, "Caller.cs::Runner.Run", "Types.cs::E.Foo")
require.NotNil(t, edge)
assert.False(t, edge.IsSpeculative(), "extension binding stays visible")
}
// TestReceiverGate_PreservesSiblingCall: when a caller has both a misattributed
// and a correctly-typed call to the same target, only the misattribution is
// demoted; the coarse RemoveEdge must not drop the legitimate sibling.
func TestReceiverGate_PreservesSiblingCall(t *testing.T) {
g := buildCSharpResolverGraph(t, map[string]string{
"Types.cs": `namespace App {
public class Gadget {}
public class Ukrainian { public string Convert() { return "uk"; } }
}`,
"Caller.cs": `namespace App {
public class Runner {
public void Run() {
Gadget g = new Gadget();
g.Convert();
Ukrainian u = new Ukrainian();
u.Convert();
}
}
}`,
})
New(g).ResolveAll()
n := demoteCSharpMisattributedMemberCalls(g)
require.Equal(t, 1, n, "only the Gadget-receiver misattribution is demoted")
var visible, speculative int
for _, e := range g.GetOutEdges("Caller.cs::Runner.Run") {
if e.Kind != graph.EdgeCalls || e.To != "Types.cs::Ukrainian.Convert" {
continue
}
if e.IsSpeculative() {
speculative++
} else {
visible++
}
}
assert.Equal(t, 1, visible, "the correctly-typed u.Convert() call stays visible")
assert.Equal(t, 1, speculative, "the Gadget-receiver call is demoted")
}
// TestReceiverGate_PreservesInheritedCall: a call on a subtype receiver bound to
// a method declared on its base type is a legitimate inherited call; the gate
// must NOT demote it (no false negatives on polymorphism).
func TestReceiverGate_PreservesInheritedCall(t *testing.T) {
g := buildCSharpResolverGraph(t, map[string]string{
"Base.cs": `namespace App {
public class Base {
public string Describe() { return "base"; }
}
}`,
"Derived.cs": `namespace App {
public class Derived : Base {}
public class User {
public void Use() {
Derived d = new Derived();
d.Describe();
}
}
}`,
})
New(g).ResolveAll()
edge := findCallEdge(g, "Derived.cs::User.Use", "Base.cs::Base.Describe")
require.NotNil(t, edge, "inherited call should bind to the base method")
require.Equal(t, "Derived", edge.Meta["receiver_type"])
n := demoteCSharpMisattributedMemberCalls(g)
assert.Equal(t, 0, n, "an inherited (related-type) call must not be demoted")
assert.False(t, edge.IsSpeculative())
}
// TestReceiverGate_PreservesCallThroughIncompleteHierarchy: a receiver whose
// base/interface is defined outside the indexed set (another assembly) has an
// unresolved supertype edge, so its hierarchy is only partially known. A call
// that locality-binds to a same-named method on an unrelated indexed type must
// NOT be demoted — the real target may live on the unindexed supertype. Without
// the hierarchy-completeness guard this legitimate polymorphic call is trimmed.
func TestReceiverGate_PreservesCallThroughIncompleteHierarchy(t *testing.T) {
g := buildCSharpResolverGraph(t, map[string]string{
"Shape.cs": `namespace App {
public class Shape {
public void Draw() {}
}
}`,
"Widget.cs": `namespace App {
public class Widget : IExternalDrawable {
public void Render() {
Widget w = new Widget();
w.Draw();
}
}
}`,
})
New(g).ResolveAll()
edge := findCallEdge(g, "Widget.cs::Widget.Render", "Shape.cs::Shape.Draw")
require.NotNil(t, edge, "w.Draw() locality-binds to the only indexed Draw")
require.Equal(t, "Widget", edge.Meta["receiver_type"])
require.False(t, edge.IsSpeculative())
n := demoteCSharpMisattributedMemberCalls(g)
assert.Equal(t, 0, n, "a call through an incompletely-indexed hierarchy must not be demoted")
edge = findCallEdge(g, "Widget.cs::Widget.Render", "Shape.cs::Shape.Draw")
require.NotNil(t, edge)
assert.False(t, edge.IsSpeculative(),
"a possibly-legitimate polymorphic call through an unindexed supertype stays visible")
}