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
182 lines
5.7 KiB
Go
182 lines
5.7 KiB
Go
package resolver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// fooCallTarget returns the To-end of the single Foo call edge leaving fromID.
|
|
func fooCallTarget(g graph.Store, fromID string) string {
|
|
for _, e := range g.GetOutEdges(fromID) {
|
|
if e.Kind != graph.EdgeCalls {
|
|
continue
|
|
}
|
|
if graph.IsUnresolvedTarget(e.To) {
|
|
if graph.UnresolvedName(e.To) == "*.Foo" || graph.UnresolvedName(e.To) == "Foo" {
|
|
return e.To
|
|
}
|
|
continue
|
|
}
|
|
if n := g.GetNode(e.To); n != nil && n.Name == "Foo" {
|
|
return e.To
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// TestResolveCSharpExtension_UniqueBinds: a call on a literal receiver with no
|
|
// type evidence binds to the sole extension method of that name.
|
|
func TestResolveCSharpExtension_UniqueBinds(t *testing.T) {
|
|
g := buildCSharpResolverGraph(t, map[string]string{
|
|
"Ext.cs": `namespace App {
|
|
public static class StringExt {
|
|
public static int Foo(this string s) { return 1; }
|
|
}
|
|
}`,
|
|
"Caller.cs": `namespace App {
|
|
public class Runner {
|
|
public void Run() {
|
|
"a".Foo();
|
|
}
|
|
}
|
|
}`,
|
|
})
|
|
New(g).ResolveAll()
|
|
|
|
target := fooCallTarget(g, "Caller.cs::Runner.Run")
|
|
require.Equal(t, "Ext.cs::StringExt.Foo", target)
|
|
e := g.GetNode(target)
|
|
require.NotNil(t, e)
|
|
}
|
|
|
|
// TestResolveCSharpExtension_TypedDisambiguates: with a known receiver type, the
|
|
// extension whose this_param_type matches wins over a same-named extension on
|
|
// another type.
|
|
func TestResolveCSharpExtension_TypedDisambiguates(t *testing.T) {
|
|
g := buildCSharpResolverGraph(t, map[string]string{
|
|
"Exts.cs": `namespace App {
|
|
public static class E {
|
|
public static int Foo(this Widget w) { return 1; }
|
|
public static int Foo(this Gadget x) { return 2; }
|
|
}
|
|
public class Widget {}
|
|
public class Gadget {}
|
|
}`,
|
|
"Caller.cs": `namespace App {
|
|
public class Runner {
|
|
public void Run() {
|
|
Widget w = new Widget();
|
|
w.Foo();
|
|
}
|
|
}
|
|
}`,
|
|
})
|
|
New(g).ResolveAll()
|
|
|
|
target := fooCallTarget(g, "Caller.cs::Runner.Run")
|
|
require.NotEmpty(t, target)
|
|
require.False(t, graph.IsUnresolvedTarget(target), "typed receiver should resolve")
|
|
n := g.GetNode(target)
|
|
require.NotNil(t, n)
|
|
assert.Equal(t, "Widget", n.Meta["this_param_type"], "should bind the Widget extension")
|
|
}
|
|
|
|
// TestResolveCSharpExtension_AmbiguousStaysUnresolved: two same-named extensions
|
|
// on different types + a receiver with no type evidence must not be guessed —
|
|
// neither the extension rule nor the locality fallback may pick one.
|
|
func TestResolveCSharpExtension_AmbiguousStaysUnresolved(t *testing.T) {
|
|
g := buildCSharpResolverGraph(t, map[string]string{
|
|
"Exts.cs": `namespace App {
|
|
public static class E1 { public static int Foo(this string s) { return 1; } }
|
|
public static class E2 { public static int Foo(this int n) { return 2; } }
|
|
}`,
|
|
"Caller.cs": `namespace App {
|
|
public class Runner {
|
|
public void Run(Thing t) {
|
|
t.Foo();
|
|
}
|
|
}
|
|
}`,
|
|
})
|
|
New(g).ResolveAll()
|
|
|
|
target := fooCallTarget(g, "Caller.cs::Runner.Run")
|
|
require.NotEmpty(t, target)
|
|
assert.True(t, graph.IsUnresolvedTarget(target),
|
|
"ambiguous extension call must stay unresolved, got %q", target)
|
|
}
|
|
|
|
// TestResolveCSharpExtension_InstanceWinsUntypedReceiver: with an untyped
|
|
// receiver (a field, no receiver_type), the sole extension must NOT preempt a
|
|
// same-named instance method — the locality fallback binds the instance method.
|
|
func TestResolveCSharpExtension_InstanceWinsUntypedReceiver(t *testing.T) {
|
|
g := buildCSharpResolverGraph(t, map[string]string{
|
|
"Types.cs": `namespace App {
|
|
public class Repo { public int Count() { return 0; } }
|
|
public static class StringExt { public static int Count(this string s) { return s.Length; } }
|
|
}`,
|
|
"Consumer.cs": `namespace App {
|
|
public class Consumer {
|
|
private Repo _repo = new Repo();
|
|
public void Run() {
|
|
_repo.Count();
|
|
}
|
|
}
|
|
}`,
|
|
})
|
|
New(g).ResolveAll()
|
|
|
|
var target string
|
|
for _, e := range g.GetOutEdges("Consumer.cs::Consumer.Run") {
|
|
if e.Kind == graph.EdgeCalls && !graph.IsUnresolvedTarget(e.To) {
|
|
if n := g.GetNode(e.To); n != nil && n.Name == "Count" {
|
|
target = e.To
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, "Types.cs::Repo.Count", target,
|
|
"an untyped receiver must not let the extension preempt the instance method")
|
|
}
|
|
|
|
// TestIsCSharpExtension_LanguageGated: the extension guard is C#-only, so a
|
|
// Scala extension-method node (which also stamps Meta[extension]) is never
|
|
// treated as a C# extension — keeping Scala's locality resolution unchanged.
|
|
func TestIsCSharpExtension_LanguageGated(t *testing.T) {
|
|
cs := &graph.Node{Kind: graph.KindMethod, Language: "csharp", Meta: map[string]any{"extension": true}}
|
|
scala := &graph.Node{Kind: graph.KindMethod, Language: "scala", Meta: map[string]any{"extension": true}}
|
|
assert.True(t, isCSharpExtension(cs))
|
|
assert.False(t, isCSharpExtension(scala),
|
|
"a Scala extension node must not be treated as a C# extension")
|
|
}
|
|
|
|
// TestResolveCSharpExtension_InstanceWins: an instance method beats an extension
|
|
// of the same name (C# member-lookup precedence).
|
|
func TestResolveCSharpExtension_InstanceWins(t *testing.T) {
|
|
g := buildCSharpResolverGraph(t, map[string]string{
|
|
"Types.cs": `namespace App {
|
|
public class Widget {
|
|
public int Foo() { return 0; }
|
|
}
|
|
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()
|
|
|
|
target := fooCallTarget(g, "Caller.cs::Runner.Run")
|
|
require.Equal(t, "Types.cs::Widget.Foo", target, "instance method should win over the extension")
|
|
}
|