Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .mex/ROUTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ Then read this file fully before doing anything else in this session.
**Known issues:**
- Pagination breaks on filtered queries with more than 1000 results -->

**Working:**
- Code graph extraction for TypeScript, TSX, JavaScript, JSX, Python, Rust, and Go
- Go language support with tree-sitter-go grammar (structs, interfaces, type aliases, functions, methods, generics, imports, calls, struct fields)
- Cross-file reference resolution and grounding

**Not yet built:**
- Framework resolvers for Go (e.g., Gin, Echo, Chi)
- Additional language extractors (Java, C#, etc.)

**Known issues:**
- None for Go extractor in current scope

## Routing Table

Load the relevant file based on the current task. Always load `context/architecture.md` first if not already in context this session.
Expand Down
3 changes: 2 additions & 1 deletion docs/code-graph-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ extractor registry lives in
| **Supported** | JSX | `.jsx` | [`jsx-component.jsx`](../src/graph/__tests__/fixtures/jsx-component.jsx) and [`extraction-regression.test.ts`](../src/graph/__tests__/extraction-regression.test.ts) cover components, imports, calls, and construction. |
| **Supported** | Python | `.py` | [`sample.py`](../src/graph/__tests__/fixtures/sample.py), [`extractor-python.test.ts`](../src/graph/__tests__/extractor-python.test.ts), and the [`python-package`](../src/graph/__tests__/fixtures/python-package) integration fixture cover extraction and cross-file package resolution. |
| **Supported** | Rust | `.rs` | [`sample.rs`](../src/graph/__tests__/fixtures/sample.rs) and [`extractor-rust.test.ts`](../src/graph/__tests__/extractor-rust.test.ts) cover structs, traits, enums, modules, functions, methods, generics, imports, calls, implementations, construction, returns, and field types. |
| **Unsupported** | Go and other languages | All other extensions | These names may be reserved in [`src/graph/types.ts`](../src/graph/types.ts), but no grammar or extractor is registered for them. Unsupported files are skipped rather than failing a graph build. |
| **Supported** | Go | `.go` | [`sample.go`](../src/graph/__tests__/fixtures/sample.go) and [`extractor-go.test.ts`](../src/graph/__tests__/extractor-go.test.ts) cover structs, interfaces, type aliases, functions, methods, generics, imports, calls, and struct field types. |
| **Unsupported** | Other languages | All other extensions | These names may be reserved in [`src/graph/types.ts`](../src/graph/types.ts), but no grammar or extractor is registered for them. Unsupported files are skipped rather than failing a graph build. |

`src/graph/types.ts` contains a wider future-facing language vocabulary. A name
in that type union is not a support promise; the grammar and extractor
Expand Down
8 changes: 8 additions & 0 deletions docs/extractors.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ When adding a language, document the grammar source and version.
- **Upstream grammar:** [tree-sitter/tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust)
- **Upstream grammar license:** MIT

### Go
- **Binary source:** `tree-sitter-wasms` package, version `0.1.13`
- **Vendored path:** `src/graph/wasm/tree-sitter-go.wasm`
- **SHA-256:** matches `node_modules/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm` exactly
- **Binary package license:** Unlicense
- **Upstream grammar:** [tree-sitter/tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go)
- **Upstream grammar license:** MIT

## Pull request proof

Before opening a pull request, run:
Expand Down
122 changes: 122 additions & 0 deletions src/graph/__tests__/extractor-go.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";
import { extractFile, loadGrammars } from "../extraction/index.js";
import type { FileExtraction } from "../extraction/index.js";

const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "sample.go");

