vkr/prog/arch-researcher/utils/code_analyzer.py
2026-05-21 08:09:36 +05:00

178 lines
5.8 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++.
Точность: comparable с lizard при отсутствии установленного Clang.
Для продакшн-использования рекомендуется установить lizard:
pip install lizard
"""
from __future__ import annotations
import re
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,
)
# Сигнатуры функций C/C++
_CPP_FUNC = re.compile(
r'^(?:[\w:*&<>\s]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:noexcept[^{]*)?\{',
re.MULTILINE,
)
# Строки и комментарии — для удаления перед анализом
_CPP_COMMENT_LINE = re.compile(r'//[^\n]*')
_CPP_COMMENT_BLOCK = re.compile(r'/\*.*?\*/', re.DOTALL)
_CPP_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
@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()
if suffix not in (".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h"):
return None
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
functions = _analyze_cpp(text, file_path)
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
# ------------------------------------------------------------------
# Вспомогательные функции
# ------------------------------------------------------------------
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