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
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# hanus/suggestions/proactive.py
|
|
2
|
+
"""
|
|
3
|
+
Sistema de sugerencias proactivas.
|
|
4
|
+
|
|
5
|
+
Analiza código durante operaciones de lectura y sugiere mejoras
|
|
6
|
+
sin ser solicitado explícitamente.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import re
|
|
10
|
+
import ast
|
|
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 SuggestionType(Enum):
|
|
19
|
+
"""Tipo de sugerencia."""
|
|
20
|
+
UNUSED_IMPORT = "unused_import"
|
|
21
|
+
CODE_DUPLICATION = "code_duplication"
|
|
22
|
+
MISSING_DOCSTRING = "missing_docstring"
|
|
23
|
+
COMPLEX_FUNCTION = "complex_function"
|
|
24
|
+
NAMING_CONVENTION = "naming_convention"
|
|
25
|
+
SECURITY_ISSUE = "security_issue"
|
|
26
|
+
PERFORMANCE = "performance"
|
|
27
|
+
DEPRECATED_USAGE = "deprecated_usage"
|
|
28
|
+
MISSING_TYPE_HINT = "missing_type_hint"
|
|
29
|
+
TODO_COMMENT = "todo_comment"
|
|
30
|
+
HARDCODED_VALUE = "hardcoded_value"
|
|
31
|
+
ERROR_HANDLING = "error_handling"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SuggestionPriority(Enum):
|
|
35
|
+
"""Prioridad de la sugerencia."""
|
|
36
|
+
LOW = 1 # Nice to have
|
|
37
|
+
MEDIUM = 2 # Should fix
|
|
38
|
+
HIGH = 3 # Important
|
|
39
|
+
CRITICAL = 4 # Security or correctness issue
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Suggestion:
|
|
44
|
+
"""Una sugerencia proactiva."""
|
|
45
|
+
id: str
|
|
46
|
+
type: SuggestionType
|
|
47
|
+
priority: SuggestionPriority
|
|
48
|
+
title: str
|
|
49
|
+
description: str
|
|
50
|
+
file_path: str
|
|
51
|
+
line_start: int
|
|
52
|
+
line_end: int
|
|
53
|
+
suggestion: str # Código sugerido o acción
|
|
54
|
+
auto_fixable: bool = False
|
|
55
|
+
fix_template: Optional[str] = None # Template para auto-fix
|
|
56
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> Dict:
|
|
59
|
+
return {
|
|
60
|
+
"id": self.id,
|
|
61
|
+
"type": self.type.value,
|
|
62
|
+
"priority": self.priority.value,
|
|
63
|
+
"title": self.title,
|
|
64
|
+
"description": self.description,
|
|
65
|
+
"file_path": self.file_path,
|
|
66
|
+
"line_start": self.line_start,
|
|
67
|
+
"line_end": self.line_end,
|
|
68
|
+
"suggestion": self.suggestion,
|
|
69
|
+
"auto_fixable": self.auto_fixable,
|
|
70
|
+
"fix_template": self.fix_template,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ProactiveSuggester:
|
|
75
|
+
"""
|
|
76
|
+
Analiza código y genera sugerencias proactivas.
|
|
77
|
+
|
|
78
|
+
Características:
|
|
79
|
+
- Detección de imports sin usar
|
|
80
|
+
- Detección de código duplicado
|
|
81
|
+
- Detección de problemas de seguridad
|
|
82
|
+
- Detección de complejidad excesiva
|
|
83
|
+
- Detección de convenciones de nombre
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, proactivity_level: str = "normal"):
|
|
87
|
+
"""
|
|
88
|
+
Args:
|
|
89
|
+
proactivity_level: "minimal", "normal", or "aggressive"
|
|
90
|
+
"""
|
|
91
|
+
self.proactivity_level = proactivity_level
|
|
92
|
+
self._seen_files: Set[str] = set()
|
|
93
|
+
self._all_imports: Dict[str, Set[str]] = {} # file -> imports
|
|
94
|
+
self._all_functions: Dict[str, List[str]] = {} # file -> function names
|
|
95
|
+
self._suggestion_counter = 0
|
|
96
|
+
|
|
97
|
+
def analyze_file(
|
|
98
|
+
self,
|
|
99
|
+
file_path: str,
|
|
100
|
+
content: str
|
|
101
|
+
) -> List[Suggestion]:
|
|
102
|
+
"""
|
|
103
|
+
Analiza un archivo y genera sugerencias.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
file_path: Ruta del archivo
|
|
107
|
+
content: Contenido del archivo
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Lista de sugerencias encontradas
|
|
111
|
+
"""
|
|
112
|
+
suggestions = []
|
|
113
|
+
ext = Path(file_path).suffix.lower()
|
|
114
|
+
|
|
115
|
+
# Solo analizar archivos de código
|
|
116
|
+
if ext not in (".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go"):
|
|
117
|
+
return suggestions
|
|
118
|
+
|
|
119
|
+
self._seen_files.add(file_path)
|
|
120
|
+
|
|
121
|
+
if ext == ".py":
|
|
122
|
+
suggestions.extend(self._analyze_python(file_path, content))
|
|
123
|
+
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
124
|
+
suggestions.extend(self._analyze_javascript(file_path, content))
|
|
125
|
+
|
|
126
|
+
return suggestions
|
|
127
|
+
|
|
128
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
129
|
+
"""Obtiene un resumen del análisis."""
|
|
130
|
+
return {
|
|
131
|
+
"files_analyzed": len(self._seen_files),
|
|
132
|
+
"total_imports": sum(len(i) for i in self._all_imports.values()),
|
|
133
|
+
"total_functions": sum(len(f) for f in self._all_functions.values()),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
# ANÁLISIS PYTHON
|
|
138
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
def _analyze_python(self, file_path: str, content: str) -> List[Suggestion]:
|
|
141
|
+
"""Analiza código Python."""
|
|
142
|
+
suggestions = []
|
|
143
|
+
lines = content.split('\n')
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
tree = ast.parse(content)
|
|
147
|
+
except SyntaxError:
|
|
148
|
+
# Si hay error de sintaxis, no analizar
|
|
149
|
+
return suggestions
|
|
150
|
+
|
|
151
|
+
# Analizar imports
|
|
152
|
+
imports = set()
|
|
153
|
+
import_nodes = []
|
|
154
|
+
|
|
155
|
+
for node in ast.walk(tree):
|
|
156
|
+
if isinstance(node, ast.Import):
|
|
157
|
+
for alias in node.names:
|
|
158
|
+
name = alias.asname if alias.asname else alias.name
|
|
159
|
+
imports.add(name.split('.')[0])
|
|
160
|
+
import_nodes.append((node.lineno, name))
|
|
161
|
+
|
|
162
|
+
elif isinstance(node, ast.ImportFrom):
|
|
163
|
+
module = node.module or ""
|
|
164
|
+
for alias in node.names:
|
|
165
|
+
name = alias.asname if alias.asname else alias.name
|
|
166
|
+
imports.add(name)
|
|
167
|
+
import_nodes.append((node.lineno, name))
|
|
168
|
+
|
|
169
|
+
self._all_imports[file_path] = imports
|
|
170
|
+
|
|
171
|
+
# Verificar imports usados
|
|
172
|
+
used_names = set()
|
|
173
|
+
for node in ast.walk(tree):
|
|
174
|
+
if isinstance(node, ast.Name):
|
|
175
|
+
used_names.add(node.id)
|
|
176
|
+
elif isinstance(node, ast.Attribute):
|
|
177
|
+
if isinstance(node.value, ast.Name):
|
|
178
|
+
used_names.add(node.value.id)
|
|
179
|
+
|
|
180
|
+
unused = imports - used_names - {'__future__'}
|
|
181
|
+
if unused and self._should_suggest("unused_import"):
|
|
182
|
+
for line, imp in import_nodes:
|
|
183
|
+
if imp in unused or imp.split('.')[0] in unused:
|
|
184
|
+
suggestions.append(self._create_suggestion(
|
|
185
|
+
SuggestionType.UNUSED_IMPORT,
|
|
186
|
+
SuggestionPriority.LOW,
|
|
187
|
+
"Unused Import",
|
|
188
|
+
f"Import '{imp}' is not used in this file",
|
|
189
|
+
file_path, line, line,
|
|
190
|
+
f"Remove: import {imp}",
|
|
191
|
+
auto_fixable=True,
|
|
192
|
+
fix_template=f"Remove line {line}"
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
# Analizar funciones
|
|
196
|
+
for node in ast.walk(tree):
|
|
197
|
+
if isinstance(node, ast.FunctionDef):
|
|
198
|
+
# Verificar docstring
|
|
199
|
+
if not ast.get_docstring(node):
|
|
200
|
+
if self._should_suggest("missing_docstring"):
|
|
201
|
+
suggestions.append(self._create_suggestion(
|
|
202
|
+
SuggestionType.MISSING_DOCSTRING,
|
|
203
|
+
SuggestionPriority.LOW,
|
|
204
|
+
"Missing Docstring",
|
|
205
|
+
f"Function '{node.name}' has no docstring",
|
|
206
|
+
file_path, node.lineno, node.lineno,
|
|
207
|
+
f'Add docstring: """Description"""',
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
# Verificar complejidad
|
|
211
|
+
complexity = self._calculate_complexity(node)
|
|
212
|
+
if complexity > 10 and self._should_suggest("complex_function"):
|
|
213
|
+
suggestions.append(self._create_suggestion(
|
|
214
|
+
SuggestionType.COMPLEX_FUNCTION,
|
|
215
|
+
SuggestionPriority.MEDIUM,
|
|
216
|
+
"Complex Function",
|
|
217
|
+
f"Function '{node.name}' has high complexity ({complexity})",
|
|
218
|
+
file_path, node.lineno, node.end_lineno or node.lineno,
|
|
219
|
+
"Consider breaking into smaller functions",
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
# Verificar type hints
|
|
223
|
+
if not node.returns and self._should_suggest("missing_type_hint"):
|
|
224
|
+
suggestions.append(self._create_suggestion(
|
|
225
|
+
SuggestionType.MISSING_TYPE_HINT,
|
|
226
|
+
SuggestionPriority.LOW,
|
|
227
|
+
"Missing Return Type",
|
|
228
|
+
f"Function '{node.name}' has no return type hint",
|
|
229
|
+
file_path, node.lineno, node.lineno,
|
|
230
|
+
"Add return type hint: -> ReturnType",
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
# Verificar parámetros sin type hint
|
|
234
|
+
for arg in node.args.args:
|
|
235
|
+
if arg.annotation is None and arg.arg != 'self' and arg.arg != 'cls':
|
|
236
|
+
if self._should_suggest("missing_type_hint"):
|
|
237
|
+
suggestions.append(self._create_suggestion(
|
|
238
|
+
SuggestionType.MISSING_TYPE_HINT,
|
|
239
|
+
SuggestionPriority.LOW,
|
|
240
|
+
"Missing Parameter Type",
|
|
241
|
+
f"Parameter '{arg.arg}' in '{node.name}' has no type hint",
|
|
242
|
+
file_path, node.lineno, node.lineno,
|
|
243
|
+
f"Add type hint: {arg.arg}: Type",
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
# Buscar TODO comments
|
|
247
|
+
for i, line in enumerate(lines, 1):
|
|
248
|
+
if 'TODO' in line or 'FIXME' in line or 'XXX' in line:
|
|
249
|
+
if self._should_suggest("todo_comment"):
|
|
250
|
+
suggestions.append(self._create_suggestion(
|
|
251
|
+
SuggestionType.TODO_COMMENT,
|
|
252
|
+
SuggestionPriority.LOW,
|
|
253
|
+
"TODO Comment",
|
|
254
|
+
f"Pending task found",
|
|
255
|
+
file_path, i, i,
|
|
256
|
+
"Consider implementing or tracking this task",
|
|
257
|
+
))
|
|
258
|
+
|
|
259
|
+
# Buscar problemas de seguridad potenciales
|
|
260
|
+
security_patterns = [
|
|
261
|
+
(r'password\s*=\s*["\'][^"\']+["\']', "Hardcoded password"),
|
|
262
|
+
(r'api_key\s*=\s*["\'][^"\']+["\']', "Hardcoded API key"),
|
|
263
|
+
(r'secret\s*=\s*["\'][^"\']+["\']', "Hardcoded secret"),
|
|
264
|
+
(r'eval\s*\(', "Use of eval() - potential security risk"),
|
|
265
|
+
(r'exec\s*\(', "Use of exec() - potential security risk"),
|
|
266
|
+
(r'shell=True', "subprocess with shell=True - potential injection risk"),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
for i, line in enumerate(lines, 1):
|
|
270
|
+
for pattern, message in security_patterns:
|
|
271
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
272
|
+
if self._should_suggest("security_issue"):
|
|
273
|
+
suggestions.append(self._create_suggestion(
|
|
274
|
+
SuggestionType.SECURITY_ISSUE,
|
|
275
|
+
SuggestionPriority.HIGH,
|
|
276
|
+
"Security Issue",
|
|
277
|
+
message,
|
|
278
|
+
file_path, i, i,
|
|
279
|
+
"Review and secure this code",
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
# Buscar hardcoded values (IPs, URLs, paths)
|
|
283
|
+
hardcoded_patterns = [
|
|
284
|
+
(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', "Hardcoded IP address"),
|
|
285
|
+
(r'["\']https?://[^"\']+["\']', "Hardcoded URL"),
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
for i, line in enumerate(lines, 1):
|
|
289
|
+
# Skip comments
|
|
290
|
+
if line.strip().startswith('#'):
|
|
291
|
+
continue
|
|
292
|
+
for pattern, message in hardcoded_patterns:
|
|
293
|
+
if re.search(pattern, line):
|
|
294
|
+
if self._should_suggest("hardcoded_value"):
|
|
295
|
+
suggestions.append(self._create_suggestion(
|
|
296
|
+
SuggestionType.HARDCODED_VALUE,
|
|
297
|
+
SuggestionPriority.LOW,
|
|
298
|
+
"Hardcoded Value",
|
|
299
|
+
message,
|
|
300
|
+
file_path, i, i,
|
|
301
|
+
"Consider using a configuration file or environment variable",
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
return suggestions
|
|
305
|
+
|
|
306
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
307
|
+
# ANÁLISIS JAVASCRIPT
|
|
308
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
309
|
+
|
|
310
|
+
def _analyze_javascript(self, file_path: str, content: str) -> List[Suggestion]:
|
|
311
|
+
"""Analiza código JavaScript/TypeScript."""
|
|
312
|
+
suggestions = []
|
|
313
|
+
lines = content.split('\n')
|
|
314
|
+
|
|
315
|
+
# Buscar console.log
|
|
316
|
+
for i, line in enumerate(lines, 1):
|
|
317
|
+
if 'console.log' in line:
|
|
318
|
+
if self._should_suggest("deprecated_usage"):
|
|
319
|
+
suggestions.append(self._create_suggestion(
|
|
320
|
+
SuggestionType.DEPRECATED_USAGE,
|
|
321
|
+
SuggestionPriority.LOW,
|
|
322
|
+
"Console Log",
|
|
323
|
+
"console.log found - remove before production",
|
|
324
|
+
file_path, i, i,
|
|
325
|
+
"Remove or replace with proper logging",
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
# Buscar var (debería usar let/const)
|
|
329
|
+
for i, line in enumerate(lines, 1):
|
|
330
|
+
if re.search(r'\bvar\s+\w+', line):
|
|
331
|
+
if self._should_suggest("deprecated_usage"):
|
|
332
|
+
suggestions.append(self._create_suggestion(
|
|
333
|
+
SuggestionType.DEPRECATED_USAGE,
|
|
334
|
+
SuggestionPriority.MEDIUM,
|
|
335
|
+
"Use of 'var'",
|
|
336
|
+
"Use 'let' or 'const' instead of 'var'",
|
|
337
|
+
file_path, i, i,
|
|
338
|
+
"Replace 'var' with 'let' or 'const'",
|
|
339
|
+
))
|
|
340
|
+
|
|
341
|
+
# Buscar problemas de seguridad
|
|
342
|
+
security_patterns = [
|
|
343
|
+
(r'innerHTML\s*=', "innerHTML - potential XSS risk"),
|
|
344
|
+
(r'dangerouslySetInnerHTML', "dangerouslySetInnerHTML - potential XSS risk"),
|
|
345
|
+
(r'eval\s*\(', "Use of eval() - potential security risk"),
|
|
346
|
+
(r'password\s*[=:]\s*["\'][^"\']+["\']', "Hardcoded password"),
|
|
347
|
+
(r'apiKey\s*[=:]\s*["\'][^"\']+["\']', "Hardcoded API key"),
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for i, line in enumerate(lines, 1):
|
|
351
|
+
for pattern, message in security_patterns:
|
|
352
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
353
|
+
if self._should_suggest("security_issue"):
|
|
354
|
+
suggestions.append(self._create_suggestion(
|
|
355
|
+
SuggestionType.SECURITY_ISSUE,
|
|
356
|
+
SuggestionPriority.HIGH,
|
|
357
|
+
"Security Issue",
|
|
358
|
+
message,
|
|
359
|
+
file_path, i, i,
|
|
360
|
+
"Review and secure this code",
|
|
361
|
+
))
|
|
362
|
+
|
|
363
|
+
# Buscar TODO comments
|
|
364
|
+
for i, line in enumerate(lines, 1):
|
|
365
|
+
if 'TODO' in line or 'FIXME' in line or 'XXX' in line:
|
|
366
|
+
if self._should_suggest("todo_comment"):
|
|
367
|
+
suggestions.append(self._create_suggestion(
|
|
368
|
+
SuggestionType.TODO_COMMENT,
|
|
369
|
+
SuggestionPriority.LOW,
|
|
370
|
+
"TODO Comment",
|
|
371
|
+
"Pending task found",
|
|
372
|
+
file_path, i, i,
|
|
373
|
+
"Consider implementing or tracking this task",
|
|
374
|
+
))
|
|
375
|
+
|
|
376
|
+
return suggestions
|
|
377
|
+
|
|
378
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
379
|
+
# MÉTODOS PRIVADOS
|
|
380
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
381
|
+
|
|
382
|
+
def _calculate_complexity(self, node: ast.FunctionDef) -> int:
|
|
383
|
+
"""Calcula la complejidad ciclomática de una función."""
|
|
384
|
+
complexity = 1 # Base
|
|
385
|
+
|
|
386
|
+
for child in ast.walk(node):
|
|
387
|
+
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
|
|
388
|
+
complexity += 1
|
|
389
|
+
elif isinstance(child, ast.BoolOp):
|
|
390
|
+
complexity += len(child.values) - 1
|
|
391
|
+
elif isinstance(child, ast.comprehension):
|
|
392
|
+
complexity += 1
|
|
393
|
+
if child.ifs:
|
|
394
|
+
complexity += len(child.ifs)
|
|
395
|
+
|
|
396
|
+
return complexity
|
|
397
|
+
|
|
398
|
+
def _should_suggest(self, suggestion_type: str) -> bool:
|
|
399
|
+
"""Determina si se debe hacer una sugerencia según el nivel de proactividad."""
|
|
400
|
+
if self.proactivity_level == "minimal":
|
|
401
|
+
# Solo sugerencias críticas
|
|
402
|
+
return suggestion_type in ("security_issue",)
|
|
403
|
+
elif self.proactivity_level == "normal":
|
|
404
|
+
# Excluir algunas sugerencias de baja prioridad
|
|
405
|
+
return suggestion_type not in ("missing_docstring", "todo_comment")
|
|
406
|
+
else: # aggressive
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
def _create_suggestion(
|
|
410
|
+
self,
|
|
411
|
+
type: SuggestionType,
|
|
412
|
+
priority: SuggestionPriority,
|
|
413
|
+
title: str,
|
|
414
|
+
description: str,
|
|
415
|
+
file_path: str,
|
|
416
|
+
line_start: int,
|
|
417
|
+
line_end: int,
|
|
418
|
+
suggestion: str,
|
|
419
|
+
auto_fixable: bool = False,
|
|
420
|
+
fix_template: str = None,
|
|
421
|
+
) -> Suggestion:
|
|
422
|
+
"""Crea una nueva sugerencia."""
|
|
423
|
+
self._suggestion_counter += 1
|
|
424
|
+
return Suggestion(
|
|
425
|
+
id=f"sugg_{self._suggestion_counter}_{type.value}",
|
|
426
|
+
type=type,
|
|
427
|
+
priority=priority,
|
|
428
|
+
title=title,
|
|
429
|
+
description=description,
|
|
430
|
+
file_path=file_path,
|
|
431
|
+
line_start=line_start,
|
|
432
|
+
line_end=line_end,
|
|
433
|
+
suggestion=suggestion,
|
|
434
|
+
auto_fixable=auto_fixable,
|
|
435
|
+
fix_template=fix_template,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
440
|
+
# INSTANCIA GLOBAL
|
|
441
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
442
|
+
|
|
443
|
+
_suggester_instance: Optional[ProactiveSuggester] = None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def get_suggester() -> ProactiveSuggester:
|
|
447
|
+
"""Obtiene la instancia global del sugeridor."""
|
|
448
|
+
global _suggester_instance
|
|
449
|
+
if _suggester_instance is None:
|
|
450
|
+
_suggester_instance = ProactiveSuggester()
|
|
451
|
+
return _suggester_instance
|
hanus/tasks/__init__.py
ADDED