""" Встроенный анализатор сложности кода — не требует 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