@@ -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