131 lines
6.1 KiB
Python
131 lines
6.1 KiB
Python
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') |