Skip to content

Commit ea6ce97

Browse files
committed
feat: bidirectional call graph with BFS depth 2+ and parser call extraction
Extend SymbolIndex with full call graph capabilities: - parser.py: extract_calls() method for AST-based call extraction (supports simple calls, method calls, scoped identifiers) - symbol_index.py: add_references() for storing caller→callee relationships - symbol_index.py: build_call_graph() rewritten with BFS traversal (configurable depth 1-5, cycle detection, call chain context) - symbol_index.py: get_call_chain() for upstream/downstream traversal - symbol_index.py: get_symbol_context() enriched with calls info - index_project() now extracts calls during indexing - 22 new unit tests covering references, BFS, cycles, edge cases - Updated docs (ARCHITECTURE, CHANGELOG, README, TESTING) Co-Authored-By: Claude Opus 4.6 <<EMAIL>>
1 parent 8cf97a3 commit ea6ce97

8 files changed

Lines changed: 940 additions & 68 deletions

File tree

‎AGENT_DIARY.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,45 @@
6666

6767
---
6868

69+
## [2026-06-28 21:00] — [Type: Feature] — Deep Call Graph (BFS Depth 2+)
70+
71+
**Проблема:**
72+
1. `symbol_index.py` хранил только definitions — граф вызовов был плоским
73+
2. `_references` всегда был пуст — парсер не извлекал вызовы функций
74+
3. `build_call_graph` находил только прямые связи без глубины
75+
76+
**Решение:**
77+
- `parser.py` — добавлен метод `extract_calls()`:
78+
- Рекурсивный обход AST для извлечения call_expression
79+
- Поддержка method calls (obj.method()), scoped (module::func)
80+
- Возвращает список {caller, callee, line, file}
81+
- `symbol_index.py` — добавлен метод `add_references()`:
82+
- Хранит двунаправленные связи (caller → callee и callee ← caller)
83+
- Дедупликация, пропуск self-calls
84+
- `symbol_index.py` — переписан `build_call_graph()`:
85+
- BFS (breadth-first search) с настраиваемой глубиной (1-5)
86+
- Отдельные проходы для callers (вверх) и callees (вниз)
87+
- Защита от циклов через visited-множества
88+
- Формирование call_chain для контекста
89+
- `symbol_index.py` — добавлен `get_call_chain()`:
90+
- direction: 'up' (callers), 'down' (callees), 'both'
91+
- max_depth до 3 по умолчанию
92+
- `symbol_index.py` — расширен `get_symbol_context()`:
93+
- Теперь включает calls_count и calls (кого вызывает символ)
94+
- `index_project()` — теперь вызывает `extract_calls()` и `add_references()`
95+
- 22 новых теста в `tests/test_symbol_index_call_graph.py`
96+
- References: создание связей, дедупликация, skip self-calls
97+
- Call Graph: callees, callers, depth=2, impact_files, call_chain
98+
- Call Chain: up, down, both, total_connected
99+
- Edge cases: несуществующий символ, лимит глубины, циклы
100+
- Parser: extract_calls для Python, method calls, пустой файл
101+
102+
**Инструменты:** read_file, write_file, edit_file, diagnostics, terminal (pytest)
103+
**Файлы:** src/core/parser.py (extract_calls), src/core/symbol_index.py (add_references, BFS), tests/test_symbol_index_call_graph.py
104+
**Статус:** ✅ Все 67 тестов проходят (20 reranker + 25 agentic + 22 call graph)
105+
106+
---
107+
69108
## [2026-06-28 14:00] — [Type: Feature] — Agentic Code Search (arxiv 2505.14321)
70109

71110
**Проблема:**

‎ARCHITECTURE.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ db_name = f"index_{project_name}_{project_hash}.db"
7272
| ContextEngine | `src/core/context_engine.py` | Сжатый контекст для AI |
7373
| RemoteEmbedder | `src/core/remote_embedder.py` | LM Studio / Ollama |
7474
| Reranker | `src/core/reranker.py` | Multi-Provider Reranker (Ollama/LM Studio) |
75+
| SymbolIndex | `src/core/symbol_index.py` | Bidirectional Call Graph (BFS depth 2+) + References |
7576
| Parser | `src/core/parser.py` | Tree-sitter AST парсер |
7677
| FileGuard | `src/core/file_guard.py` | Фильтрация файлов + gitignore |
7778
| Integrity | `src/core/integrity.py` | Merkle Tree для детекции изменений |
@@ -191,6 +192,45 @@ db_name = f"index_{project_name}_{project_hash}.db"
191192
- Windows native (без Docker/WSL)
192193
- Зависимости: только `httpx` (без onnxruntime/torch/transformers)
193194

