Skip to content

Commit 2bd91b6

Browse files
authored
Merge pull request #11 from Helweg/feature/call-graph
feat: add call graph extraction and query support
2 parents fde17a4 + 4f3d6f7 commit 2bd91b6

26 files changed

+3106
-92
lines changed

AGENTS.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Semantic codebase indexing plugin for OpenCode. Hybrid TypeScript/Rust architecture:
66
- **TypeScript** (`src/`): Plugin logic, embedding providers, OpenCode tools
7-
- **Rust** (`native/`): Tree-sitter parsing, usearch vectors, SQLite storage, BM25 inverted index
7+
- **Rust** (`native/`): Tree-sitter parsing, usearch vectors, SQLite storage, BM25 inverted index, call graph extraction
88

99
## Build/Test/Lint
1010

@@ -52,10 +52,11 @@ native/src/
5252
├── parser.rs # Tree-sitter parsing (14 languages: TS, JS, Python, Rust, Go, Java, C#, Ruby, Bash, C, C++, JSON, TOML, YAML)
5353
├── chunker.rs # Semantic chunking with overlap
5454
├── store.rs # usearch vector store (F16 quantization)
55-
├── db.rs # SQLite: embeddings, chunks, branch catalog
55+
├── db.rs # SQLite: embeddings, chunks, branch catalog, symbols, call edges
56+
├── call_extractor.rs # Tree-sitter query-based call extraction (TS/JS, Python, Go, Rust)
5657
├── inverted_index.rs # BM25 keyword search
5758
├── hasher.rs # xxhash content hashing
58-
└── types.rs # Shared types
59+
└── types.rs # Shared types (Language enum with from_string)
5960
6061
tests/ # Vitest tests (30s timeout for native ops)
6162
commands/ # Slash command definitions (/search, /find, /index)
@@ -75,6 +76,8 @@ skill/ # OpenCode skill guidance
7576
| Add slash command | `commands/` + register in `src/index.ts` config() |
7677

7778
| Add/modify MCP tool | `src/mcp-server.ts` (createMcpServer) |
79+
| Modify call graph extraction | `native/src/call_extractor.rs` + query files in `native/queries/` |
80+
| Add call graph language | `native/queries/<lang>-calls.scm` + update `call_extractor.rs` |
7881
## CODE MAP
7982

8083
### TypeScript Exports (`src/index.ts`)
@@ -89,12 +92,13 @@ skill/ # OpenCode skill guidance
8992
| `index_health_check` | Tool | GC orphaned embeddings/chunks |
9093
| `index_metrics` | Tool | Get performance metrics (requires debug.enabled + debug.metrics) |
9194
| `index_logs` | Tool | Get debug logs (requires debug.enabled) |
95+
| `call_graph` | Tool | Query call graph for callers/callees of functions |
9296

9397

9498
### MCP Server Exports (`src/mcp-server.ts`)
9599
| Symbol | Type | Purpose |
96100
|--------|------|---------|
97-
| `createMcpServer` | fn | Creates MCP Server with 8 tools + 4 prompts, lazy Indexer init |
101+
| `createMcpServer` | fn | Creates MCP Server with 9 tools + 4 prompts, lazy Indexer init |
98102

99103
### CLI Entry (`src/cli.ts`)
100104
| Symbol | Type | Purpose |
@@ -218,6 +222,7 @@ afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); });
218222
| `logger.test.ts` | Logger utility, metrics collection |
219223

220224
| `mcp-server.test.ts` | MCP server: tool/prompt registration, execution via InMemoryTransport |
225+
| `call-graph.test.ts` | Call extraction, storage, resolution, branch awareness, integration |
221226
### Benchmarks
222227
```bash
223228
npx tsx benchmarks/run.ts # Performance testing for native operations

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
## [Unreleased]
2222

23+
### Added
24+
- **Call graph extraction and query**: Tree-sitter query-based extraction of function calls, method calls, constructors, and imports across 5 languages (TypeScript/JavaScript, Python, Go, Rust)
25+
- **`call_graph` tool**: Query callers or callees of any function/method with branch-aware filtering
26+
- **DB schema v2**: `symbols`, `call_edges`, and `branch_symbols` tables with full CRUD, GC, and batch operations
27+
- **Same-file call resolution**: Automatically resolves call edges to symbols defined in the same file during indexing
28+
29+
### Fixed
30+
- **Missing `call_graph` export**: The `call_graph` tool was not exported from the plugin entry point — now available to OpenCode users
31+
2332
## [0.5.0] - 2026-02-23
2433

2534
### Added

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Use the same semantic search from any MCP-compatible client. Index once, search
8282
npx opencode-codebase-index-mcp # uses current directory
8383
```
8484