describe("Go extractor", () => {
let result: FileExtraction;

beforeAll(async () => {
await loadGrammars(["go"]);
const source = readFileSync(FIXTURE, "utf-8");
result = extractFile("fixtures/sample.go", source, "go")!;
expect(result).not.toBeNull();
});

const node = (kind: string, name: string) =>
result.nodes.find((n) => n.kind === kind && n.name === name);
const hasEdge = (kind: string, targetName: string) =>
result.edges.some((e) => e.kind === kind && e.targetName === targetName);

it("emits a file node and stamps the language", () => {
expect(result.language).toBe("go");
expect(node("file", "sample.go")).toBeDefined();
});

it("extracts structs", () => {
const user = node("class", "User");
expect(user).toBeDefined();
expect(user!.isExported).toBe(true);
expect(user!.docstring).toContain("represents a user");

expect(node("property", "Name")).toBeDefined();
expect(node("property", "Age")).toBeDefined();

const order = node("class", "Order");
expect(order).toBeDefined();
expect(node("property", "ID")).toBeDefined();
expect(node("property", "Items")).toBeDefined();
});

it("extracts type aliases", () => {
expect(node("type_alias", "Role")).toBeDefined();
});

it("extracts functions and methods", () => {
const createUser = node("function", "CreateUser");
expect(createUser).toBeDefined();
expect(createUser!.isExported).toBe(true);

const greet = result.nodes.find(
(n) => n.kind === "method" && n.qualifiedName === "User::Greet",
);
expect(greet).toBeDefined();
expect(greet!.qualifiedName).toBe("User::Greet");
});

it("extracts interfaces", () => {
const greeter = node("interface", "Greeter");
expect(greeter).toBeDefined();
expect(greeter!.isExported).toBe(true);

const repo = node("interface", "Repo");
expect(repo).toBeDefined();
expect(repo!.typeParameters).toEqual(["T"]);
});

it("extracts constants and variables", () => {
expect(node("constant", "RoleAdmin")).toBeDefined();
expect(node("constant", "RoleMember")).toBeDefined();
expect(node("variable", "globalFlag")).toBeDefined();
});

it("emits import edges", () => {
expect(hasEdge("imports", "fmt")).toBe(true);
expect(hasEdge("imports", "strings")).toBe(true);
});

it("emits calls and implements references", () => {
expect(hasEdge("calls", "fmt.Sprintf")).toBe(true);
expect(hasEdge("calls", "processOrder")).toBe(true);
expect(hasEdge("calls", "consume")).toBe(true);
expect(hasEdge("instantiates", "Box")).toBe(true);
expect(hasEdge("instantiates", "Order")).toBe(true);
expect(hasEdge("instantiates", "User")).toBe(true);
});

it("nests methods under their class via contains edges", () => {
const userClass = node("class", "User")!;
const greet = result.nodes.find(
(n) => n.kind === "method" && n.qualifiedName === "User::Greet",
)!;
expect(
result.edges.some(
(e) => e.kind === "contains" && e.source === userClass.id && e.target === greet.id,
),
).toBe(true);
});

it("emits `type_of` edge for struct fields", () => {
const nameField = result.nodes.find(
(n) => n.kind === "property" && n.name === "Name",
)!;
expect(nameField).toBeDefined();
expect(
result.edges.some(
(e) => e.kind === "type_of" && e.source === nameField.id && e.targetName === "string",
),
).toBe(true);
});

it("handles generic type parameters", () => {
const box = node("class", "Box")!;
expect(box.typeParameters).toEqual(["T"]);

const makeBox = node("function", "makeBox")!;
expect(makeBox.typeParameters).toEqual(["T"]);
});
});
67 changes: 67 additions & 0 deletions src/graph/__tests__/fixtures/sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Package main is a sample Go file for testing the code-graph extractor.

package main

import (
"fmt"
"strings"
)

// User represents a user in the system.
type User struct {
Name string
Age int
}

type Role string

const (
RoleAdmin Role = "admin"
RoleMember Role = "member"
)

var globalFlag = true

// Greeter is an interface for greeting.
type Greeter interface {
Greet() string
}

func (u *User) Greet() string {
return fmt.Sprintf("Hello, my name is %s", u.Name)
}

func processOrder(order *Order) {
fmt.Println("Processing order:", order.ID)
}

type Order struct {
ID int
Items []string
}

func (o *Order) AddItem(item string) {
o.Items = append(o.Items, item)
}

type Box[T any] struct {
Value T
}

func makeBox[T any](val T) Box[T] {
return Box[T]{Value: val}
}

func consume(val any) {}

func CreateUser(name string) *User {
u := &User{Name: name, Age: 30}
processOrder(&Order{ID: 1})
u.Greet()
consume(makeBox(42))
return u
}

type Repo[T any] interface {
Get() T
}
2 changes: 2 additions & 0 deletions src/graph/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const WASM_GRAMMAR_FILES: Partial<Record<Language, string>> = {
jsx: "tree-sitter-javascript.wasm",
python: "tree-sitter-python.wasm",
rust: "tree-sitter-rust.wasm",
go: "tree-sitter-go.wasm",
};
let grammarHashCache: string | null = null;

Expand Down Expand Up @@ -63,6 +64,7 @@ const EXTENSION_MAP: Record<string, Language> = {
".jsx": "jsx",
".py": "python",
".rs": "rust",
".go": "go",
};

/** Glob pattern for every extension registered above. */
Expand Down
Loading