Files
agents/analyzer.py
T

131 lines
6.1 KiB
Python
Raw Normal View History

2026-09-19 13:18:49 +00:00
import json
import tree_sitter
from tree_sitter import Language, Parser
import tree_sitter_python as tspython
import tree_sitter_javascript as tsjavascript
import tree_sitter_typescript as tstypescript
import tree_sitter_html as tshtml
from pathlib import Path
from typing import Dict, List, Any, Optional
class CodeAnalyzer:
"""Анализирует код с помощью tree-sitter и извлекает структуру."""
LANGUAGES = {
'.py': Language(tspython.language()),
'.js': Language(tsjavascript.language()),
'.ts': Language(tstypescript.language_typescript()),
'.tsx': Language(tstypescript.language_tsx()),
'.html': Language(tshtml.language()),
}
def __init__(self):
self.parsers = {}
for ext, language in self.LANGUAGES.items():
# Язык теперь передаётся сразу при создании парсера
self.parsers[ext] = Parser(language)
def analyze_file(self, content: str, extension: str) -> Dict[str, Any]:
"""Анализирует содержимое файла и возвращает структурированную информацию."""
parser = self.parsers.get(extension)
if not parser:
# Для неподдерживаемых расширений возвращаем сырой текст (обрезанный до разумного)
return {"raw": content[:1000] + "..." if len(content) > 1000 else content}
tree = parser.parse(bytes(content, 'utf-8'))
root = tree.root_node
info = {
"imports": [],
"exports": [],
"functions": [],
"classes": [],
"variables": [],
"has_side_effects": False,
"jsx_elements": [] if extension in ('.tsx', '.jsx') else None
}
# Обходим все узлы
self._traverse(root, info, extension)
return info
def _traverse(self, node, info, extension):
"""Рекурсивно обходит AST и заполняет info."""
if node.type == 'function_definition' or node.type == 'function_declaration':
func_name = self._get_node_text(node.child_by_field_name('name'))
if func_name:
params = []
params_node = node.child_by_field_name('parameters')
if params_node:
for p in params_node.children:
if p.type == 'identifier' or p.type == 'formal_parameter':
params.append(self._get_node_text(p))
info['functions'].append({
"name": func_name,
"params": params
})
elif node.type == 'class_definition' or node.type == 'class_declaration':
class_name = self._get_node_text(node.child_by_field_name('name'))
if class_name:
methods = []
body = node.child_by_field_name('body')
if body:
for child in body.children:
if child.type == 'method_definition' or child.type == 'function_definition':
mname = self._get_node_text(child.child_by_field_name('name'))
if mname:
methods.append(mname)
info['classes'].append({
"name": class_name,
"methods": methods
})
elif node.type == 'import_statement' or node.type == 'import_from_statement':
# Для Python и JS/TS импорты выглядят по-разному, но мы упростим
import_text = self._get_node_text(node)
info['imports'].append(import_text.strip())
elif node.type == 'export_statement' or node.type == 'export_named_statement':
export_text = self._get_node_text(node)
info['exports'].append(export_text.strip())
elif node.type == 'assignment' or node.type == 'variable_declaration':
# Попытка извлечь имена переменных верхнего уровня
var_names = []
if node.type == 'assignment':
left = node.child_by_field_name('left')
if left and left.type == 'identifier':
var_names.append(self._get_node_text(left))
elif node.type == 'variable_declaration':
for child in node.children:
if child.type == 'variable_declarator':
name_node = child.child_by_field_name('name')
if name_node:
var_names.append(self._get_node_text(name_node))
info['variables'].extend(var_names)
# Побочные эффекты: выражение верхнего уровня, не являющееся импортом/экспортом/декларацией
if node.type == 'expression_statement':
# Исключаем вызовы, которые являются частью модуля (например, вызов ReactDOM.render)
# Считаем, что любое выражение верхнего уровня — потенциальный side-effect
# Можно уточнить позже
info['has_side_effects'] = True
if extension in ('.tsx', '.jsx') and node.type == 'jsx_element':
# Извлекаем название компонента
opening = node.child_by_field_name('opening_element')
if opening:
name_node = opening.child_by_field_name('name')
if name_node:
info['jsx_elements'].append(self._get_node_text(name_node))
# Рекурсивно обходим детей
for child in node.children:
self._traverse(child, info, extension)
@staticmethod
def _get_node_text(node) -> str:
if node is None:
return ""
return node.text.decode('utf-8')