195+
## 9. Call Graph (Граф вызовов)
196+
197+
Модуль `src/core/symbol_index.py` реализует двунаправленный граф вызовов:
198+
199+
### Архитектура
200+
201+
```
202+
[SymbolIndex]
203+
│
204+
├── _definitions: symbol → [SymbolRef] (кто определён где)
205+
├── _references: symbol → [SymbolRef] (кто вызывает символ)
206+
├── _file_to_defs: file → {symbols} (символы в файле)
207+
└── _file_to_calls: file → {symbols} (вызовы в файле)
208+
```
209+
210+
### Алгоритм BFS
211+
212+
```
213+
build_call_graph(symbol, depth=2):
214+
1. Находим определения символа
215+
2. BFS вверх (callers): кто вызывает → кто вызывает вызывающих
216+
3. BFS вниз (callees): кого вызывает → кого вызывает вызываемый
217+
4. Собираем impact_files (все затронутые файлы)
218+
5. Формируем call_chain для контекста
219+
```
220+
221+
### Защита от циклов
222+
223+
- Множество `visited_callers` и `visited_callees`
224+
- Ограничение глубины до 5 (автоматически)
225+
- Дедупликация результатов
226+
227+
### Извлечение вызовов
228+
229+
`parser.py` → `extract_calls()`:
230+
- Рекурсивный обход AST
231+
- Идентификация `call_expression`, `function_invocation`
232+
- Поддержка method calls (obj.method()), scoped (module::func)
233+
194234
---
195235

196236
*Последнее обновление: 2026-06-28*

‎CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# CHANGELOG
22

3+
## [1.4.0] — 2026-06-28
4+
5+
### 🚀 Major Release — Deep Call Graph (Depth 2+)
6+
7+
**New Features:**
8+
- **Bidirectional Call Graph** (`src/core/symbol_index.py`):
9+
- BFS-based graph traversal with configurable depth (1-5)
10+
- `build_call_graph()` now finds both callers and callees at depth 2+
11+
- Cycle detection to prevent infinite loops
12+
- `get_call_chain()` for upstream/downstream traversal
13+
- `get_symbol_context()` enriched with calls info
14+
- **Call Extraction** (`src/core/parser.py`):
15+
- `extract_calls()` method extracts function invocations from AST
16+
- Supports: simple calls, method calls (obj.method()), scoped (module::func)
17+
- `add_references()` in SymbolIndex for storing call relationships
18+
- **Integration** — `index_project()` now extracts calls during indexing
19+
- **Tests** — 22 new unit tests in `tests/test_symbol_index_call_graph.py`
20+
21+
### 📊 Test Coverage
22+
- **153 unit tests** + **7 benchmark tests** = **160 total**
23+
24+
---
25+
326
## [1.3.0] — 2026-06-28
427

528
### 🚀 Major Release — Multi-Provider Reranking

‎README.md‎

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

