graphlens-go 0.5.0__tar.gz
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.
- graphlens_go-0.5.0/PKG-INFO +8 -0
- graphlens_go-0.5.0/pyproject.toml +38 -0
- graphlens_go-0.5.0/src/graphlens_go/__init__.py +6 -0
- graphlens_go-0.5.0/src/graphlens_go/_adapter.py +471 -0
- graphlens_go-0.5.0/src/graphlens_go/_boundary.py +297 -0
- graphlens_go-0.5.0/src/graphlens_go/_deps.py +80 -0
- graphlens_go-0.5.0/src/graphlens_go/_project_detector.py +42 -0
- graphlens_go-0.5.0/src/graphlens_go/_queries.py +26 -0
- graphlens_go-0.5.0/src/graphlens_go/_resolver.py +456 -0
- graphlens_go-0.5.0/src/graphlens_go/_visitor.py +352 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "graphlens-go"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Go language adapter for graphlens"
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"graphlens",
|
|
8
|
+
"tree-sitter>=0.24",
|
|
9
|
+
"tree-sitter-go>=0.23",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["uv_build>=0.9.18,<0.12.0"]
|
|
14
|
+
build-backend = "uv_build"
|
|
15
|
+
|
|
16
|
+
[tool.uv.sources]
|
|
17
|
+
graphlens = { workspace = true }
|
|
18
|
+
|
|
19
|
+
[project.entry-points."graphlens.adapters"]
|
|
20
|
+
go = "graphlens_go:GoAdapter"
|
|
21
|
+
|
|
22
|
+
[tool.bandit]
|
|
23
|
+
skips = ["B101", "B404", "B603"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[tool.coverage.run]
|
|
29
|
+
source = ["graphlens_go"]
|
|
30
|
+
|
|
31
|
+
[tool.coverage.report]
|
|
32
|
+
fail_under = 100
|
|
33
|
+
show_missing = true
|
|
34
|
+
exclude_lines = [
|
|
35
|
+
"pragma: no cover",
|
|
36
|
+
"if TYPE_CHECKING:",
|
|
37
|
+
"\\.\\.\\.",
|
|
38
|
+
]
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""GoAdapter — orchestrates structural analysis of Go projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from functools import partial
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from graphlens import (
|
|
11
|
+
RESOLVER_STATUS_KEY,
|
|
12
|
+
AdapterError,
|
|
13
|
+
BoundaryRef,
|
|
14
|
+
GraphLens,
|
|
15
|
+
LanguageAdapter,
|
|
16
|
+
Node,
|
|
17
|
+
NodeKind,
|
|
18
|
+
Relation,
|
|
19
|
+
RelationKind,
|
|
20
|
+
ResolverStatus,
|
|
21
|
+
make_boundary_id,
|
|
22
|
+
)
|
|
23
|
+
from graphlens.utils import SpanIndex, make_node_id
|
|
24
|
+
from graphlens.utils.roots import filter_nested_root_files
|
|
25
|
+
|
|
26
|
+
from graphlens_go._boundary import (
|
|
27
|
+
GO_DEFAULT_BOUNDARY_EXTRACTORS,
|
|
28
|
+
GoBoundaryExtractor,
|
|
29
|
+
)
|
|
30
|
+
from graphlens_go._deps import (
|
|
31
|
+
GO_DEFAULT_DEP_PARSERS,
|
|
32
|
+
classify_go_import,
|
|
33
|
+
read_module_path,
|
|
34
|
+
)
|
|
35
|
+
from graphlens_go._project_detector import find_go_roots, is_go_project
|
|
36
|
+
from graphlens_go._resolver import GoResolver
|
|
37
|
+
from graphlens_go._visitor import (
|
|
38
|
+
GoFileContext,
|
|
39
|
+
GoStructureExtractor,
|
|
40
|
+
parse_go,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from graphlens.contracts import DependencyFileParser, SymbolResolver
|
|
45
|
+
from tree_sitter import Node as TSNode
|
|
46
|
+
|
|
47
|
+
from graphlens_go._visitor import OccurrenceRef
|
|
48
|
+
|
|
49
|
+
# Occurrence role -> the edge kind the resolution pass emits for it.
|
|
50
|
+
_ROLE_TO_KIND = {
|
|
51
|
+
"call": RelationKind.CALLS,
|
|
52
|
+
"base": RelationKind.INHERITS_FROM,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger("graphlens_go")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GoAdapter(LanguageAdapter):
|
|
59
|
+
"""Language adapter for Go projects (structure + imports)."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
dep_parsers: list[DependencyFileParser] | None = None,
|
|
64
|
+
resolver: SymbolResolver | None = None,
|
|
65
|
+
boundary_extractors: list[GoBoundaryExtractor] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Initialise with optional custom dep parsers and resolver."""
|
|
68
|
+
self._dep_parsers = (
|
|
69
|
+
dep_parsers
|
|
70
|
+
if dep_parsers is not None
|
|
71
|
+
else GO_DEFAULT_DEP_PARSERS
|
|
72
|
+
)
|
|
73
|
+
self._resolver = resolver if resolver is not None else GoResolver()
|
|
74
|
+
self._boundary_extractors = (
|
|
75
|
+
boundary_extractors
|
|
76
|
+
if boundary_extractors is not None
|
|
77
|
+
else GO_DEFAULT_BOUNDARY_EXTRACTORS
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def language(self) -> str:
|
|
81
|
+
return "go"
|
|
82
|
+
|
|
83
|
+
def file_extensions(self) -> set[str]:
|
|
84
|
+
return {".go"}
|
|
85
|
+
|
|
86
|
+
def can_handle(self, project_root: str | Path) -> bool:
|
|
87
|
+
return is_go_project(Path(project_root))
|
|
88
|
+
|
|
89
|
+
def analyze(
|
|
90
|
+
self,
|
|
91
|
+
project_root: str | Path,
|
|
92
|
+
files: list[Path] | None = None,
|
|
93
|
+
*,
|
|
94
|
+
strict: bool = False,
|
|
95
|
+
) -> GraphLens:
|
|
96
|
+
project_root = Path(project_root).resolve()
|
|
97
|
+
graph = GraphLens()
|
|
98
|
+
statuses: list[ResolverStatus] = []
|
|
99
|
+
|
|
100
|
+
if files is not None:
|
|
101
|
+
_analyze_root(
|
|
102
|
+
graph,
|
|
103
|
+
project_root,
|
|
104
|
+
project_root,
|
|
105
|
+
files,
|
|
106
|
+
self._dep_parsers,
|
|
107
|
+
self._resolver,
|
|
108
|
+
self._boundary_extractors,
|
|
109
|
+
)
|
|
110
|
+
statuses.append(self._resolver.status())
|
|
111
|
+
else:
|
|
112
|
+
roots = find_go_roots(project_root)
|
|
113
|
+
for go_root in roots:
|
|
114
|
+
root_files = filter_nested_root_files(
|
|
115
|
+
self.collect_files(go_root),
|
|
116
|
+
go_root,
|
|
117
|
+
roots,
|
|
118
|
+
)
|
|
119
|
+
_analyze_root(
|
|
120
|
+
graph,
|
|
121
|
+
project_root,
|
|
122
|
+
go_root,
|
|
123
|
+
root_files,
|
|
124
|
+
self._dep_parsers,
|
|
125
|
+
self._resolver,
|
|
126
|
+
self._boundary_extractors,
|
|
127
|
+
)
|
|
128
|
+
statuses.append(self._resolver.status())
|
|
129
|
+
|
|
130
|
+
status = ResolverStatus.combine(statuses)
|
|
131
|
+
graph.metadata[RESOLVER_STATUS_KEY] = status.value
|
|
132
|
+
if strict and status is not ResolverStatus.OK:
|
|
133
|
+
msg = (
|
|
134
|
+
f"Go resolver status is '{status.value}'; refusing to "
|
|
135
|
+
"return a degraded graph in strict mode"
|
|
136
|
+
)
|
|
137
|
+
raise AdapterError(msg)
|
|
138
|
+
return graph
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _analyze_root( # noqa: PLR0913
|
|
142
|
+
graph: GraphLens,
|
|
143
|
+
project_root: Path,
|
|
144
|
+
go_root: Path,
|
|
145
|
+
files: list[Path],
|
|
146
|
+
dep_parsers: list[DependencyFileParser],
|
|
147
|
+
resolver: SymbolResolver,
|
|
148
|
+
boundary_extractors: list[GoBoundaryExtractor],
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Analyse one Go module root and populate ``graph`` in place."""
|
|
151
|
+
module_path = read_module_path(go_root) or go_root.name
|
|
152
|
+
project_name = module_path.rstrip("/").split("/")[-1]
|
|
153
|
+
|
|
154
|
+
required: set[str] = set()
|
|
155
|
+
for parser in dep_parsers:
|
|
156
|
+
if parser.can_parse(go_root):
|
|
157
|
+
required |= parser.parse(go_root)
|
|
158
|
+
classify = partial(
|
|
159
|
+
classify_go_import, module_path=module_path, required=required
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
project_id = make_node_id(
|
|
163
|
+
project_name, module_path, NodeKind.PROJECT.value
|
|
164
|
+
)
|
|
165
|
+
if project_id not in graph.nodes:
|
|
166
|
+
graph.add_node(
|
|
167
|
+
Node(
|
|
168
|
+
id=project_id,
|
|
169
|
+
kind=NodeKind.PROJECT,
|
|
170
|
+
qualified_name=module_path,
|
|
171
|
+
name=project_name,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
packages: dict[str, str] = {}
|
|
176
|
+
parsed_files: list[tuple[str, str, TSNode]] = []
|
|
177
|
+
occurrences: list[tuple[str, OccurrenceRef]] = []
|
|
178
|
+
internal_imports: list[tuple[str, str]] = []
|
|
179
|
+
for file in files:
|
|
180
|
+
pkg_qname = _package_qname(file, go_root, module_path)
|
|
181
|
+
module_id = _ensure_package(
|
|
182
|
+
graph, project_name, pkg_qname, project_id, packages
|
|
183
|
+
)
|
|
184
|
+
file_id = _ensure_file(
|
|
185
|
+
graph, project_name, project_root, go_root, file, module_id
|
|
186
|
+
)
|
|
187
|
+
try:
|
|
188
|
+
source = file.read_bytes()
|
|
189
|
+
except OSError as exc:
|
|
190
|
+
logger.warning("Cannot read %s: %s — skipping", file, exc)
|
|
191
|
+
continue
|
|
192
|
+
file_rel = graph.nodes[file_id].qualified_name
|
|
193
|
+
ctx = GoFileContext(
|
|
194
|
+
project_name=project_name,
|
|
195
|
+
package_qname=pkg_qname,
|
|
196
|
+
file_id=file_id,
|
|
197
|
+
file_rel=file_rel,
|
|
198
|
+
)
|
|
199
|
+
root = parse_go(source).root_node
|
|
200
|
+
extractor = GoStructureExtractor(graph, ctx, classify)
|
|
201
|
+
extractor.extract(root)
|
|
202
|
+
parsed_files.append((file_rel, file_id, root))
|
|
203
|
+
occurrences.extend(
|
|
204
|
+
(str(file), occ) for occ in extractor.occurrences
|
|
205
|
+
)
|
|
206
|
+
internal_imports.extend(extractor.internal_imports)
|
|
207
|
+
|
|
208
|
+
# Bind internal imports to the MODULE node they reference (now that every
|
|
209
|
+
# package exists), falling back to an EXTERNAL_SYMBOL when none matches.
|
|
210
|
+
_resolve_internal_imports(graph, project_name, internal_imports, packages)
|
|
211
|
+
|
|
212
|
+
# Resolution pass: bind occurrences to real nodes or EXTERNAL_SYMBOL.
|
|
213
|
+
span_index = SpanIndex.from_graph(graph)
|
|
214
|
+
resolver.prepare(go_root, files)
|
|
215
|
+
_resolve_occurrences(
|
|
216
|
+
graph, project_name, project_root, resolver, span_index, occurrences
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
_extract_boundaries(graph, parsed_files, boundary_extractors)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _resolve_internal_imports(
|
|
223
|
+
graph: GraphLens,
|
|
224
|
+
project_name: str,
|
|
225
|
+
internal_imports: list[tuple[str, str]],
|
|
226
|
+
packages: dict[str, str],
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Resolve each ``internal`` import to its MODULE node (per CLAUDE.md §9).
|
|
230
|
+
|
|
231
|
+
A Go import path of an internal package equals that package's qualified
|
|
232
|
+
name, so a direct lookup binds ``RESOLVES_TO`` to the real MODULE node.
|
|
233
|
+
Imports whose package was not analyzed fall back to an EXTERNAL_SYMBOL so
|
|
234
|
+
the edge is never missing.
|
|
235
|
+
"""
|
|
236
|
+
for imp_id, import_path in internal_imports:
|
|
237
|
+
target_id = packages.get(import_path)
|
|
238
|
+
if target_id is None:
|
|
239
|
+
target_id = _ensure_external_symbol(
|
|
240
|
+
graph, project_name, import_path, "internal"
|
|
241
|
+
)
|
|
242
|
+
graph.add_relation(
|
|
243
|
+
Relation(imp_id, target_id, RelationKind.RESOLVES_TO)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _ensure_external_symbol(
|
|
248
|
+
graph: GraphLens, project_name: str, qname: str, origin: str
|
|
249
|
+
) -> str:
|
|
250
|
+
"""Return the id of an EXTERNAL_SYMBOL node, creating it if needed."""
|
|
251
|
+
sym_id = make_node_id(
|
|
252
|
+
project_name, qname, NodeKind.EXTERNAL_SYMBOL.value
|
|
253
|
+
)
|
|
254
|
+
if sym_id not in graph.nodes:
|
|
255
|
+
graph.add_node(
|
|
256
|
+
Node(
|
|
257
|
+
id=sym_id,
|
|
258
|
+
kind=NodeKind.EXTERNAL_SYMBOL,
|
|
259
|
+
qualified_name=qname,
|
|
260
|
+
name=qname.rsplit(".", maxsplit=1)[-1],
|
|
261
|
+
metadata={"origin": origin},
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
return sym_id
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _relative_to(file_path: Path, project_root: Path) -> str:
|
|
268
|
+
"""
|
|
269
|
+
Map an absolute resolver path to the graph's project-relative form.
|
|
270
|
+
|
|
271
|
+
The resolver (gopls) returns absolute paths, but graph nodes store
|
|
272
|
+
file paths relative to ``project_root``; reconcile them so SpanIndex
|
|
273
|
+
lookups hit. Falls back to the absolute string for paths outside the
|
|
274
|
+
project (which then resolve to an EXTERNAL_SYMBOL).
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
return str(file_path.resolve().relative_to(project_root))
|
|
278
|
+
except (ValueError, OSError):
|
|
279
|
+
return str(file_path)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _resolve_occurrences( # noqa: PLR0913
|
|
283
|
+
graph: GraphLens,
|
|
284
|
+
project_name: str,
|
|
285
|
+
project_root: Path,
|
|
286
|
+
resolver: SymbolResolver,
|
|
287
|
+
span_index: SpanIndex,
|
|
288
|
+
occurrences: list[tuple[str, OccurrenceRef]],
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Resolve each occurrence to a definition node and emit its edge."""
|
|
291
|
+
for abs_path, occ in occurrences:
|
|
292
|
+
rel_kind = _ROLE_TO_KIND[occ.role]
|
|
293
|
+
ref = resolver.definition_at(Path(abs_path), occ.line, occ.col)
|
|
294
|
+
if ref is None:
|
|
295
|
+
continue
|
|
296
|
+
target_id: str | None = None
|
|
297
|
+
if ref.origin == "internal" and ref.file_path is not None:
|
|
298
|
+
target_id = span_index.at(
|
|
299
|
+
_relative_to(ref.file_path, project_root),
|
|
300
|
+
ref.line,
|
|
301
|
+
ref.col,
|
|
302
|
+
)
|
|
303
|
+
if target_id is None:
|
|
304
|
+
fallback_qname = (
|
|
305
|
+
ref.full_name
|
|
306
|
+
if ref.full_name
|
|
307
|
+
else f"{occ.role}@{occ.line}:{occ.col}"
|
|
308
|
+
)
|
|
309
|
+
target_id = _ensure_external_symbol(
|
|
310
|
+
graph, project_name, fallback_qname, ref.origin
|
|
311
|
+
)
|
|
312
|
+
graph.add_relation(
|
|
313
|
+
Relation(
|
|
314
|
+
source_id=occ.enclosing_id,
|
|
315
|
+
target_id=target_id,
|
|
316
|
+
kind=rel_kind,
|
|
317
|
+
metadata={"span": occ.span},
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _extract_boundaries(
|
|
323
|
+
graph: GraphLens,
|
|
324
|
+
parsed_files: list[tuple[str, str, TSNode]],
|
|
325
|
+
extractors: list[GoBoundaryExtractor],
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Run boundary extractors and emit BOUNDARY nodes + EXPOSES/CONSUMES."""
|
|
328
|
+
if not extractors:
|
|
329
|
+
return
|
|
330
|
+
enclosers: dict[str, list[Node]] = {}
|
|
331
|
+
for node in graph.nodes.values():
|
|
332
|
+
if (
|
|
333
|
+
node.kind in (NodeKind.FUNCTION, NodeKind.METHOD)
|
|
334
|
+
and node.span is not None
|
|
335
|
+
and node.file_path is not None
|
|
336
|
+
):
|
|
337
|
+
enclosers.setdefault(node.file_path, []).append(node)
|
|
338
|
+
|
|
339
|
+
for file_rel, file_id, root in parsed_files:
|
|
340
|
+
candidates = enclosers.get(file_rel, [])
|
|
341
|
+
for extractor in extractors:
|
|
342
|
+
for ref in extractor.extract(root):
|
|
343
|
+
enclosing_id = (
|
|
344
|
+
_innermost_enclosing(candidates, ref.line, ref.col)
|
|
345
|
+
or file_id
|
|
346
|
+
)
|
|
347
|
+
_add_boundary(graph, enclosing_id, ref)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _innermost_enclosing(
|
|
351
|
+
candidates: list[Node], line: int, col: int
|
|
352
|
+
) -> str | None:
|
|
353
|
+
"""Return the id of the deepest function/method containing (line, col)."""
|
|
354
|
+
best_id: str | None = None
|
|
355
|
+
best_start: tuple[int, int] | None = None
|
|
356
|
+
for node in candidates:
|
|
357
|
+
span = node.span
|
|
358
|
+
if span is None:
|
|
359
|
+
continue # pragma: no cover - filtered before insertion
|
|
360
|
+
start = (span.start_line, span.start_col)
|
|
361
|
+
end = (span.end_line, span.end_col)
|
|
362
|
+
if start <= (line, col) <= end and (
|
|
363
|
+
best_start is None or start >= best_start
|
|
364
|
+
):
|
|
365
|
+
best_id = node.id
|
|
366
|
+
best_start = start
|
|
367
|
+
return best_id
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _add_boundary(
|
|
371
|
+
graph: GraphLens, enclosing_id: str, ref: BoundaryRef
|
|
372
|
+
) -> None:
|
|
373
|
+
boundary_id = make_boundary_id(ref.mechanism, ref.key)
|
|
374
|
+
if boundary_id not in graph.nodes:
|
|
375
|
+
graph.add_node(
|
|
376
|
+
Node(
|
|
377
|
+
id=boundary_id,
|
|
378
|
+
kind=NodeKind.BOUNDARY,
|
|
379
|
+
qualified_name=f"{ref.mechanism}:{ref.key}",
|
|
380
|
+
name=ref.key,
|
|
381
|
+
metadata={"mechanism": ref.mechanism, "key": ref.key},
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
kind = (
|
|
385
|
+
RelationKind.EXPOSES
|
|
386
|
+
if ref.role == "server"
|
|
387
|
+
else RelationKind.CONSUMES
|
|
388
|
+
)
|
|
389
|
+
metadata: dict[str, object] = {
|
|
390
|
+
"mechanism": ref.mechanism,
|
|
391
|
+
"key": ref.key,
|
|
392
|
+
"confidence": ref.confidence,
|
|
393
|
+
"role": ref.role,
|
|
394
|
+
"line": ref.line,
|
|
395
|
+
"col": ref.col,
|
|
396
|
+
}
|
|
397
|
+
metadata.update(ref.detail)
|
|
398
|
+
graph.add_relation(
|
|
399
|
+
Relation(
|
|
400
|
+
source_id=enclosing_id,
|
|
401
|
+
target_id=boundary_id,
|
|
402
|
+
kind=kind,
|
|
403
|
+
metadata=metadata,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _package_qname(file: Path, go_root: Path, module_path: str) -> str:
|
|
409
|
+
try:
|
|
410
|
+
rel_dir = file.parent.relative_to(go_root)
|
|
411
|
+
except ValueError:
|
|
412
|
+
# A file passed explicitly (incremental update) that lives outside
|
|
413
|
+
# the module root — treat it as the root package rather than crash.
|
|
414
|
+
return module_path
|
|
415
|
+
if str(rel_dir) == ".":
|
|
416
|
+
return module_path
|
|
417
|
+
return f"{module_path}/{rel_dir.as_posix()}"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _ensure_package(
|
|
421
|
+
graph: GraphLens,
|
|
422
|
+
project_name: str,
|
|
423
|
+
pkg_qname: str,
|
|
424
|
+
project_id: str,
|
|
425
|
+
packages: dict[str, str],
|
|
426
|
+
) -> str:
|
|
427
|
+
if pkg_qname in packages:
|
|
428
|
+
return packages[pkg_qname]
|
|
429
|
+
module_id = make_node_id(project_name, pkg_qname, NodeKind.MODULE.value)
|
|
430
|
+
graph.add_node(
|
|
431
|
+
Node(
|
|
432
|
+
id=module_id,
|
|
433
|
+
kind=NodeKind.MODULE,
|
|
434
|
+
qualified_name=pkg_qname,
|
|
435
|
+
name=pkg_qname.rsplit("/", maxsplit=1)[-1],
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
graph.add_relation(
|
|
439
|
+
Relation(project_id, module_id, RelationKind.CONTAINS)
|
|
440
|
+
)
|
|
441
|
+
packages[pkg_qname] = module_id
|
|
442
|
+
return module_id
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _ensure_file( # noqa: PLR0913
|
|
446
|
+
graph: GraphLens,
|
|
447
|
+
project_name: str,
|
|
448
|
+
project_root: Path,
|
|
449
|
+
go_root: Path,
|
|
450
|
+
file: Path,
|
|
451
|
+
module_id: str,
|
|
452
|
+
) -> str:
|
|
453
|
+
try:
|
|
454
|
+
file_rel = str(file.relative_to(project_root))
|
|
455
|
+
except ValueError:
|
|
456
|
+
file_rel = str(file.relative_to(go_root))
|
|
457
|
+
file_id = make_node_id(project_name, file_rel, NodeKind.FILE.value)
|
|
458
|
+
if file_id not in graph.nodes:
|
|
459
|
+
graph.add_node(
|
|
460
|
+
Node(
|
|
461
|
+
id=file_id,
|
|
462
|
+
kind=NodeKind.FILE,
|
|
463
|
+
qualified_name=file_rel,
|
|
464
|
+
name=file.name,
|
|
465
|
+
file_path=file_rel,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
graph.add_relation(
|
|
469
|
+
Relation(module_id, file_id, RelationKind.CONTAINS)
|
|
470
|
+
)
|
|
471
|
+
return file_id
|