playbook/antigravity-awesome-skills/skills/matematico-tao/scripts/complexity_analyzer.py

545 lines
20 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Prof. Euler — Complexity Analyzer
Análise automática de complexidade ciclomática, cognitiva e acoplamento
para projetos Kotlin/Android.
Uso:
python complexity_analyzer.py [path] [--module MODULE] [--threshold N]
python complexity_analyzer.py C:/Users/renat/earbudllm
python complexity_analyzer.py C:/Users/renat/earbudllm --module llm --threshold 10
"""
import os
import re
import sys
import json
import argparse
from pathlib import Path
# Fix Unicode output on Windows (cp1252 terminal)
if sys.platform == 'win32':
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except AttributeError:
pass # Python < 3.7 fallback
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from collections import defaultdict
@dataclass
class FunctionMetrics:
name: str
file: str
line: int
cyclomatic: int
cognitive: int
lines: int
parameters: int
nullable_params: int
has_try_catch: bool
coroutine: bool # is suspend fun
@dataclass
class FileMetrics:
path: str
module: str
functions: List[FunctionMetrics] = field(default_factory=list)
imports: List[str] = field(default_factory=list)
total_lines: int = 0
blank_lines: int = 0
comment_lines: int = 0
@property
def code_lines(self):
return self.total_lines - self.blank_lines - self.comment_lines
@property
def max_cyclomatic(self):
return max((f.cyclomatic for f in self.functions), default=0)
@property
def avg_cyclomatic(self):
if not self.functions:
return 0.0
return sum(f.cyclomatic for f in self.functions) / len(self.functions)
@property
def max_cognitive(self):
return max((f.cognitive for f in self.functions), default=0)
@dataclass
class ModuleCoupling:
module: str
efferent: List[str] = field(default_factory=list) # Ce: modules this depends on
afferent: List[str] = field(default_factory=list) # Ca: modules that depend on this
@property
def instability(self) -> float:
ca = len(self.afferent)
ce = len(self.efferent)
if ca + ce == 0:
return 0.0
return ce / (ca + ce)
class KotlinComplexityAnalyzer:
"""Analisa complexidade de código Kotlin com rigor matemático."""
# Tokens que incrementam complexidade ciclomática
CYCLOMATIC_TOKENS = [
r'\bif\b', r'\belse if\b', r'\bwhen\b', r'\bfor\b', r'\bwhile\b',
r'\bdo\b', r'\btry\b', r'\bcatch\b', r'\band\b', r'\bor\b',
r'\?\?', r'\?\.', # Elvis operator e safe call
r'&&', r'\|\|',
]
# Tokens de quebra de fluxo (aumentam complexidade cognitiva mais que cicl.)
COGNITIVE_BREAK_TOKENS = [
r'\bbreak\b', r'\bcontinue\b', r'\breturn\b(?!\s+\w+\s*$)', # return no meio
r'\bthrow\b',
]
def __init__(self, project_root: str, threshold: int = 10):
self.project_root = Path(project_root)
self.threshold = threshold
self.metrics: List[FileMetrics] = []
def analyze(self, module_filter: Optional[str] = None) -> None:
"""Analisa todos os arquivos Kotlin no projeto."""
pattern = "**/*.kt"
kt_files = list(self.project_root.glob(pattern))
for kt_file in kt_files:
# Filtra arquivos de teste se não especificado
if 'test' in kt_file.parts and module_filter is None:
continue
# Filtro de módulo
module = self._detect_module(kt_file)
if module_filter and module != module_filter:
continue
metrics = self._analyze_file(kt_file, module)
self.metrics.append(metrics)
def _detect_module(self, file_path: Path) -> str:
"""Detecta qual módulo um arquivo pertence."""
parts = file_path.parts
known_modules = ['app', 'bluetooth', 'audio', 'voice', 'llm',
'integrations', 'core-logging']
for part in parts:
if part in known_modules:
return part
return 'unknown'
def _analyze_file(self, file_path: Path, module: str) -> FileMetrics:
"""Analisa um arquivo Kotlin."""
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
except Exception:
return FileMetrics(str(file_path), module)
lines = content.split('\n')
metrics = FileMetrics(
path=str(file_path.relative_to(self.project_root)),
module=module,
total_lines=len(lines)
)
# Contar linhas
for line in lines:
stripped = line.strip()
if not stripped:
metrics.blank_lines += 1
elif stripped.startswith('//') or stripped.startswith('*') or stripped.startswith('/*'):
metrics.comment_lines += 1
# Extrair imports
metrics.imports = re.findall(r'^import\s+(.+)$', content, re.MULTILINE)
# Analisar funções
metrics.functions = self._extract_functions(content, str(file_path))
return metrics
def _extract_functions(self, content: str, filepath: str) -> List[FunctionMetrics]:
"""Extrai e analisa todas as funções do arquivo."""
functions = []
lines = content.split('\n')
# Pattern para declarações de função Kotlin
fun_pattern = re.compile(
r'^(\s*)((?:suspend\s+)?(?:private\s+|protected\s+|internal\s+|public\s+)?'
r'(?:override\s+)?(?:suspend\s+)?fun\s+(\w+)\s*\(([^)]*)\))',
re.MULTILINE
)
for match in fun_pattern.finditer(content):
fun_name = match.group(3)
params_str = match.group(4)
is_suspend = 'suspend' in match.group(2)
line_num = content[:match.start()].count('\n') + 1
# Extrair corpo da função
body = self._extract_function_body(content, match.end())
if not body:
continue
# Calcular métricas
cc = self._cyclomatic_complexity(body)
cog = self._cognitive_complexity(body)
params = self._count_parameters(params_str)
nullable = self._count_nullable_params(params_str)
has_try = bool(re.search(r'\btry\b', body))
functions.append(FunctionMetrics(
name=fun_name,
file=filepath,
line=line_num,
cyclomatic=cc,
cognitive=cog,
lines=body.count('\n'),
parameters=params,
nullable_params=nullable,
has_try_catch=has_try,
coroutine=is_suspend
))
return functions
def _extract_function_body(self, content: str, start: int) -> str:
"""Extrai o corpo de uma função por contagem de chaves."""
i = start
depth = 0
started = False
while i < len(content):
c = content[i]
if c == '{':
depth += 1
started = True
elif c == '}':
depth -= 1
if started and depth == 0:
return content[start:i+1]
i += 1
return content[start:min(start + 500, len(content))]
def _cyclomatic_complexity(self, code: str) -> int:
"""Calcula CC de McCabe."""
cc = 1 # Base
for pattern in self.CYCLOMATIC_TOKENS:
matches = re.findall(pattern, code)
cc += len(matches)
return cc
def _cognitive_complexity(self, code: str) -> int:
"""Calcula complexidade cognitiva (aproximação)."""
cog = 0
nesting = 0
lines = code.split('\n')
for line in lines:
stripped = line.strip()
# Aumenta nesting
if re.search(r'\b(if|when|for|while|try)\b', stripped):
cog += (1 + nesting)
nesting += 1
# Fecha nesting
elif stripped == '}':
nesting = max(0, nesting - 1)
# Breaks de fluxo
for pattern in self.COGNITIVE_BREAK_TOKENS:
if re.search(pattern, stripped):
cog += 1
return cog
def _count_parameters(self, params_str: str) -> int:
"""Conta parâmetros de uma função."""
if not params_str.strip():
return 0
return len([p for p in params_str.split(',') if p.strip()])
def _count_nullable_params(self, params_str: str) -> int:
"""Conta parâmetros nullable (tipo?)."""
return len(re.findall(r'\w+\?', params_str))
def analyze_coupling(self) -> Dict[str, ModuleCoupling]:
"""Analisa acoplamento entre módulos."""
module_imports: Dict[str, set] = defaultdict(set)
for file_metrics in self.metrics:
for imp in file_metrics.imports:
# Detecta qual módulo está sendo importado
for mod in ['bluetooth', 'audio', 'voice', 'llm', 'integrations', 'core.logging']:
if mod in imp:
module_imports[file_metrics.module].add(mod.replace('.', '-'))
coupling = {}
all_modules = set(m.module for m in self.metrics)
for mod in all_modules:
c = ModuleCoupling(module=mod)
c.efferent = list(module_imports.get(mod, set()))
for other_mod, deps in module_imports.items():
if mod.replace('-', '.') in deps or mod in deps:
c.afferent.append(other_mod)
coupling[mod] = c
return coupling
def generate_report(self) -> Dict:
"""Gera relatório completo com análise matemática."""
# Funções problemáticas
high_cc = []
high_cognitive = []
too_long = []
too_many_params = []
unsafe_nullable = []
coroutine_issues = []
for file_m in self.metrics:
for func in file_m.functions:
if func.cyclomatic > self.threshold:
high_cc.append({
'function': func.name,
'file': file_m.path,
'line': func.line,
'cc': func.cyclomatic,
'risk': self._cc_risk_label(func.cyclomatic)
})
if func.cognitive > self.threshold * 1.5:
high_cognitive.append({
'function': func.name,
'file': file_m.path,
'line': func.line,
'cognitive': func.cognitive
})
if func.lines > 50:
too_long.append({
'function': func.name,
'file': file_m.path,
'line': func.line,
'lines': func.lines
})
if func.parameters > 5:
too_many_params.append({
'function': func.name,
'file': file_m.path,
'params': func.parameters
})
if func.nullable_params > 2:
unsafe_nullable.append({
'function': func.name,
'file': file_m.path,
'nullable_params': func.nullable_params
})
if func.coroutine and func.has_try_catch:
coroutine_issues.append({
'function': func.name,
'file': file_m.path,
'note': 'suspend fun with try-catch: verify structured concurrency'
})
# Ordenar por severidade
high_cc.sort(key=lambda x: x['cc'], reverse=True)
# Módulos com maior complexidade
module_stats = defaultdict(lambda: {'total_cc': 0, 'max_cc': 0, 'functions': 0, 'files': 0})
for file_m in self.metrics:
mod = file_m.module
module_stats[mod]['files'] += 1
module_stats[mod]['functions'] += len(file_m.functions)
for f in file_m.functions:
module_stats[mod]['total_cc'] += f.cyclomatic
module_stats[mod]['max_cc'] = max(module_stats[mod]['max_cc'], f.cyclomatic)
# Calcular média CC por módulo
for mod, stats in module_stats.items():
if stats['functions'] > 0:
stats['avg_cc'] = round(stats['total_cc'] / stats['functions'], 2)
else:
stats['avg_cc'] = 0
coupling = self.analyze_coupling()
coupling_data = {}
for mod, c in coupling.items():
coupling_data[mod] = {
'Ca': len(c.afferent),
'Ce': len(c.efferent),
'instability': round(c.instability, 3),
'depends_on': c.efferent,
'depended_by': c.afferent
}
return {
'summary': {
'total_files': len(self.metrics),
'total_functions': sum(len(f.functions) for f in self.metrics),
'total_code_lines': sum(f.code_lines for f in self.metrics),
'high_cc_functions': len(high_cc),
'high_cognitive_functions': len(high_cognitive),
},
'high_cyclomatic_complexity': high_cc[:20], # top 20
'high_cognitive_complexity': high_cognitive[:10],
'overly_long_functions': too_long[:10],
'too_many_parameters': too_many_params[:10],
'nullable_risks': unsafe_nullable[:10],
'coroutine_issues': coroutine_issues[:10],
'module_statistics': dict(module_stats),
'module_coupling': coupling_data,
}
def _cc_risk_label(self, cc: int) -> str:
if cc <= 5:
return "LOW"
elif cc <= 10:
return "MODERATE"
elif cc <= 20:
return "HIGH — refactor recommended"
else:
return "CRITICAL — must refactor"
def print_report(self, report: Dict) -> None:
"""Imprime relatório formatado."""
print("\n" + "="*70)
print(" PROF. EULER — ANÁLISE DE COMPLEXIDADE MATEMÁTICA")
print("="*70)
s = report['summary']
print(f"\n📊 RESUMO:")
print(f" Arquivos analisados: {s['total_files']}")
print(f" Funções analisadas: {s['total_functions']}")
print(f" Linhas de código: {s['total_code_lines']}")
print(f" Funções alta CC: {s['high_cc_functions']} (threshold={self.threshold})")
print(f" Funções alta cognitiva: {s['high_cognitive_functions']}")
if report['high_cyclomatic_complexity']:
print(f"\n⚠️ TOP FUNÇÕES POR COMPLEXIDADE CICLOMÁTICA:")
print(f" {'Função':<35} {'CC':>5} {'Arquivo':<40} Risco")
print(f" {'-'*35} {'-'*5} {'-'*40} {'-'*25}")
for item in report['high_cyclomatic_complexity'][:10]:
fname = item['function'][:34]
ffile = item['file'][:39]
print(f" {fname:<35} {item['cc']:>5} {ffile:<40} {item['risk']}")
if report['module_statistics']:
print(f"\n📦 ESTATÍSTICAS POR MÓDULO:")
print(f" {'Módulo':<20} {'Arquivos':>8} {'Funções':>8} {'CC Médio':>10} {'CC Máximo':>10} {'Instabilidade':>14}")
print(f" {'-'*20} {'-'*8} {'-'*8} {'-'*10} {'-'*10} {'-'*14}")
coupling = report['module_coupling']
for mod, stats in sorted(report['module_statistics'].items()):
inst = coupling.get(mod, {}).get('instability', 'N/A')
inst_str = f"{inst:.3f}" if isinstance(inst, float) else inst
print(f" {mod:<20} {stats['files']:>8} {stats['functions']:>8} "
f"{stats['avg_cc']:>10.2f} {stats['max_cc']:>10} {inst_str:>14}")
if report['coroutine_issues']:
print(f"\n🔄 PROBLEMAS POTENCIAIS EM COROUTINES:")
for item in report['coroutine_issues'][:5]:
print(f" ⚠️ {item['function']} ({item['file']})")
print(f"{item['note']}")
if report['nullable_risks']:
print(f"\n❓ NULLABLE RISKS (muitos parâmetros nullable):")
for item in report['nullable_risks'][:5]:
print(f" {item['function']}: {item['nullable_params']} nullable params ({item['file']})")
print("\n" + "="*70)
print(" Análise matemática completa. Consulte Prof. Euler para interpretação.")
print("="*70 + "\n")
def main():
parser = argparse.ArgumentParser(
description='Prof. Euler — Análise de Complexidade Matemática para Kotlin/Android'
)
parser.add_argument('path', nargs='?',
default=r'C:\Users\renat\earbudllm',
help='Caminho raiz do projeto')
parser.add_argument('--module', '-m', help='Analisar apenas este módulo')
parser.add_argument('--threshold', '-t', type=int, default=10,
help='Threshold de complexidade ciclomática (default: 10)')
parser.add_argument('--json', '-j', action='store_true',
help='Saída em formato JSON')
parser.add_argument('--output', '-o', help='Salvar relatório em arquivo')
args = parser.parse_args()
print(f"🔬 Prof. Euler analisando: {args.path}")
if args.module:
print(f" Módulo: {args.module}")
analyzer = KotlinComplexityAnalyzer(args.path, threshold=args.threshold)
analyzer.analyze(module_filter=args.module)
report = analyzer.generate_report()
if args.json:
output = json.dumps(report, indent=2, ensure_ascii=False)
print(output)
else:
analyzer.print_report(report)
if args.output:
output_path = Path(args.output)
if args.json:
output_path.write_text(json.dumps(report, indent=2, ensure_ascii=False))
else:
# Salvar como markdown
save_as_markdown(report, output_path, args.threshold)
print(f"✅ Relatório salvo em: {args.output}")
return report
def save_as_markdown(report: Dict, path: Path, threshold: int) -> None:
"""Salva relatório em formato Markdown."""
lines = [
"# Prof. Euler — Relatório de Complexidade Matemática\n",
f"Threshold CC: {threshold}\n\n",
"## Resumo\n",
f"- **Arquivos**: {report['summary']['total_files']}\n",
f"- **Funções**: {report['summary']['total_functions']}\n",
f"- **Linhas de código**: {report['summary']['total_code_lines']}\n",
f"- **Alta CC**: {report['summary']['high_cc_functions']}\n\n",
"## Funções de Alta Complexidade Ciclomática\n",
"| Função | CC | Arquivo | Risco |\n",
"|--------|-----|---------|-------|\n",
]
for item in report['high_cyclomatic_complexity'][:15]:
lines.append(
f"| `{item['function']}` | {item['cc']} | `{item['file']}` | {item['risk']} |\n"
)
lines.append("\n## Acoplamento de Módulos\n")
lines.append("| Módulo | Ca | Ce | Instabilidade | Depende de |\n")
lines.append("|--------|----|----|--------------|------------|\n")
for mod, data in sorted(report['module_coupling'].items()):
deps = ', '.join(data['depends_on']) or 'nenhum'
lines.append(
f"| {mod} | {data['Ca']} | {data['Ce']} | {data['instability']:.3f} | {deps} |\n"
)
path.write_text(''.join(lines), encoding='utf-8')
if __name__ == '__main__':
main()