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

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")
}