55
**AI-powered semantic code search for Zed IDE viaPython 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
66
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7-
[![Tests](https://img.shields.io/badge/tests-138%20passed-brightgreen.svg)]()
7+
[![Tests](https://img.shields.io/badge/tests-160%20passed-brightgreen.svg)]()
88
[![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/)
99

1010
[Features](#-features) • [Quick Start](#-quick-start) • [Architecture](#-architecture) • [Performance](#-performance-tuning) • [Benchmarks](#-benchmarks)
@@ -22,7 +22,7 @@
2222
| 🔄 **Agentic Deep Search** | Iterative search with query refinement across multiple passes |
2323
| 🌐 **Cross-repo Search** | Search across multiple indexed projects with `@mention` syntax |
2424
| 📊 **Progress Tracking** | Real-time indexing progress with phase, percent, files done/total |
25-
| 🌳 **Call Graph** | Find definitions, callees, and impact scope for any symbol |
25+
| 🌳 **Call Graph** | Bidirectional BFS (depth 2+): callers, callees, call chains, impact analysis |
2626
| � **Structural Search** | 13 AST patterns (class_inheritance, decorator, async, etc.) |
2727
| � **Context Search** | Find similar code by embedding selected fragment |
2828
| � **LSP Integration** | Auto-index on file save |
@@ -288,9 +288,10 @@ MSCodeBase/
288288
│ └── utils/
289289
│ ├── paths.py
290290
│ └── zed_config.py
291-
├── tests/ # 131 unit tests
291+
├── tests/ # 153 unit tests
292292
│ ├── test_agentic_search.py # 25 tests
293293
│ ├── test_reranker.py # 20 tests (Multi-Provider Reranker)
294+
│ ├── test_symbol_index_call_graph.py # 22 tests (Call Graph BFS)
294295
│ ├── test_deep_search.py # 15 tests
295296
│ ├── test_cross_repo_search.py # 21 tests
296297
│ ├── test_index_progress.py # 11 tests

‎TESTING.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
| `tests/test_searcher.py` | 4 | Hybrid search (BM25 + Dense + RRF) |
1818
| `test_connection.py` | 1 | Smoke test: FileGuard + MCP server init |
1919
| `test_automation.py` | 1 | Automated test runner |
20-
| **Total** | **131** | |
20+
| `tests/test_symbol_index_call_graph.py` | 22 | Call Graph BFS, references, cyclic calls, parser extract_calls |
21+
| **Total** | **153** | |
2122

2223
## Running Tests
2324

‎src/core/parser.py‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ class CodeParser:
2424
"function_declaration",
2525
}
2626

27+
# Узлы вызовов функций — для построения графа вызовов
28+
CALL_NODES = {
29+
"call_expression", # Python, JS, Go, Rust
30+
"call", # Альтернативные грамматики
31+
"function_invocation", # Java
32+
"invocation_expression", # Java (method invocation)
33+
"macro_invocation", # Rust macros!
34+
}
35+
36+
# Типы узлов, которые мы считаем "идентификаторами" при поиске вызовов
37+
CALL_IDENTIFIER_TYPES = {
38+
"identifier",
39+
"type_identifier",
40+
"field_expression", # obj.method()
41+
"scoped_identifier", # module::func()
42+
}
43+
2744
# Узлы-контейнеры, чьи имена мы запоминаем для контекста
2845
CONTAINER_NODES = {
2946
"class_definition",
@@ -246,6 +263,121 @@ def _find_child_by_type(self, node, node_type: str):
246263
return child
247264
return None
248265

266+
def extract_calls(self, file_path: Path) -> List[Dict]:
267+
"""Извлекает все вызовы функций из файла для построения графа вызовов.
268+
269+
Возвращает список словарей:
270+
[
271+
{
272+
"caller": "function_name", # кто вызывает
273+
"callee": "called_function", # кого вызывают
274+
"line": 42, # строка вызова
275+
"file": "path/to/file.py", # файл
276+
},
277+
...
278+
]
279+
"""
280+
ext = file_path.suffix.lower()
281+
if ext not in self.parsers or ext == ".md":
282+
return []
283+
284+
try:
285+
with open(file_path, "rb") as f:
286+
code = f.read()
287+
except Exception:
288+
return []
289+
290+
if not code.strip():
291+
return []
292+
293+
parser = self.parsers[ext]
294+
tree = parser.parse(code)
295+
296+
calls = []
297+
self._extract_calls_recursive(
298+
tree.root_node, code, file_path, calls, current_function=""
299+
)
300+
return calls
301+
302+
def _extract_calls_recursive(
303+
self,
304+
node,
305+
code: bytes,
306+
file_path: Path,
307+
calls: List[Dict],
308+
current_function: str,
309+
):
310+
"""Рекурсивно извлекает вызовы функций из AST.
311+
312+
Args:
313+
node: Текущий узел AST
314+
code: Исходный код файла
315+
file_path: Путь к файлу
316+
calls: Накопитель результатов
317+
current_function: Имя текущей функции (контекст вызова)
318+
"""
319+
# Обновляем контекст текущей функции
320+
if node.type in self.TARGET_NODES:
321+
name_node = self._find_child_by_type(
322+
node, "identifier"
323+
) or self._find_child_by_type(node, "name")
324+
if name_node:
325+
current_function = code[
326+
name_node.start_byte : name_node.end_byte
327+
].decode("utf-8", errors="ignore")
328+
329+
# Если это узел вызова — извлекаем имя вызываемой функции
330+
if node.type in self.CALL_NODES:
331+
callee_name = self._extract_callee_name(node, code)
332+
if callee_name and current_function:
333+
calls.append({
334+
"caller": current_function,
335+
"callee": callee_name,
336+
"line": node.start_point[0],
337+
"file": str(file_path),
338+
})
339+
340+
# Рекурсивно обходим детей
341+
for child in node.children:
342+
self._extract_calls_recursive(
343+
child, code, file_path, calls, current_function
344+
)
345+
346+
def _extract_callee_name(self, call_node, code: bytes) -> str:
347+
"""Извлекает имя вызываемой функции из узла вызова.
348+
349+
Поддерживает:
350+
- Простые вызовы: func() → "func"
351+
- Методы объектов: obj.method() → "method"
352+
- Цепочки: a.b.c() → "c"
353+
- Scoped: module::func() → "func"
354+
"""
355+
# Ищем идентификатор среди прямых детей
356+
for child in call_node.children:
357+
if child.type in self.CALL_IDENTIFIER_TYPES:
358+
name = code[child.start_byte : child.end_byte].decode(
359+
"utf-8", errors="ignore"
360+
)
361+
# Для field_expression (obj.method) берём последнюю часть
362+
if child.type == "field_expression":
363+
# field_expression: obj.field → ищем identifier внутри
364+
for subchild in child.children:
365+
if subchild.type == "identifier":
366+
return code[
367+
subchild.start_byte : subchild.end_byte
368+
].decode("utf-8", errors="ignore")
369+
elif subchild.type == "property_identifier":
370+
return code[
371+
subchild.start_byte : subchild.end_byte
372+
].decode("utf-8", errors="ignore")
373+
# Для scoped_identifier (module::func) берём последний сегмент
374+
elif child.type == "scoped_identifier":
375+
parts = name.split("::")
376+
return parts[-1] if parts else name
377+
else:
378+
return name
379+
return ""
380+
249381
def _chunk_giant_text(
250382
self,
251383
lines: List[str],

0 commit comments

Comments
 (0)