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,475 @@
|
|
|
1
|
+
# hanus/analysis/dependencies.py
|
|
2
|
+
"""
|
|
3
|
+
Analizador de dependencias de cΓ³digo.
|
|
4
|
+
|
|
5
|
+
Funcionalidades:
|
|
6
|
+
- AnΓ‘lisis de imports/dependencias
|
|
7
|
+
- DetecciΓ³n de dependencias circulares
|
|
8
|
+
- VisualizaciΓ³n de grafo de dependencias
|
|
9
|
+
- MΓ©tricas de acoplamiento
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import ast
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Set, Optional, Any, Tuple
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DependencyType(Enum):
|
|
22
|
+
"""Tipo de dependencia."""
|
|
23
|
+
IMPORT = "import" # import X
|
|
24
|
+
FROM_IMPORT = "from" # from X import Y
|
|
25
|
+
RELATIVE = "relative" # from . import X
|
|
26
|
+
DYNAMIC = "dynamic" # __import__, importlib
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NodeType(Enum):
|
|
30
|
+
"""Tipo de nodo en el grafo."""
|
|
31
|
+
FILE = "file"
|
|
32
|
+
MODULE = "module"
|
|
33
|
+
PACKAGE = "package"
|
|
34
|
+
CLASS = "class"
|
|
35
|
+
FUNCTION = "function"
|
|
36
|
+
VARIABLE = "variable"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DependencyNode:
|
|
41
|
+
"""Un nodo en el grafo de dependencias."""
|
|
42
|
+
id: str # Identificador ΓΊnico (ruta o nombre)
|
|
43
|
+
name: str # Nombre legible
|
|
44
|
+
type: NodeType
|
|
45
|
+
path: Optional[str] = None # Ruta del archivo si aplica
|
|
46
|
+
imports: List[str] = field(default_factory=list) # Lo que este nodo importa
|
|
47
|
+
imported_by: List[str] = field(default_factory=list) # Quien importa este nodo
|
|
48
|
+
exports: List[str] = field(default_factory=list) # Lo que exporta
|
|
49
|
+
loc: int = 0 # Lines of code
|
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict:
|
|
53
|
+
return {
|
|
54
|
+
"id": self.id,
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"type": self.type.value,
|
|
57
|
+
"path": self.path,
|
|
58
|
+
"imports": self.imports,
|
|
59
|
+
"imported_by": self.imported_by,
|
|
60
|
+
"exports": self.exports,
|
|
61
|
+
"loc": self.loc,
|
|
62
|
+
"metadata": self.metadata,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class DependencyEdge:
|
|
68
|
+
"""Una arista en el grafo de dependencias."""
|
|
69
|
+
source: str # ID del nodo origen
|
|
70
|
+
target: str # ID del nodo destino
|
|
71
|
+
type: DependencyType
|
|
72
|
+
symbols: List[str] = field(default_factory=list) # SΓmbolos importados
|
|
73
|
+
line: int = 0 # LΓnea donde aparece el import
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict:
|
|
76
|
+
return {
|
|
77
|
+
"source": self.source,
|
|
78
|
+
"target": self.target,
|
|
79
|
+
"type": self.type.value,
|
|
80
|
+
"symbols": self.symbols,
|
|
81
|
+
"line": self.line,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class DependencyGraph:
|
|
87
|
+
"""Grafo completo de dependencias."""
|
|
88
|
+
nodes: Dict[str, DependencyNode] = field(default_factory=dict)
|
|
89
|
+
edges: List[DependencyEdge] = field(default_factory=list)
|
|
90
|
+
|
|
91
|
+
# Γndices para bΓΊsqueda rΓ‘pida
|
|
92
|
+
_outgoing: Dict[str, List[str]] = field(default_factory=dict) # nodo -> [nodos destino]
|
|
93
|
+
_incoming: Dict[str, List[str]] = field(default_factory=dict) # nodo -> [nodos origen]
|
|
94
|
+
|
|
95
|
+
def add_node(self, node: DependencyNode) -> None:
|
|
96
|
+
"""AΓ±ade un nodo al grafo."""
|
|
97
|
+
self.nodes[node.id] = node
|
|
98
|
+
if node.id not in self._outgoing:
|
|
99
|
+
self._outgoing[node.id] = []
|
|
100
|
+
if node.id not in self._incoming:
|
|
101
|
+
self._incoming[node.id] = []
|
|
102
|
+
|
|
103
|
+
def add_edge(self, edge: DependencyEdge) -> None:
|
|
104
|
+
"""AΓ±ade una arista al grafo."""
|
|
105
|
+
self.edges.append(edge)
|
|
106
|
+
if edge.source not in self._outgoing:
|
|
107
|
+
self._outgoing[edge.source] = []
|
|
108
|
+
self._outgoing[edge.source].append(edge.target)
|
|
109
|
+
|
|
110
|
+
if edge.target not in self._incoming:
|
|
111
|
+
self._incoming[edge.target] = []
|
|
112
|
+
self._incoming[edge.target].append(edge.source)
|
|
113
|
+
|
|
114
|
+
def get_dependencies(self, node_id: str) -> List[str]:
|
|
115
|
+
"""Obtiene los nodos de los que depende un nodo."""
|
|
116
|
+
return self._outgoing.get(node_id, [])
|
|
117
|
+
|
|
118
|
+
def get_dependents(self, node_id: str) -> List[str]:
|
|
119
|
+
"""Obtiene los nodos que dependen de un nodo."""
|
|
120
|
+
return self._incoming.get(node_id, [])
|
|
121
|
+
|
|
122
|
+
def find_cycles(self) -> List[List[str]]:
|
|
123
|
+
"""Encuentra todos los ciclos en el grafo."""
|
|
124
|
+
cycles = []
|
|
125
|
+
visited = set()
|
|
126
|
+
rec_stack = set()
|
|
127
|
+
path = []
|
|
128
|
+
|
|
129
|
+
def dfs(node: str) -> None:
|
|
130
|
+
visited.add(node)
|
|
131
|
+
rec_stack.add(node)
|
|
132
|
+
path.append(node)
|
|
133
|
+
|
|
134
|
+
for neighbor in self._outgoing.get(node, []):
|
|
135
|
+
if neighbor not in visited:
|
|
136
|
+
dfs(neighbor)
|
|
137
|
+
elif neighbor in rec_stack:
|
|
138
|
+
# Encontramos un ciclo
|
|
139
|
+
cycle_start = path.index(neighbor)
|
|
140
|
+
cycle = path[cycle_start:] + [neighbor]
|
|
141
|
+
cycles.append(cycle)
|
|
142
|
+
|
|
143
|
+
path.pop()
|
|
144
|
+
rec_stack.remove(node)
|
|
145
|
+
|
|
146
|
+
for node in self.nodes:
|
|
147
|
+
if node not in visited:
|
|
148
|
+
dfs(node)
|
|
149
|
+
|
|
150
|
+
return cycles
|
|
151
|
+
|
|
152
|
+
def get_coupling_metrics(self) -> Dict[str, Any]:
|
|
153
|
+
"""Calcula mΓ©tricas de acoplamiento."""
|
|
154
|
+
afferent = {} # CuΓ‘ntos nodos dependen de este (incoming)
|
|
155
|
+
efferent = {} # De cuΓ‘ntos nodos depende este (outgoing)
|
|
156
|
+
|
|
157
|
+
for node_id in self.nodes:
|
|
158
|
+
afferent[node_id] = len(self._incoming.get(node_id, []))
|
|
159
|
+
efferent[node_id] = len(self._outgoing.get(node_id, []))
|
|
160
|
+
|
|
161
|
+
# Calcular inestabilidad: I = Ce / (Ca + Ce)
|
|
162
|
+
# I cercano a 0 = estable, I cercano a 1 = inestable
|
|
163
|
+
instability = {}
|
|
164
|
+
for node_id in self.nodes:
|
|
165
|
+
ca = afferent.get(node_id, 0)
|
|
166
|
+
ce = efferent.get(node_id, 0)
|
|
167
|
+
total = ca + ce
|
|
168
|
+
instability[node_id] = ce / total if total > 0 else 0
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"afferent_coupling": afferent, # Afferent coupling (Ca)
|
|
172
|
+
"efferent_coupling": efferent, # Efferent coupling (Ce)
|
|
173
|
+
"instability": instability,
|
|
174
|
+
"total_nodes": len(self.nodes),
|
|
175
|
+
"total_edges": len(self.edges),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def get_top_dependencies(self, limit: int = 10) -> List[Dict]:
|
|
179
|
+
"""Obtiene los nodos mΓ‘s importados."""
|
|
180
|
+
incoming_counts = [
|
|
181
|
+
(node_id, len(self._incoming.get(node_id, [])))
|
|
182
|
+
for node_id in self.nodes
|
|
183
|
+
]
|
|
184
|
+
incoming_counts.sort(key=lambda x: -x[1])
|
|
185
|
+
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
"id": node_id,
|
|
189
|
+
"name": self.nodes[node_id].name,
|
|
190
|
+
"imported_by_count": count,
|
|
191
|
+
}
|
|
192
|
+
for node_id, count in incoming_counts[:limit]
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> Dict:
|
|
196
|
+
"""Convierte el grafo a diccionario para serializaciΓ³n."""
|
|
197
|
+
return {
|
|
198
|
+
"nodes": [n.to_dict() for n in self.nodes.values()],
|
|
199
|
+
"edges": [e.to_dict() for e in self.edges],
|
|
200
|
+
"metrics": self.get_coupling_metrics(),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
def to_vis_format(self) -> Dict:
|
|
204
|
+
"""Convierte el grafo a formato para visualizaciΓ³n (vis.js, d3.js)."""
|
|
205
|
+
nodes = []
|
|
206
|
+
for node in self.nodes.values():
|
|
207
|
+
# Determinar grupo/color
|
|
208
|
+
if node.type == NodeType.FILE:
|
|
209
|
+
group = "file"
|
|
210
|
+
elif node.type == NodeType.MODULE:
|
|
211
|
+
group = "module"
|
|
212
|
+
else:
|
|
213
|
+
group = "other"
|
|
214
|
+
|
|
215
|
+
nodes.append({
|
|
216
|
+
"id": node.id,
|
|
217
|
+
"label": node.name,
|
|
218
|
+
"group": group,
|
|
219
|
+
"title": f"{node.name}\nImports: {len(node.imports)}\nImported by: {len(node.imported_by)}",
|
|
220
|
+
"value": len(node.imported_by) + 1, # TamaΓ±o basado en uso
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
edges = []
|
|
224
|
+
for edge in self.edges:
|
|
225
|
+
edges.append({
|
|
226
|
+
"from": edge.source,
|
|
227
|
+
"to": edge.target,
|
|
228
|
+
"arrows": "to",
|
|
229
|
+
"title": f"{edge.type.value}: {', '.join(edge.symbols[:3])}",
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return {"nodes": nodes, "edges": edges}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class DependencyAnalyzer:
|
|
236
|
+
"""
|
|
237
|
+
Analiza dependencias de cΓ³digo en un proyecto.
|
|
238
|
+
|
|
239
|
+
Soporta:
|
|
240
|
+
- Python (.py)
|
|
241
|
+
- JavaScript/TypeScript (.js, .ts)
|
|
242
|
+
- JSON dependencies (package.json, requirements.txt)
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
PYTHON_IMPORT_PATTERNS = [
|
|
246
|
+
# import X
|
|
247
|
+
re.compile(r'^import\s+([a-zA-Z_][\w\.]*)', re.MULTILINE),
|
|
248
|
+
# from X import Y
|
|
249
|
+
re.compile(r'^from\s+([a-zA-Z_][\w\.]*)\s+import', re.MULTILINE),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
JS_IMPORT_PATTERNS = [
|
|
253
|
+
# import X from 'Y'
|
|
254
|
+
re.compile(r'''import\s+.*?\s+from\s+['"]([^'"]+)['"]'''),
|
|
255
|
+
# import 'X'
|
|
256
|
+
re.compile(r'''import\s+['"]([^'"]+)['"]'''),
|
|
257
|
+
# require('X')
|
|
258
|
+
re.compile(r'''require\s*\(\s*['"]([^'"]+)['"]\s*\)'''),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
def __init__(self, project_root: Path):
|
|
262
|
+
self.project_root = project_root
|
|
263
|
+
self._file_cache: Dict[str, str] = {}
|
|
264
|
+
|
|
265
|
+
def analyze_project(
|
|
266
|
+
self,
|
|
267
|
+
include_patterns: List[str] = None,
|
|
268
|
+
exclude_patterns: List[str] = None
|
|
269
|
+
) -> DependencyGraph:
|
|
270
|
+
"""
|
|
271
|
+
Analiza todo el proyecto y construye el grafo de dependencias.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
include_patterns: Patrones de archivos a incluir (ej: ["*.py", "*.js"])
|
|
275
|
+
exclude_patterns: Patrones a excluir (ej: ["test_*", "__pycache__"])
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
DependencyGraph con todas las dependencias
|
|
279
|
+
"""
|
|
280
|
+
graph = DependencyGraph()
|
|
281
|
+
|
|
282
|
+
include_patterns = include_patterns or ["*.py", "*.js", "*.ts"]
|
|
283
|
+
exclude_patterns = exclude_patterns or ["__pycache__", "node_modules", ".git", "venv", "env"]
|
|
284
|
+
|
|
285
|
+
# Recopilar todos los archivos
|
|
286
|
+
files = self._collect_files(include_patterns, exclude_patterns)
|
|
287
|
+
|
|
288
|
+
# Analizar cada archivo
|
|
289
|
+
for file_path in files:
|
|
290
|
+
self._analyze_file(file_path, graph)
|
|
291
|
+
|
|
292
|
+
# Construir Γndices inversos
|
|
293
|
+
self._build_reverse_indices(graph)
|
|
294
|
+
|
|
295
|
+
return graph
|
|
296
|
+
|
|
297
|
+
def analyze_file(self, file_path: Path) -> DependencyGraph:
|
|
298
|
+
"""Analiza un solo archivo."""
|
|
299
|
+
graph = DependencyGraph()
|
|
300
|
+
self._analyze_file(file_path, graph)
|
|
301
|
+
return graph
|
|
302
|
+
|
|
303
|
+
def find_circular_dependencies(self, graph: DependencyGraph) -> List[List[str]]:
|
|
304
|
+
"""Encuentra dependencias circulares."""
|
|
305
|
+
return graph.find_cycles()
|
|
306
|
+
|
|
307
|
+
def get_dependency_path(self, graph: DependencyGraph, from_node: str, to_node: str) -> List[str]:
|
|
308
|
+
"""Encuentra el camino de dependencia entre dos nodos."""
|
|
309
|
+
visited = set()
|
|
310
|
+
path = []
|
|
311
|
+
|
|
312
|
+
def dfs(current: str) -> bool:
|
|
313
|
+
if current == to_node:
|
|
314
|
+
path.append(current)
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
if current in visited:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
visited.add(current)
|
|
321
|
+
path.append(current)
|
|
322
|
+
|
|
323
|
+
for neighbor in graph.get_dependencies(current):
|
|
324
|
+
if dfs(neighbor):
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
path.pop()
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
dfs(from_node)
|
|
331
|
+
return path
|
|
332
|
+
|
|
333
|
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
334
|
+
# MΓTODOS PRIVADOS
|
|
335
|
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
336
|
+
|
|
337
|
+
def _collect_files(
|
|
338
|
+
self,
|
|
339
|
+
include_patterns: List[str],
|
|
340
|
+
exclude_patterns: List[str]
|
|
341
|
+
) -> List[Path]:
|
|
342
|
+
"""Recolecta archivos del proyecto."""
|
|
343
|
+
files = []
|
|
344
|
+
|
|
345
|
+
for pattern in include_patterns:
|
|
346
|
+
for file_path in self.project_root.rglob(pattern):
|
|
347
|
+
# Verificar exclusiones
|
|
348
|
+
rel_path = file_path.relative_to(self.project_root)
|
|
349
|
+
if any(exc in str(rel_path) for exc in exclude_patterns):
|
|
350
|
+
continue
|
|
351
|
+
files.append(file_path)
|
|
352
|
+
|
|
353
|
+
return files
|
|
354
|
+
|
|
355
|
+
def _analyze_file(self, file_path: Path, graph: DependencyGraph) -> None:
|
|
356
|
+
"""Analiza un archivo y aΓ±ade sus dependencias al grafo."""
|
|
357
|
+
try:
|
|
358
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
359
|
+
except Exception:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
rel_path = str(file_path.relative_to(self.project_root))
|
|
363
|
+
node_id = rel_path.replace("/", ".").replace("\\", ".")
|
|
364
|
+
|
|
365
|
+
# Crear nodo para el archivo
|
|
366
|
+
node = DependencyNode(
|
|
367
|
+
id=node_id,
|
|
368
|
+
name=file_path.name,
|
|
369
|
+
type=NodeType.FILE,
|
|
370
|
+
path=str(file_path),
|
|
371
|
+
loc=content.count("\n") + 1,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Detectar imports segΓΊn el tipo de archivo
|
|
375
|
+
ext = file_path.suffix.lower()
|
|
376
|
+
|
|
377
|
+
if ext == ".py":
|
|
378
|
+
imports = self._parse_python_imports(content, file_path)
|
|
379
|
+
elif ext in (".js", ".ts", ".jsx", ".tsx"):
|
|
380
|
+
imports = self._parse_js_imports(content, file_path)
|
|
381
|
+
else:
|
|
382
|
+
imports = []
|
|
383
|
+
|
|
384
|
+
node.imports = [imp["module"] for imp in imports]
|
|
385
|
+
|
|
386
|
+
# AΓ±adir nodo y aristas
|
|
387
|
+
graph.add_node(node)
|
|
388
|
+
|
|
389
|
+
for imp in imports:
|
|
390
|
+
target_id = imp["module"]
|
|
391
|
+
|
|
392
|
+
# Crear nodo para el mΓ³dulo importado si no existe
|
|
393
|
+
if target_id not in graph.nodes:
|
|
394
|
+
graph.add_node(DependencyNode(
|
|
395
|
+
id=target_id,
|
|
396
|
+
name=target_id.split(".")[-1],
|
|
397
|
+
type=NodeType.MODULE,
|
|
398
|
+
))
|
|
399
|
+
|
|
400
|
+
# AΓ±adir arista
|
|
401
|
+
graph.add_edge(DependencyEdge(
|
|
402
|
+
source=node_id,
|
|
403
|
+
target=target_id,
|
|
404
|
+
type=DependencyType(imp["type"]),
|
|
405
|
+
symbols=imp.get("names", []),
|
|
406
|
+
line=imp.get("line", 0),
|
|
407
|
+
))
|
|
408
|
+
|
|
409
|
+
def _parse_python_imports(self, content: str, file_path: Path) -> List[Dict]:
|
|
410
|
+
"""Parsea imports de Python usando AST."""
|
|
411
|
+
imports = []
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
tree = ast.parse(content)
|
|
415
|
+
|
|
416
|
+
for node in ast.walk(tree):
|
|
417
|
+
if isinstance(node, ast.Import):
|
|
418
|
+
for alias in node.names:
|
|
419
|
+
imports.append({
|
|
420
|
+
"module": alias.name,
|
|
421
|
+
"names": [alias.name],
|
|
422
|
+
"type": "import",
|
|
423
|
+
"line": node.lineno,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
elif isinstance(node, ast.ImportFrom):
|
|
427
|
+
module = node.module or ""
|
|
428
|
+
if node.level > 0:
|
|
429
|
+
# Import relativo
|
|
430
|
+
module = "." * node.level + module
|
|
431
|
+
|
|
432
|
+
names = [alias.name for alias in node.names]
|
|
433
|
+
imports.append({
|
|
434
|
+
"module": module,
|
|
435
|
+
"names": names,
|
|
436
|
+
"type": "from" if node.level == 0 else "relative",
|
|
437
|
+
"line": node.lineno,
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
except SyntaxError:
|
|
441
|
+
# Fallback a regex si hay error de sintaxis
|
|
442
|
+
imports = self._parse_imports_regex(content, self.PYTHON_IMPORT_PATTERNS, "import")
|
|
443
|
+
|
|
444
|
+
return imports
|
|
445
|
+
|
|
446
|
+
def _parse_js_imports(self, content: str, file_path: Path) -> List[Dict]:
|
|
447
|
+
"""Parsea imports de JavaScript/TypeScript."""
|
|
448
|
+
return self._parse_imports_regex(content, self.JS_IMPORT_PATTERNS, "import")
|
|
449
|
+
|
|
450
|
+
def _parse_imports_regex(
|
|
451
|
+
self,
|
|
452
|
+
content: str,
|
|
453
|
+
patterns: List[re.Pattern],
|
|
454
|
+
default_type: str
|
|
455
|
+
) -> List[Dict]:
|
|
456
|
+
"""Parsea imports usando expresiones regulares."""
|
|
457
|
+
imports = []
|
|
458
|
+
|
|
459
|
+
for pattern in patterns:
|
|
460
|
+
for match in pattern.finditer(content):
|
|
461
|
+
module = match.group(1)
|
|
462
|
+
imports.append({
|
|
463
|
+
"module": module,
|
|
464
|
+
"names": [],
|
|
465
|
+
"type": default_type,
|
|
466
|
+
"line": content[:match.start()].count("\n") + 1,
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
return imports
|
|
470
|
+
|
|
471
|
+
def _build_reverse_indices(self, graph: DependencyGraph) -> None:
|
|
472
|
+
"""Construye Γndices inversos (imported_by) para cada nodo."""
|
|
473
|
+
for edge in graph.edges:
|
|
474
|
+
if edge.target in graph.nodes:
|
|
475
|
+
graph.nodes[edge.target].imported_by.append(edge.source)
|