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
311 lines
8.4 KiB
Go
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
|
|
}
|