sourcecode 0.3.0__py3-none-any.whl → 0.5.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.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +111 -1
- sourcecode/graph_analyzer.py +1173 -0
- sourcecode/schema.py +55 -0
- {sourcecode-0.3.0.dist-info → sourcecode-0.5.0.dist-info}/METADATA +76 -1
- {sourcecode-0.3.0.dist-info → sourcecode-0.5.0.dist-info}/RECORD +8 -7
- {sourcecode-0.3.0.dist-info → sourcecode-0.5.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.3.0.dist-info → sourcecode-0.5.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Optional
|
|
6
|
+
from typing import Any, Optional, cast
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
@@ -16,6 +16,8 @@ app = typer.Typer(
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
FORMAT_CHOICES = ["json", "yaml"]
|
|
19
|
+
GRAPH_DETAIL_CHOICES = ["high", "medium", "full"]
|
|
20
|
+
GRAPH_EDGE_CHOICES = {"imports", "calls", "contains", "extends"}
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def version_callback(value: bool) -> None:
|
|
@@ -51,6 +53,28 @@ def main(
|
|
|
51
53
|
"--dependencies",
|
|
52
54
|
help="Incluir dependencias directas, versiones exactas y transitivas cuando haya lockfiles compatibles",
|
|
53
55
|
),
|
|
56
|
+
graph_modules: bool = typer.Option(
|
|
57
|
+
False,
|
|
58
|
+
"--graph-modules",
|
|
59
|
+
help="Incluir grafo estructural de modulos, imports y relaciones simples del codigo",
|
|
60
|
+
),
|
|
61
|
+
graph_detail: str = typer.Option(
|
|
62
|
+
"high",
|
|
63
|
+
"--graph-detail",
|
|
64
|
+
help="Nivel de detalle del grafo: high|medium|full",
|
|
65
|
+
show_default=True,
|
|
66
|
+
),
|
|
67
|
+
max_nodes: Optional[int] = typer.Option(
|
|
68
|
+
None,
|
|
69
|
+
"--max-nodes",
|
|
70
|
+
help="Limite de nodos para `--graph-modules` en modos high/medium",
|
|
71
|
+
min=1,
|
|
72
|
+
),
|
|
73
|
+
graph_edges: Optional[str] = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--graph-edges",
|
|
76
|
+
help="Tipos de arista para `--graph-modules` separados por comas: imports,calls,contains,extends",
|
|
77
|
+
),
|
|
54
78
|
no_redact: bool = typer.Option(
|
|
55
79
|
False,
|
|
56
80
|
"--no-redact",
|
|
@@ -80,6 +104,12 @@ def main(
|
|
|
80
104
|
err=True,
|
|
81
105
|
)
|
|
82
106
|
raise typer.Exit(code=1)
|
|
107
|
+
if graph_detail not in GRAPH_DETAIL_CHOICES:
|
|
108
|
+
typer.echo(
|
|
109
|
+
f"Error: valor invalido '{graph_detail}' para --graph-detail. Opciones: {', '.join(GRAPH_DETAIL_CHOICES)}",
|
|
110
|
+
err=True,
|
|
111
|
+
)
|
|
112
|
+
raise typer.Exit(code=1)
|
|
83
113
|
|
|
84
114
|
# Resolver y validar path
|
|
85
115
|
target = path.resolve()
|
|
@@ -95,6 +125,7 @@ def main(
|
|
|
95
125
|
|
|
96
126
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
|
97
127
|
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
128
|
+
from sourcecode.graph_analyzer import GraphAnalyzer, GraphDetail
|
|
98
129
|
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
99
130
|
from sourcecode.scanner import FileScanner
|
|
100
131
|
from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
|
|
@@ -118,11 +149,48 @@ def main(
|
|
|
118
149
|
filtered[name] = value
|
|
119
150
|
return filtered
|
|
120
151
|
|
|
152
|
+
def prune_workspace_paths(
|
|
153
|
+
tree: dict[str, Any], workspace_paths: list[str]
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
pruned = dict(tree)
|
|
156
|
+
for workspace_path in workspace_paths:
|
|
157
|
+
parts = [part for part in workspace_path.split("/") if part]
|
|
158
|
+
if not parts:
|
|
159
|
+
continue
|
|
160
|
+
node = pruned
|
|
161
|
+
for index, part in enumerate(parts):
|
|
162
|
+
if not isinstance(node, dict) or part not in node:
|
|
163
|
+
break
|
|
164
|
+
if index == len(parts) - 1:
|
|
165
|
+
node.pop(part, None)
|
|
166
|
+
break
|
|
167
|
+
child = node.get(part)
|
|
168
|
+
if not isinstance(child, dict):
|
|
169
|
+
break
|
|
170
|
+
node = child
|
|
171
|
+
return pruned
|
|
172
|
+
|
|
121
173
|
file_tree = filter_sensitive_files(raw_tree)
|
|
122
174
|
manifests = scanner.find_manifests()
|
|
123
175
|
detector = ProjectDetector(build_default_detectors())
|
|
124
176
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
125
177
|
dependency_analyzer = DependencyAnalyzer() if dependencies else None
|
|
178
|
+
graph_analyzer = GraphAnalyzer() if graph_modules else None
|
|
179
|
+
parsed_graph_edges = (
|
|
180
|
+
{edge.strip() for edge in graph_edges.split(",") if edge.strip()}
|
|
181
|
+
if graph_edges
|
|
182
|
+
else None
|
|
183
|
+
)
|
|
184
|
+
if parsed_graph_edges is not None:
|
|
185
|
+
invalid_edges = sorted(parsed_graph_edges - GRAPH_EDGE_CHOICES)
|
|
186
|
+
if invalid_edges:
|
|
187
|
+
typer.echo(
|
|
188
|
+
"Error: valores invalidos para --graph-edges: "
|
|
189
|
+
f"{', '.join(invalid_edges)}. Opciones: {', '.join(sorted(GRAPH_EDGE_CHOICES))}",
|
|
190
|
+
err=True,
|
|
191
|
+
)
|
|
192
|
+
raise typer.Exit(code=1)
|
|
193
|
+
graph_detail_typed = cast(GraphDetail, graph_detail)
|
|
126
194
|
|
|
127
195
|
root_manifests = [
|
|
128
196
|
manifest
|
|
@@ -142,6 +210,24 @@ def main(
|
|
|
142
210
|
root_dependencies, root_summary = dependency_analyzer.analyze(target)
|
|
143
211
|
dependency_records.extend(root_dependencies)
|
|
144
212
|
dependency_summaries.append(root_summary)
|
|
213
|
+
module_graphs = []
|
|
214
|
+
if graph_analyzer is not None:
|
|
215
|
+
root_graph_tree = (
|
|
216
|
+
prune_workspace_paths(
|
|
217
|
+
file_tree,
|
|
218
|
+
[workspace.path for workspace in workspace_analysis.workspaces],
|
|
219
|
+
)
|
|
220
|
+
if workspace_analysis.workspaces
|
|
221
|
+
else file_tree
|
|
222
|
+
)
|
|
223
|
+
module_graphs.append(
|
|
224
|
+
graph_analyzer.analyze(
|
|
225
|
+
target,
|
|
226
|
+
root_graph_tree,
|
|
227
|
+
detail="full",
|
|
228
|
+
entry_points=entry_points,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
145
231
|
|
|
146
232
|
for workspace in workspace_analysis.workspaces:
|
|
147
233
|
workspace_root = target / workspace.path
|
|
@@ -174,6 +260,17 @@ def main(
|
|
|
174
260
|
)
|
|
175
261
|
dependency_records.extend(workspace_dependencies)
|
|
176
262
|
dependency_summaries.append(workspace_summary)
|
|
263
|
+
if graph_analyzer is not None:
|
|
264
|
+
workspace_graph = graph_analyzer.analyze(
|
|
265
|
+
workspace_root,
|
|
266
|
+
workspace_tree,
|
|
267
|
+
workspace=workspace.path,
|
|
268
|
+
detail="full",
|
|
269
|
+
entry_points=workspace_entry_points,
|
|
270
|
+
)
|
|
271
|
+
module_graphs.append(
|
|
272
|
+
graph_analyzer.prefix_graph(workspace_graph, workspace.path, workspace.path)
|
|
273
|
+
)
|
|
177
274
|
|
|
178
275
|
stacks, project_type = detector.classify_results(
|
|
179
276
|
file_tree,
|
|
@@ -186,6 +283,17 @@ def main(
|
|
|
186
283
|
if dependency_analyzer is not None
|
|
187
284
|
else None
|
|
188
285
|
)
|
|
286
|
+
module_graph = (
|
|
287
|
+
graph_analyzer.merge_graphs(
|
|
288
|
+
module_graphs,
|
|
289
|
+
detail=graph_detail_typed,
|
|
290
|
+
edge_kinds=parsed_graph_edges,
|
|
291
|
+
max_nodes=max_nodes,
|
|
292
|
+
entry_points=entry_points,
|
|
293
|
+
)
|
|
294
|
+
if graph_analyzer is not None
|
|
295
|
+
else None
|
|
296
|
+
)
|
|
189
297
|
|
|
190
298
|
# 3. Construir el schema
|
|
191
299
|
metadata = AnalysisMetadata(analyzed_path=str(target))
|
|
@@ -197,6 +305,8 @@ def main(
|
|
|
197
305
|
entry_points=entry_points,
|
|
198
306
|
dependencies=dependency_records,
|
|
199
307
|
dependency_summary=dependency_summary,
|
|
308
|
+
module_graph=module_graph,
|
|
309
|
+
module_graph_summary=module_graph.summary if module_graph is not None else None,
|
|
200
310
|
)
|
|
201
311
|
|
|
202
312
|
# 4. Serializar (con o sin modo compact)
|