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
347 lines
13 KiB
Go
347 lines
13 KiB
Go
package resolver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// TestScope_CStaticPreference asserts that a C `static` function in
|
|
// the caller's file wins over a globally-linked function with the
|
|
// same name in another file. Without the scope rule, the generic
|
|
// locality cascade would still prefer the same-directory candidate;
|
|
// this test puts both candidates in the SAME directory so only the
|
|
// static-linkage stamp can disambiguate.
|
|
func TestScope_CStaticPreference(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"})
|
|
g.AddNode(&graph.Node{ID: "pkg/b.c", Kind: graph.KindFile, FilePath: "pkg/b.c", Language: "c"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
Meta: map[string]any{MetaScopeStatic: true},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/b.c::helper", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "pkg/b.c", Language: "c",
|
|
})
|
|
e := &graph.Edge{
|
|
From: "pkg/a.c::caller", To: "unresolved::helper",
|
|
Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "pkg/a.c::helper", e.To,
|
|
"file-local static must win over a same-name candidate in another file")
|
|
}
|
|
|
|
// TestScope_CppSameNamespacePreference asserts a C++ function in the
|
|
// caller's namespace beats a same-named function in a different
|
|
// namespace, even when both live in the same directory.
|
|
func TestScope_CppSameNamespacePreference(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "src/a.cpp", Kind: graph.KindFile, FilePath: "src/a.cpp", Language: "cpp"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/a.cpp::caller", Kind: graph.KindFunction, Name: "caller",
|
|
FilePath: "src/a.cpp", Language: "cpp",
|
|
Meta: map[string]any{MetaScopeNamespace: "app"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/a.cpp::helper#app", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "src/a.cpp", Language: "cpp",
|
|
Meta: map[string]any{MetaScopeNamespace: "app"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/a.cpp::helper#util", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "src/a.cpp", Language: "cpp",
|
|
Meta: map[string]any{MetaScopeNamespace: "util"},
|
|
})
|
|
e := &graph.Edge{
|
|
From: "src/a.cpp::caller", To: "unresolved::helper",
|
|
Kind: graph.EdgeCalls, FilePath: "src/a.cpp", Line: 3,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "src/a.cpp::helper#app", e.To,
|
|
"same-namespace candidate must win over a same-directory cross-namespace one")
|
|
}
|
|
|
|
// TestScope_CppADLViaArgType asserts argument-dependent lookup: an
|
|
// unqualified call to `foo` whose argument hints at a type in
|
|
// namespace N must bind to a `foo` defined in namespace N, even when
|
|
// the caller's namespace has no such function.
|
|
func TestScope_CppADLViaArgType(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "src/a.cpp", Kind: graph.KindFile, FilePath: "src/a.cpp", Language: "cpp"})
|
|
g.AddNode(&graph.Node{ID: "src/b.cpp", Kind: graph.KindFile, FilePath: "src/b.cpp", Language: "cpp"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/a.cpp::caller", Kind: graph.KindFunction, Name: "caller",
|
|
FilePath: "src/a.cpp", Language: "cpp",
|
|
Meta: map[string]any{MetaScopeNamespace: "app"},
|
|
})
|
|
// The only "process" candidate is in namespace `util` — same-
|
|
// namespace lookup would miss it; ADL via the arg-type hint
|
|
// (`util::Widget`) is the only path that lands the bind.
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/b.cpp::process#util", Kind: graph.KindFunction, Name: "process",
|
|
FilePath: "src/b.cpp", Language: "cpp",
|
|
Meta: map[string]any{MetaScopeNamespace: "util"},
|
|
})
|
|
e := &graph.Edge{
|
|
From: "src/a.cpp::caller", To: "unresolved::process",
|
|
Kind: graph.EdgeCalls, FilePath: "src/a.cpp", Line: 4,
|
|
Meta: map[string]any{MetaScopeArgTypes: "util::Widget"},
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "src/b.cpp::process#util", e.To,
|
|
"ADL must walk arg-type namespaces when same-namespace lookup misses")
|
|
}
|
|
|
|
// TestScope_JavaEnclosingClassPreference asserts an unqualified call
|
|
// inside class X binds to X's method first, even when a same-named
|
|
// method exists on another class in the same package.
|
|
func TestScope_JavaEnclosingClassPreference(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "app/User.java", Kind: graph.KindFile, FilePath: "app/User.java", Language: "java"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/User.java::User", Kind: graph.KindType, Name: "User",
|
|
FilePath: "app/User.java", Language: "java",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/User.java::User.save", Kind: graph.KindMethod, Name: "save",
|
|
FilePath: "app/User.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "User", MetaScopeClass: "User"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/User.java::User.validate", Kind: graph.KindMethod, Name: "validate",
|
|
FilePath: "app/User.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "User", MetaScopeClass: "User"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Other.java::Other.validate", Kind: graph.KindMethod, Name: "validate",
|
|
FilePath: "app/Other.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"},
|
|
})
|
|
// User.save() calls validate() unqualified — must bind to User.validate.
|
|
e := &graph.Edge{
|
|
From: "app/User.java::User.save", To: "unresolved::*.validate",
|
|
Kind: graph.EdgeCalls, FilePath: "app/User.java", Line: 12,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "app/User.java::User.validate", e.To,
|
|
"unqualified call in class X must bind to X's method, not a same-named one elsewhere")
|
|
}
|
|
|
|
// TestScope_JavaSuperChainWalk asserts the scope resolver walks the
|
|
// extends chain when the unqualified call doesn't bind to the
|
|
// enclosing class.
|
|
func TestScope_JavaSuperChainWalk(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "app/Base.java", Kind: graph.KindFile, FilePath: "app/Base.java", Language: "java"})
|
|
g.AddNode(&graph.Node{ID: "app/Child.java", Kind: graph.KindFile, FilePath: "app/Child.java", Language: "java"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Base.java::Base", Kind: graph.KindType, Name: "Base",
|
|
FilePath: "app/Base.java", Language: "java",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Child.java::Child", Kind: graph.KindType, Name: "Child",
|
|
FilePath: "app/Child.java", Language: "java",
|
|
Meta: map[string]any{MetaScopeParentClass: "Base"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Base.java::Base.helper", Kind: graph.KindMethod, Name: "helper",
|
|
FilePath: "app/Base.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "Base", MetaScopeClass: "Base"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Child.java::Child.run", Kind: graph.KindMethod, Name: "run",
|
|
FilePath: "app/Child.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "Child", MetaScopeClass: "Child"},
|
|
})
|
|
// Decoy: another class has a same-name `helper`.
|
|
g.AddNode(&graph.Node{
|
|
ID: "app/Other.java::Other.helper", Kind: graph.KindMethod, Name: "helper",
|
|
FilePath: "app/Other.java", Language: "java",
|
|
Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"},
|
|
})
|
|
// Child.run() calls helper() — should walk to Base.helper.
|
|
e := &graph.Edge{
|
|
From: "app/Child.java::Child.run", To: "unresolved::*.helper",
|
|
Kind: graph.EdgeCalls, FilePath: "app/Child.java", Line: 4,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "app/Base.java::Base.helper", e.To,
|
|
"super-chain walk must land helper() on the parent class")
|
|
}
|
|
|
|
// TestScope_PhpParentCall asserts that `parent::foo()` in PHP binds
|
|
// to the parent class's foo, even with a same-name decoy elsewhere.
|
|
func TestScope_PhpParentCall(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "src/Base.php", Kind: graph.KindFile, FilePath: "src/Base.php", Language: "php"})
|
|
g.AddNode(&graph.Node{ID: "src/Child.php", Kind: graph.KindFile, FilePath: "src/Child.php", Language: "php"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Base.php::Base", Kind: graph.KindType, Name: "Base",
|
|
FilePath: "src/Base.php", Language: "php",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Child.php::Child", Kind: graph.KindType, Name: "Child",
|
|
FilePath: "src/Child.php", Language: "php",
|
|
Meta: map[string]any{MetaScopeParentClass: "Base"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Base.php::Base.handle", Kind: graph.KindMethod, Name: "handle",
|
|
FilePath: "src/Base.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Base", MetaScopeClass: "Base"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Child.php::Child.handle", Kind: graph.KindMethod, Name: "handle",
|
|
FilePath: "src/Child.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Child", MetaScopeClass: "Child"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Other.php::Other.handle", Kind: graph.KindMethod, Name: "handle",
|
|
FilePath: "src/Other.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"},
|
|
})
|
|
// Child.handle() calls parent::handle() — must bind to Base.handle.
|
|
e := &graph.Edge{
|
|
From: "src/Child.php::Child.handle", To: "unresolved::*.handle",
|
|
Kind: graph.EdgeCalls, FilePath: "src/Child.php", Line: 7,
|
|
Meta: map[string]any{MetaScopeKind: ScopeKindParent},
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "src/Base.php::Base.handle", e.To,
|
|
"parent:: must walk extends chain to Base.handle")
|
|
}
|
|
|
|
// TestScope_PhpSelfCall asserts `self::foo()` pins to the enclosing
|
|
// class's method, regardless of where the same-named decoy lives.
|
|
func TestScope_PhpSelfCall(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "src/Service.php", Kind: graph.KindFile, FilePath: "src/Service.php", Language: "php"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Service.php::Service.boot", Kind: graph.KindMethod, Name: "boot",
|
|
FilePath: "src/Service.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Service", MetaScopeClass: "Service"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Service.php::Service.init", Kind: graph.KindMethod, Name: "init",
|
|
FilePath: "src/Service.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Service", MetaScopeClass: "Service"},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "src/Other.php::Other.init", Kind: graph.KindMethod, Name: "init",
|
|
FilePath: "src/Other.php", Language: "php",
|
|
Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"},
|
|
})
|
|
e := &graph.Edge{
|
|
From: "src/Service.php::Service.boot", To: "unresolved::*.init",
|
|
Kind: graph.EdgeCalls, FilePath: "src/Service.php", Line: 3,
|
|
Meta: map[string]any{MetaScopeKind: ScopeKindSelf},
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "src/Service.php::Service.init", e.To,
|
|
"self:: must pin to the enclosing class's method")
|
|
}
|
|
|
|
// TestScope_StampedAsScopeResolution asserts the scope path stamps
|
|
// `resolution=scope` on the edge so the gain telemetry + agent
|
|
// observability can tell scope wins from generic-locality wins.
|
|
func TestScope_StampedAsScopeResolution(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
Meta: map[string]any{MetaScopeStatic: true},
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/b.c::helper", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "pkg/b.c", Language: "c",
|
|
})
|
|
e := &graph.Edge{
|
|
From: "pkg/a.c::caller", To: "unresolved::helper",
|
|
Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
New(g).ResolveAll()
|
|
|
|
require.NotNil(t, e.Meta)
|
|
assert.Equal(t, "scope", e.Meta["resolution"],
|
|
"scope-resolved edge must carry resolution=scope so telemetry can distinguish it")
|
|
assert.Equal(t, graph.OriginASTResolved, e.Origin,
|
|
"scope-resolved edge must stamp OriginASTResolved")
|
|
}
|
|
|
|
// TestScope_FallthroughWhenNoEvidence asserts the scope resolver is a
|
|
// no-op when none of the language-specific evidence is present —
|
|
// behaviour stays identical to pre-N8 for unaffected call sites.
|
|
func TestScope_FallthroughWhenNoEvidence(t *testing.T) {
|
|
g := graph.New()
|
|
g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
})
|
|
g.AddNode(&graph.Node{
|
|
ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper",
|
|
FilePath: "pkg/a.c", Language: "c",
|
|
// No scope_static, no scope_ns, no scope_class.
|
|
})
|
|
e := &graph.Edge{
|
|
From: "pkg/a.c::caller", To: "unresolved::helper",
|
|
Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5,
|
|
}
|
|
g.AddEdge(e)
|
|
|
|
stats := New(g).ResolveAll()
|
|
|
|
require.Equal(t, 1, stats.Resolved)
|
|
assert.Equal(t, "pkg/a.c::helper", e.To,
|
|
"no-evidence edge must still resolve via the generic cascade")
|
|
// Without scope evidence, resolution should NOT be tagged
|
|
// "scope" — the generic locality path took over.
|
|
if e.Meta != nil {
|
|
assert.NotEqual(t, "scope", e.Meta["resolution"],
|
|
"generic-locality resolution must not falsely claim scope evidence")
|
|
}
|
|
}
|