hanuscode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/analysis/debt.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
# hanus/analysis/debt.py
|
|
2
|
+
"""
|
|
3
|
+
Análisis de deuda técnica.
|
|
4
|
+
|
|
5
|
+
Detecta código muerto, complejidad ciclomática,
|
|
6
|
+
score de mantenibilidad y sugiere refactors.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import ast
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Optional, Any, Set, Tuple
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DebtType(Enum):
|
|
19
|
+
"""Tipo de deuda técnica."""
|
|
20
|
+
CODE_DUPLICATION = "code_duplication"
|
|
21
|
+
DEAD_CODE = "dead_code"
|
|
22
|
+
HIGH_COMPLEXITY = "high_complexity"
|
|
23
|
+
LONG_FUNCTION = "long_function"
|
|
24
|
+
DEEP_NESTING = "deep_nesting"
|
|
25
|
+
TOO_MANY_PARAMETERS = "too_many_parameters"
|
|
26
|
+
MISSING_TESTS = "missing_tests"
|
|
27
|
+
UNUSED_IMPORTS = "unused_imports"
|
|
28
|
+
UNUSED_VARIABLES = "unused_variables"
|
|
29
|
+
MAGIC_NUMBERS = "magic_numbers"
|
|
30
|
+
TODO_COMMENTS = "todo_comments"
|
|
31
|
+
HARDCODED_VALUES = "hardcoded_values"
|
|
32
|
+
GOD_CLASS = "god_class"
|
|
33
|
+
FEATURE_ENVY = "feature_envy"
|
|
34
|
+
LONG_PARAMETER_LIST = "long_parameter_list"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DebtSeverity(Enum):
|
|
38
|
+
"""Severidad de la deuda."""
|
|
39
|
+
LOW = 1
|
|
40
|
+
MEDIUM = 2
|
|
41
|
+
HIGH = 3
|
|
42
|
+
CRITICAL = 4
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class DebtIssue:
|
|
47
|
+
"""Un problema de deuda técnica detectado."""
|
|
48
|
+
id: str
|
|
49
|
+
type: DebtType
|
|
50
|
+
severity: DebtSeverity
|
|
51
|
+
file_path: str
|
|
52
|
+
line_start: int
|
|
53
|
+
line_end: int
|
|
54
|
+
description: str
|
|
55
|
+
suggestion: str
|
|
56
|
+
effort_minutes: int # Estimación de esfuerzo para arreglar
|
|
57
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict:
|
|
60
|
+
return {
|
|
61
|
+
"id": self.id,
|
|
62
|
+
"type": self.type.value,
|
|
63
|
+
"severity": self.severity.value,
|
|
64
|
+
"file_path": self.file_path,
|
|
65
|
+
"line_start": self.line_start,
|
|
66
|
+
"line_end": self.line_end,
|
|
67
|
+
"description": self.description,
|
|
68
|
+
"suggestion": self.suggestion,
|
|
69
|
+
"effort_minutes": self.effort_minutes,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class FileMetrics:
|
|
75
|
+
"""Métricas de un archivo."""
|
|
76
|
+
file_path: str
|
|
77
|
+
lines_of_code: int
|
|
78
|
+
cyclomatic_complexity: float
|
|
79
|
+
cognitive_complexity: float
|
|
80
|
+
maintainability_index: float # 0-100, higher is better
|
|
81
|
+
function_count: int
|
|
82
|
+
class_count: int
|
|
83
|
+
comment_ratio: float
|
|
84
|
+
duplication_ratio: float
|
|
85
|
+
issues: List[DebtIssue] = field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> Dict:
|
|
88
|
+
return {
|
|
89
|
+
"file_path": self.file_path,
|
|
90
|
+
"lines_of_code": self.lines_of_code,
|
|
91
|
+
"cyclomatic_complexity": round(self.cyclomatic_complexity, 2),
|
|
92
|
+
"cognitive_complexity": round(self.cognitive_complexity, 2),
|
|
93
|
+
"maintainability_index": round(self.maintainability_index, 2),
|
|
94
|
+
"function_count": self.function_count,
|
|
95
|
+
"class_count": self.class_count,
|
|
96
|
+
"comment_ratio": round(self.comment_ratio, 2),
|
|
97
|
+
"duplication_ratio": round(self.duplication_ratio, 2),
|
|
98
|
+
"issue_count": len(self.issues),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class DebtReport:
|
|
104
|
+
"""Reporte completo de deuda técnica."""
|
|
105
|
+
project_root: str
|
|
106
|
+
files_analyzed: int
|
|
107
|
+
total_issues: int
|
|
108
|
+
total_effort_hours: float
|
|
109
|
+
maintainability_score: float # 0-100, higher is better
|
|
110
|
+
metrics_by_file: Dict[str, FileMetrics] = field(default_factory=dict)
|
|
111
|
+
issues: List[DebtIssue] = field(default_factory=list)
|
|
112
|
+
|
|
113
|
+
def get_summary(self) -> Dict:
|
|
114
|
+
"""Obtiene resumen ejecutivo."""
|
|
115
|
+
by_severity = defaultdict(int)
|
|
116
|
+
by_type = defaultdict(int)
|
|
117
|
+
|
|
118
|
+
for issue in self.issues:
|
|
119
|
+
by_severity[issue.severity.name] += 1
|
|
120
|
+
by_type[issue.type.value] += 1
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"files_analyzed": self.files_analyzed,
|
|
124
|
+
"total_issues": self.total_issues,
|
|
125
|
+
"total_effort_hours": round(self.total_effort_hours, 1),
|
|
126
|
+
"maintainability_score": round(self.maintainability_score, 1),
|
|
127
|
+
"by_severity": dict(by_severity),
|
|
128
|
+
"by_type": dict(by_type),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DebtAnalyzer:
|
|
133
|
+
"""
|
|
134
|
+
Analiza deuda técnica en un proyecto.
|
|
135
|
+
|
|
136
|
+
Métricas:
|
|
137
|
+
- Complejidad ciclomática
|
|
138
|
+
- Líneas por función
|
|
139
|
+
- Profundidad de anidación
|
|
140
|
+
- Duplicación de código
|
|
141
|
+
- Imports no usados
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Umbrales configurables
|
|
145
|
+
MAX_FUNCTION_LINES = 50
|
|
146
|
+
MAX_FUNCTION_PARAMS = 5
|
|
147
|
+
MAX_NESTING_DEPTH = 4
|
|
148
|
+
MAX_CLASS_METHODS = 15
|
|
149
|
+
MAX_CYCLOMATIC_COMPLEXITY = 10
|
|
150
|
+
MIN_MAINTAINABILITY_INDEX = 20
|
|
151
|
+
|
|
152
|
+
def __init__(self, project_root: Path):
|
|
153
|
+
self.project_root = project_root
|
|
154
|
+
self._issue_counter = 0
|
|
155
|
+
|
|
156
|
+
def analyze_project(
|
|
157
|
+
self,
|
|
158
|
+
include_patterns: List[str] = None,
|
|
159
|
+
exclude_patterns: List[str] = None
|
|
160
|
+
) -> DebtReport:
|
|
161
|
+
"""
|
|
162
|
+
Analiza todo el proyecto.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
include_patterns: Patrones de archivos a incluir
|
|
166
|
+
exclude_patterns: Patrones a excluir
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
DebtReport con el análisis completo
|
|
170
|
+
"""
|
|
171
|
+
include_patterns = include_patterns or ["*.py", "*.js", "*.ts"]
|
|
172
|
+
exclude_patterns = exclude_patterns or [
|
|
173
|
+
"node_modules", "__pycache__", ".git", "venv", "env",
|
|
174
|
+
"dist", "build", "test", "tests"
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
report = DebtReport(
|
|
178
|
+
project_root=str(self.project_root),
|
|
179
|
+
files_analyzed=0,
|
|
180
|
+
total_issues=0,
|
|
181
|
+
total_effort_hours=0,
|
|
182
|
+
maintainability_score=100,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Recolectar archivos
|
|
186
|
+
files = self._collect_files(include_patterns, exclude_patterns)
|
|
187
|
+
|
|
188
|
+
# Analizar cada archivo
|
|
189
|
+
for file_path in files:
|
|
190
|
+
metrics = self.analyze_file(file_path)
|
|
191
|
+
report.metrics_by_file[str(file_path)] = metrics
|
|
192
|
+
report.issues.extend(metrics.issues)
|
|
193
|
+
report.files_analyzed += 1
|
|
194
|
+
|
|
195
|
+
# Calcular totales
|
|
196
|
+
report.total_issues = len(report.issues)
|
|
197
|
+
report.total_effort_hours = sum(i.effort_minutes for i in report.issues) / 60
|
|
198
|
+
|
|
199
|
+
# Calcular score de mantenibilidad promedio
|
|
200
|
+
if report.metrics_by_file:
|
|
201
|
+
report.maintainability_score = sum(
|
|
202
|
+
m.maintainability_index for m in report.metrics_by_file.values()
|
|
203
|
+
) / len(report.metrics_by_file)
|
|
204
|
+
|
|
205
|
+
return report
|
|
206
|
+
|
|
207
|
+
def analyze_file(self, file_path: Path) -> FileMetrics:
|
|
208
|
+
"""
|
|
209
|
+
Analiza un archivo específico.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
file_path: Ruta del archivo
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
FileMetrics con el análisis
|
|
216
|
+
"""
|
|
217
|
+
ext = file_path.suffix.lower()
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
221
|
+
except Exception:
|
|
222
|
+
return FileMetrics(
|
|
223
|
+
file_path=str(file_path),
|
|
224
|
+
lines_of_code=0,
|
|
225
|
+
cyclomatic_complexity=0,
|
|
226
|
+
cognitive_complexity=0,
|
|
227
|
+
maintainability_index=0,
|
|
228
|
+
function_count=0,
|
|
229
|
+
class_count=0,
|
|
230
|
+
comment_ratio=0,
|
|
231
|
+
duplication_ratio=0,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
lines = content.split('\n')
|
|
235
|
+
loc = len([l for l in lines if l.strip() and not l.strip().startswith('#')])
|
|
236
|
+
|
|
237
|
+
if ext == '.py':
|
|
238
|
+
return self._analyze_python(file_path, content, lines, loc)
|
|
239
|
+
elif ext in ('.js', '.ts', '.jsx', '.tsx'):
|
|
240
|
+
return self._analyze_javascript(file_path, content, lines, loc)
|
|
241
|
+
|
|
242
|
+
return FileMetrics(
|
|
243
|
+
file_path=str(file_path),
|
|
244
|
+
lines_of_code=loc,
|
|
245
|
+
cyclomatic_complexity=0,
|
|
246
|
+
cognitive_complexity=0,
|
|
247
|
+
maintainability_index=50,
|
|
248
|
+
function_count=0,
|
|
249
|
+
class_count=0,
|
|
250
|
+
comment_ratio=0,
|
|
251
|
+
duplication_ratio=0,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
255
|
+
# ANÁLISIS PYTHON
|
|
256
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
def _analyze_python(
|
|
259
|
+
self,
|
|
260
|
+
file_path: Path,
|
|
261
|
+
content: str,
|
|
262
|
+
lines: List[str],
|
|
263
|
+
loc: int
|
|
264
|
+
) -> FileMetrics:
|
|
265
|
+
"""Analiza código Python."""
|
|
266
|
+
issues = []
|
|
267
|
+
function_count = 0
|
|
268
|
+
class_count = 0
|
|
269
|
+
total_complexity = 0
|
|
270
|
+
total_cognitive = 0
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
tree = ast.parse(content)
|
|
274
|
+
except SyntaxError:
|
|
275
|
+
return FileMetrics(
|
|
276
|
+
file_path=str(file_path),
|
|
277
|
+
lines_of_code=loc,
|
|
278
|
+
cyclomatic_complexity=0,
|
|
279
|
+
cognitive_complexity=0,
|
|
280
|
+
maintainability_index=0,
|
|
281
|
+
function_count=0,
|
|
282
|
+
class_count=0,
|
|
283
|
+
comment_ratio=0,
|
|
284
|
+
duplication_ratio=0,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Analizar imports no usados
|
|
288
|
+
imports = set()
|
|
289
|
+
for node in ast.walk(tree):
|
|
290
|
+
if isinstance(node, ast.Import):
|
|
291
|
+
for alias in node.names:
|
|
292
|
+
imports.add(alias.asname or alias.name.split('.')[0])
|
|
293
|
+
elif isinstance(node, ast.ImportFrom):
|
|
294
|
+
for alias in node.names:
|
|
295
|
+
imports.add(alias.asname or alias.name)
|
|
296
|
+
|
|
297
|
+
# Encontrar nombres usados
|
|
298
|
+
used_names = set()
|
|
299
|
+
for node in ast.walk(tree):
|
|
300
|
+
if isinstance(node, ast.Name):
|
|
301
|
+
used_names.add(node.id)
|
|
302
|
+
|
|
303
|
+
unused_imports = imports - used_names
|
|
304
|
+
for imp in unused_imports:
|
|
305
|
+
issues.append(self._create_issue(
|
|
306
|
+
DebtType.UNUSED_IMPORTS,
|
|
307
|
+
DebtSeverity.LOW,
|
|
308
|
+
str(file_path),
|
|
309
|
+
1, 1,
|
|
310
|
+
f"Unused import: {imp}",
|
|
311
|
+
f"Remove import: {imp}",
|
|
312
|
+
2
|
|
313
|
+
))
|
|
314
|
+
|
|
315
|
+
# Analizar clases y funciones
|
|
316
|
+
for node in ast.iter_child_nodes(tree):
|
|
317
|
+
if isinstance(node, ast.ClassDef):
|
|
318
|
+
class_count += 1
|
|
319
|
+
issues.extend(self._analyze_class(node, str(file_path)))
|
|
320
|
+
|
|
321
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
322
|
+
function_count += 1
|
|
323
|
+
complexity = self._calculate_complexity(node)
|
|
324
|
+
cognitive = self._calculate_cognitive_complexity(node)
|
|
325
|
+
total_complexity += complexity
|
|
326
|
+
total_cognitive += cognitive
|
|
327
|
+
|
|
328
|
+
issues.extend(self._analyze_function(node, str(file_path), complexity))
|
|
329
|
+
|
|
330
|
+
# Detectar código duplicado (simplificado)
|
|
331
|
+
duplication = self._detect_duplication(lines)
|
|
332
|
+
if duplication > 0.1:
|
|
333
|
+
issues.append(self._create_issue(
|
|
334
|
+
DebtType.CODE_DUPLICATION,
|
|
335
|
+
DebtSeverity.MEDIUM,
|
|
336
|
+
str(file_path),
|
|
337
|
+
1, loc,
|
|
338
|
+
f"Code duplication detected ({duplication:.0%})",
|
|
339
|
+
"Refactor duplicated code into reusable functions",
|
|
340
|
+
int(duplication * loc * 2)
|
|
341
|
+
))
|
|
342
|
+
|
|
343
|
+
# Detectar TODOs
|
|
344
|
+
for i, line in enumerate(lines, 1):
|
|
345
|
+
if 'TODO' in line or 'FIXME' in line:
|
|
346
|
+
issues.append(self._create_issue(
|
|
347
|
+
DebtType.TODO_COMMENTS,
|
|
348
|
+
DebtSeverity.LOW,
|
|
349
|
+
str(file_path),
|
|
350
|
+
i, i,
|
|
351
|
+
"TODO/FIXME comment found",
|
|
352
|
+
"Implement or remove TODO",
|
|
353
|
+
15
|
|
354
|
+
))
|
|
355
|
+
|
|
356
|
+
# Calcular métricas
|
|
357
|
+
avg_complexity = total_complexity / function_count if function_count > 0 else 0
|
|
358
|
+
avg_cognitive = total_cognitive / function_count if function_count > 0 else 0
|
|
359
|
+
|
|
360
|
+
# Maintainability Index (simplificado)
|
|
361
|
+
# MI = 171 - 5.2 * ln(V) - 0.23 * G - 16.2 * ln(LOC)
|
|
362
|
+
# Simplificado para este análisis
|
|
363
|
+
mi = max(0, min(100, 100 - avg_complexity * 5 - (loc / 100) * 5))
|
|
364
|
+
|
|
365
|
+
# Comment ratio
|
|
366
|
+
comment_lines = len([l for l in lines if l.strip().startswith('#')])
|
|
367
|
+
comment_ratio = comment_lines / loc if loc > 0 else 0
|
|
368
|
+
|
|
369
|
+
return FileMetrics(
|
|
370
|
+
file_path=str(file_path),
|
|
371
|
+
lines_of_code=loc,
|
|
372
|
+
cyclomatic_complexity=avg_complexity,
|
|
373
|
+
cognitive_complexity=avg_cognitive,
|
|
374
|
+
maintainability_index=mi,
|
|
375
|
+
function_count=function_count,
|
|
376
|
+
class_count=class_count,
|
|
377
|
+
comment_ratio=comment_ratio,
|
|
378
|
+
duplication_ratio=duplication,
|
|
379
|
+
issues=issues,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def _analyze_class(self, node: ast.ClassDef, file_path: str) -> List[DebtIssue]:
|
|
383
|
+
"""Analiza una clase."""
|
|
384
|
+
issues = []
|
|
385
|
+
|
|
386
|
+
# God Class: demasiados métodos
|
|
387
|
+
methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
388
|
+
if len(methods) > self.MAX_CLASS_METHODS:
|
|
389
|
+
issues.append(self._create_issue(
|
|
390
|
+
DebtType.GOD_CLASS,
|
|
391
|
+
DebtSeverity.HIGH,
|
|
392
|
+
file_path,
|
|
393
|
+
node.lineno, node.end_lineno or node.lineno,
|
|
394
|
+
f"Class '{node.name}' has {len(methods)} methods (max {self.MAX_CLASS_METHODS})",
|
|
395
|
+
"Split class into smaller, focused classes",
|
|
396
|
+
len(methods) * 5
|
|
397
|
+
))
|
|
398
|
+
|
|
399
|
+
# Verificar métodos individuales
|
|
400
|
+
for method in methods:
|
|
401
|
+
issues.extend(self._analyze_function(method, file_path, 0, parent_class=node.name))
|
|
402
|
+
|
|
403
|
+
return issues
|
|
404
|
+
|
|
405
|
+
def _analyze_function(
|
|
406
|
+
self,
|
|
407
|
+
node: ast.FunctionDef,
|
|
408
|
+
file_path: str,
|
|
409
|
+
complexity: int,
|
|
410
|
+
parent_class: str = None
|
|
411
|
+
) -> List[DebtIssue]:
|
|
412
|
+
"""Analiza una función/método."""
|
|
413
|
+
issues = []
|
|
414
|
+
func_name = f"{parent_class}.{node.name}" if parent_class else node.name
|
|
415
|
+
|
|
416
|
+
# Líneas de código
|
|
417
|
+
func_lines = (node.end_lineno or node.lineno) - node.lineno + 1
|
|
418
|
+
|
|
419
|
+
# Función larga
|
|
420
|
+
if func_lines > self.MAX_FUNCTION_LINES:
|
|
421
|
+
issues.append(self._create_issue(
|
|
422
|
+
DebtType.LONG_FUNCTION,
|
|
423
|
+
DebtSeverity.MEDIUM,
|
|
424
|
+
file_path,
|
|
425
|
+
node.lineno, node.end_lineno or node.lineno,
|
|
426
|
+
f"Function '{func_name}' has {func_lines} lines (max {self.MAX_FUNCTION_LINES})",
|
|
427
|
+
"Break into smaller functions",
|
|
428
|
+
func_lines // 10
|
|
429
|
+
))
|
|
430
|
+
|
|
431
|
+
# Muchos parámetros
|
|
432
|
+
param_count = len(node.args.args) + len(node.args.kwonlyargs)
|
|
433
|
+
if node.args.vararg:
|
|
434
|
+
param_count += 1
|
|
435
|
+
if node.args.kwarg:
|
|
436
|
+
param_count += 1
|
|
437
|
+
|
|
438
|
+
if param_count > self.MAX_FUNCTION_PARAMS:
|
|
439
|
+
issues.append(self._create_issue(
|
|
440
|
+
DebtType.TOO_MANY_PARAMETERS,
|
|
441
|
+
DebtSeverity.MEDIUM,
|
|
442
|
+
file_path,
|
|
443
|
+
node.lineno, node.lineno,
|
|
444
|
+
f"Function '{func_name}' has {param_count} parameters (max {self.MAX_FUNCTION_PARAMS})",
|
|
445
|
+
"Use parameter object or refactor",
|
|
446
|
+
param_count * 3
|
|
447
|
+
))
|
|
448
|
+
|
|
449
|
+
# Alta complejidad ciclomática
|
|
450
|
+
if complexity > self.MAX_CYCLOMATIC_COMPLEXITY:
|
|
451
|
+
issues.append(self._create_issue(
|
|
452
|
+
DebtType.HIGH_COMPLEXITY,
|
|
453
|
+
DebtSeverity.HIGH,
|
|
454
|
+
file_path,
|
|
455
|
+
node.lineno, node.end_lineno or node.lineno,
|
|
456
|
+
f"Function '{func_name}' has complexity {complexity} (max {self.MAX_CYCLOMATIC_COMPLEXITY})",
|
|
457
|
+
"Simplify control flow, extract methods",
|
|
458
|
+
complexity * 5
|
|
459
|
+
))
|
|
460
|
+
|
|
461
|
+
# Profundidad de anidación
|
|
462
|
+
max_depth = self._calculate_nesting_depth(node)
|
|
463
|
+
if max_depth > self.MAX_NESTING_DEPTH:
|
|
464
|
+
issues.append(self._create_issue(
|
|
465
|
+
DebtType.DEEP_NESTING,
|
|
466
|
+
DebtSeverity.MEDIUM,
|
|
467
|
+
file_path,
|
|
468
|
+
node.lineno, node.end_lineno or node.lineno,
|
|
469
|
+
f"Function '{func_name}' has nesting depth {max_depth} (max {self.MAX_NESTING_DEPTH})",
|
|
470
|
+
"Extract nested logic into separate functions",
|
|
471
|
+
max_depth * 10
|
|
472
|
+
))
|
|
473
|
+
|
|
474
|
+
# Números mágicos
|
|
475
|
+
for child in ast.walk(node):
|
|
476
|
+
if isinstance(child, ast.Constant) and isinstance(child.value, (int, float)):
|
|
477
|
+
# Ignorar 0, 1, -1 y otros comunes
|
|
478
|
+
if child.value not in (0, 1, -1, 2, 10, 100, 1000, True, False):
|
|
479
|
+
issues.append(self._create_issue(
|
|
480
|
+
DebtType.MAGIC_NUMBERS,
|
|
481
|
+
DebtSeverity.LOW,
|
|
482
|
+
file_path,
|
|
483
|
+
child.lineno, child.lineno,
|
|
484
|
+
f"Magic number {child.value} in function '{func_name}'",
|
|
485
|
+
"Define as named constant",
|
|
486
|
+
2
|
|
487
|
+
))
|
|
488
|
+
|
|
489
|
+
return issues
|
|
490
|
+
|
|
491
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
492
|
+
# ANÁLISIS JAVASCRIPT
|
|
493
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
def _analyze_javascript(
|
|
496
|
+
self,
|
|
497
|
+
file_path: Path,
|
|
498
|
+
content: str,
|
|
499
|
+
lines: List[str],
|
|
500
|
+
loc: int
|
|
501
|
+
) -> FileMetrics:
|
|
502
|
+
"""Analiza código JavaScript."""
|
|
503
|
+
issues = []
|
|
504
|
+
function_count = 0
|
|
505
|
+
class_count = 0
|
|
506
|
+
|
|
507
|
+
# Contar funciones
|
|
508
|
+
func_pattern = r'(?:export\s+)?(?:async\s+)?function\s+(\w+)'
|
|
509
|
+
function_count = len(re.findall(func_pattern, content))
|
|
510
|
+
|
|
511
|
+
# Contar clases
|
|
512
|
+
class_pattern = r'(?:export\s+)?class\s+(\w+)'
|
|
513
|
+
class_count = len(re.findall(class_pattern, content))
|
|
514
|
+
|
|
515
|
+
# Detectar var (debería usar let/const)
|
|
516
|
+
var_pattern = r'\bvar\s+\w+'
|
|
517
|
+
for i, line in enumerate(lines, 1):
|
|
518
|
+
if re.search(var_pattern, line):
|
|
519
|
+
issues.append(self._create_issue(
|
|
520
|
+
DebtType.DEPRECATED_USAGE,
|
|
521
|
+
DebtSeverity.LOW,
|
|
522
|
+
str(file_path),
|
|
523
|
+
i, i,
|
|
524
|
+
"Use of 'var' - prefer 'let' or 'const'",
|
|
525
|
+
"Replace 'var' with 'let' or 'const'",
|
|
526
|
+
2
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
# Detectar console.log
|
|
530
|
+
for i, line in enumerate(lines, 1):
|
|
531
|
+
if 'console.log' in line:
|
|
532
|
+
issues.append(self._create_issue(
|
|
533
|
+
DebtType.DEAD_CODE,
|
|
534
|
+
DebtSeverity.LOW,
|
|
535
|
+
str(file_path),
|
|
536
|
+
i, i,
|
|
537
|
+
"console.log statement found",
|
|
538
|
+
"Remove or replace with proper logging",
|
|
539
|
+
1
|
|
540
|
+
))
|
|
541
|
+
|
|
542
|
+
# TODOs
|
|
543
|
+
for i, line in enumerate(lines, 1):
|
|
544
|
+
if 'TODO' in line or 'FIXME' in line:
|
|
545
|
+
issues.append(self._create_issue(
|
|
546
|
+
DebtType.TODO_COMMENTS,
|
|
547
|
+
DebtSeverity.LOW,
|
|
548
|
+
str(file_path),
|
|
549
|
+
i, i,
|
|
550
|
+
"TODO/FIXME comment found",
|
|
551
|
+
"Implement or remove TODO",
|
|
552
|
+
15
|
|
553
|
+
))
|
|
554
|
+
|
|
555
|
+
# Estimar complejidad (simplificado)
|
|
556
|
+
complexity = len(re.findall(r'\b(if|for|while|switch|catch|&&|\|\|)\b', content))
|
|
557
|
+
|
|
558
|
+
return FileMetrics(
|
|
559
|
+
file_path=str(file_path),
|
|
560
|
+
lines_of_code=loc,
|
|
561
|
+
cyclomatic_complexity=complexity / function_count if function_count > 0 else 0,
|
|
562
|
+
cognitive_complexity=0,
|
|
563
|
+
maintainability_index=max(0, min(100, 100 - complexity)),
|
|
564
|
+
function_count=function_count,
|
|
565
|
+
class_count=class_count,
|
|
566
|
+
comment_ratio=0,
|
|
567
|
+
duplication_ratio=0,
|
|
568
|
+
issues=issues,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
572
|
+
# MÉTODOS PRIVADOS
|
|
573
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
574
|
+
|
|
575
|
+
def _calculate_complexity(self, node: ast.FunctionDef) -> int:
|
|
576
|
+
"""Calcula complejidad ciclomática."""
|
|
577
|
+
complexity = 1
|
|
578
|
+
|
|
579
|
+
for child in ast.walk(node):
|
|
580
|
+
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
|
|
581
|
+
complexity += 1
|
|
582
|
+
elif isinstance(child, ast.BoolOp):
|
|
583
|
+
complexity += len(child.values) - 1
|
|
584
|
+
elif isinstance(child, ast.comprehension):
|
|
585
|
+
complexity += 1
|
|
586
|
+
complexity += len(child.ifs)
|
|
587
|
+
|
|
588
|
+
return complexity
|
|
589
|
+
|
|
590
|
+
def _calculate_cognitive_complexity(self, node: ast.FunctionDef) -> int:
|
|
591
|
+
"""Calcula complejidad cognitiva."""
|
|
592
|
+
complexity = 0
|
|
593
|
+
nesting = 0
|
|
594
|
+
|
|
595
|
+
def visit(n, depth=0):
|
|
596
|
+
nonlocal complexity, nesting
|
|
597
|
+
|
|
598
|
+
if isinstance(n, (ast.If, ast.While, ast.For)):
|
|
599
|
+
complexity += depth + 1
|
|
600
|
+
for child in ast.iter_child_nodes(n):
|
|
601
|
+
visit(child, depth + 1)
|
|
602
|
+
elif isinstance(n, ast.ExceptHandler):
|
|
603
|
+
complexity += depth + 1
|
|
604
|
+
for child in ast.iter_child_nodes(n):
|
|
605
|
+
visit(child, depth + 1)
|
|
606
|
+
else:
|
|
607
|
+
for child in ast.iter_child_nodes(n):
|
|
608
|
+
visit(child, depth)
|
|
609
|
+
|
|
610
|
+
visit(node)
|
|
611
|
+
return complexity
|
|
612
|
+
|
|
613
|
+
def _calculate_nesting_depth(self, node: ast.FunctionDef) -> int:
|
|
614
|
+
"""Calcula profundidad máxima de anidación."""
|
|
615
|
+
max_depth = 0
|
|
616
|
+
|
|
617
|
+
def visit(n, depth=0):
|
|
618
|
+
nonlocal max_depth
|
|
619
|
+
|
|
620
|
+
if isinstance(n, (ast.If, ast.While, ast.For, ast.With, ast.Try)):
|
|
621
|
+
max_depth = max(max_depth, depth + 1)
|
|
622
|
+
for child in ast.iter_child_nodes(n):
|
|
623
|
+
visit(child, depth + 1)
|
|
624
|
+
else:
|
|
625
|
+
for child in ast.iter_child_nodes(n):
|
|
626
|
+
visit(child, depth)
|
|
627
|
+
|
|
628
|
+
visit(node)
|
|
629
|
+
return max_depth
|
|
630
|
+
|
|
631
|
+
def _detect_duplication(self, lines: List[str]) -> float:
|
|
632
|
+
"""Detecta duplicación de código (simplificado)."""
|
|
633
|
+
if len(lines) < 10:
|
|
634
|
+
return 0
|
|
635
|
+
|
|
636
|
+
# Buscar líneas idénticas
|
|
637
|
+
line_counts = defaultdict(int)
|
|
638
|
+
for line in lines:
|
|
639
|
+
stripped = line.strip()
|
|
640
|
+
if stripped and not stripped.startswith('#'):
|
|
641
|
+
line_counts[stripped] += 1
|
|
642
|
+
|
|
643
|
+
duplicated = sum(count - 1 for count in line_counts.values() if count > 1)
|
|
644
|
+
return duplicated / len(lines) if lines else 0
|
|
645
|
+
|
|
646
|
+
def _collect_files(
|
|
647
|
+
self,
|
|
648
|
+
include_patterns: List[str],
|
|
649
|
+
exclude_patterns: List[str]
|
|
650
|
+
) -> List[Path]:
|
|
651
|
+
"""Recolecta archivos del proyecto."""
|
|
652
|
+
files = []
|
|
653
|
+
|
|
654
|
+
for pattern in include_patterns:
|
|
655
|
+
for file_path in self.project_root.rglob(pattern):
|
|
656
|
+
# Verificar exclusiones
|
|
657
|
+
rel_path = str(file_path.relative_to(self.project_root))
|
|
658
|
+
if any(exc in rel_path for exc in exclude_patterns):
|
|
659
|
+
continue
|
|
660
|
+
files.append(file_path)
|
|
661
|
+
|
|
662
|
+
return files
|
|
663
|
+
|
|
664
|
+
def _create_issue(
|
|
665
|
+
self,
|
|
666
|
+
type: DebtType,
|
|
667
|
+
severity: DebtSeverity,
|
|
668
|
+
file_path: str,
|
|
669
|
+
line_start: int,
|
|
670
|
+
line_end: int,
|
|
671
|
+
description: str,
|
|
672
|
+
suggestion: str,
|
|
673
|
+
effort_minutes: int
|
|
674
|
+
) -> DebtIssue:
|
|
675
|
+
"""Crea un issue de deuda técnica."""
|
|
676
|
+
self._issue_counter += 1
|
|
677
|
+
return DebtIssue(
|
|
678
|
+
id=f"debt_{self._issue_counter}",
|
|
679
|
+
type=type,
|
|
680
|
+
severity=severity,
|
|
681
|
+
file_path=file_path,
|
|
682
|
+
line_start=line_start,
|
|
683
|
+
line_end=line_end,
|
|
684
|
+
description=description,
|
|
685
|
+
suggestion=suggestion,
|
|
686
|
+
effort_minutes=effort_minutes,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
691
|
+
# INSTANCIA GLOBAL
|
|
692
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
693
|
+
|
|
694
|
+
_analyzer_instance: Optional[DebtAnalyzer] = None
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def get_debt_analyzer(project_root: Path = None) -> DebtAnalyzer:
|
|
698
|
+
"""Obtiene la instancia global del analizador de deuda."""
|
|
699
|
+
global _analyzer_instance
|
|
700
|
+
if _analyzer_instance is None or (project_root and _analyzer_instance.project_root != project_root):
|
|
701
|
+
_analyzer_instance = DebtAnalyzer(project_root or Path.cwd())
|
|
702
|
+
return _analyzer_instance
|