Files
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

311 lines
8.4 KiB
Go

package resolver
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser/languages"
)
// buildRustGraph extracts each file via the real Rust extractor and
// loads the result into a fresh in-memory graph, mirroring the
// indexer's per-file ingest.
func buildRustGraph(t *testing.T, files map[string]string) graph.Store {
t.Helper()
g := graph.New()
e := languages.NewRustExtractor()
for path, src := range files {
r, err := e.Extract(path, []byte(src))
require.NoError(t, err, "rust extract %s", path)
for _, n := range r.Nodes {
g.AddNode(n)
}
for _, ed := range r.Edges {
g.AddEdge(ed)
}
}
return g
}
// callTargetsFromRust collects the To-end of every call edge leaving
// fromID, so a test can assert on the post-resolution shape of a
// caller's outbound calls.
func callTargetsFromRust(g graph.Store, fromID string) []string {
var out []string
for _, e := range g.GetOutEdges(fromID) {
if e.Kind == graph.EdgeCalls {
out = append(out, e.To)
}
}
return out
}
// TestRustScope_ImplMethodOwner: `Foo::new()` lands on impl Foo's new.
func TestRustScope_ImplMethodOwner(t *testing.T) {
src := `
struct Foo {}
impl Foo {
fn new() -> Foo { Foo {} }
}
struct Bar {}
impl Bar {
fn new() -> Bar { Bar {} }
}
fn run() {
let _f = Foo::new();
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
landed := ResolveRustScopeCalls(g)
require.GreaterOrEqual(t, landed, 1, "expected at least Foo::new to land")
wantNew := "lib.rs::Foo.new"
require.NotNil(t, g.GetNode(wantNew), "Foo.new method node should exist")
targets := callTargetsFromRust(g, "lib.rs::run")
require.Contains(t, targets, wantNew, "Foo::new() should resolve to impl Foo's new, got %v", targets)
// The other type's new must NOT be picked.
require.NotContains(t, targets, "lib.rs::Bar.new")
}
// TestRustScope_SelfReceiver: `self.bar()` inside impl Foo lands on
// Foo.bar; `Self::new()` lands on Foo.new.
func TestRustScope_SelfReceiver(t *testing.T) {
src := `
struct Foo {}
impl Foo {
fn new() -> Foo { Foo {} }
fn bar(&self) {}
fn run(&self) {
self.bar();
let _x = Self::new();
}
}
struct Other {}
impl Other {
fn bar(&self) {}
fn new() -> Other { Other {} }
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
ResolveRustScopeCalls(g)
targets := callTargetsFromRust(g, "lib.rs::Foo.run")
require.Contains(t, targets, "lib.rs::Foo.bar", "self.bar() should resolve to Foo.bar, got %v", targets)
require.Contains(t, targets, "lib.rs::Foo.new", "Self::new() should resolve to Foo.new, got %v", targets)
require.NotContains(t, targets, "lib.rs::Other.bar")
require.NotContains(t, targets, "lib.rs::Other.new")
}
// TestRustScope_ModulePath: `crate::util::helper()` resolves to the
// helper free function. Gortex doesn't node-model the module tree, so
// the trailing-segment binding rides at ast_inferred.
func TestRustScope_ModulePath(t *testing.T) {
src := `
fn helper() {}
fn run() {
crate::util::helper();
super::helper();
self::helper();
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
ResolveRustScopeCalls(g)
wantHelper := "lib.rs::helper"
require.NotNil(t, g.GetNode(wantHelper))
targets := callTargetsFromRust(g, "lib.rs::run")
count := 0
for _, tgt := range targets {
if tgt == wantHelper {
count++
}
}
require.GreaterOrEqual(t, count, 1, "crate::util::helper() should resolve to helper, got %v", targets)
// The module-path bindings should ride at ast_inferred.
for _, e := range g.GetOutEdges("lib.rs::run") {
if e.Kind == graph.EdgeCalls && e.To == wantHelper {
require.Equal(t, graph.OriginASTInferred, e.Origin, "module-path binding should be ast_inferred")
}
}
}
// TestRustScope_LocalShadowsImport: when a local binding (here a
// parameter) shares a name with an imported item, the local wins — the
// shadowed name is NOT resolved to the (module-path) free function.
func TestRustScope_LocalShadowsImport(t *testing.T) {
src := `
fn helper() {}
fn run(helper: u32) {
self::helper();
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
ResolveRustScopeCalls(g)
// The param `helper` shadows the free function, so the module-path
// call must stay unresolved rather than binding to lib.rs::helper.
for _, e := range g.GetOutEdges("lib.rs::run") {
if e.Kind != graph.EdgeCalls {
continue
}
require.NotEqual(t, "lib.rs::helper", e.To,
"shadowed self::helper() must not resolve to the free function while a local `helper` is in scope")
require.True(t, graph.IsUnresolvedTarget(e.To),
"shadowed call should remain unresolved, got %s", e.To)
}
}
// TestRustScope_AmbiguousStaysUnresolved: a `Type::method` call where
// two types of the same name each have the method is ambiguous and must
// be left unresolved.
func TestRustScope_AmbiguousStaysUnresolved(t *testing.T) {
// Two `impl Widget` blocks in two files, both defining `build`.
// Foo::build()-style ambiguity: a single owner name `Widget` mapping
// to two distinct method nodes (different files) → ambiguous.
fileA := `
struct Widget {}
impl Widget {
fn build() -> Widget { Widget {} }
}
`
fileB := `
struct Widget {}
impl Widget {
fn build() -> Widget { Widget {} }
}
fn run() {
let _w = Widget::build();
}
`
g := buildRustGraph(t, map[string]string{"a.rs": fileA, "b.rs": fileB})
ResolveRustScopeCalls(g)
for _, e := range g.GetOutEdges("b.rs::run") {
if e.Kind != graph.EdgeCalls {
continue
}
require.True(t, graph.IsUnresolvedTarget(e.To),
"ambiguous Widget::build() (two same-name owners) must stay unresolved, got %s", e.To)
}
}
// TestRustScope_ConstructorReceiverBind: a `let c = Config::default()` local
// seeds tenv[c]=Config in the extractor, so the selector call `c.tune()` binds
// to Config.tune end to end through the receiver_type path. A same-named method
// on a different type must NOT be picked — the inferred owner is exact.
func TestRustScope_ConstructorReceiverBind(t *testing.T) {
src := `
struct Config {}
impl Config {
fn default() -> Config { Config {} }
fn tune(&self) {}
}
struct Other {}
impl Other {
fn tune(&self) {}
}
fn run() {
let c = Config::default();
c.tune();
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
ResolveRustScopeCalls(g)
targets := callTargetsFromRust(g, "lib.rs::run")
require.Contains(t, targets, "lib.rs::Config.tune",
"c.tune() where c = Config::default() should bind to Config.tune, got %v", targets)
require.NotContains(t, targets, "lib.rs::Other.tune",
"the same-named method on Other must not be picked")
}
// TestRustScope_Idempotent: a second run does not change the resolved
// state nor produce additional rebindings.
func TestRustScope_Idempotent(t *testing.T) {
src := `
struct Foo {}
impl Foo {
fn new() -> Foo { Foo {} }
fn run(&self) {
let _f = Foo::new();
self.helper();
}
fn helper(&self) {}
}
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
first := ResolveRustScopeCalls(g)
require.GreaterOrEqual(t, first, 2)
before := callTargetsFromRust(g, "lib.rs::Foo.run")
second := ResolveRustScopeCalls(g)
require.Equal(t, 0, second, "second run should land no NEW edges (already resolved)")
after := callTargetsFromRust(g, "lib.rs::Foo.run")
require.ElementsMatch(t, before, after, "edge targets must be stable across runs")
}
// TestRustScope_NoOpOnNonRust: the pass is a no-op when there are no
// Rust nodes.
func TestRustScope_NoOpOnNonRust(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "x.go::F", Kind: graph.KindFunction, Name: "F", Language: "go"})
require.Equal(t, 0, ResolveRustScopeCalls(g))
}
// Guard that the extractor stamps the metadata this pass depends on, so
// a future extractor change that drops it fails loudly here rather than
// silently regressing resolution.
func TestRustScope_ExtractorStampsPathMeta(t *testing.T) {
src := `
struct Foo {}
impl Foo { fn new() -> Foo { Foo {} } }
fn run() { let _f = Foo::new(); }
`
g := buildRustGraph(t, map[string]string{"lib.rs": src})
e := findCallWithPathMeta(g, "lib.rs::run")
require.NotNil(t, e, "Foo::new() call edge should carry rust_path meta")
require.Equal(t, "Foo::new", e.Meta["rust_path"])
}
func findCallWithPathMeta(g graph.Store, fromID string) *graph.Edge {
for _, e := range g.GetOutEdges(fromID) {
if e.Kind != graph.EdgeCalls || e.Meta == nil {
continue
}
if _, ok := e.Meta["rust_path"]; ok {
return e
}
}
return nil
}