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,597 @@
|
|
|
1
|
+
"""Service / DI extraction for all supported frameworks.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
extract_services(file_path, framework) -> tuple[list[ServiceInfo], list[UnresolvedRef]]
|
|
5
|
+
|
|
6
|
+
Design:
|
|
7
|
+
- Run the framework's .scm query once per file
|
|
8
|
+
- Collect service classes/functions + DI dependencies
|
|
9
|
+
- DI dependencies are returned as UnresolvedRef (resolved later in Pass 2)
|
|
10
|
+
- UnresolvedRef.source_node_id uses f"{file_path}::{class_name}" format
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from codebeacon.common.types import ServiceInfo, UnresolvedRef
|
|
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
|
+
"spring-boot": "spring_boot",
|
|
31
|
+
"express": "express",
|
|
32
|
+
"koa": "express",
|
|
33
|
+
"fastify": "express",
|
|
34
|
+
"nestjs": "nestjs",
|
|
35
|
+
"nextjs": "react",
|
|
36
|
+
"react": "react",
|
|
37
|
+
"fastapi": "fastapi",
|
|
38
|
+
"django": "django",
|
|
39
|
+
"flask": "flask",
|
|
40
|
+
"gin": "gin",
|
|
41
|
+
"echo": "gin",
|
|
42
|
+
"fiber": "gin",
|
|
43
|
+
"go": "gin",
|
|
44
|
+
"rails": "rails",
|
|
45
|
+
"laravel": "laravel",
|
|
46
|
+
"aspnet": "aspnet",
|
|
47
|
+
"actix": "actix",
|
|
48
|
+
"axum": "actix",
|
|
49
|
+
"rust": "actix",
|
|
50
|
+
"vapor": "vapor",
|
|
51
|
+
"ktor": "ktor",
|
|
52
|
+
"vue": "vue",
|
|
53
|
+
"nuxt": "vue",
|
|
54
|
+
"sveltekit": "svelte",
|
|
55
|
+
"angular": "angular",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Public function ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def extract_services(
|
|
62
|
+
file_path: str,
|
|
63
|
+
framework: str,
|
|
64
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
65
|
+
"""Extract service classes and DI dependencies from *file_path*."""
|
|
66
|
+
fw = framework.lower()
|
|
67
|
+
query_name = _FW_TO_QUERY.get(fw)
|
|
68
|
+
if not query_name:
|
|
69
|
+
return [], []
|
|
70
|
+
|
|
71
|
+
query_src = load_query_file(query_name)
|
|
72
|
+
if not query_src:
|
|
73
|
+
return [], []
|
|
74
|
+
|
|
75
|
+
# SFC dispatch
|
|
76
|
+
ext = Path(file_path).suffix.lower()
|
|
77
|
+
if ext in (".vue", ".svelte"):
|
|
78
|
+
sfc = extract_sfc_sections(file_path)
|
|
79
|
+
if sfc is None:
|
|
80
|
+
return [], []
|
|
81
|
+
parsed = parse_sfc_script(sfc)
|
|
82
|
+
else:
|
|
83
|
+
parsed = parse_file(file_path)
|
|
84
|
+
|
|
85
|
+
if parsed is None:
|
|
86
|
+
return [], []
|
|
87
|
+
root, lang = parsed
|
|
88
|
+
|
|
89
|
+
from codebeacon.extract.base import is_grammar_allowed
|
|
90
|
+
if not is_grammar_allowed(query_name, lang):
|
|
91
|
+
return [], []
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
matches = run_query(lang, query_src, root)
|
|
95
|
+
except Exception:
|
|
96
|
+
return [], []
|
|
97
|
+
|
|
98
|
+
_interpreters = {
|
|
99
|
+
"spring_boot": _interpret_spring_boot,
|
|
100
|
+
"express": _interpret_express,
|
|
101
|
+
"nestjs": _interpret_nestjs,
|
|
102
|
+
"fastapi": _interpret_fastapi,
|
|
103
|
+
"django": _interpret_noop,
|
|
104
|
+
"flask": _interpret_noop,
|
|
105
|
+
"gin": _interpret_gin,
|
|
106
|
+
"rails": _interpret_rails,
|
|
107
|
+
"laravel": _interpret_laravel,
|
|
108
|
+
"aspnet": _interpret_aspnet,
|
|
109
|
+
"actix": _interpret_actix,
|
|
110
|
+
"vapor": _interpret_vapor,
|
|
111
|
+
"ktor": _interpret_ktor,
|
|
112
|
+
"react": _interpret_noop,
|
|
113
|
+
"vue": _interpret_noop,
|
|
114
|
+
"svelte": _interpret_noop,
|
|
115
|
+
"angular": _interpret_angular,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interpreter = _interpreters.get(query_name, _interpret_noop)
|
|
119
|
+
try:
|
|
120
|
+
return interpreter(file_path, matches, fw)
|
|
121
|
+
except Exception:
|
|
122
|
+
return [], []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def _nid(file_path: str, name: str) -> str:
|
|
128
|
+
"""Build a stable node ID for UnresolvedRef.source_node_id."""
|
|
129
|
+
return f"{file_path}::{name}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _interpret_noop(
|
|
133
|
+
file_path: str, matches: list, framework: str,
|
|
134
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
135
|
+
return [], []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Per-framework interpreters ────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def _interpret_spring_boot(
|
|
141
|
+
file_path: str, matches: list, framework: str,
|
|
142
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
143
|
+
"""Spring Boot: @Service/@Component/@Repository + @Autowired / constructor injection."""
|
|
144
|
+
services: dict[int, ServiceInfo] = {} # class start_byte → ServiceInfo
|
|
145
|
+
unresolved: list[UnresolvedRef] = []
|
|
146
|
+
# class byte ranges for DI correlation
|
|
147
|
+
class_ranges: dict[int, tuple[int, int, str]] = {} # start → (start, end, class_name)
|
|
148
|
+
|
|
149
|
+
for _idx, caps in matches:
|
|
150
|
+
# @Service / @Component / @Repository class
|
|
151
|
+
if "service.class" in caps and "service.class_name" in caps:
|
|
152
|
+
cls = caps["service.class"][0]
|
|
153
|
+
name = node_text(caps["service.class_name"][0])
|
|
154
|
+
ann = node_text(caps["service.annotation"][0]) if "service.annotation" in caps else ""
|
|
155
|
+
key = cls.start_byte
|
|
156
|
+
if key not in services:
|
|
157
|
+
services[key] = ServiceInfo(
|
|
158
|
+
name=name,
|
|
159
|
+
class_name=name,
|
|
160
|
+
source_file=file_path,
|
|
161
|
+
line=cls.start_point[0] + 1,
|
|
162
|
+
framework="spring-boot",
|
|
163
|
+
annotations=[ann] if ann else [],
|
|
164
|
+
)
|
|
165
|
+
class_ranges[key] = (cls.start_byte, cls.end_byte, name)
|
|
166
|
+
elif ann and ann not in services[key].annotations:
|
|
167
|
+
services[key].annotations.append(ann)
|
|
168
|
+
|
|
169
|
+
# Implemented interfaces
|
|
170
|
+
if "service.with_interface" in caps and "service.interface" in caps:
|
|
171
|
+
cls = caps["service.with_interface"][0]
|
|
172
|
+
iface = node_text(caps["service.interface"][0])
|
|
173
|
+
for key, info in services.items():
|
|
174
|
+
start, end, _ = class_ranges.get(key, (0, 0, ""))
|
|
175
|
+
if start <= cls.start_byte <= end:
|
|
176
|
+
if iface not in info.annotations:
|
|
177
|
+
info.annotations.append(f"implements:{iface}")
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
# @Autowired field injection
|
|
181
|
+
if "di.autowired_field" in caps and "di.field_type" in caps:
|
|
182
|
+
field_node = caps["di.autowired_field"][0]
|
|
183
|
+
dep_type = node_text(caps["di.field_type"][0])
|
|
184
|
+
# Find enclosing class
|
|
185
|
+
for key, (start, end, cls_name) in class_ranges.items():
|
|
186
|
+
if start <= field_node.start_byte <= end:
|
|
187
|
+
if dep_type not in services[key].dependencies:
|
|
188
|
+
services[key].dependencies.append(dep_type)
|
|
189
|
+
unresolved.append(UnresolvedRef(
|
|
190
|
+
source_node_id=_nid(file_path, cls_name),
|
|
191
|
+
ref_type="autowired",
|
|
192
|
+
ref_name=dep_type,
|
|
193
|
+
framework="spring-boot",
|
|
194
|
+
))
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# Constructor injection
|
|
198
|
+
if "di.constructor" in caps and "di.ctor_param_type" in caps:
|
|
199
|
+
ctor_node = caps["di.constructor"][0]
|
|
200
|
+
for param_type_node in caps["di.ctor_param_type"]:
|
|
201
|
+
dep_type = node_text(param_type_node)
|
|
202
|
+
for key, (start, end, cls_name) in class_ranges.items():
|
|
203
|
+
if start <= ctor_node.start_byte <= end:
|
|
204
|
+
if dep_type not in services[key].dependencies:
|
|
205
|
+
services[key].dependencies.append(dep_type)
|
|
206
|
+
unresolved.append(UnresolvedRef(
|
|
207
|
+
source_node_id=_nid(file_path, cls_name),
|
|
208
|
+
ref_type="autowired",
|
|
209
|
+
ref_name=dep_type,
|
|
210
|
+
framework="spring-boot",
|
|
211
|
+
))
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
return list(services.values()), unresolved
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _interpret_express(
|
|
218
|
+
file_path: str, matches: list, framework: str,
|
|
219
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
220
|
+
"""Express/Koa/Fastify: exported classes as services (no DI framework)."""
|
|
221
|
+
services: list[ServiceInfo] = []
|
|
222
|
+
seen: set[str] = set()
|
|
223
|
+
|
|
224
|
+
for _idx, caps in matches:
|
|
225
|
+
if "service.name" in caps:
|
|
226
|
+
name = node_text(caps["service.name"][0])
|
|
227
|
+
if name in seen:
|
|
228
|
+
continue
|
|
229
|
+
seen.add(name)
|
|
230
|
+
node = caps.get("service.export_class", caps.get("service.class", [None]))[0]
|
|
231
|
+
line = node.start_point[0] + 1 if node else 1
|
|
232
|
+
services.append(ServiceInfo(
|
|
233
|
+
name=name,
|
|
234
|
+
class_name=name,
|
|
235
|
+
source_file=file_path,
|
|
236
|
+
line=line,
|
|
237
|
+
framework=framework,
|
|
238
|
+
))
|
|
239
|
+
return services, []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _interpret_nestjs(
|
|
243
|
+
file_path: str, matches: list, framework: str,
|
|
244
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
245
|
+
"""NestJS: @Injectable + constructor injection.
|
|
246
|
+
|
|
247
|
+
Injectable decorator is sibling of class_declaration in export_statement.
|
|
248
|
+
Constructor DI is matched separately via service.constructor_di pattern.
|
|
249
|
+
Uses byte-position matching to correlate DI with enclosing class.
|
|
250
|
+
"""
|
|
251
|
+
services: dict[str, ServiceInfo] = {} # class_name → ServiceInfo
|
|
252
|
+
# Track byte ranges for each service's enclosing export_statement
|
|
253
|
+
svc_ranges: dict[str, tuple[int, int]] = {} # class_name → (start, end)
|
|
254
|
+
unresolved: list[UnresolvedRef] = []
|
|
255
|
+
|
|
256
|
+
# Pass 1: collect @Injectable classes
|
|
257
|
+
for _idx, caps in matches:
|
|
258
|
+
for key in ("service.injectable", "service.injectable_noexport"):
|
|
259
|
+
if key in caps and "service.class_name" in caps:
|
|
260
|
+
name = node_text(caps["service.class_name"][0])
|
|
261
|
+
if name not in services:
|
|
262
|
+
cls = caps[key][0]
|
|
263
|
+
services[name] = ServiceInfo(
|
|
264
|
+
name=name,
|
|
265
|
+
class_name=name,
|
|
266
|
+
source_file=file_path,
|
|
267
|
+
line=cls.start_point[0] + 1,
|
|
268
|
+
framework="nestjs",
|
|
269
|
+
annotations=["Injectable"],
|
|
270
|
+
)
|
|
271
|
+
svc_ranges[name] = (cls.start_byte, cls.end_byte)
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
# Pass 2: collect constructor DI, matching to enclosing class by position
|
|
275
|
+
for _idx, caps in matches:
|
|
276
|
+
if "service.constructor_di" in caps and "service.inject_type" in caps:
|
|
277
|
+
ctor_node = caps["service.constructor_di"][0]
|
|
278
|
+
ctor_start = ctor_node.start_byte
|
|
279
|
+
# Find enclosing service by byte range
|
|
280
|
+
enclosing_name = ""
|
|
281
|
+
for name, (start, end) in svc_ranges.items():
|
|
282
|
+
if start <= ctor_start <= end:
|
|
283
|
+
enclosing_name = name
|
|
284
|
+
break
|
|
285
|
+
if not enclosing_name:
|
|
286
|
+
continue
|
|
287
|
+
svc = services[enclosing_name]
|
|
288
|
+
for dep_node in caps["service.inject_type"]:
|
|
289
|
+
dep = node_text(dep_node)
|
|
290
|
+
if dep not in svc.dependencies:
|
|
291
|
+
svc.dependencies.append(dep)
|
|
292
|
+
unresolved.append(UnresolvedRef(
|
|
293
|
+
source_node_id=_nid(file_path, enclosing_name),
|
|
294
|
+
ref_type="inject",
|
|
295
|
+
ref_name=dep,
|
|
296
|
+
framework="nestjs",
|
|
297
|
+
))
|
|
298
|
+
|
|
299
|
+
return list(services.values()), unresolved
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _interpret_fastapi(
|
|
303
|
+
file_path: str, matches: list, framework: str,
|
|
304
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
305
|
+
"""FastAPI: Depends() function-based DI."""
|
|
306
|
+
services: list[ServiceInfo] = []
|
|
307
|
+
unresolved: list[UnresolvedRef] = []
|
|
308
|
+
seen_funcs: set[str] = set()
|
|
309
|
+
|
|
310
|
+
for _idx, caps in matches:
|
|
311
|
+
# Functions that accept typed parameters (potential services)
|
|
312
|
+
if "service.function" in caps and "service.func_name" in caps:
|
|
313
|
+
name = node_text(caps["service.func_name"][0])
|
|
314
|
+
if name in seen_funcs:
|
|
315
|
+
continue
|
|
316
|
+
seen_funcs.add(name)
|
|
317
|
+
node = caps["service.function"][0]
|
|
318
|
+
services.append(ServiceInfo(
|
|
319
|
+
name=name,
|
|
320
|
+
class_name=name,
|
|
321
|
+
source_file=file_path,
|
|
322
|
+
line=node.start_point[0] + 1,
|
|
323
|
+
framework="fastapi",
|
|
324
|
+
))
|
|
325
|
+
|
|
326
|
+
# Depends(func) calls
|
|
327
|
+
if "service.depends" in caps and "service.depends_func" in caps:
|
|
328
|
+
dep_func = node_text(caps["service.depends_func"][0])
|
|
329
|
+
# Find enclosing function for the unresolved ref
|
|
330
|
+
depends_node = caps["service.depends"][0]
|
|
331
|
+
enclosing = ""
|
|
332
|
+
for svc in services:
|
|
333
|
+
if svc.name in seen_funcs:
|
|
334
|
+
enclosing = svc.name
|
|
335
|
+
if dep_func not in (s.name for s in services):
|
|
336
|
+
unresolved.append(UnresolvedRef(
|
|
337
|
+
source_node_id=_nid(file_path, enclosing or "unknown"),
|
|
338
|
+
ref_type="depends",
|
|
339
|
+
ref_name=dep_func,
|
|
340
|
+
framework="fastapi",
|
|
341
|
+
))
|
|
342
|
+
|
|
343
|
+
return services, unresolved
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _interpret_gin(
|
|
347
|
+
file_path: str, matches: list, framework: str,
|
|
348
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
349
|
+
"""Go: service structs with embedded field types as dependencies."""
|
|
350
|
+
services: list[ServiceInfo] = []
|
|
351
|
+
seen: set[str] = set()
|
|
352
|
+
|
|
353
|
+
for _idx, caps in matches:
|
|
354
|
+
if ("service.struct" in caps or "service.struct_plain" in caps) and "service.struct_name" in caps:
|
|
355
|
+
name = node_text(caps["service.struct_name"][0])
|
|
356
|
+
if name in seen:
|
|
357
|
+
continue
|
|
358
|
+
seen.add(name)
|
|
359
|
+
deps = [node_text(n) for n in caps.get("service.field_type", [])]
|
|
360
|
+
node = caps.get("service.struct", caps.get("service.struct_plain", [None]))[0]
|
|
361
|
+
line = node.start_point[0] + 1 if node else 1
|
|
362
|
+
services.append(ServiceInfo(
|
|
363
|
+
name=name,
|
|
364
|
+
class_name=name,
|
|
365
|
+
source_file=file_path,
|
|
366
|
+
line=line,
|
|
367
|
+
framework=framework,
|
|
368
|
+
dependencies=deps,
|
|
369
|
+
))
|
|
370
|
+
return services, []
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _interpret_rails(
|
|
374
|
+
file_path: str, matches: list, framework: str,
|
|
375
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
376
|
+
"""Rails: plain Ruby classes as services."""
|
|
377
|
+
services: list[ServiceInfo] = []
|
|
378
|
+
seen: set[str] = set()
|
|
379
|
+
|
|
380
|
+
for _idx, caps in matches:
|
|
381
|
+
if "service.class" in caps and "service.class_name" in caps:
|
|
382
|
+
name = node_text(caps["service.class_name"][0])
|
|
383
|
+
if name in seen:
|
|
384
|
+
continue
|
|
385
|
+
seen.add(name)
|
|
386
|
+
node = caps["service.class"][0]
|
|
387
|
+
services.append(ServiceInfo(
|
|
388
|
+
name=name,
|
|
389
|
+
class_name=name,
|
|
390
|
+
source_file=file_path,
|
|
391
|
+
line=node.start_point[0] + 1,
|
|
392
|
+
framework="rails",
|
|
393
|
+
))
|
|
394
|
+
return services, []
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _interpret_laravel(
|
|
398
|
+
file_path: str, matches: list, framework: str,
|
|
399
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
400
|
+
"""Laravel: service classes + $this->app->bind() DI bindings."""
|
|
401
|
+
services: list[ServiceInfo] = []
|
|
402
|
+
unresolved: list[UnresolvedRef] = []
|
|
403
|
+
seen: set[str] = set()
|
|
404
|
+
|
|
405
|
+
for _idx, caps in matches:
|
|
406
|
+
if "service.class" in caps and "service.class_name" in caps:
|
|
407
|
+
name = node_text(caps["service.class_name"][0])
|
|
408
|
+
if name in seen:
|
|
409
|
+
continue
|
|
410
|
+
seen.add(name)
|
|
411
|
+
node = caps["service.class"][0]
|
|
412
|
+
services.append(ServiceInfo(
|
|
413
|
+
name=name,
|
|
414
|
+
class_name=name,
|
|
415
|
+
source_file=file_path,
|
|
416
|
+
line=node.start_point[0] + 1,
|
|
417
|
+
framework="laravel",
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
if "di.binding" in caps and "di.interface" in caps and "di.implementation" in caps:
|
|
421
|
+
iface = node_text(caps["di.interface"][0])
|
|
422
|
+
impl = node_text(caps["di.implementation"][0])
|
|
423
|
+
unresolved.append(UnresolvedRef(
|
|
424
|
+
source_node_id=_nid(file_path, impl),
|
|
425
|
+
ref_type="bind",
|
|
426
|
+
ref_name=iface,
|
|
427
|
+
framework="laravel",
|
|
428
|
+
))
|
|
429
|
+
|
|
430
|
+
return services, unresolved
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _interpret_aspnet(
|
|
434
|
+
file_path: str, matches: list, framework: str,
|
|
435
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
436
|
+
"""ASP.NET: service classes with interfaces + AddScoped<IFoo, FooImpl>() DI."""
|
|
437
|
+
services: list[ServiceInfo] = []
|
|
438
|
+
unresolved: list[UnresolvedRef] = []
|
|
439
|
+
seen: set[str] = set()
|
|
440
|
+
|
|
441
|
+
for _idx, caps in matches:
|
|
442
|
+
if "service.class" in caps and "service.class_name" in caps:
|
|
443
|
+
name = node_text(caps["service.class_name"][0])
|
|
444
|
+
if name in seen:
|
|
445
|
+
continue
|
|
446
|
+
seen.add(name)
|
|
447
|
+
iface = node_text(caps["service.interface"][0]) if "service.interface" in caps else ""
|
|
448
|
+
node = caps["service.class"][0]
|
|
449
|
+
svc = ServiceInfo(
|
|
450
|
+
name=name,
|
|
451
|
+
class_name=name,
|
|
452
|
+
source_file=file_path,
|
|
453
|
+
line=node.start_point[0] + 1,
|
|
454
|
+
framework="aspnet",
|
|
455
|
+
)
|
|
456
|
+
if iface:
|
|
457
|
+
svc.annotations.append(f"implements:{iface}")
|
|
458
|
+
services.append(svc)
|
|
459
|
+
|
|
460
|
+
if "di.generic_registration" in caps and "di.service_type" in caps and "di.impl_type" in caps:
|
|
461
|
+
iface = node_text(caps["di.service_type"][0])
|
|
462
|
+
impl = node_text(caps["di.impl_type"][0])
|
|
463
|
+
unresolved.append(UnresolvedRef(
|
|
464
|
+
source_node_id=_nid(file_path, impl),
|
|
465
|
+
ref_type="bind",
|
|
466
|
+
ref_name=iface,
|
|
467
|
+
framework="aspnet",
|
|
468
|
+
))
|
|
469
|
+
|
|
470
|
+
return services, unresolved
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _interpret_actix(
|
|
474
|
+
file_path: str, matches: list, framework: str,
|
|
475
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
476
|
+
"""Actix/Axum: AppState and other service structs."""
|
|
477
|
+
services: list[ServiceInfo] = []
|
|
478
|
+
seen: set[str] = set()
|
|
479
|
+
|
|
480
|
+
for _idx, caps in matches:
|
|
481
|
+
if "service.struct" in caps and "service.struct_name" in caps:
|
|
482
|
+
name = node_text(caps["service.struct_name"][0])
|
|
483
|
+
if name in seen:
|
|
484
|
+
continue
|
|
485
|
+
seen.add(name)
|
|
486
|
+
node = caps["service.struct"][0]
|
|
487
|
+
services.append(ServiceInfo(
|
|
488
|
+
name=name,
|
|
489
|
+
class_name=name,
|
|
490
|
+
source_file=file_path,
|
|
491
|
+
line=node.start_point[0] + 1,
|
|
492
|
+
framework=framework,
|
|
493
|
+
))
|
|
494
|
+
return services, []
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _interpret_vapor(
|
|
498
|
+
file_path: str, matches: list, framework: str,
|
|
499
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
500
|
+
"""Vapor: route configuration functions as services."""
|
|
501
|
+
services: list[ServiceInfo] = []
|
|
502
|
+
seen: set[str] = set()
|
|
503
|
+
|
|
504
|
+
for _idx, caps in matches:
|
|
505
|
+
if "service.func" in caps and "service.func_name" in caps:
|
|
506
|
+
name = node_text(caps["service.func_name"][0])
|
|
507
|
+
if name in seen:
|
|
508
|
+
continue
|
|
509
|
+
seen.add(name)
|
|
510
|
+
node = caps["service.func"][0]
|
|
511
|
+
services.append(ServiceInfo(
|
|
512
|
+
name=name,
|
|
513
|
+
class_name=name,
|
|
514
|
+
source_file=file_path,
|
|
515
|
+
line=node.start_point[0] + 1,
|
|
516
|
+
framework="vapor",
|
|
517
|
+
))
|
|
518
|
+
return services, []
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _interpret_ktor(
|
|
522
|
+
file_path: str, matches: list, framework: str,
|
|
523
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
524
|
+
"""Ktor: Koin DI single{}/factory{} + regular Kotlin classes."""
|
|
525
|
+
services: list[ServiceInfo] = []
|
|
526
|
+
unresolved: list[UnresolvedRef] = []
|
|
527
|
+
seen: set[str] = set()
|
|
528
|
+
|
|
529
|
+
for _idx, caps in matches:
|
|
530
|
+
# Koin: single { UserService(get()) }
|
|
531
|
+
if "service.koin_binding" in caps and "service.koin_type" in caps:
|
|
532
|
+
name = node_text(caps["service.koin_type"][0])
|
|
533
|
+
if name not in seen:
|
|
534
|
+
seen.add(name)
|
|
535
|
+
node = caps["service.koin_binding"][0]
|
|
536
|
+
services.append(ServiceInfo(
|
|
537
|
+
name=name,
|
|
538
|
+
class_name=name,
|
|
539
|
+
source_file=file_path,
|
|
540
|
+
line=node.start_point[0] + 1,
|
|
541
|
+
framework="ktor",
|
|
542
|
+
annotations=["koin"],
|
|
543
|
+
))
|
|
544
|
+
|
|
545
|
+
# Regular class
|
|
546
|
+
if "service.class" in caps and "service.class_name" in caps:
|
|
547
|
+
name = node_text(caps["service.class_name"][0])
|
|
548
|
+
if name not in seen:
|
|
549
|
+
seen.add(name)
|
|
550
|
+
node = caps["service.class"][0]
|
|
551
|
+
services.append(ServiceInfo(
|
|
552
|
+
name=name,
|
|
553
|
+
class_name=name,
|
|
554
|
+
source_file=file_path,
|
|
555
|
+
line=node.start_point[0] + 1,
|
|
556
|
+
framework="ktor",
|
|
557
|
+
))
|
|
558
|
+
|
|
559
|
+
return services, unresolved
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _interpret_angular(
|
|
563
|
+
file_path: str, matches: list, framework: str,
|
|
564
|
+
) -> tuple[list[ServiceInfo], list[UnresolvedRef]]:
|
|
565
|
+
"""Angular: @Injectable + constructor DI."""
|
|
566
|
+
services: dict[int, ServiceInfo] = {}
|
|
567
|
+
unresolved: list[UnresolvedRef] = []
|
|
568
|
+
|
|
569
|
+
for _idx, caps in matches:
|
|
570
|
+
if "service.injectable" in caps and "service.class_name" in caps:
|
|
571
|
+
cls = caps["service.injectable"][0]
|
|
572
|
+
name = node_text(caps["service.class_name"][0])
|
|
573
|
+
services[cls.start_byte] = ServiceInfo(
|
|
574
|
+
name=name,
|
|
575
|
+
class_name=name,
|
|
576
|
+
source_file=file_path,
|
|
577
|
+
line=cls.start_point[0] + 1,
|
|
578
|
+
framework="angular",
|
|
579
|
+
annotations=["Injectable"],
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if "service.constructor_di" in caps and "service.inject_type" in caps:
|
|
583
|
+
for dep_node in caps["service.inject_type"]:
|
|
584
|
+
dep = node_text(dep_node)
|
|
585
|
+
# Assign to closest service by position
|
|
586
|
+
for svc in services.values():
|
|
587
|
+
if dep not in svc.dependencies:
|
|
588
|
+
svc.dependencies.append(dep)
|
|
589
|
+
unresolved.append(UnresolvedRef(
|
|
590
|
+
source_node_id=_nid(file_path, svc.name),
|
|
591
|
+
ref_type="inject",
|
|
592
|
+
ref_name=dep,
|
|
593
|
+
framework="angular",
|
|
594
|
+
))
|
|
595
|
+
break # assign to first service (constructor_di is inside a class)
|
|
596
|
+
|
|
597
|
+
return list(services.values()), unresolved
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Graph construction, enrichment, clustering, and analysis."""
|