85-
The MCP server exposes all 8 tools (`codebase_search`, `codebase_peek`, `find_similar`, `index_codebase`, `index_status`, `index_health_check`, `index_metrics`, `index_logs`) and 4 prompts (`search`, `find`, `index`, `status`).
85+
The MCP server exposes all 9 tools (`codebase_search`, `codebase_peek`, `find_similar`, `call_graph`, `index_codebase`, `index_status`, `index_health_check`, `index_metrics`, `index_logs`) and 4 prompts (`search`, `find`, `index`, `status`).
8686

8787
The MCP dependencies (`@modelcontextprotocol/sdk`, `zod`) are optional peer dependencies — they're only needed if you use the MCP server.
8888

@@ -112,6 +112,7 @@ src/api/checkout.ts:89 (Route handler for /pay)
112112
| Don't know the function name | `codebase_search` | Semantic search finds by meaning |
113113
| Exploring unfamiliar codebase | `codebase_search` | Discovers related code across files |
114114
| Just need to find locations | `codebase_peek` | Returns metadata only, saves ~90% tokens |
115+
| Understand code flow | `call_graph` | Find callers/callees of any function |
115116
| Know exact identifier | `grep` | Faster, finds all occurrences |
116117
| Need ALL matches | `grep` | Semantic returns top N only |
117118
| Mixed discovery + precision | `/find` (hybrid) | Best of both worlds |
@@ -211,7 +212,7 @@ When you switch branches, code changes but embeddings for unchanged content rema
211212

212213
```
213214
.opencode/index/
214-
├── codebase.db # SQLite: embeddings, chunks, branch catalog
215+
├── codebase.db # SQLite: embeddings, chunks, branch catalog, symbols, call edges
215216
├── vectors.usearch # Vector index (uSearch)
216217
├── inverted-index.json # BM25 keyword index
217218
└── file-hashes.json # File change detection
@@ -267,6 +268,12 @@ Returns collected metrics about indexing and search performance. Requires `debug
267268
Returns recent debug logs with optional filtering.
268269
- **Parameters**: `category` (optional: `search`, `embedding`, `cache`, `gc`, `branch`), `level` (optional: `error`, `warn`, `info`, `debug`), `limit` (default: 50).
269270

271+
### `call_graph`
272+
Query the call graph to find callers or callees of a function/method. Automatically built during indexing for TypeScript, JavaScript, Python, Go, and Rust.
273+
- **Use for**: Understanding code flow, tracing dependencies, impact analysis.
274+
- **Parameters**: `name` (function name), `direction` (`callers` or `callees`), `symbolId` (required for `callees`, returned by previous queries).
275+
- **Example**: Find who calls `validateToken``call_graph(name="validateToken", direction="callers")`
276+
270277
## 🎮 Slash Commands
271278

272279
The plugin automatically registers these slash commands:
@@ -585,8 +592,9 @@ CI will automatically run tests and type checking on your PR.
585592
The Rust native module handles performance-critical operations:
586593
- **tree-sitter**: Language-aware code parsing with JSDoc/docstring extraction
587594
- **usearch**: High-performance vector similarity search with F16 quantization
588-
- **SQLite**: Persistent storage for embeddings, chunks, and branch catalog
595+
- **SQLite**: Persistent storage for embeddings, chunks, branch catalog, symbols, and call edges
589596
- **BM25 inverted index**: Fast keyword search for hybrid retrieval
597+
- **Call graph extraction**: Tree-sitter query-based extraction of function calls, method calls, constructors, and imports (TypeScript/JavaScript, Python, Go, Rust)
590598
- **xxhash**: Fast content hashing for change detection
591599

592600
Rebuild with: `npm run build:native` (requires Rust toolchain)

commands/call-graph.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
description: Trace callers or callees using the call graph
3+
---
4+
5+
Trace function dependencies using the `call_graph` tool.
6+
7+
User input: $ARGUMENTS
8+
9+
Interpret input as follows:
10+
- Default to `direction="callers"` unless input asks for callees/calls/makes calls.
11+
- `name=<function>` or plain text function name sets `name`.
12+
- `symbolId=<id>` is required for `direction="callees"`.
13+
14+
Execution flow:
15+
1. If direction is `callers`, call `call_graph` with `{ name, direction: "callers" }`.
16+
2. If direction is `callees` and `symbolId` is present, call `call_graph` with `{ name, direction: "callees", symbolId }`.
17+
3. If direction is `callees` and `symbolId` is missing, first call `call_graph` with `direction="callers"` to get symbol IDs, then ask the user to choose one if multiple are returned.
18+
19+
Examples:
20+
- `/call-graph Database` → callers for `Database`
21+
- `/call-graph callers name=Indexer` → callers for `Indexer`
22+
- `/call-graph callees name=Database symbolId=sym_abc123` → callees for selected symbol
23+
24+
If output says no callers found, suggest running `/index force` first.

native/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

