codebeacon 0.1.2__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.
- codebeacon/__init__.py +1 -0
- codebeacon/__main__.py +3 -0
- codebeacon/cache.py +136 -0
- codebeacon/cli.py +391 -0
- codebeacon/common/__init__.py +0 -0
- codebeacon/common/filters.py +170 -0
- codebeacon/common/symbols.py +121 -0
- codebeacon/common/types.py +98 -0
- codebeacon/config.py +144 -0
- codebeacon/contextmap/__init__.py +0 -0
- codebeacon/contextmap/generator.py +602 -0
- codebeacon/discover/__init__.py +0 -0
- codebeacon/discover/detector.py +388 -0
- codebeacon/discover/scanner.py +192 -0
- codebeacon/export/__init__.py +0 -0
- codebeacon/export/mcp.py +515 -0
- codebeacon/export/obsidian.py +812 -0
- codebeacon/extract/__init__.py +22 -0
- codebeacon/extract/base.py +372 -0
- codebeacon/extract/components.py +357 -0
- codebeacon/extract/dependencies.py +140 -0
- codebeacon/extract/entities.py +575 -0
- codebeacon/extract/queries/README.md +116 -0
- codebeacon/extract/queries/actix.scm +115 -0
- codebeacon/extract/queries/angular.scm +155 -0
- codebeacon/extract/queries/aspnet.scm +159 -0
- codebeacon/extract/queries/django.scm +122 -0
- codebeacon/extract/queries/express.scm +124 -0
- codebeacon/extract/queries/fastapi.scm +152 -0
- codebeacon/extract/queries/flask.scm +120 -0
- codebeacon/extract/queries/gin.scm +142 -0
- codebeacon/extract/queries/ktor.scm +144 -0
- codebeacon/extract/queries/laravel.scm +172 -0
- codebeacon/extract/queries/nestjs.scm +183 -0
- codebeacon/extract/queries/rails.scm +114 -0
- codebeacon/extract/queries/react.scm +111 -0
- codebeacon/extract/queries/spring_boot.scm +204 -0
- codebeacon/extract/queries/svelte.scm +73 -0
- codebeacon/extract/queries/vapor.scm +130 -0
- codebeacon/extract/queries/vue.scm +123 -0
- codebeacon/extract/routes.py +910 -0
- codebeacon/extract/semantic.py +280 -0
- codebeacon/extract/services.py +597 -0
- codebeacon/graph/__init__.py +1 -0
- codebeacon/graph/analyze.py +281 -0
- codebeacon/graph/build.py +320 -0
- codebeacon/graph/cluster.py +160 -0
- codebeacon/graph/enrich.py +206 -0
- codebeacon/skill/SKILL.md +127 -0
- codebeacon/wave.py +292 -0
- codebeacon/wiki/__init__.py +0 -0
- codebeacon/wiki/generator.py +376 -0
- codebeacon/wiki/index.py +95 -0
- codebeacon/wiki/templates.py +467 -0
- codebeacon-0.1.2.dist-info/METADATA +319 -0
- codebeacon-0.1.2.dist-info/RECORD +59 -0
- codebeacon-0.1.2.dist-info/WHEEL +4 -0
- codebeacon-0.1.2.dist-info/entry_points.txt +2 -0
- codebeacon-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Frontend component extraction for React, Vue, Svelte, Angular.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
extract_components(file_path, framework, project_path="") -> list[ComponentInfo]
|
|
5
|
+
|
|
6
|
+
Extracts:
|
|
7
|
+
- React: uppercase function/arrow components, hooks, props
|
|
8
|
+
- Vue: defineComponent / SFC, composables, props
|
|
9
|
+
- Svelte: SFC files, exported props, runes
|
|
10
|
+
- Angular: @Component class, selector, templateUrl
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from codebeacon.common.types import ComponentInfo
|
|
17
|
+
from codebeacon.extract.base import (
|
|
18
|
+
extract_sfc_sections,
|
|
19
|
+
load_query_file,
|
|
20
|
+
node_text,
|
|
21
|
+
parse_file,
|
|
22
|
+
parse_sfc_script,
|
|
23
|
+
run_query,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── Framework → query file stem ───────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
_FW_TO_QUERY: dict[str, str] = {
|
|
30
|
+
"react": "react",
|
|
31
|
+
"nextjs": "react",
|
|
32
|
+
"vue": "vue",
|
|
33
|
+
"nuxt": "vue",
|
|
34
|
+
"sveltekit": "svelte",
|
|
35
|
+
"angular": "angular",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Public function ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def extract_components(
|
|
42
|
+
file_path: str,
|
|
43
|
+
framework: str,
|
|
44
|
+
project_path: str = "",
|
|
45
|
+
) -> list[ComponentInfo]:
|
|
46
|
+
"""Extract frontend component declarations from *file_path*."""
|
|
47
|
+
fw = framework.lower()
|
|
48
|
+
query_name = _FW_TO_QUERY.get(fw)
|
|
49
|
+
if not query_name:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
query_src = load_query_file(query_name)
|
|
53
|
+
if not query_src:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
# SFC dispatch
|
|
57
|
+
ext = Path(file_path).suffix.lower()
|
|
58
|
+
if ext in (".vue", ".svelte"):
|
|
59
|
+
sfc = extract_sfc_sections(file_path)
|
|
60
|
+
if sfc is None:
|
|
61
|
+
return []
|
|
62
|
+
parsed = parse_sfc_script(sfc)
|
|
63
|
+
else:
|
|
64
|
+
parsed = parse_file(file_path)
|
|
65
|
+
|
|
66
|
+
if parsed is None:
|
|
67
|
+
return []
|
|
68
|
+
root, lang = parsed
|
|
69
|
+
|
|
70
|
+
from codebeacon.extract.base import is_grammar_allowed
|
|
71
|
+
if not is_grammar_allowed(query_name, lang):
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
matches = run_query(lang, query_src, root)
|
|
76
|
+
except Exception:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
_interpreters = {
|
|
80
|
+
"react": _interpret_react,
|
|
81
|
+
"vue": _interpret_vue,
|
|
82
|
+
"svelte": _interpret_svelte,
|
|
83
|
+
"angular": _interpret_angular,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interpreter = _interpreters.get(query_name)
|
|
87
|
+
if interpreter is None:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
components = interpreter(file_path, matches, fw)
|
|
92
|
+
except Exception:
|
|
93
|
+
components = []
|
|
94
|
+
|
|
95
|
+
# For SFC files, ensure at least one component with the filename as name
|
|
96
|
+
if ext in (".vue", ".svelte") and not components:
|
|
97
|
+
stem = Path(file_path).stem
|
|
98
|
+
components = [ComponentInfo(
|
|
99
|
+
name=stem,
|
|
100
|
+
source_file=file_path,
|
|
101
|
+
line=1,
|
|
102
|
+
framework=fw,
|
|
103
|
+
)]
|
|
104
|
+
|
|
105
|
+
# Derive route info for page components
|
|
106
|
+
if project_path:
|
|
107
|
+
_annotate_page_routes(components, file_path, fw, project_path)
|
|
108
|
+
|
|
109
|
+
return components
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def _annotate_page_routes(
|
|
115
|
+
components: list[ComponentInfo],
|
|
116
|
+
file_path: str,
|
|
117
|
+
framework: str,
|
|
118
|
+
project_path: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Mark components as page components and set route_path for file-system routed frameworks."""
|
|
121
|
+
try:
|
|
122
|
+
rel = Path(file_path).relative_to(Path(project_path))
|
|
123
|
+
except ValueError:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
parts = rel.parts
|
|
127
|
+
is_page = False
|
|
128
|
+
route_path = ""
|
|
129
|
+
|
|
130
|
+
if framework in ("nextjs", "react"):
|
|
131
|
+
if parts and parts[0] == "pages":
|
|
132
|
+
is_page = True
|
|
133
|
+
elif len(parts) >= 2 and parts[0] == "app":
|
|
134
|
+
stem = Path(parts[-1]).stem
|
|
135
|
+
if stem in ("page", "layout", "route"):
|
|
136
|
+
is_page = True
|
|
137
|
+
elif framework == "nuxt":
|
|
138
|
+
if parts and parts[0] == "pages":
|
|
139
|
+
is_page = True
|
|
140
|
+
elif framework == "sveltekit":
|
|
141
|
+
if len(parts) >= 3 and parts[0] == "src" and parts[1] == "routes":
|
|
142
|
+
stem = Path(parts[-1]).stem
|
|
143
|
+
if stem.startswith("+"):
|
|
144
|
+
is_page = True
|
|
145
|
+
|
|
146
|
+
if is_page:
|
|
147
|
+
for comp in components:
|
|
148
|
+
comp.is_page = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── Per-framework interpreters ────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def _interpret_react(
|
|
154
|
+
file_path: str, matches: list, framework: str,
|
|
155
|
+
) -> list[ComponentInfo]:
|
|
156
|
+
"""React/Next.js: exported uppercase functions/arrows + hooks + props."""
|
|
157
|
+
components: dict[str, ComponentInfo] = {} # name → ComponentInfo
|
|
158
|
+
hooks: list[str] = []
|
|
159
|
+
props: list[str] = []
|
|
160
|
+
imports: list[str] = []
|
|
161
|
+
|
|
162
|
+
for _idx, caps in matches:
|
|
163
|
+
# Exported function component
|
|
164
|
+
for cap_key in ("component.func_name", "component.arrow_name", "component.memo_name"):
|
|
165
|
+
if cap_key in caps:
|
|
166
|
+
name = node_text(caps[cap_key][0])
|
|
167
|
+
if name and name not in components:
|
|
168
|
+
# Determine line from the parent export/declaration
|
|
169
|
+
parent_key = None
|
|
170
|
+
for k in ("component.export_func", "component.export_func_upper",
|
|
171
|
+
"component.export_default_func", "component.export_arrow",
|
|
172
|
+
"component.local_arrow", "component.hoc"):
|
|
173
|
+
if k in caps:
|
|
174
|
+
parent_key = k
|
|
175
|
+
break
|
|
176
|
+
line = caps[parent_key][0].start_point[0] + 1 if parent_key else 1
|
|
177
|
+
components[name] = ComponentInfo(
|
|
178
|
+
name=name,
|
|
179
|
+
source_file=file_path,
|
|
180
|
+
line=line,
|
|
181
|
+
framework=framework,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Hooks
|
|
185
|
+
if "hook.name" in caps:
|
|
186
|
+
hook_name = node_text(caps["hook.name"][0])
|
|
187
|
+
if hook_name not in hooks:
|
|
188
|
+
hooks.append(hook_name)
|
|
189
|
+
|
|
190
|
+
# Props destructuring
|
|
191
|
+
if "prop.name" in caps:
|
|
192
|
+
for pn in caps["prop.name"]:
|
|
193
|
+
p = node_text(pn)
|
|
194
|
+
if p not in props:
|
|
195
|
+
props.append(p)
|
|
196
|
+
|
|
197
|
+
# Imports (for imported component tracking)
|
|
198
|
+
if "import.path" in caps:
|
|
199
|
+
path = node_text(caps["import.path"][0]).strip("'\"")
|
|
200
|
+
if path not in imports and not path.startswith("."):
|
|
201
|
+
imports.append(path)
|
|
202
|
+
|
|
203
|
+
# Assign file-level hooks/imports to all components.
|
|
204
|
+
# Props are assigned only to the first component (typically the main one).
|
|
205
|
+
comp_list = list(components.values())
|
|
206
|
+
for i, comp in enumerate(comp_list):
|
|
207
|
+
comp.hooks = hooks[:]
|
|
208
|
+
comp.imports = imports[:]
|
|
209
|
+
if i == 0:
|
|
210
|
+
comp.props = props[:]
|
|
211
|
+
|
|
212
|
+
return comp_list
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _interpret_vue(
|
|
216
|
+
file_path: str, matches: list, framework: str,
|
|
217
|
+
) -> list[ComponentInfo]:
|
|
218
|
+
"""Vue: defineComponent / export default class + composables + defineProps."""
|
|
219
|
+
name = ""
|
|
220
|
+
line = 1
|
|
221
|
+
props: list[str] = []
|
|
222
|
+
composables: list[str] = []
|
|
223
|
+
imports: list[str] = []
|
|
224
|
+
|
|
225
|
+
for _idx, caps in matches:
|
|
226
|
+
# defineComponent({ name: "..." })
|
|
227
|
+
if "component.name" in caps:
|
|
228
|
+
name = node_text(caps["component.name"][0])
|
|
229
|
+
if "component.define" in caps:
|
|
230
|
+
line = caps["component.define"][0].start_point[0] + 1
|
|
231
|
+
|
|
232
|
+
# export default class ComponentName
|
|
233
|
+
if "component.class_name" in caps:
|
|
234
|
+
name = node_text(caps["component.class_name"][0])
|
|
235
|
+
if "component.class" in caps:
|
|
236
|
+
line = caps["component.class"][0].start_point[0] + 1
|
|
237
|
+
|
|
238
|
+
# defineComponent without name (anonymous)
|
|
239
|
+
if "component.define_anon" in caps and not name:
|
|
240
|
+
name = Path(file_path).stem
|
|
241
|
+
|
|
242
|
+
# defineProps({ key: ... })
|
|
243
|
+
if "prop.name" in caps:
|
|
244
|
+
for pn in caps["prop.name"]:
|
|
245
|
+
p = node_text(pn)
|
|
246
|
+
if p not in props:
|
|
247
|
+
props.append(p)
|
|
248
|
+
|
|
249
|
+
# Composable usage (useX)
|
|
250
|
+
if "composable.name" in caps:
|
|
251
|
+
c = node_text(caps["composable.name"][0])
|
|
252
|
+
if c not in composables:
|
|
253
|
+
composables.append(c)
|
|
254
|
+
|
|
255
|
+
# Imports
|
|
256
|
+
if "import.path" in caps:
|
|
257
|
+
path = node_text(caps["import.path"][0]).strip("'\"")
|
|
258
|
+
if path not in imports:
|
|
259
|
+
imports.append(path)
|
|
260
|
+
|
|
261
|
+
if not name:
|
|
262
|
+
name = Path(file_path).stem
|
|
263
|
+
|
|
264
|
+
return [ComponentInfo(
|
|
265
|
+
name=name,
|
|
266
|
+
source_file=file_path,
|
|
267
|
+
line=line,
|
|
268
|
+
framework=framework,
|
|
269
|
+
props=props,
|
|
270
|
+
hooks=composables,
|
|
271
|
+
imports=imports,
|
|
272
|
+
)]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _interpret_svelte(
|
|
276
|
+
file_path: str, matches: list, framework: str,
|
|
277
|
+
) -> list[ComponentInfo]:
|
|
278
|
+
"""Svelte: SFC with export let props, runes, stores."""
|
|
279
|
+
name = Path(file_path).stem
|
|
280
|
+
props: list[str] = []
|
|
281
|
+
hooks: list[str] = []
|
|
282
|
+
imports: list[str] = []
|
|
283
|
+
|
|
284
|
+
for _idx, caps in matches:
|
|
285
|
+
# export let prop (Svelte 4)
|
|
286
|
+
if "prop.name" in caps:
|
|
287
|
+
for pn in caps["prop.name"]:
|
|
288
|
+
p = node_text(pn)
|
|
289
|
+
if p not in props:
|
|
290
|
+
props.append(p)
|
|
291
|
+
|
|
292
|
+
# Svelte 5 runes ($state, $derived, etc.)
|
|
293
|
+
if "rune.name" in caps:
|
|
294
|
+
r = node_text(caps["rune.name"][0])
|
|
295
|
+
if r not in hooks:
|
|
296
|
+
hooks.append(r)
|
|
297
|
+
|
|
298
|
+
# Stores (writable, readable)
|
|
299
|
+
if "store.name" in caps:
|
|
300
|
+
s = node_text(caps["store.name"][0])
|
|
301
|
+
if s not in hooks:
|
|
302
|
+
hooks.append(s)
|
|
303
|
+
|
|
304
|
+
# Component name override
|
|
305
|
+
if "component.name" in caps:
|
|
306
|
+
name = node_text(caps["component.name"][0])
|
|
307
|
+
|
|
308
|
+
# Imports
|
|
309
|
+
if "import.path" in caps:
|
|
310
|
+
path = node_text(caps["import.path"][0]).strip("'\"")
|
|
311
|
+
if path not in imports:
|
|
312
|
+
imports.append(path)
|
|
313
|
+
|
|
314
|
+
return [ComponentInfo(
|
|
315
|
+
name=name,
|
|
316
|
+
source_file=file_path,
|
|
317
|
+
line=1,
|
|
318
|
+
framework=framework,
|
|
319
|
+
props=props,
|
|
320
|
+
hooks=hooks,
|
|
321
|
+
imports=imports,
|
|
322
|
+
)]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _interpret_angular(
|
|
326
|
+
file_path: str, matches: list, framework: str,
|
|
327
|
+
) -> list[ComponentInfo]:
|
|
328
|
+
"""Angular: @Component({ selector, templateUrl }) class."""
|
|
329
|
+
components: list[ComponentInfo] = []
|
|
330
|
+
seen: set[str] = set()
|
|
331
|
+
|
|
332
|
+
for _idx, caps in matches:
|
|
333
|
+
if "component.class" in caps and "component.class_name" in caps:
|
|
334
|
+
name = node_text(caps["component.class_name"][0])
|
|
335
|
+
if name in seen:
|
|
336
|
+
continue
|
|
337
|
+
seen.add(name)
|
|
338
|
+
selector = node_text(caps["component.selector"][0]) if "component.selector" in caps else ""
|
|
339
|
+
node = caps["component.class"][0]
|
|
340
|
+
comp = ComponentInfo(
|
|
341
|
+
name=name,
|
|
342
|
+
source_file=file_path,
|
|
343
|
+
line=node.start_point[0] + 1,
|
|
344
|
+
framework="angular",
|
|
345
|
+
)
|
|
346
|
+
if selector:
|
|
347
|
+
comp.hooks.append(f"selector:{selector}")
|
|
348
|
+
components.append(comp)
|
|
349
|
+
|
|
350
|
+
# templateUrl capture (separate pattern)
|
|
351
|
+
if "component.template_url_decorator" in caps and "component.template_url" in caps:
|
|
352
|
+
template_url = node_text(caps["component.template_url"][0])
|
|
353
|
+
# Assign to last component in list
|
|
354
|
+
if components:
|
|
355
|
+
components[-1].imports.append(template_url)
|
|
356
|
+
|
|
357
|
+
return components
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Import / dependency graph extraction for all supported languages.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
extract_dependencies(file_path, framework) -> list[Edge]
|
|
5
|
+
|
|
6
|
+
Near-generic: every .scm query file uses `@import.path` captures.
|
|
7
|
+
This module runs any framework's query and collects all import.path captures,
|
|
8
|
+
returning Edge objects with relation="imports_from".
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from codebeacon.common.types import Edge
|
|
15
|
+
from codebeacon.extract.base import (
|
|
16
|
+
extract_sfc_sections,
|
|
17
|
+
load_query_file,
|
|
18
|
+
node_text,
|
|
19
|
+
parse_file,
|
|
20
|
+
parse_sfc_script,
|
|
21
|
+
run_query,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Framework → query file stem ───────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
_FW_TO_QUERY: dict[str, str] = {
|
|
28
|
+
"spring-boot": "spring_boot",
|
|
29
|
+
"express": "express",
|
|
30
|
+
"koa": "express",
|
|
31
|
+
"fastify": "express",
|
|
32
|
+
"nestjs": "nestjs",
|
|
33
|
+
"nextjs": "react",
|
|
34
|
+
"react": "react",
|
|
35
|
+
"fastapi": "fastapi",
|
|
36
|
+
"django": "django",
|
|
37
|
+
"flask": "flask",
|
|
38
|
+
"gin": "gin",
|
|
39
|
+
"echo": "gin",
|
|
40
|
+
"fiber": "gin",
|
|
41
|
+
"go": "gin",
|
|
42
|
+
"rails": "rails",
|
|
43
|
+
"laravel": "laravel",
|
|
44
|
+
"aspnet": "aspnet",
|
|
45
|
+
"actix": "actix",
|
|
46
|
+
"axum": "actix",
|
|
47
|
+
"rust": "actix",
|
|
48
|
+
"vapor": "vapor",
|
|
49
|
+
"ktor": "ktor",
|
|
50
|
+
"vue": "vue",
|
|
51
|
+
"nuxt": "vue",
|
|
52
|
+
"sveltekit": "svelte",
|
|
53
|
+
"angular": "angular",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Public function ───────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def extract_dependencies(file_path: str, framework: str) -> list[Edge]:
|
|
60
|
+
"""Extract import/require/use statements and return list[Edge] with relation='imports_from'.
|
|
61
|
+
|
|
62
|
+
Each Edge has:
|
|
63
|
+
- source: file_path (the file that contains the import)
|
|
64
|
+
- target: the imported path/module (raw string from the import statement)
|
|
65
|
+
- relation: "imports_from"
|
|
66
|
+
- confidence: "EXTRACTED"
|
|
67
|
+
- confidence_score: 1.0
|
|
68
|
+
- source_file: file_path
|
|
69
|
+
"""
|
|
70
|
+
fw = framework.lower()
|
|
71
|
+
query_name = _FW_TO_QUERY.get(fw)
|
|
72
|
+
if not query_name:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
query_src = load_query_file(query_name)
|
|
76
|
+
if not query_src:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
# SFC dispatch
|
|
80
|
+
ext = Path(file_path).suffix.lower()
|
|
81
|
+
if ext in (".vue", ".svelte"):
|
|
82
|
+
sfc = extract_sfc_sections(file_path)
|
|
83
|
+
if sfc is None:
|
|
84
|
+
return []
|
|
85
|
+
parsed = parse_sfc_script(sfc)
|
|
86
|
+
else:
|
|
87
|
+
parsed = parse_file(file_path)
|
|
88
|
+
|
|
89
|
+
if parsed is None:
|
|
90
|
+
return []
|
|
91
|
+
root, lang = parsed
|
|
92
|
+
|
|
93
|
+
from codebeacon.extract.base import is_grammar_allowed
|
|
94
|
+
if not is_grammar_allowed(query_name, lang):
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
matches = run_query(lang, query_src, root)
|
|
99
|
+
except Exception:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
# Generic: collect all import.path captures across all patterns
|
|
103
|
+
edges: list[Edge] = []
|
|
104
|
+
seen: set[str] = set()
|
|
105
|
+
|
|
106
|
+
for _idx, caps in matches:
|
|
107
|
+
# All query files use @import.path for the imported module string
|
|
108
|
+
if "import.path" not in caps:
|
|
109
|
+
continue
|
|
110
|
+
for import_node in caps["import.path"]:
|
|
111
|
+
raw = node_text(import_node).strip("'\"` ")
|
|
112
|
+
if not raw or raw in seen:
|
|
113
|
+
continue
|
|
114
|
+
seen.add(raw)
|
|
115
|
+
edges.append(Edge(
|
|
116
|
+
source=file_path,
|
|
117
|
+
target=raw,
|
|
118
|
+
relation="imports_from",
|
|
119
|
+
confidence="EXTRACTED",
|
|
120
|
+
confidence_score=1.0,
|
|
121
|
+
source_file=file_path,
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
# Vapor uses @import.name instead of @import.path
|
|
125
|
+
if "import.name" in caps:
|
|
126
|
+
for import_node in caps["import.name"]:
|
|
127
|
+
raw = node_text(import_node).strip()
|
|
128
|
+
if not raw or raw in seen:
|
|
129
|
+
continue
|
|
130
|
+
seen.add(raw)
|
|
131
|
+
edges.append(Edge(
|
|
132
|
+
source=file_path,
|
|
133
|
+
target=raw,
|
|
134
|
+
relation="imports_from",
|
|
135
|
+
confidence="EXTRACTED",
|
|
136
|
+
confidence_score=1.0,
|
|
137
|
+
source_file=file_path,
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
return edges
|