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
264 lines
12 KiB
Go
264 lines
12 KiB
Go
package resolver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// fnValueCandidateEdge mirrors what the per-language capture emits: a
|
|
// placeholder reference into the fn-value namespace, carrying the captured name
|
|
// in Meta for the gate to bind.
|
|
func fnValueCandidateEdge(from, name, file string, line int) *graph.Edge {
|
|
return &graph.Edge{
|
|
From: from,
|
|
To: fnValueUnresolvedPrefix + name,
|
|
Kind: graph.EdgeReferences,
|
|
FilePath: file,
|
|
Line: line,
|
|
Origin: graph.OriginSpeculative,
|
|
Meta: map[string]any{
|
|
"via": fnValueCandidateVia,
|
|
"fn_value_name": name,
|
|
},
|
|
}
|
|
}
|
|
|
|
const fnValueUnresolvedPrefix = "unresolved::fnvalue::"
|
|
|
|
func boundCallbackEdge(g graph.Store, from, to string) *graph.Edge {
|
|
for _, e := range g.GetOutEdges(from) {
|
|
if e.To != to || e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v == fnValueRegistrationVia {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestCallbackGateRejectsUnboundIdentifiers is the A3 named test: the gate binds
|
|
// a captured value-position identifier that names a same-file function and
|
|
// drops one that resolves to nothing, and the bound edge rides a filterable
|
|
// provenance tier rather than a flat heuristic flag.
|
|
func TestCallbackGateRejectsUnboundIdentifiers(t *testing.T) {
|
|
g := graph.New()
|
|
// A real same-file function the registration can bind to.
|
|
g.AddNode(&graph.Node{
|
|
ID: "router.go::handler", Kind: graph.KindFunction, Name: "handler",
|
|
FilePath: "router.go", StartLine: 10, Language: "go",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "router.go::register", Kind: graph.KindFunction, Name: "register",
|
|
FilePath: "router.go", StartLine: 3, Language: "go",
|
|
})
|
|
// One bindable candidate (handler exists) and one unbound (ghost is a
|
|
// local / undefined name — never a function node in this file).
|
|
g.AddEdge(fnValueCandidateEdge("router.go::register", "handler", "router.go", 4))
|
|
g.AddEdge(fnValueCandidateEdge("router.go::register", "ghost", "router.go", 5))
|
|
// A builtin-shaped candidate must also be skipped before any lookup.
|
|
g.AddEdge(fnValueCandidateEdge("router.go::register", "nil", "router.go", 6))
|
|
|
|
landed := ResolveFnValueCallbacks(g)
|
|
assert.Equal(t, 1, landed, "only the bound candidate should land")
|
|
|
|
bound := boundCallbackEdge(g, "router.go::register", "router.go::handler")
|
|
require.NotNil(t, bound, "the bound handler should produce a callback-registration edge")
|
|
assert.Equal(t, graph.EdgeReferences, bound.Kind)
|
|
assert.Equal(t, graph.OriginASTInferred, bound.Origin, "callback edge must ride a filterable tier")
|
|
assert.Equal(t, SynthFnValueCallback, bound.Meta[MetaSynthesizedBy])
|
|
assert.Equal(t, "handler", bound.Meta["fn_value_name"])
|
|
|
|
// The unbound and builtin candidates must not have produced any real edge.
|
|
for _, e := range g.GetOutEdges("router.go::register") {
|
|
if e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v == fnValueRegistrationVia {
|
|
assert.Equal(t, "router.go::handler", e.To, "no registration edge should bind ghost/nil")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCallbackGateIdempotent confirms a second pass lands nothing new — the
|
|
// synthesizer is a safe full-recompute.
|
|
func TestCallbackGateIdempotent(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{
|
|
ID: "h.go::onClick", Kind: graph.KindFunction, Name: "onClick",
|
|
FilePath: "h.go", StartLine: 8, Language: "go",
|
|
})
|
|
g.AddEdge(fnValueCandidateEdge("h.go::wire", "onClick", "h.go", 2))
|
|
|
|
first := ResolveFnValueCallbacks(g)
|
|
second := ResolveFnValueCallbacks(g)
|
|
assert.Equal(t, 1, first)
|
|
assert.Equal(t, 1, second, "the bound edge re-derives identically; AddEdge dedupes")
|
|
}
|
|
|
|
// ungatedFnValueCandidateEdge is a qualified-path candidate the gate may resolve
|
|
// cross-module.
|
|
func ungatedFnValueCandidateEdge(from, name, file string, line int) *graph.Edge {
|
|
e := fnValueCandidateEdge(from, name, file, line)
|
|
e.Meta["fn_value_ungated"] = true
|
|
return e
|
|
}
|
|
|
|
// TestCallbackGateSameFileTier pins the high-confidence tier for a same-file
|
|
// binding.
|
|
func TestCallbackGateSameFileTier(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "a.go::handler", Kind: graph.KindFunction, Name: "handler", FilePath: "a.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "a.go::register", Kind: graph.KindFunction, Name: "register", FilePath: "a.go", Language: "go"})
|
|
g.AddEdge(fnValueCandidateEdge("a.go::register", "handler", "a.go", 4))
|
|
|
|
assert.Equal(t, 1, ResolveFnValueCallbacks(g))
|
|
e := boundCallbackEdge(g, "a.go::register", "a.go::handler")
|
|
require.NotNil(t, e)
|
|
assert.Equal(t, 0.6, e.Confidence, "same-file binding rides the high-confidence tier")
|
|
}
|
|
|
|
// TestCallbackGateCrossModuleUngated pins that a qualified-path (ungated)
|
|
// candidate binds to a uniquely-named function cross-module at a lower tier.
|
|
func TestCallbackGateCrossModuleUngated(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "lib.rs::process", Kind: graph.KindFunction, Name: "process", FilePath: "lib.rs", Language: "rust"})
|
|
g.AddNode(&graph.Node{ID: "main.rs::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.rs", Language: "rust"})
|
|
g.AddEdge(ungatedFnValueCandidateEdge("main.rs::run", "process", "main.rs", 3))
|
|
|
|
assert.Equal(t, 1, ResolveFnValueCallbacks(g))
|
|
e := boundCallbackEdge(g, "main.rs::run", "lib.rs::process")
|
|
require.NotNil(t, e, "cross-module ungated candidate binds")
|
|
assert.Equal(t, 0.45, e.Confidence, "cross-module binding rides the lower tier")
|
|
}
|
|
|
|
// TestCallbackGateCrossModuleAmbiguousDropped pins that an ungated candidate
|
|
// matching more than one function anywhere is refused.
|
|
func TestCallbackGateCrossModuleAmbiguousDropped(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "a.rs::process", Kind: graph.KindFunction, Name: "process", FilePath: "a.rs", Language: "rust"})
|
|
g.AddNode(&graph.Node{ID: "b.rs::process", Kind: graph.KindFunction, Name: "process", FilePath: "b.rs", Language: "rust"})
|
|
g.AddNode(&graph.Node{ID: "main.rs::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.rs", Language: "rust"})
|
|
g.AddEdge(ungatedFnValueCandidateEdge("main.rs::run", "process", "main.rs", 3))
|
|
|
|
assert.Equal(t, 0, ResolveFnValueCallbacks(g), "ambiguous cross-module candidate dropped")
|
|
}
|
|
|
|
// TestCallbackGateNonUngatedStaysSameFile pins that a non-ungated candidate is
|
|
// never resolved cross-module even when a unique match exists elsewhere.
|
|
func TestCallbackGateNonUngatedStaysSameFile(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "lib.go::process", Kind: graph.KindFunction, Name: "process", FilePath: "lib.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "main.go::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.go", Language: "go"})
|
|
g.AddEdge(fnValueCandidateEdge("main.go::run", "process", "main.go", 3)) // not ungated
|
|
|
|
assert.Equal(t, 0, ResolveFnValueCallbacks(g), "non-ungated candidate never binds cross-module")
|
|
}
|
|
|
|
func specialFnValueEdge(from, name, file string, line int, recvHint string) *graph.Edge {
|
|
e := fnValueCandidateEdge(from, name, file, line)
|
|
e.Meta["fn_ref_form"] = "special"
|
|
if recvHint != "" {
|
|
e.Meta["fn_ref_recv_hint"] = recvHint
|
|
}
|
|
return e
|
|
}
|
|
|
|
// TestCallbackGateSpecialSelfMember pins that a `this.m` reference binds to the
|
|
// enclosing type's method, never a coincidentally-named top-level function.
|
|
func TestCallbackGateSpecialSelfMember(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "c.ts::C.handle", Kind: graph.KindMethod, Name: "handle", FilePath: "c.ts", Meta: map[string]any{"receiver": "C"}})
|
|
g.AddNode(&graph.Node{ID: "c.ts::C.wire", Kind: graph.KindMethod, Name: "wire", FilePath: "c.ts", Meta: map[string]any{"receiver": "C"}})
|
|
g.AddNode(&graph.Node{ID: "other.ts::handle", Kind: graph.KindFunction, Name: "handle", FilePath: "other.ts"})
|
|
g.AddEdge(specialFnValueEdge("c.ts::C.wire", "handle", "c.ts", 5, "<self>"))
|
|
|
|
ResolveFnValueCallbacks(g)
|
|
bound := boundCallbackEdge(g, "c.ts::C.wire", "c.ts::C.handle")
|
|
require.NotNil(t, bound, "this.handle binds to the enclosing class's handle")
|
|
assert.Equal(t, "special", bound.Meta["fn_ref_form"])
|
|
assert.Equal(t, graph.OriginASTResolved, bound.Origin)
|
|
assert.Nil(t, boundCallbackEdge(g, "c.ts::C.wire", "other.ts::handle"), "must not bind the top-level handle")
|
|
}
|
|
|
|
// TestCallbackGateSpecialTypeQualified pins that `Foo::bar` binds to type Foo's
|
|
// bar method, not a same-named free function.
|
|
func TestCallbackGateSpecialTypeQualified(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "f.java::Foo.bar", Kind: graph.KindMethod, Name: "bar", FilePath: "f.java", Meta: map[string]any{"receiver": "Foo"}})
|
|
g.AddNode(&graph.Node{ID: "g.java::bar", Kind: graph.KindFunction, Name: "bar", FilePath: "g.java"})
|
|
g.AddNode(&graph.Node{ID: "m.java::M.run", Kind: graph.KindMethod, Name: "run", FilePath: "m.java", Meta: map[string]any{"receiver": "M"}})
|
|
e := specialFnValueEdge("m.java::M.run", "bar", "m.java", 3, "Foo")
|
|
e.Meta["fn_value_ungated"] = true
|
|
g.AddEdge(e)
|
|
|
|
ResolveFnValueCallbacks(g)
|
|
bound := boundCallbackEdge(g, "m.java::M.run", "f.java::Foo.bar")
|
|
require.NotNil(t, bound, "Foo::bar binds to Foo's bar method")
|
|
assert.Equal(t, graph.OriginASTResolved, bound.Origin)
|
|
assert.Equal(t, "special", bound.Meta["fn_ref_form"])
|
|
assert.Nil(t, boundCallbackEdge(g, "m.java::M.run", "g.java::bar"), "must not bind the free function")
|
|
}
|
|
|
|
// TestCallbackGateSkipGateUnique pins a curated-HOF string callable binding to
|
|
// the sole repo-wide function of that name at the gate-skip tier.
|
|
func TestCallbackGateSkipGateUnique(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "lib.php::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "lib.php"})
|
|
g.AddNode(&graph.Node{ID: "main.php::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.php"})
|
|
e := fnValueCandidateEdge("main.php::run", "helper", "main.php", 3)
|
|
e.Meta["skip_gate"] = true
|
|
e.Meta["fn_ref_form"] = "php_string_callable"
|
|
g.AddEdge(e)
|
|
|
|
ResolveFnValueCallbacks(g)
|
|
bound := boundCallbackEdge(g, "main.php::run", "lib.php::helper")
|
|
require.NotNil(t, bound, "unique string callable binds cross-file")
|
|
assert.Equal(t, 0.5, bound.Confidence)
|
|
assert.Equal(t, "php_string_callable", bound.Meta["fn_ref_form"])
|
|
}
|
|
|
|
// TestCallbackGateSkipGateAmbiguousDropped pins that a string callable whose
|
|
// name has two definitions is dropped.
|
|
func TestCallbackGateSkipGateAmbiguousDropped(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "a.php::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "a.php"})
|
|
g.AddNode(&graph.Node{ID: "b.php::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "b.php"})
|
|
g.AddNode(&graph.Node{ID: "main.php::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.php"})
|
|
e := fnValueCandidateEdge("main.php::run", "helper", "main.php", 3)
|
|
e.Meta["skip_gate"] = true
|
|
g.AddEdge(e)
|
|
|
|
assert.Equal(t, 0, ResolveFnValueCallbacks(g), "ambiguous string callable dropped")
|
|
}
|
|
|
|
// TestCallbackGateSkipGateStaticString pins that a `'Foo::bar'` string callable
|
|
// binds to Foo's bar method, not a same-named free function.
|
|
func TestCallbackGateSkipGateStaticString(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "f.php::Foo.bar", Kind: graph.KindMethod, Name: "bar", FilePath: "f.php", Meta: map[string]any{"receiver": "Foo"}})
|
|
g.AddNode(&graph.Node{ID: "g.php::bar", Kind: graph.KindFunction, Name: "bar", FilePath: "g.php"})
|
|
g.AddNode(&graph.Node{ID: "main.php::run", Kind: graph.KindFunction, Name: "run", FilePath: "main.php"})
|
|
e := fnValueCandidateEdge("main.php::run", "bar", "main.php", 3)
|
|
e.Meta["skip_gate"] = true
|
|
e.Meta["fn_ref_recv_hint"] = "Foo"
|
|
g.AddEdge(e)
|
|
|
|
ResolveFnValueCallbacks(g)
|
|
require.NotNil(t, boundCallbackEdge(g, "main.php::run", "f.php::Foo.bar"), "Foo::bar binds to Foo.bar")
|
|
assert.Nil(t, boundCallbackEdge(g, "main.php::run", "g.php::bar"), "must not bind the free function")
|
|
}
|
|
|
|
func TestResolveMemberByType(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "a::Foo.m", Kind: graph.KindMethod, Name: "m", Meta: map[string]any{"receiver": "Foo"}})
|
|
g.AddNode(&graph.Node{ID: "a::Bar.m", Kind: graph.KindMethod, Name: "m", Meta: map[string]any{"receiver": "Bar"}})
|
|
assert.Equal(t, "a::Foo.m", resolveMemberByType(g, "Foo", "m"))
|
|
assert.Equal(t, "a::Bar.m", resolveMemberByType(g, "Bar", "m"))
|
|
assert.Equal(t, "", resolveMemberByType(g, "Baz", "m"), "unknown type does not bind")
|
|
}
|