package languages import ( "testing" "github.com/zzet/gortex/internal/graph" ) // refEdge finds the first edge with the given kind whose To matches target // and (when useKind != "") whose Meta["ref_context"] matches. func findRefEdge(edges []*graph.Edge, kind graph.EdgeKind, target, useKind string) *graph.Edge { for _, e := range edges { if e.Kind != kind || e.To != target { continue } if useKind != "" { if uk, _ := e.Meta["ref_context"].(string); uk != useKind { continue } } return e } return nil } func hasEdgeTo(edges []*graph.Edge, target string) bool { for _, e := range edges { if e.To == target { return true } } return false } // TestPyRefForm_Instantiation: `HttpResponse(...)` emits an instantiates // edge to the class, stamped OriginASTResolved with ref_context=instantiate. func TestPyRefForm_Instantiation(t *testing.T) { src := `from django.http import HttpResponse def view(request): return HttpResponse("ok") ` _, edges := runPyExtract(t, "app/views.py", src) e := findRefEdge(edges, graph.EdgeInstantiates, "unresolved::HttpResponse", "instantiate") if e == nil { t.Fatalf("expected EdgeInstantiates -> unresolved::HttpResponse (ref_context=instantiate); edges=%v", edgeDump(edges)) } if e.Origin != graph.OriginASTResolved { t.Errorf("instantiate edge Origin = %q, want OriginASTResolved", e.Origin) } if e.From != "app/views.py::view" { t.Errorf("instantiate edge From = %q, want app/views.py::view", e.From) } } // TestPyRefForm_DottedInstantiation: `http.HttpResponse(...)` — the // Capitalized leaf attribute is the constructed type. func TestPyRefForm_DottedInstantiation(t *testing.T) { src := `import django.http as http def view(request): return http.HttpResponse("ok") ` _, edges := runPyExtract(t, "app/v.py", src) if findRefEdge(edges, graph.EdgeInstantiates, "unresolved::HttpResponse", "instantiate") == nil { t.Fatalf("expected instantiate edge for http.HttpResponse(...); edges=%v", edgeDump(edges)) } } // TestPyRefForm_Inheritance: `class V(HttpResponse):` emits a references // edge (ref_context=inherit), OriginASTResolved. func TestPyRefForm_Inheritance(t *testing.T) { src := `from django.http import HttpResponse class MyResponse(HttpResponse): pass ` _, edges := runPyExtract(t, "app/r.py", src) e := findRefEdge(edges, graph.EdgeReferences, "unresolved::HttpResponse", "inherit") if e == nil { t.Fatalf("expected EdgeReferences -> unresolved::HttpResponse (ref_context=inherit); edges=%v", edgeDump(edges)) } if e.Origin != graph.OriginASTResolved { t.Errorf("inherit edge Origin = %q, want OriginASTResolved (else cross_pkg_guard reverts it)", e.Origin) } } // TestPyRefForm_DottedInheritance: `class V(http.HttpResponse):`. func TestPyRefForm_DottedInheritance(t *testing.T) { src := `import django.http as http class MyResponse(http.HttpResponse): pass ` _, edges := runPyExtract(t, "app/r2.py", src) if findRefEdge(edges, graph.EdgeReferences, "unresolved::HttpResponse", "inherit") == nil { t.Fatalf("expected inherit edge for http.HttpResponse base; edges=%v", edgeDump(edges)) } } // TestPyRefForm_IsInstance: `isinstance(x, HttpResponse)` references the // 2nd argument as a type (ref_context=cast). Tuple second arg references each. func TestPyRefForm_IsInstance(t *testing.T) { src := `from django.http import HttpResponse, JsonResponse def check(x): if isinstance(x, HttpResponse): return True return issubclass(type(x), (HttpResponse, JsonResponse)) ` _, edges := runPyExtract(t, "app/c.py", src) e := findRefEdge(edges, graph.EdgeReferences, "unresolved::HttpResponse", "cast") if e == nil { t.Fatalf("expected EdgeReferences -> unresolved::HttpResponse (ref_context=cast); edges=%v", edgeDump(edges)) } if e.Origin != graph.OriginASTResolved { t.Errorf("cast edge Origin = %q, want OriginASTResolved", e.Origin) } if findRefEdge(edges, graph.EdgeReferences, "unresolved::JsonResponse", "cast") == nil { t.Errorf("expected cast edge for JsonResponse in the issubclass tuple; edges=%v", edgeDump(edges)) } // The first arg of isinstance (`x`) must NOT produce a type ref. if hasEdgeTo(edges, "unresolved::x") { t.Errorf("first isinstance arg `x` must not emit a type reference") } } // TestPyRefForm_StaticAccess: `HttpResponse.status_code` emits a // references edge (ref_context=static_access) to the class. func TestPyRefForm_StaticAccess(t *testing.T) { src := `from django.http import HttpResponse def f(): return HttpResponse.status_code ` _, edges := runPyExtract(t, "app/s.py", src) e := findRefEdge(edges, graph.EdgeReferences, "unresolved::HttpResponse", "static_access") if e == nil { t.Fatalf("expected EdgeReferences -> unresolved::HttpResponse (ref_context=static_access); edges=%v", edgeDump(edges)) } if e.Origin != graph.OriginASTResolved { t.Errorf("static_access edge Origin = %q, want OriginASTResolved", e.Origin) } } // TestPyRefForm_Decorator: `@Validator(...)` / `@Validator` references the // Capitalized decorator name; lowercase `@property` / `@app.route` do not. func TestPyRefForm_Decorator(t *testing.T) { src := `from x import Validator @Validator def a(): pass @Validator("strict") def b(): pass @property def c(self): pass @app.route("/x") def d(): pass ` _, edges := runPyExtract(t, "app/d.py", src) if findRefEdge(edges, graph.EdgeReferences, "unresolved::Validator", "static_access") == nil { t.Fatalf("expected reference edge for @Validator decorator; edges=%v", edgeDump(edges)) } if hasEdgeTo(edges, "unresolved::property") { t.Errorf("lowercase decorator @property must not emit a type reference") } if hasEdgeTo(edges, "unresolved::route") { t.Errorf("lowercase decorator @app.route must not emit a type reference") } } // TestPyRefForm_NegativeCases: lowercase calls, method calls on instances, // and bare builtins must not emit reference/instantiate edges. func TestPyRefForm_NegativeCases(t *testing.T) { src := `def run(obj): foo() obj.method() x = dict() y = list() z = helper.compute() return x, y, z ` _, edges := runPyExtract(t, "app/n.py", src) for _, bad := range []string{ "unresolved::foo", "unresolved::method", "unresolved::compute", "unresolved::helper", } { for _, e := range edges { if (e.Kind == graph.EdgeInstantiates || (e.Kind == graph.EdgeReferences && e.Meta["ref_context"] != nil)) && e.To == bad { t.Errorf("lowercase callee/method %q must not emit an instantiate/reference type edge (got kind=%s)", bad, e.Kind) } } } // Lowercase builtins dict()/list() must not produce instantiate edges. for _, b := range []string{"unresolved::dict", "unresolved::list"} { if findRefEdge(edges, graph.EdgeInstantiates, b, "instantiate") != nil { t.Errorf("lowercase builtin %q must not emit an instantiate edge", b) } } } // TestPyRefForm_GenericArgs: element types inside a type-annotation // subscript (`List[Foo]`, `Dict[str, Bar]`, nested `Dict[str, List[Foo]]`, // and a parameter annotation) emit a "generic_arg" reference to each named // type, OriginASTResolved. Builtins (str/int) inside the subscript and a // runtime subscript (`arr[0]`) emit NO type reference. func TestPyRefForm_GenericArgs(t *testing.T) { src := `from m import Foo, Bar, Baz xs: List[Foo] = [] mapping: Dict[str, Bar] = {} nested: Dict[str, List[Foo]] = {} def handle(items: List[Baz]) -> None: arr = [1, 2, 3] return arr[0] ` _, edges := runPyExtract(t, "app/g.py", src) for _, want := range []string{"Foo", "Bar", "Baz"} { e := findRefEdge(edges, graph.EdgeReferences, "unresolved::"+want, "generic_arg") if e == nil { t.Fatalf("expected EdgeReferences -> unresolved::%s (ref_context=generic_arg); edges=%v", want, edgeDump(edges)) } if e.Origin != graph.OriginASTResolved { t.Errorf("generic_arg edge for %s Origin = %q, want OriginASTResolved", want, e.Origin) } } // The element type inside the parameter annotation is attributed to the // enclosing function, not the file. if e := findRefEdge(edges, graph.EdgeReferences, "unresolved::Baz", "generic_arg"); e != nil && e.From != "app/g.py::handle" { t.Errorf("parameter generic_arg From = %q, want app/g.py::handle", e.From) } // Builtins inside the subscript (str, int) must not produce a type ref. for _, bad := range []string{"unresolved::str", "unresolved::int"} { if hasEdgeTo(edges, bad) { t.Errorf("builtin %q inside a subscript must not emit a type reference", bad) } } // A runtime subscript `arr[0]` must not emit any generic_arg reference. for _, e := range edges { if uk, _ := e.Meta["ref_context"].(string); uk == "generic_arg" && e.To == "unresolved::arr" { t.Errorf("runtime subscript arr[0] must not emit a generic_arg reference") } } } // edgeDump renders edges compactly for failure messages. func edgeDump(edges []*graph.Edge) []string { out := make([]string, 0, len(edges)) for _, e := range edges { uk, _ := e.Meta["ref_context"].(string) out = append(out, string(e.Kind)+" "+e.From+" -> "+e.To+" ["+uk+"]") } return out }