sourcecode 0.3.0__py3-none-any.whl → 0.4.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 +50 -0
- sourcecode/graph_analyzer.py +759 -0
- sourcecode/schema.py +46 -0
- {sourcecode-0.3.0.dist-info → sourcecode-0.4.0.dist-info}/METADATA +64 -1
- {sourcecode-0.3.0.dist-info → sourcecode-0.4.0.dist-info}/RECORD +8 -7
- {sourcecode-0.3.0.dist-info → sourcecode-0.4.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.3.0.dist-info → sourcecode-0.4.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -51,6 +51,11 @@ def main(
|
|
|
51
51
|
"--dependencies",
|
|
52
52
|
help="Incluir dependencias directas, versiones exactas y transitivas cuando haya lockfiles compatibles",
|
|
53
53
|
),
|
|
54
|
+
graph_modules: bool = typer.Option(
|
|
55
|
+
False,
|
|
56
|
+
"--graph-modules",
|
|
57
|
+
help="Incluir grafo estructural de modulos, imports y relaciones simples del codigo",
|
|
58
|
+
),
|
|
54
59
|
no_redact: bool = typer.Option(
|
|
55
60
|
False,
|
|
56
61
|
"--no-redact",
|
|
@@ -95,6 +100,7 @@ def main(
|
|
|
95
100
|
|
|
96
101
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
|
97
102
|
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
103
|
+
from sourcecode.graph_analyzer import GraphAnalyzer
|
|
98
104
|
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
99
105
|
from sourcecode.scanner import FileScanner
|
|
100
106
|
from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
|
|
@@ -118,11 +124,33 @@ def main(
|
|
|
118
124
|
filtered[name] = value
|
|
119
125
|
return filtered
|
|
120
126
|
|
|
127
|
+
def prune_workspace_paths(
|
|
128
|
+
tree: dict[str, Any], workspace_paths: list[str]
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
pruned = dict(tree)
|
|
131
|
+
for workspace_path in workspace_paths:
|
|
132
|
+
parts = [part for part in workspace_path.split("/") if part]
|
|
133
|
+
if not parts:
|
|
134
|
+
continue
|
|
135
|
+
node = pruned
|
|
136
|
+
for index, part in enumerate(parts):
|
|
137
|
+
if not isinstance(node, dict) or part not in node:
|
|
138
|
+
break
|
|
139
|
+
if index == len(parts) - 1:
|
|
140
|
+
node.pop(part, None)
|
|
141
|
+
break
|
|
142
|
+
child = node.get(part)
|
|
143
|
+
if not isinstance(child, dict):
|
|
144
|
+
break
|
|
145
|
+
node = child
|
|
146
|
+
return pruned
|
|
147
|
+
|
|
121
148
|
file_tree = filter_sensitive_files(raw_tree)
|
|
122
149
|
manifests = scanner.find_manifests()
|
|
123
150
|
detector = ProjectDetector(build_default_detectors())
|
|
124
151
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
125
152
|
dependency_analyzer = DependencyAnalyzer() if dependencies else None
|
|
153
|
+
graph_analyzer = GraphAnalyzer() if graph_modules else None
|
|
126
154
|
|
|
127
155
|
root_manifests = [
|
|
128
156
|
manifest
|
|
@@ -142,6 +170,17 @@ def main(
|
|
|
142
170
|
root_dependencies, root_summary = dependency_analyzer.analyze(target)
|
|
143
171
|
dependency_records.extend(root_dependencies)
|
|
144
172
|
dependency_summaries.append(root_summary)
|
|
173
|
+
module_graphs = []
|
|
174
|
+
if graph_analyzer is not None:
|
|
175
|
+
root_graph_tree = (
|
|
176
|
+
prune_workspace_paths(
|
|
177
|
+
file_tree,
|
|
178
|
+
[workspace.path for workspace in workspace_analysis.workspaces],
|
|
179
|
+
)
|
|
180
|
+
if workspace_analysis.workspaces
|
|
181
|
+
else file_tree
|
|
182
|
+
)
|
|
183
|
+
module_graphs.append(graph_analyzer.analyze(target, root_graph_tree))
|
|
145
184
|
|
|
146
185
|
for workspace in workspace_analysis.workspaces:
|
|
147
186
|
workspace_root = target / workspace.path
|
|
@@ -174,6 +213,15 @@ def main(
|
|
|
174
213
|
)
|
|
175
214
|
dependency_records.extend(workspace_dependencies)
|
|
176
215
|
dependency_summaries.append(workspace_summary)
|
|
216
|
+
if graph_analyzer is not None:
|
|
217
|
+
workspace_graph = graph_analyzer.analyze(
|
|
218
|
+
workspace_root,
|
|
219
|
+
workspace_tree,
|
|
220
|
+
workspace=workspace.path,
|
|
221
|
+
)
|
|
222
|
+
module_graphs.append(
|
|
223
|
+
graph_analyzer.prefix_graph(workspace_graph, workspace.path, workspace.path)
|
|
224
|
+
)
|
|
177
225
|
|
|
178
226
|
stacks, project_type = detector.classify_results(
|
|
179
227
|
file_tree,
|
|
@@ -186,6 +234,7 @@ def main(
|
|
|
186
234
|
if dependency_analyzer is not None
|
|
187
235
|
else None
|
|
188
236
|
)
|
|
237
|
+
module_graph = graph_analyzer.merge_graphs(module_graphs) if graph_analyzer is not None else None
|
|
189
238
|
|
|
190
239
|
# 3. Construir el schema
|
|
191
240
|
metadata = AnalysisMetadata(analyzed_path=str(target))
|
|
@@ -197,6 +246,7 @@ def main(
|
|
|
197
246
|
entry_points=entry_points,
|
|
198
247
|
dependencies=dependency_records,
|
|
199
248
|
dependency_summary=dependency_summary,
|
|
249
|
+
module_graph=module_graph,
|
|
200
250
|
)
|
|
201
251
|
|
|
202
252
|
# 4. Serializar (con o sin modo compact)
|
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"""Analisis estructural offline para modulos, imports y relaciones simples."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from sourcecode.schema import GraphEdge, GraphNode, ModuleGraph, ModuleGraphSummary
|
|
11
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GraphAnalyzer:
|
|
15
|
+
"""Construye un grafo estructural parcial y seguro del proyecto."""
|
|
16
|
+
|
|
17
|
+
_PYTHON_EXTENSIONS = {".py"}
|
|
18
|
+
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
19
|
+
_GO_EXTENSIONS = {".go"}
|
|
20
|
+
_JVM_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
21
|
+
_SUPPORTED_EXTENSIONS = _PYTHON_EXTENSIONS | _NODE_EXTENSIONS | _GO_EXTENSIONS | _JVM_EXTENSIONS
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
max_files: int = 200,
|
|
27
|
+
max_file_size: int = 200_000,
|
|
28
|
+
max_nodes: int = 1_000,
|
|
29
|
+
max_edges: int = 2_000,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.max_files = max_files
|
|
32
|
+
self.max_file_size = max_file_size
|
|
33
|
+
self.max_nodes = max_nodes
|
|
34
|
+
self.max_edges = max_edges
|
|
35
|
+
|
|
36
|
+
def analyze(
|
|
37
|
+
self,
|
|
38
|
+
root: Path,
|
|
39
|
+
file_tree: dict[str, Any],
|
|
40
|
+
*,
|
|
41
|
+
workspace: str | None = None,
|
|
42
|
+
) -> ModuleGraph:
|
|
43
|
+
source_files = [
|
|
44
|
+
path
|
|
45
|
+
for path in flatten_file_tree(file_tree)
|
|
46
|
+
if Path(path).suffix in self._SUPPORTED_EXTENSIONS and (root / path).is_file()
|
|
47
|
+
]
|
|
48
|
+
limitations: list[str] = []
|
|
49
|
+
if len(source_files) > self.max_files:
|
|
50
|
+
limitations.append(
|
|
51
|
+
f"max_files_reached:{len(source_files)}>{self.max_files}"
|
|
52
|
+
)
|
|
53
|
+
source_files = source_files[: self.max_files]
|
|
54
|
+
|
|
55
|
+
nodes: list[GraphNode] = []
|
|
56
|
+
edges: list[GraphEdge] = []
|
|
57
|
+
node_ids: set[str] = set()
|
|
58
|
+
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
59
|
+
|
|
60
|
+
python_modules = self._build_python_module_map(source_files)
|
|
61
|
+
node_modules = {path: path for path in source_files if Path(path).suffix in self._NODE_EXTENSIONS}
|
|
62
|
+
go_imports = self._build_go_import_map(root, source_files)
|
|
63
|
+
jvm_types = self._build_jvm_type_map(root, source_files)
|
|
64
|
+
|
|
65
|
+
for relative_path in source_files:
|
|
66
|
+
absolute_path = root / relative_path
|
|
67
|
+
try:
|
|
68
|
+
if absolute_path.stat().st_size > self.max_file_size:
|
|
69
|
+
limitations.append(f"file_too_large:{relative_path}")
|
|
70
|
+
continue
|
|
71
|
+
content = absolute_path.read_text(encoding="utf-8", errors="replace")
|
|
72
|
+
except OSError:
|
|
73
|
+
limitations.append(f"read_error:{relative_path}")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
suffix = absolute_path.suffix
|
|
77
|
+
module_node = GraphNode(
|
|
78
|
+
id=f"module:{relative_path}",
|
|
79
|
+
kind="module",
|
|
80
|
+
language=self._language_for_suffix(suffix),
|
|
81
|
+
path=relative_path,
|
|
82
|
+
display_name=relative_path,
|
|
83
|
+
workspace=workspace,
|
|
84
|
+
)
|
|
85
|
+
self._append_node(nodes, node_ids, module_node)
|
|
86
|
+
|
|
87
|
+
file_nodes: list[GraphNode] = []
|
|
88
|
+
file_edges: list[GraphEdge] = []
|
|
89
|
+
file_limitations: list[str] = []
|
|
90
|
+
|
|
91
|
+
if suffix in self._PYTHON_EXTENSIONS:
|
|
92
|
+
file_nodes, file_edges, file_limitations = self._analyze_python_file(
|
|
93
|
+
relative_path, content, python_modules, workspace
|
|
94
|
+
)
|
|
95
|
+
elif suffix in self._NODE_EXTENSIONS:
|
|
96
|
+
file_nodes, file_edges, file_limitations = self._analyze_node_file(
|
|
97
|
+
relative_path, content, node_modules, workspace
|
|
98
|
+
)
|
|
99
|
+
elif suffix in self._GO_EXTENSIONS:
|
|
100
|
+
file_nodes, file_edges, file_limitations = self._analyze_go_file(
|
|
101
|
+
relative_path, content, go_imports, workspace
|
|
102
|
+
)
|
|
103
|
+
elif suffix in self._JVM_EXTENSIONS:
|
|
104
|
+
file_nodes, file_edges, file_limitations = self._analyze_jvm_file(
|
|
105
|
+
relative_path, content, jvm_types, workspace
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
limitations.extend(file_limitations)
|
|
109
|
+
for node in file_nodes:
|
|
110
|
+
self._append_node(nodes, node_ids, node)
|
|
111
|
+
for edge in file_edges:
|
|
112
|
+
self._append_edge(edges, edge_keys, edge)
|
|
113
|
+
if len(nodes) >= self.max_nodes or len(edges) >= self.max_edges:
|
|
114
|
+
limitations.append("graph_budget_reached")
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if len(nodes) > self.max_nodes:
|
|
118
|
+
nodes = nodes[: self.max_nodes]
|
|
119
|
+
if len(edges) > self.max_edges:
|
|
120
|
+
edges = edges[: self.max_edges]
|
|
121
|
+
|
|
122
|
+
summary = ModuleGraphSummary(
|
|
123
|
+
requested=True,
|
|
124
|
+
node_count=len(nodes),
|
|
125
|
+
edge_count=len(edges),
|
|
126
|
+
languages=sorted({node.language for node in nodes}),
|
|
127
|
+
methods=sorted({edge.method for edge in edges}),
|
|
128
|
+
limitations=self._unique(limitations),
|
|
129
|
+
)
|
|
130
|
+
return ModuleGraph(nodes=nodes, edges=edges, summary=summary)
|
|
131
|
+
|
|
132
|
+
def merge_graphs(self, graphs: list[ModuleGraph]) -> ModuleGraph:
|
|
133
|
+
nodes: list[GraphNode] = []
|
|
134
|
+
edges: list[GraphEdge] = []
|
|
135
|
+
limitations: list[str] = []
|
|
136
|
+
node_ids: set[str] = set()
|
|
137
|
+
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
138
|
+
for graph in graphs:
|
|
139
|
+
for node in graph.nodes:
|
|
140
|
+
self._append_node(nodes, node_ids, node)
|
|
141
|
+
for edge in graph.edges:
|
|
142
|
+
self._append_edge(edges, edge_keys, edge)
|
|
143
|
+
limitations.extend(graph.summary.limitations)
|
|
144
|
+
return ModuleGraph(
|
|
145
|
+
nodes=nodes,
|
|
146
|
+
edges=edges,
|
|
147
|
+
summary=ModuleGraphSummary(
|
|
148
|
+
requested=bool(graphs),
|
|
149
|
+
node_count=len(nodes),
|
|
150
|
+
edge_count=len(edges),
|
|
151
|
+
languages=sorted({node.language for node in nodes}),
|
|
152
|
+
methods=sorted({edge.method for edge in edges}),
|
|
153
|
+
limitations=self._unique(limitations),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def prefix_graph(self, graph: ModuleGraph, prefix: str, workspace: str) -> ModuleGraph:
|
|
158
|
+
"""Reescribe ids y paths de un grafo de workspace al espacio de paths del repo."""
|
|
159
|
+
node_id_map: dict[str, str] = {}
|
|
160
|
+
prefixed_nodes: list[GraphNode] = []
|
|
161
|
+
clean_prefix = prefix.strip("/")
|
|
162
|
+
for node in graph.nodes:
|
|
163
|
+
prefixed_path = f"{clean_prefix}/{node.path}".strip("/")
|
|
164
|
+
new_id = self._node_id(node.kind, prefixed_path, node.symbol)
|
|
165
|
+
node_id_map[node.id] = new_id
|
|
166
|
+
prefixed_nodes.append(
|
|
167
|
+
replace(
|
|
168
|
+
node,
|
|
169
|
+
id=new_id,
|
|
170
|
+
path=prefixed_path,
|
|
171
|
+
workspace=workspace,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
prefixed_edges: list[GraphEdge] = []
|
|
175
|
+
for edge in graph.edges:
|
|
176
|
+
prefixed_edges.append(
|
|
177
|
+
replace(
|
|
178
|
+
edge,
|
|
179
|
+
source=node_id_map.get(edge.source, edge.source),
|
|
180
|
+
target=node_id_map.get(edge.target, edge.target),
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return ModuleGraph(
|
|
184
|
+
nodes=prefixed_nodes,
|
|
185
|
+
edges=prefixed_edges,
|
|
186
|
+
summary=replace(graph.summary),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _analyze_python_file(
|
|
190
|
+
self,
|
|
191
|
+
relative_path: str,
|
|
192
|
+
content: str,
|
|
193
|
+
module_map: dict[str, str],
|
|
194
|
+
workspace: str | None,
|
|
195
|
+
) -> tuple[list[GraphNode], list[GraphEdge], list[str]]:
|
|
196
|
+
limitations: list[str] = []
|
|
197
|
+
try:
|
|
198
|
+
tree = ast.parse(content, filename=relative_path)
|
|
199
|
+
except SyntaxError:
|
|
200
|
+
return [], [], [f"python_parse_error:{relative_path}"]
|
|
201
|
+
|
|
202
|
+
module_node_id = f"module:{relative_path}"
|
|
203
|
+
current_module = module_map.get(relative_path)
|
|
204
|
+
nodes: list[GraphNode] = []
|
|
205
|
+
edges: list[GraphEdge] = []
|
|
206
|
+
function_node_ids: dict[str, str] = {}
|
|
207
|
+
class_node_ids: dict[str, str] = {}
|
|
208
|
+
imported_function_node_ids: dict[str, str] = {}
|
|
209
|
+
imported_class_node_ids: dict[str, str] = {}
|
|
210
|
+
|
|
211
|
+
for node in tree.body:
|
|
212
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
213
|
+
function_node_id = f"function:{relative_path}:{node.name}"
|
|
214
|
+
function_node_ids[node.name] = function_node_id
|
|
215
|
+
nodes.append(
|
|
216
|
+
GraphNode(
|
|
217
|
+
id=function_node_id,
|
|
218
|
+
kind="function",
|
|
219
|
+
language="python",
|
|
220
|
+
path=relative_path,
|
|
221
|
+
symbol=node.name,
|
|
222
|
+
display_name=node.name,
|
|
223
|
+
workspace=workspace,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
edges.append(
|
|
227
|
+
GraphEdge(
|
|
228
|
+
source=module_node_id,
|
|
229
|
+
target=function_node_id,
|
|
230
|
+
kind="contains",
|
|
231
|
+
confidence="high",
|
|
232
|
+
method="ast",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
elif isinstance(node, ast.ClassDef):
|
|
236
|
+
class_node_id = f"class:{relative_path}:{node.name}"
|
|
237
|
+
class_node_ids[node.name] = class_node_id
|
|
238
|
+
nodes.append(
|
|
239
|
+
GraphNode(
|
|
240
|
+
id=class_node_id,
|
|
241
|
+
kind="class",
|
|
242
|
+
language="python",
|
|
243
|
+
path=relative_path,
|
|
244
|
+
symbol=node.name,
|
|
245
|
+
display_name=node.name,
|
|
246
|
+
workspace=workspace,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
edges.append(
|
|
250
|
+
GraphEdge(
|
|
251
|
+
source=module_node_id,
|
|
252
|
+
target=class_node_id,
|
|
253
|
+
kind="contains",
|
|
254
|
+
confidence="high",
|
|
255
|
+
method="ast",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
for ast_node in ast.walk(tree):
|
|
260
|
+
if isinstance(ast_node, ast.Import):
|
|
261
|
+
for alias in ast_node.names:
|
|
262
|
+
target_path = self._resolve_python_import(alias.name, module_map)
|
|
263
|
+
if target_path is None:
|
|
264
|
+
continue
|
|
265
|
+
edges.append(
|
|
266
|
+
GraphEdge(
|
|
267
|
+
source=module_node_id,
|
|
268
|
+
target=f"module:{target_path}",
|
|
269
|
+
kind="imports",
|
|
270
|
+
confidence="high",
|
|
271
|
+
method="ast",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
elif isinstance(ast_node, ast.ImportFrom):
|
|
275
|
+
target_path = self._resolve_python_from_import(
|
|
276
|
+
ast_node, current_module, module_map
|
|
277
|
+
)
|
|
278
|
+
if target_path is None:
|
|
279
|
+
continue
|
|
280
|
+
edges.append(
|
|
281
|
+
GraphEdge(
|
|
282
|
+
source=module_node_id,
|
|
283
|
+
target=f"module:{target_path}",
|
|
284
|
+
kind="imports",
|
|
285
|
+
confidence="high",
|
|
286
|
+
method="ast",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
for alias in ast_node.names:
|
|
290
|
+
local_name = alias.asname or alias.name
|
|
291
|
+
imported_function_node_ids[local_name] = (
|
|
292
|
+
f"function:{target_path}:{alias.name}"
|
|
293
|
+
)
|
|
294
|
+
imported_class_node_ids[local_name] = (
|
|
295
|
+
f"class:{target_path}:{alias.name}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
for top_level in tree.body:
|
|
299
|
+
if isinstance(top_level, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
300
|
+
source_id = function_node_ids.get(top_level.name)
|
|
301
|
+
if source_id is None:
|
|
302
|
+
continue
|
|
303
|
+
for nested in ast.walk(top_level):
|
|
304
|
+
if isinstance(nested, ast.Call) and isinstance(nested.func, ast.Name):
|
|
305
|
+
target_id = function_node_ids.get(nested.func.id) or imported_function_node_ids.get(
|
|
306
|
+
nested.func.id
|
|
307
|
+
)
|
|
308
|
+
if target_id is not None and target_id != source_id:
|
|
309
|
+
edges.append(
|
|
310
|
+
GraphEdge(
|
|
311
|
+
source=source_id,
|
|
312
|
+
target=target_id,
|
|
313
|
+
kind="calls",
|
|
314
|
+
confidence="medium",
|
|
315
|
+
method="ast",
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
elif isinstance(top_level, ast.ClassDef):
|
|
319
|
+
source_id = class_node_ids.get(top_level.name)
|
|
320
|
+
if source_id is None:
|
|
321
|
+
continue
|
|
322
|
+
for base in top_level.bases:
|
|
323
|
+
if isinstance(base, ast.Name):
|
|
324
|
+
target_id = class_node_ids.get(base.id) or imported_class_node_ids.get(
|
|
325
|
+
base.id
|
|
326
|
+
)
|
|
327
|
+
if target_id is not None:
|
|
328
|
+
edges.append(
|
|
329
|
+
GraphEdge(
|
|
330
|
+
source=source_id,
|
|
331
|
+
target=target_id,
|
|
332
|
+
kind="extends",
|
|
333
|
+
confidence="medium",
|
|
334
|
+
method="ast",
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return nodes, edges, limitations
|
|
338
|
+
|
|
339
|
+
def _analyze_node_file(
|
|
340
|
+
self,
|
|
341
|
+
relative_path: str,
|
|
342
|
+
content: str,
|
|
343
|
+
module_map: dict[str, str],
|
|
344
|
+
workspace: str | None,
|
|
345
|
+
) -> tuple[list[GraphNode], list[GraphEdge], list[str]]:
|
|
346
|
+
module_node_id = f"module:{relative_path}"
|
|
347
|
+
nodes: list[GraphNode] = []
|
|
348
|
+
edges: list[GraphEdge] = []
|
|
349
|
+
limitations: list[str] = []
|
|
350
|
+
|
|
351
|
+
function_pattern = re.compile(
|
|
352
|
+
r"^\s*(?:export\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)",
|
|
353
|
+
re.MULTILINE,
|
|
354
|
+
)
|
|
355
|
+
class_pattern = re.compile(
|
|
356
|
+
r"^\s*(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+extends\s+([A-Za-z_][A-Za-z0-9_]*))?",
|
|
357
|
+
re.MULTILINE,
|
|
358
|
+
)
|
|
359
|
+
function_node_ids: dict[str, str] = {}
|
|
360
|
+
class_node_ids: dict[str, str] = {}
|
|
361
|
+
|
|
362
|
+
for match in function_pattern.finditer(content):
|
|
363
|
+
name = match.group(1)
|
|
364
|
+
node_id = f"function:{relative_path}:{name}"
|
|
365
|
+
function_node_ids[name] = node_id
|
|
366
|
+
nodes.append(
|
|
367
|
+
GraphNode(
|
|
368
|
+
id=node_id,
|
|
369
|
+
kind="function",
|
|
370
|
+
language="nodejs",
|
|
371
|
+
path=relative_path,
|
|
372
|
+
symbol=name,
|
|
373
|
+
display_name=name,
|
|
374
|
+
workspace=workspace,
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
edges.append(
|
|
378
|
+
GraphEdge(
|
|
379
|
+
source=module_node_id,
|
|
380
|
+
target=node_id,
|
|
381
|
+
kind="contains",
|
|
382
|
+
confidence="medium",
|
|
383
|
+
method="heuristic",
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for match in class_pattern.finditer(content):
|
|
388
|
+
name = match.group(1)
|
|
389
|
+
base = match.group(2)
|
|
390
|
+
node_id = f"class:{relative_path}:{name}"
|
|
391
|
+
class_node_ids[name] = node_id
|
|
392
|
+
nodes.append(
|
|
393
|
+
GraphNode(
|
|
394
|
+
id=node_id,
|
|
395
|
+
kind="class",
|
|
396
|
+
language="nodejs",
|
|
397
|
+
path=relative_path,
|
|
398
|
+
symbol=name,
|
|
399
|
+
display_name=name,
|
|
400
|
+
workspace=workspace,
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
edges.append(
|
|
404
|
+
GraphEdge(
|
|
405
|
+
source=module_node_id,
|
|
406
|
+
target=node_id,
|
|
407
|
+
kind="contains",
|
|
408
|
+
confidence="medium",
|
|
409
|
+
method="heuristic",
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
if base and base in class_node_ids:
|
|
413
|
+
edges.append(
|
|
414
|
+
GraphEdge(
|
|
415
|
+
source=node_id,
|
|
416
|
+
target=class_node_ids[base],
|
|
417
|
+
kind="extends",
|
|
418
|
+
confidence="low",
|
|
419
|
+
method="heuristic",
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
patterns = [
|
|
424
|
+
re.compile(r"""import\s+(?:[^'"]+?\s+from\s+)?['"]([^'"]+)['"]"""),
|
|
425
|
+
re.compile(r"""require\(\s*['"]([^'"]+)['"]\s*\)"""),
|
|
426
|
+
]
|
|
427
|
+
found_specs: set[str] = set()
|
|
428
|
+
for pattern in patterns:
|
|
429
|
+
for match in pattern.finditer(content):
|
|
430
|
+
spec = match.group(1)
|
|
431
|
+
if spec in found_specs:
|
|
432
|
+
continue
|
|
433
|
+
found_specs.add(spec)
|
|
434
|
+
if spec.startswith("."):
|
|
435
|
+
target_path = self._resolve_node_import(relative_path, spec, module_map)
|
|
436
|
+
if target_path is None:
|
|
437
|
+
limitations.append(f"node_unresolved:{relative_path}:{spec}")
|
|
438
|
+
continue
|
|
439
|
+
edges.append(
|
|
440
|
+
GraphEdge(
|
|
441
|
+
source=module_node_id,
|
|
442
|
+
target=f"module:{target_path}",
|
|
443
|
+
kind="imports",
|
|
444
|
+
confidence="medium",
|
|
445
|
+
method="heuristic",
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
limitations.append(f"node_external:{relative_path}:{spec}")
|
|
450
|
+
|
|
451
|
+
return nodes, edges, limitations
|
|
452
|
+
|
|
453
|
+
def _analyze_go_file(
|
|
454
|
+
self,
|
|
455
|
+
relative_path: str,
|
|
456
|
+
content: str,
|
|
457
|
+
import_map: dict[str, str],
|
|
458
|
+
workspace: str | None,
|
|
459
|
+
) -> tuple[list[GraphNode], list[GraphEdge], list[str]]:
|
|
460
|
+
module_node_id = f"module:{relative_path}"
|
|
461
|
+
nodes: list[GraphNode] = []
|
|
462
|
+
edges: list[GraphEdge] = []
|
|
463
|
+
limitations: list[str] = []
|
|
464
|
+
|
|
465
|
+
for match in re.finditer(r"(?m)^func\s+([A-ZA-Za-z_][A-Za-z0-9_]*)\s*\(", content):
|
|
466
|
+
name = match.group(1)
|
|
467
|
+
node_id = f"function:{relative_path}:{name}"
|
|
468
|
+
nodes.append(
|
|
469
|
+
GraphNode(
|
|
470
|
+
id=node_id,
|
|
471
|
+
kind="function",
|
|
472
|
+
language="go",
|
|
473
|
+
path=relative_path,
|
|
474
|
+
symbol=name,
|
|
475
|
+
display_name=name,
|
|
476
|
+
workspace=workspace,
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
edges.append(
|
|
480
|
+
GraphEdge(
|
|
481
|
+
source=module_node_id,
|
|
482
|
+
target=node_id,
|
|
483
|
+
kind="contains",
|
|
484
|
+
confidence="medium",
|
|
485
|
+
method="heuristic",
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
import_specs = re.findall(r'"([^"]+)"', self._extract_go_import_block(content))
|
|
490
|
+
for spec in import_specs:
|
|
491
|
+
target_path = import_map.get(spec)
|
|
492
|
+
if target_path is None:
|
|
493
|
+
continue
|
|
494
|
+
edges.append(
|
|
495
|
+
GraphEdge(
|
|
496
|
+
source=module_node_id,
|
|
497
|
+
target=f"module:{target_path}",
|
|
498
|
+
kind="imports",
|
|
499
|
+
confidence="medium",
|
|
500
|
+
method="heuristic",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
return nodes, edges, limitations
|
|
504
|
+
|
|
505
|
+
def _analyze_jvm_file(
|
|
506
|
+
self,
|
|
507
|
+
relative_path: str,
|
|
508
|
+
content: str,
|
|
509
|
+
type_map: dict[str, tuple[str, str]],
|
|
510
|
+
workspace: str | None,
|
|
511
|
+
) -> tuple[list[GraphNode], list[GraphEdge], list[str]]:
|
|
512
|
+
module_node_id = f"module:{relative_path}"
|
|
513
|
+
language = self._language_for_suffix(Path(relative_path).suffix)
|
|
514
|
+
nodes: list[GraphNode] = []
|
|
515
|
+
edges: list[GraphEdge] = []
|
|
516
|
+
limitations: list[str] = []
|
|
517
|
+
imported_type_map = {
|
|
518
|
+
imported.split(".")[-1]: imported
|
|
519
|
+
for imported in re.findall(r"(?m)^\s*import\s+([A-Za-z0-9_.]+)", content)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
class_pattern = re.compile(
|
|
523
|
+
r"(?m)^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?"
|
|
524
|
+
r"(?:class|interface|object|trait)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
525
|
+
r"(?:\s+extends\s+([A-Za-z_][A-Za-z0-9_.]*))?"
|
|
526
|
+
)
|
|
527
|
+
local_class_ids: dict[str, str] = {}
|
|
528
|
+
for match in class_pattern.finditer(content):
|
|
529
|
+
name = match.group(1)
|
|
530
|
+
base = match.group(2)
|
|
531
|
+
node_id = f"class:{relative_path}:{name}"
|
|
532
|
+
local_class_ids[name] = node_id
|
|
533
|
+
nodes.append(
|
|
534
|
+
GraphNode(
|
|
535
|
+
id=node_id,
|
|
536
|
+
kind="class",
|
|
537
|
+
language=language,
|
|
538
|
+
path=relative_path,
|
|
539
|
+
symbol=name,
|
|
540
|
+
display_name=name,
|
|
541
|
+
workspace=workspace,
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
edges.append(
|
|
545
|
+
GraphEdge(
|
|
546
|
+
source=module_node_id,
|
|
547
|
+
target=node_id,
|
|
548
|
+
kind="contains",
|
|
549
|
+
confidence="medium",
|
|
550
|
+
method="heuristic",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
if base:
|
|
554
|
+
base_short = base.split(".")[-1]
|
|
555
|
+
if base_short in local_class_ids:
|
|
556
|
+
edges.append(
|
|
557
|
+
GraphEdge(
|
|
558
|
+
source=node_id,
|
|
559
|
+
target=local_class_ids[base_short],
|
|
560
|
+
kind="extends",
|
|
561
|
+
confidence="low",
|
|
562
|
+
method="heuristic",
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
imported_base = imported_type_map.get(base_short)
|
|
567
|
+
fqcn = imported_base or base
|
|
568
|
+
mapping = type_map.get(fqcn)
|
|
569
|
+
if mapping is None:
|
|
570
|
+
continue
|
|
571
|
+
base_path, _base_name = mapping
|
|
572
|
+
edges.append(
|
|
573
|
+
GraphEdge(
|
|
574
|
+
source=node_id,
|
|
575
|
+
target=f"class:{base_path}:{base_short}",
|
|
576
|
+
kind="extends",
|
|
577
|
+
confidence="low",
|
|
578
|
+
method="heuristic",
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
for imported in imported_type_map.values():
|
|
583
|
+
mapping = type_map.get(imported)
|
|
584
|
+
if mapping is None:
|
|
585
|
+
limitations.append(f"jvm_unresolved:{relative_path}:{imported}")
|
|
586
|
+
continue
|
|
587
|
+
target_path, _symbol = mapping
|
|
588
|
+
edges.append(
|
|
589
|
+
GraphEdge(
|
|
590
|
+
source=module_node_id,
|
|
591
|
+
target=f"module:{target_path}",
|
|
592
|
+
kind="imports",
|
|
593
|
+
confidence="medium",
|
|
594
|
+
method="heuristic",
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
return nodes, edges, limitations
|
|
598
|
+
|
|
599
|
+
def _build_python_module_map(self, source_files: list[str]) -> dict[str, str]:
|
|
600
|
+
module_map: dict[str, str] = {}
|
|
601
|
+
for relative_path in source_files:
|
|
602
|
+
path = PurePosixPath(relative_path)
|
|
603
|
+
if path.suffix not in self._PYTHON_EXTENSIONS:
|
|
604
|
+
continue
|
|
605
|
+
if path.name == "__init__.py":
|
|
606
|
+
module_name = ".".join(path.parts[:-1])
|
|
607
|
+
else:
|
|
608
|
+
module_name = ".".join(path.with_suffix("").parts)
|
|
609
|
+
if module_name:
|
|
610
|
+
module_map[relative_path] = module_name
|
|
611
|
+
return module_map
|
|
612
|
+
|
|
613
|
+
def _build_go_import_map(self, root: Path, source_files: list[str]) -> dict[str, str]:
|
|
614
|
+
go_mod = root / "go.mod"
|
|
615
|
+
if not go_mod.exists():
|
|
616
|
+
return {}
|
|
617
|
+
module_name = ""
|
|
618
|
+
for line in go_mod.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
619
|
+
stripped = line.strip()
|
|
620
|
+
if stripped.startswith("module "):
|
|
621
|
+
module_name = stripped.removeprefix("module ").strip()
|
|
622
|
+
break
|
|
623
|
+
if not module_name:
|
|
624
|
+
return {}
|
|
625
|
+
mapping: dict[str, str] = {}
|
|
626
|
+
for relative_path in source_files:
|
|
627
|
+
path = PurePosixPath(relative_path)
|
|
628
|
+
if path.suffix not in self._GO_EXTENSIONS:
|
|
629
|
+
continue
|
|
630
|
+
directory = "." if len(path.parts) == 1 else "/".join(path.parts[:-1])
|
|
631
|
+
import_path = module_name if directory == "." else f"{module_name}/{directory}"
|
|
632
|
+
mapping[import_path] = relative_path
|
|
633
|
+
return mapping
|
|
634
|
+
|
|
635
|
+
def _build_jvm_type_map(self, root: Path, source_files: list[str]) -> dict[str, tuple[str, str]]:
|
|
636
|
+
mapping: dict[str, tuple[str, str]] = {}
|
|
637
|
+
package_pattern = re.compile(r"(?m)^\s*package\s+([A-Za-z0-9_.]+)")
|
|
638
|
+
type_pattern = re.compile(
|
|
639
|
+
r"(?m)^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?"
|
|
640
|
+
r"(?:class|interface|object|trait)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
641
|
+
)
|
|
642
|
+
for relative_path in source_files:
|
|
643
|
+
if Path(relative_path).suffix not in self._JVM_EXTENSIONS:
|
|
644
|
+
continue
|
|
645
|
+
try:
|
|
646
|
+
content = (root / relative_path).read_text(encoding="utf-8", errors="replace")
|
|
647
|
+
except OSError:
|
|
648
|
+
continue
|
|
649
|
+
package_match = package_pattern.search(content)
|
|
650
|
+
type_match = type_pattern.search(content)
|
|
651
|
+
if type_match is None:
|
|
652
|
+
continue
|
|
653
|
+
package_name = package_match.group(1) if package_match else ""
|
|
654
|
+
symbol = type_match.group(1)
|
|
655
|
+
fqcn = f"{package_name}.{symbol}" if package_name else symbol
|
|
656
|
+
mapping[fqcn] = (relative_path, symbol)
|
|
657
|
+
return mapping
|
|
658
|
+
|
|
659
|
+
def _resolve_python_import(
|
|
660
|
+
self, module_name: str, module_map: dict[str, str]
|
|
661
|
+
) -> Optional[str]:
|
|
662
|
+
for path, candidate in module_map.items():
|
|
663
|
+
if candidate == module_name:
|
|
664
|
+
return path
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
def _resolve_python_from_import(
|
|
668
|
+
self,
|
|
669
|
+
node: ast.ImportFrom,
|
|
670
|
+
current_module: str | None,
|
|
671
|
+
module_map: dict[str, str],
|
|
672
|
+
) -> Optional[str]:
|
|
673
|
+
if node.level and current_module:
|
|
674
|
+
package_parts = current_module.split(".")
|
|
675
|
+
base_parts = package_parts[:-1]
|
|
676
|
+
if node.level > 1:
|
|
677
|
+
base_parts = base_parts[: max(0, len(base_parts) - (node.level - 1))]
|
|
678
|
+
target_module = ".".join(base_parts + ([node.module] if node.module else []))
|
|
679
|
+
else:
|
|
680
|
+
target_module = node.module or ""
|
|
681
|
+
if not target_module:
|
|
682
|
+
return None
|
|
683
|
+
return self._resolve_python_import(target_module, module_map)
|
|
684
|
+
|
|
685
|
+
def _resolve_node_import(
|
|
686
|
+
self, source_path: str, spec: str, module_map: dict[str, str]
|
|
687
|
+
) -> Optional[str]:
|
|
688
|
+
base_dir = PurePosixPath(source_path).parent
|
|
689
|
+
spec_path = PurePosixPath(spec)
|
|
690
|
+
candidate_base = (base_dir / spec_path).as_posix()
|
|
691
|
+
candidates = [
|
|
692
|
+
candidate_base,
|
|
693
|
+
f"{candidate_base}.js",
|
|
694
|
+
f"{candidate_base}.jsx",
|
|
695
|
+
f"{candidate_base}.ts",
|
|
696
|
+
f"{candidate_base}.tsx",
|
|
697
|
+
f"{candidate_base}/index.js",
|
|
698
|
+
f"{candidate_base}/index.ts",
|
|
699
|
+
f"{candidate_base}/index.tsx",
|
|
700
|
+
]
|
|
701
|
+
for candidate in candidates:
|
|
702
|
+
normalized = PurePosixPath(candidate).as_posix()
|
|
703
|
+
if normalized in module_map:
|
|
704
|
+
return normalized
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
def _extract_go_import_block(self, content: str) -> str:
|
|
708
|
+
block_match = re.search(r"import\s*\((.*?)\)", content, re.DOTALL)
|
|
709
|
+
if block_match is not None:
|
|
710
|
+
return block_match.group(1)
|
|
711
|
+
single_imports = re.findall(r'(?m)^\s*import\s+"([^"]+)"', content)
|
|
712
|
+
return "\n".join(f'"{item}"' for item in single_imports)
|
|
713
|
+
|
|
714
|
+
def _append_node(self, nodes: list[GraphNode], node_ids: set[str], node: GraphNode) -> None:
|
|
715
|
+
if len(nodes) >= self.max_nodes or node.id in node_ids:
|
|
716
|
+
return
|
|
717
|
+
node_ids.add(node.id)
|
|
718
|
+
nodes.append(node)
|
|
719
|
+
|
|
720
|
+
def _append_edge(
|
|
721
|
+
self,
|
|
722
|
+
edges: list[GraphEdge],
|
|
723
|
+
edge_keys: set[tuple[str, str, str, str]],
|
|
724
|
+
edge: GraphEdge,
|
|
725
|
+
) -> None:
|
|
726
|
+
if len(edges) >= self.max_edges:
|
|
727
|
+
return
|
|
728
|
+
key = (edge.source, edge.target, edge.kind, edge.method)
|
|
729
|
+
if key in edge_keys:
|
|
730
|
+
return
|
|
731
|
+
edge_keys.add(key)
|
|
732
|
+
edges.append(edge)
|
|
733
|
+
|
|
734
|
+
def _language_for_suffix(self, suffix: str) -> str:
|
|
735
|
+
if suffix in self._PYTHON_EXTENSIONS:
|
|
736
|
+
return "python"
|
|
737
|
+
if suffix in self._NODE_EXTENSIONS:
|
|
738
|
+
return "nodejs"
|
|
739
|
+
if suffix in self._GO_EXTENSIONS:
|
|
740
|
+
return "go"
|
|
741
|
+
if suffix == ".java":
|
|
742
|
+
return "java"
|
|
743
|
+
if suffix == ".kt":
|
|
744
|
+
return "kotlin"
|
|
745
|
+
if suffix == ".scala":
|
|
746
|
+
return "scala"
|
|
747
|
+
return "unknown"
|
|
748
|
+
|
|
749
|
+
def _node_id(self, kind: str, path: str, symbol: str | None = None) -> str:
|
|
750
|
+
if symbol:
|
|
751
|
+
return f"{kind}:{path}:{symbol}"
|
|
752
|
+
return f"{kind}:{path}"
|
|
753
|
+
|
|
754
|
+
def _unique(self, values: list[str]) -> list[str]:
|
|
755
|
+
result: list[str] = []
|
|
756
|
+
for value in values:
|
|
757
|
+
if value not in result:
|
|
758
|
+
result.append(value)
|
|
759
|
+
return result
|
sourcecode/schema.py
CHANGED
|
@@ -96,6 +96,51 @@ class DependencySummary:
|
|
|
96
96
|
limitations: list[str] = field(default_factory=list)
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
@dataclass
|
|
100
|
+
class GraphNode:
|
|
101
|
+
"""Nodo del grafo estructural del codigo."""
|
|
102
|
+
|
|
103
|
+
id: str
|
|
104
|
+
kind: str
|
|
105
|
+
language: str
|
|
106
|
+
path: str
|
|
107
|
+
symbol: Optional[str] = None
|
|
108
|
+
display_name: Optional[str] = None
|
|
109
|
+
workspace: Optional[str] = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class GraphEdge:
|
|
114
|
+
"""Arista del grafo estructural del codigo."""
|
|
115
|
+
|
|
116
|
+
source: str
|
|
117
|
+
target: str
|
|
118
|
+
kind: str
|
|
119
|
+
confidence: Literal["high", "medium", "low"] = "medium"
|
|
120
|
+
method: Literal["ast", "heuristic", "unresolved"] = "heuristic"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class ModuleGraphSummary:
|
|
125
|
+
"""Resumen del analisis estructural del grafo."""
|
|
126
|
+
|
|
127
|
+
requested: bool = False
|
|
128
|
+
node_count: int = 0
|
|
129
|
+
edge_count: int = 0
|
|
130
|
+
languages: list[str] = field(default_factory=list)
|
|
131
|
+
methods: list[str] = field(default_factory=list)
|
|
132
|
+
limitations: list[str] = field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ModuleGraph:
|
|
137
|
+
"""Grafo de modulos, simbolos y relaciones del proyecto."""
|
|
138
|
+
|
|
139
|
+
nodes: list[GraphNode] = field(default_factory=list)
|
|
140
|
+
edges: list[GraphEdge] = field(default_factory=list)
|
|
141
|
+
summary: ModuleGraphSummary = field(default_factory=ModuleGraphSummary)
|
|
142
|
+
|
|
143
|
+
|
|
99
144
|
@dataclass
|
|
100
145
|
class SourceMap:
|
|
101
146
|
"""Schema completo del output v1.0.
|
|
@@ -115,3 +160,4 @@ class SourceMap:
|
|
|
115
160
|
entry_points: list[EntryPoint] = field(default_factory=list)
|
|
116
161
|
dependencies: list[DependencyRecord] = field(default_factory=list)
|
|
117
162
|
dependency_summary: Optional[DependencySummary] = None
|
|
163
|
+
module_graph: Optional[ModuleGraph] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -52,6 +52,12 @@ Include direct dependencies, exact versions, and transitive dependencies when co
|
|
|
52
52
|
sourcecode . --dependencies
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
Include an internal module graph with imports and simple structural relations:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
sourcecode . --graph-modules
|
|
59
|
+
```
|
|
60
|
+
|
|
55
61
|
Show the version:
|
|
56
62
|
|
|
57
63
|
```bash
|
|
@@ -64,6 +70,7 @@ Main options:
|
|
|
64
70
|
- `--output PATH`: write to a file instead of `stdout`.
|
|
65
71
|
- `--compact`: return a reduced view with `schema_version`, `project_type`, `stacks`, `entry_points`, and `file_tree_depth1`.
|
|
66
72
|
- `--dependencies`: include direct dependencies, resolved versions, and transitive relationships when lockfiles make that possible.
|
|
73
|
+
- `--graph-modules`: include a structural graph with modules, imports, and simple relations such as `contains`, `calls`, or `extends`.
|
|
67
74
|
- `--depth INTEGER`: maximum file tree depth.
|
|
68
75
|
- `--no-redact`: disable secret redaction.
|
|
69
76
|
|
|
@@ -161,6 +168,7 @@ The full schema includes:
|
|
|
161
168
|
- `entry_points`: detected entry points by stack.
|
|
162
169
|
- `dependencies`: optional dependency records with declared and resolved versions.
|
|
163
170
|
- `dependency_summary`: optional summary with ecosystem coverage, counts, and known limitations.
|
|
171
|
+
- `module_graph`: optional structural graph with nodes, edges, and analysis limits.
|
|
164
172
|
|
|
165
173
|
Example dependency block:
|
|
166
174
|
|
|
@@ -204,6 +212,61 @@ Example dependency block:
|
|
|
204
212
|
|
|
205
213
|
Dependency analysis is still offline and conservative: if a lockfile does not expose a reliable transitive graph, `sourcecode` reports direct dependencies and records the limitation instead of guessing.
|
|
206
214
|
|
|
215
|
+
Example module graph block:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"module_graph": {
|
|
220
|
+
"nodes": [
|
|
221
|
+
{
|
|
222
|
+
"id": "module:app/main.py",
|
|
223
|
+
"kind": "module",
|
|
224
|
+
"language": "python",
|
|
225
|
+
"path": "app/main.py",
|
|
226
|
+
"symbol": null,
|
|
227
|
+
"display_name": "app/main.py",
|
|
228
|
+
"workspace": null
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"id": "function:app/main.py:run",
|
|
232
|
+
"kind": "function",
|
|
233
|
+
"language": "python",
|
|
234
|
+
"path": "app/main.py",
|
|
235
|
+
"symbol": "run",
|
|
236
|
+
"display_name": "run",
|
|
237
|
+
"workspace": null
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"edges": [
|
|
241
|
+
{
|
|
242
|
+
"source": "module:app/main.py",
|
|
243
|
+
"target": "module:app/utils.py",
|
|
244
|
+
"kind": "imports",
|
|
245
|
+
"confidence": "high",
|
|
246
|
+
"method": "ast"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"source": "function:app/main.py:run",
|
|
250
|
+
"target": "function:app/utils.py:helper",
|
|
251
|
+
"kind": "calls",
|
|
252
|
+
"confidence": "medium",
|
|
253
|
+
"method": "ast"
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"summary": {
|
|
257
|
+
"requested": true,
|
|
258
|
+
"node_count": 2,
|
|
259
|
+
"edge_count": 2,
|
|
260
|
+
"languages": ["python"],
|
|
261
|
+
"methods": ["ast"],
|
|
262
|
+
"limitations": []
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Graph analysis is also offline and conservative. `sourcecode` prefers partial but defensible edges over pretending to build a perfect semantic call graph, and it records parse failures, unresolved imports, or analysis budgets in `module_graph.summary.limitations`.
|
|
269
|
+
|
|
207
270
|
Detailed reference: [docs/schema.md](/Users/user/Documents/workspace/atlas/atlas-cli/docs/schema.md).
|
|
208
271
|
|
|
209
272
|
## Development
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=iBHGXEsmbN21QO7wvQ3ZZhBy96xjvWrfacPsjTSGcds,99
|
|
2
2
|
sourcecode/classifier.py,sha256=7ubGTn5vfw9z-9eosLIYDtQUu7d8dvB4XKKDxysUVSM,6910
|
|
3
|
-
sourcecode/cli.py,sha256=
|
|
3
|
+
sourcecode/cli.py,sha256=NysuO95KIxpBEpRiwor93_WFvdMvjk0ArDRv8KPX440,10136
|
|
4
4
|
sourcecode/dependency_analyzer.py,sha256=c8tSbUpug-2avKNWCP4-_e2_uhBwWl96LwL1Be8O90M,39067
|
|
5
|
+
sourcecode/graph_analyzer.py,sha256=hskmEr-c5z3HjWr2S_2ccaVSMrpikYdMY-ExbAiaK60,29353
|
|
5
6
|
sourcecode/redactor.py,sha256=abSf5jH_IzYzpjF3lEN6hCwdXxvmyHs-3DHz9fd8Mic,2883
|
|
6
7
|
sourcecode/scanner.py,sha256=TvcuD77m_NF8OlTj3XH1b_jO0qrto8c90fND96ecqb0,6182
|
|
7
|
-
sourcecode/schema.py,sha256=
|
|
8
|
+
sourcecode/schema.py,sha256=h5uemXKCrPVpEslO2iM_Kw3ax-4UmpOpkfK50HgTNo4,4517
|
|
8
9
|
sourcecode/serializer.py,sha256=hezTYOAAvZ2yPn7Te7Eu42ajbMhVCsUkuUZtqx4wjAs,3085
|
|
9
10
|
sourcecode/tree_utils.py,sha256=5WHQc2L-AGqxKFfYYcQftE6hETxgUxP9MWDuaThcAUw,991
|
|
10
11
|
sourcecode/workspace.py,sha256=1L4xH38gU89fLeMYSuF3ft7lSdCyTfMfJ9NQt5q4Ric,6904
|
|
@@ -27,7 +28,7 @@ sourcecode/detectors/rust.py,sha256=iUz9lE4HfBy8CBaDwGhfh6kLsNIovrH2ay2v3y6qNDc,
|
|
|
27
28
|
sourcecode/detectors/systems.py,sha256=qf5M9tio1H6XDorSstTWIxKe0xVLR_QGTJpFGyhDz4A,1690
|
|
28
29
|
sourcecode/detectors/terraform.py,sha256=5EOKQtTViWo7u3tFK0h45YD8GQPwhrww0cOicxGPPn4,1732
|
|
29
30
|
sourcecode/detectors/tooling.py,sha256=wPvudLCJZ8_cr9GIx44C8jkLAlEN9uTUZcJsP7eVYnQ,1937
|
|
30
|
-
sourcecode-0.
|
|
31
|
-
sourcecode-0.
|
|
32
|
-
sourcecode-0.
|
|
33
|
-
sourcecode-0.
|
|
31
|
+
sourcecode-0.4.0.dist-info/METADATA,sha256=gj14EtCMRfrSsiMpfMN1E2AaRObCQCU3Z_Io7Xpx_b4,7339
|
|
32
|
+
sourcecode-0.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
33
|
+
sourcecode-0.4.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
34
|
+
sourcecode-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|