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
225 lines
5.6 KiB
Go
225 lines
5.6 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func runPyExtract(t *testing.T, path, src string) ([]*graph.Node, []*graph.Edge) {
|
|
t.Helper()
|
|
ext := NewPythonExtractor()
|
|
result, err := ext.Extract(path, []byte(src))
|
|
if err != nil {
|
|
t.Fatalf("extract: %v", err)
|
|
}
|
|
return result.Nodes, result.Edges
|
|
}
|
|
|
|
func TestPyFunctionShape_ParamsAndReturn(t *testing.T) {
|
|
src := `def fetch(client: Client, ttl: int = 30) -> User:
|
|
return User()
|
|
`
|
|
nodes, edges := runPyExtract(t, "x.py", src)
|
|
|
|
params := nodesOfKind(nodes, graph.KindParam)
|
|
if len(params) != 2 {
|
|
t.Fatalf("expected 2 params, got %d: %v", len(params), nodeNames(params))
|
|
}
|
|
|
|
paramOf := edgesByKind(edges, graph.EdgeParamOf)
|
|
for _, e := range paramOf {
|
|
if e.To != "x.py::fetch" {
|
|
t.Errorf("param->%q, want fetch", e.To)
|
|
}
|
|
}
|
|
|
|
typed := edgesByKind(edges, graph.EdgeTypedAs)
|
|
hasClient := false
|
|
for _, e := range typed {
|
|
if e.To == "unresolved::Client" {
|
|
hasClient = true
|
|
}
|
|
}
|
|
if !hasClient {
|
|
t.Errorf("expected EdgeTypedAs → unresolved::Client; got %v", edgeTargets(typed))
|
|
}
|
|
|
|
returns := edgesByKind(edges, graph.EdgeReturns)
|
|
hasUser := false
|
|
for _, e := range returns {
|
|
if e.To == "unresolved::User" {
|
|
hasUser = true
|
|
}
|
|
}
|
|
if !hasUser {
|
|
t.Errorf("expected EdgeReturns → unresolved::User; got %v", edgeTargets(returns))
|
|
}
|
|
}
|
|
|
|
func TestPyFunctionShape_OptionalUnwrapped(t *testing.T) {
|
|
src := `def find_user(uid: int) -> Optional[User]:
|
|
return None
|
|
`
|
|
_, edges := runPyExtract(t, "x.py", src)
|
|
|
|
returns := edgesByKind(edges, graph.EdgeReturns)
|
|
hasUser := false
|
|
for _, e := range returns {
|
|
if e.To == "unresolved::User" {
|
|
hasUser = true
|
|
}
|
|
}
|
|
if !hasUser {
|
|
t.Errorf("expected unwrapped EdgeReturns → User; got %v", edgeTargets(returns))
|
|
}
|
|
}
|
|
|
|
func TestPyFunctionShape_PEP604Union(t *testing.T) {
|
|
src := `def lookup(uid: int) -> User | None:
|
|
return None
|
|
`
|
|
_, edges := runPyExtract(t, "x.py", src)
|
|
|
|
returns := edgesByKind(edges, graph.EdgeReturns)
|
|
hasUser := false
|
|
for _, e := range returns {
|
|
if e.To == "unresolved::User" {
|
|
hasUser = true
|
|
}
|
|
}
|
|
if !hasUser {
|
|
t.Errorf("expected EdgeReturns → User from PEP-604 union; got %v", edgeTargets(returns))
|
|
}
|
|
}
|
|
|
|
func TestPyFunctionShape_VariadicSplat(t *testing.T) {
|
|
src := `def fn(*args, **kwargs):
|
|
pass
|
|
`
|
|
nodes, _ := runPyExtract(t, "x.py", src)
|
|
params := nodesOfKind(nodes, graph.KindParam)
|
|
if len(params) != 2 {
|
|
t.Fatalf("expected 2 params, got %d", len(params))
|
|
}
|
|
for _, p := range params {
|
|
if v, _ := p.Meta["variadic"].(bool); !v {
|
|
t.Errorf("%s should be variadic", p.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPyFunctionShape_SkipsSelfAndCls(t *testing.T) {
|
|
src := `class C:
|
|
def m(self, x: int) -> int:
|
|
return x
|
|
|
|
@classmethod
|
|
def c(cls, y: int) -> int:
|
|
return y
|
|
`
|
|
nodes, _ := runPyExtract(t, "x.py", src)
|
|
for _, n := range nodes {
|
|
if n.Kind == graph.KindParam && (n.Name == "self" || n.Name == "cls") {
|
|
t.Errorf("KindParam %q should not be emitted (receiver)", n.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPyFunctionShape_ClassLevelPEP695Generic(t *testing.T) {
|
|
// PEP 695: `class Foo[T]:`. Depending on the bundled tree-sitter-
|
|
// python version the type_parameters child may or may not be
|
|
// present at the class level. We assert no panic + (best-effort)
|
|
// the generic parameter is attached to the class. When the
|
|
// grammar doesn't recognise it, the helper silently emits
|
|
// nothing — which is acceptable since callers are tolerant of
|
|
// missing class generics.
|
|
src := `class Repo[T]:
|
|
def get(self, x: T) -> T:
|
|
return x
|
|
`
|
|
nodes, _ := runPyExtract(t, "x.py", src)
|
|
gp := nodesOfKind(nodes, graph.KindGenericParam)
|
|
for _, n := range gp {
|
|
if n.Name == "T" && strings.Contains(n.ID, "x.py::Repo#tparam") {
|
|
return // grammar supports PEP 695: assertion satisfied.
|
|
}
|
|
}
|
|
t.Logf("PEP-695 class generic not surfaced; tree-sitter-python "+
|
|
"version may predate the syntax — got %v", nodeIDs(gp))
|
|
}
|
|
|
|
func TestPyAsyncSpawns_Await(t *testing.T) {
|
|
src := `async def load(uid):
|
|
user = await fetch_user(uid)
|
|
rows = await db.query("...")
|
|
return user
|
|
`
|
|
_, edges := runPyExtract(t, "x.py", src)
|
|
|
|
spawns := edgesByKind(edges, graph.EdgeSpawns)
|
|
want := map[string]bool{"unresolved::fetch_user": false, "unresolved::query": false}
|
|
for _, e := range spawns {
|
|
if mode, _ := e.Meta["mode"].(string); mode != "async" {
|
|
continue
|
|
}
|
|
if _, ok := want[e.To]; ok {
|
|
want[e.To] = true
|
|
}
|
|
}
|
|
for tgt, found := range want {
|
|
if !found {
|
|
t.Errorf("expected EdgeSpawns mode=async → %s; got %v", tgt, edgeTargets(spawns))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPyAsyncSpawns_AsyncioGather(t *testing.T) {
|
|
src := `import asyncio
|
|
|
|
async def loadAll():
|
|
return await asyncio.gather(loadA(), loadB())
|
|
`
|
|
_, edges := runPyExtract(t, "x.py", src)
|
|
spawns := edgesByKind(edges, graph.EdgeSpawns)
|
|
hasGather := false
|
|
for _, e := range spawns {
|
|
if e.To == "unresolved::asyncio.gather" {
|
|
hasGather = true
|
|
}
|
|
}
|
|
if !hasGather {
|
|
t.Errorf("expected EdgeSpawns → asyncio.gather; got %v", edgeTargets(spawns))
|
|
}
|
|
}
|
|
|
|
func TestCanonicalizePyTypeRef(t *testing.T) {
|
|
cases := []struct {
|
|
in, out string
|
|
}{
|
|
{"User", "User"},
|
|
{"Optional[User]", "User"},
|
|
{"List[User]", "User"},
|
|
{"list[User]", "User"},
|
|
{"Tuple[User, int]", "User"},
|
|
{"User | None", "User"},
|
|
{"None | User", "User"},
|
|
{"models.User", "User"},
|
|
{"pkg.mod.User", "User"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := canonicalizePyTypeRef(c.in); got != c.out {
|
|
t.Errorf("canonicalizePyTypeRef(%q) = %q, want %q", c.in, got, c.out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func nodeNames(nodes []*graph.Node) []string {
|
|
out := make([]string, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
out = append(out, n.Name)
|
|
}
|
|
return out
|
|
}
|