arch-researcher/code_analyzer.py
2026-04-27 23:44:22 +05:00

258 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Встроенный анализатор сложности кода — не требует lizard.
Реализует подсчёт цикломатической сложности (McCabe, 1976)
методом подсчёта ветвлений в тексте кода (лексический подход),
аналогично тому, как это делает библиотека lizard.
Поддерживаемые языки: C, C++, JavaScript, TypeScript.
Точность: comparable с lizard при отсутствии установленного Clang.
Для продакшн-использования рекомендуется установить lizard:
pip install lizard
"""
from __future__ import annotations
import re
import tokenize
import io
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
# Ключевые слова ветвления, каждое увеличивает CCN на 1
# По методу McCabe: CCN = 1 + число точек ветвления
_BRANCH_KEYWORDS_CPP = re.compile(
r'\b(if|else\s+if|for|while|do|case|catch|&&|\|\||and|or|\?)\b',
re.MULTILINE,
)
_BRANCH_KEYWORDS_JS = re.compile(
r'\b(if|else\s+if|for|while|do|switch|case|catch|&&|\|\||\?(?!:))\b',
re.MULTILINE,
)
# Сигнатуры функций C/C++
_CPP_FUNC = re.compile(
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
re.MULTILINE,
)
# Сигнатуры функций JS/TS
_JS_FUNC = re.compile(
r'(?:'
r'function\s+(\w+)\s*\(([^)]*)\)' # function foo(...)
r'|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>' # arrow
r'|(\w+)\s*:\s*(?:async\s*)?function\s*\(([^)]*)\)' # method: function
r'|(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{' # method shorthand
r')',
re.MULTILINE,
)
# Строки и комментарии — для удаления перед анализом
_CPP_COMMENT_LINE = re.compile(r'//[^\n]*')
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
_JS_TEMPLATE = re.compile(r'`[^`]*`', re.DOTALL)
@dataclass
class FunctionInfo:
"""Результат разбора одной функции."""
name: str
start_line: int
nloc: int
cyclomatic_complexity: int
parameter_count: int
@dataclass
class FileAnalysisResult:
"""Результат анализа одного файла."""
filename: str
function_list: List[FunctionInfo]
@property
def average_ccn(self) -> float:
if not self.function_list:
return 0.0
return sum(f.cyclomatic_complexity for f in self.function_list) / len(self.function_list)
def analyze_file(file_path: str) -> Optional[FileAnalysisResult]:
"""
Анализирует один файл и возвращает метрики по функциям.
Возвращает None если файл нечитаем или неподдерживаемого типа.
"""
path = Path(file_path)
suffix = path.suffix.lower()
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
if suffix in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
functions = _analyze_cpp(text, file_path)
elif suffix in (".js", ".mjs", ".cjs", ".ts"):
functions = _analyze_js(text, file_path)
else:
return None
return FileAnalysisResult(filename=file_path, function_list=functions)
# ------------------------------------------------------------------
# C / C++ анализ
# ------------------------------------------------------------------
def _analyze_cpp(text: str, filename: str) -> List[FunctionInfo]:
"""Анализирует C/C++ код."""
# Удаляем строковые литералы и комментарии
clean = _CPP_COMMENT_BLOCK.sub(' ', text)
clean = _CPP_COMMENT_LINE.sub('', clean)
clean = _CPP_STRING.sub('""', clean)
functions: List[FunctionInfo] = []
for match in _CPP_FUNC.finditer(clean):
func_name = match.group(1)
params_str = match.group(2).strip()
# Пропускаем системные/macro-like имена
if func_name in ('if', 'for', 'while', 'switch', 'catch', 'return'):
continue
start_pos = match.start()
start_line = text[:start_pos].count('\n') + 1
# Извлекаем тело функции
body = _extract_body(clean, match.end() - 1)
if body is None:
continue
nloc = _count_nloc(body)
ccn = 1 + len(_BRANCH_KEYWORDS_CPP.findall(body))
params = _count_params(params_str)
functions.append(FunctionInfo(
name=func_name,
start_line=start_line,
nloc=nloc,
cyclomatic_complexity=ccn,
parameter_count=params,
))
return functions
# ------------------------------------------------------------------
# JavaScript / TypeScript анализ
# ------------------------------------------------------------------
def _analyze_js(text: str, filename: str) -> List[FunctionInfo]:
"""Анализирует JS/TS код."""
# Удаляем шаблонные строки и комментарии
clean = _CPP_COMMENT_BLOCK.sub(' ', text)
clean = _CPP_COMMENT_LINE.sub('', clean)
clean = _JS_TEMPLATE.sub('``', clean)
clean = _CPP_STRING.sub('""', clean)
functions: List[FunctionInfo] = []
seen_positions: set = set()
for match in _JS_FUNC.finditer(clean):
# Определяем имя и параметры из разных групп
name = (
match.group(1) or match.group(3) or
match.group(5) or match.group(7) or "anonymous"
)
params_str = (
match.group(2) or match.group(4) or
match.group(6) or match.group(8) or ""
)
start_pos = match.start()
if start_pos in seen_positions:
continue
seen_positions.add(start_pos)
start_line = text[:start_pos].count('\n') + 1
# Ищем открывающую фигурную скобку
brace_pos = clean.find('{', match.end())
if brace_pos == -1:
continue
body = _extract_body(clean, brace_pos)
if body is None:
continue
nloc = _count_nloc(body)
ccn = 1 + len(_BRANCH_KEYWORDS_JS.findall(body))
params = _count_params(params_str)
functions.append(FunctionInfo(
name=name,
start_line=start_line,
nloc=nloc,
cyclomatic_complexity=ccn,
parameter_count=params,
))
return functions
# ------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------
def _extract_body(text: str, open_brace_pos: int) -> Optional[str]:
"""
Извлекает тело функции, начиная с позиции открывающей скобки.
Возвращает содержимое между { и соответствующей }.
"""
depth = 0
i = open_brace_pos
while i < len(text):
ch = text[i]
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
return text[open_brace_pos + 1:i]
i += 1
return None # Незакрытая скобка
def _count_nloc(body: str) -> int:
"""Считает непустые, неккомментарные строки."""
return sum(
1 for line in body.splitlines()
if line.strip() and not line.strip().startswith('//')
)
def _count_params(params_str: str) -> int:
"""Считает число параметров по строке параметров."""
params_str = params_str.strip()
if not params_str or params_str in ('void', '...'):
return 0
# Разбиваем по запятой, учитывая вложенные угловые скобки
depth = 0
count = 1
for ch in params_str:
if ch in '<(':
depth += 1
elif ch in '>)':
depth -= 1
elif ch == ',' and depth == 0:
count += 1
return count