native/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ walkdir = "2.5"
3737
ignore = "0.4"
3838
thiserror = "1.0"
3939
anyhow = "1.0"
40+
streaming-iterator = "0.1"
4041
# On Linux/macOS: use default features (simsimd + fp16lib) for SIMD-accelerated vector ops
4142
# On Windows: disable simsimd — MSVC lacks _mm512_reduce_add_ph intrinsic (usearch#325)
4243
[target.'cfg(not(target_os = "windows"))'.dependencies]

native/queries/go-calls.scm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
; =============================================================
2+
; Tree-sitter query for extracting function calls from Go
3+
; =============================================================
4+
5+
; Direct function calls: foo(), bar(1, 2)
6+
(call_expression
7+
function: (identifier) @callee.name) @call
8+
9+
; Method/package calls: obj.Method(), fmt.Println()
10+
(call_expression
11+
function: (selector_expression
12+
field: (field_identifier) @callee.name)) @call
13+
14+
; Import: import "fmt"
15+
(import_spec
16+
path: (interpreted_string_literal) @import.name) @import
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
; =============================================================
2+
; Tree-sitter query file for extracting function calls from TS/JS
3+
; Captures are named with @ prefix and used to extract node text
4+
; =============================================================
5+
6+
; -------------------------------------------------------------
7+
; Direct function calls: foo(), bar(1, 2)
8+
; Captures the function identifier being called
9+
; -------------------------------------------------------------
10+
(call_expression
11+
function: (identifier) @callee.name) @call
12+
13+
; -------------------------------------------------------------
14+
; Method calls: obj.method(), this.foo(), array.map()
15+
; Captures the property (method name) being called
16+
; -------------------------------------------------------------
17+
(call_expression
18+
function: (member_expression
19+
property: (property_identifier) @callee.name)) @call
20+
21+
; -------------------------------------------------------------
22+
; Constructor calls: new Foo(), new Bar(args)
23+
; Captures the class/constructor name
24+
; -------------------------------------------------------------
25+
(new_expression
26+
constructor: (identifier) @callee.name) @constructor
27+
28+
; -------------------------------------------------------------
29+
; ES6 named imports: import { foo, bar as baz } from 'module'
30+
; Captures each imported name and the source module
31+
; -------------------------------------------------------------
32+
(import_statement
33+
(import_clause
34+
(named_imports
35+
(import_specifier
36+
name: (identifier) @import.name)))
37+
source: (string) @import.source) @import
38+
39+
; -------------------------------------------------------------
40+
; Default imports: import React from 'react'
41+
; Captures the default import name and source
42+
; -------------------------------------------------------------
43+
(import_statement
44+
(import_clause
45+
(identifier) @import.default)
46+
source: (string) @import.source) @import
47+
48+
; -------------------------------------------------------------
49+
; Namespace imports: import * as utils from './utils'
50+
; Captures the namespace alias and source
51+
; -------------------------------------------------------------
52+
(import_statement
53+
(import_clause
54+
(namespace_import
55+
(identifier) @import.namespace))
56+
source: (string) @import.source) @import

native/queries/python-calls.scm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
; =============================================================
2+
; Tree-sitter query for extracting function calls from Python
3+
; =============================================================
4+
5+
; Direct function calls: foo(), bar(1, 2)
6+
(call
7+
function: (identifier) @callee.name) @call
8+
9+
; Method calls: obj.method(), self.foo()
10+
(call
11+
function: (attribute
12+
attribute: (identifier) @callee.name)) @call
13+
14+
; Constructor calls (same as function calls in Python, but capitalized by convention)
15+
; Handled by the Call type — caller can check capitalization
16+
17+
; Import: import foo
18+
(import_statement
19+
name: (dotted_name
20+
(identifier) @import.name)) @import
21+
22+
; From import: from module import foo, bar
23+
(import_from_statement
24+
name: (dotted_name
25+
(identifier) @import.name)) @import

native/queries/rust-calls.scm

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
; =============================================================
2+
; Tree-sitter query for extracting function calls from Rust
3+
; =============================================================
4+
5+
; Direct function calls: foo(), bar(1, 2)
6+
(call_expression
7+
function: (identifier) @callee.name) @call
8+
9+
; Method calls: obj.method(), self.foo()
10+
(call_expression
11+
function: (field_expression
12+
field: (field_identifier) @callee.name)) @call
13+
14+
; Path calls: std::fs::read(), Vec::new()
15+
(call_expression
16+
function: (scoped_identifier
17+
name: (identifier) @callee.name)) @call
18+
19+
; Macro calls: println!(), vec![]
20+
(macro_invocation
21+
macro: (identifier) @callee.name) @call
22+
23+
; Use imports: use std::fs;
24+
(use_declaration
25+
argument: (scoped_identifier
26+
name: (identifier) @import.name)) @import
27+
28+
; Use list imports: use std::{foo, bar};
29+
(use_declaration
30+
argument: (scoped_use_list
31+
list: (use_list
32+
(identifier) @import.name))) @import

0 commit comments

Comments
 (0)