cg-code-graph 0.10.1__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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""NestJS framework layer (on the TypeScript plugin).
|
|
2
|
+
|
|
3
|
+
Reads the extractor's decorator facts (extractor/fw.mjs) and adds:
|
|
4
|
+
|
|
5
|
+
modules @Module({imports, controllers, providers, exports}) -> class attrs.nest_module; REFERENCES/BINDS edges
|
|
6
|
+
DI constructor / property injection resolved to a provider: class tokens, @Inject('TOKEN') / @Inject(SYM),
|
|
7
|
+
{provide, useClass | useExisting | useFactory (its `new X()`) | useValue}; INJECTS consumer -> provider,
|
|
8
|
+
calls through an injected member (this.repo.find()) to the provider's method (CALLS, resolved, via di)
|
|
9
|
+
when the type checker could not (interface / token typed members); abstract class token -> BOUND_TO
|
|
10
|
+
routes @Controller(path | {path, version}) + @Get/@Post/@Put/@Patch/@Delete/@All/@Options/@Head/@Sse(path),
|
|
11
|
+
app.setGlobalPrefix(p, {exclude}), app.enableVersioning({type: URI, prefix, defaultVersion}) + @Version,
|
|
12
|
+
RouterModule.register([{path, module, children}]); route attrs: guards/interceptors/pipes/filters
|
|
13
|
+
(@UseGuards ... and global useGlobalGuards / APP_GUARD), body DTO + fields (@Body() dto: Dto), query
|
|
14
|
+
fields, custom decorators; USES_MIDDLEWARE -> guard canActivate / interceptor intercept / pipe transform
|
|
15
|
+
GraphQL @Resolver + @Query/@Mutation/@Subscription (from @nestjs/graphql) -> route `GRAPHQL <Type>.<field>`
|
|
16
|
+
entries @Cron/@Interval/@Timeout -> schedule (scheduled); Bull/BullMQ @Processor(+@Process / WorkerHost.process)
|
|
17
|
+
-> job (queue_job), producers @InjectQueue('q') + this.q.add('name') -> DISPATCHES; @OnEvent -> listener,
|
|
18
|
+
EventEmitter2.emit('evt') -> DISPATCHES; @MessagePattern/@EventPattern and @WebSocketGateway +
|
|
19
|
+
@SubscribeMessage -> message (message_handler), ClientProxy.send/emit -> DISPATCHES;
|
|
20
|
+
nest-commander @Command/@SubCommand -> command (cli_command, operator-only)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
from ...core.plugin import FrameworkPlugin, GraphBuilder, Project
|
|
28
|
+
from ..tsweb.common import (TEST_SKIP_RE, add_route, express_path, finish, fw_facts, has_server_framework, join_path,
|
|
29
|
+
last_name, merge_extractor_cfg, module_of, obj, ref_nodes, register, sval, svals)
|
|
30
|
+
|
|
31
|
+
ROUTE_DECOS = {"Get": "GET", "Post": "POST", "Put": "PUT", "Patch": "PATCH", "Delete": "DELETE", "All": "ANY",
|
|
32
|
+
"Options": "OPTIONS", "Head": "HEAD", "Search": "SEARCH", "Sse": "GET"}
|
|
33
|
+
ENHANCERS = {"UseGuards": ("guards", "canActivate"), "UseInterceptors": ("interceptors", "intercept"),
|
|
34
|
+
"UsePipes": ("pipes", "transform"), "UseFilters": ("filters", "catch")}
|
|
35
|
+
GLOBAL_ENHANCERS = {"useGlobalGuards": "UseGuards", "useGlobalInterceptors": "UseInterceptors", "useGlobalPipes": "UsePipes",
|
|
36
|
+
"useGlobalFilters": "UseFilters"}
|
|
37
|
+
APP_TOKENS = {"APP_GUARD": "UseGuards", "APP_INTERCEPTOR": "UseInterceptors", "APP_PIPE": "UsePipes", "APP_FILTER": "UseFilters"}
|
|
38
|
+
INJECTABLE = {"Injectable", "Controller", "Resolver", "Processor", "WebSocketGateway", "Command", "SubCommand", "Catch", "Module"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
ORM_INJECT = ("InjectRepository", "InjectModel", "InjectDataSource", "InjectEntityManager", "InjectConnection", "InjectRedis")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_nest(d) -> bool:
|
|
45
|
+
m = d.get("mod")
|
|
46
|
+
return m is None or m.startswith("@nestjs/") or m == "nest-commander"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _decos(x) -> dict:
|
|
50
|
+
out = {}
|
|
51
|
+
for d in x.get("decorators") or []:
|
|
52
|
+
out.setdefault(last_name(d["name"]), d)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _token(d) -> str | None:
|
|
57
|
+
"""Described provider token -> stable key."""
|
|
58
|
+
if not isinstance(d, dict):
|
|
59
|
+
return None
|
|
60
|
+
if d.get("node"):
|
|
61
|
+
return d["node"]
|
|
62
|
+
if isinstance(d.get("s"), str):
|
|
63
|
+
return "str:" + d["s"]
|
|
64
|
+
if d.get("key"):
|
|
65
|
+
return "key:" + d["key"]
|
|
66
|
+
if d.get("ref"):
|
|
67
|
+
return "ref:" + d["ref"]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _pattern(d) -> str | None:
|
|
72
|
+
if not isinstance(d, dict):
|
|
73
|
+
return None
|
|
74
|
+
if isinstance(d.get("s"), str):
|
|
75
|
+
return d["s"]
|
|
76
|
+
if d.get("obj") is not None:
|
|
77
|
+
return json.dumps({k: _pattern(v) for k, v in sorted(d["obj"].items())}, separators=(",", ":"))
|
|
78
|
+
if d.get("n") is not None:
|
|
79
|
+
return str(d["n"])
|
|
80
|
+
return d.get("ref")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class NestPlugin(FrameworkPlugin):
|
|
84
|
+
name, language = "nest", "typescript"
|
|
85
|
+
|
|
86
|
+
def detect(self, project: Project) -> bool:
|
|
87
|
+
return has_server_framework(project, "nest") or project.exists("nest-cli.json")
|
|
88
|
+
|
|
89
|
+
def register_hooks(self, ctx) -> None:
|
|
90
|
+
root = ctx.project.root
|
|
91
|
+
dirs = [d for d in ("src", "apps", "libs", "packages") if (root / d).is_dir()] or ["."]
|
|
92
|
+
merge_extractor_cfg(ctx.extractor_cfg, src_dirs=dirs, skip_re=TEST_SKIP_RE)
|
|
93
|
+
register(ctx, self.name)
|
|
94
|
+
|
|
95
|
+
def contribute(self, project: Project, b: GraphBuilder, ctx) -> dict:
|
|
96
|
+
F = fw_facts(ctx)
|
|
97
|
+
st = {"modules": 0, "controllers": 0, "routes": 0, "providers": 0, "di_params": 0, "di_resolved": 0, "di_external": 0,
|
|
98
|
+
"di_unresolved": 0, "di_calls": 0, "schedules": 0, "jobs": 0, "listeners": 0, "messages": 0, "commands": 0,
|
|
99
|
+
"graphql_ops": 0}
|
|
100
|
+
if not F:
|
|
101
|
+
return {**st, "status": "no facts"}
|
|
102
|
+
self.b, self.F = b, F
|
|
103
|
+
self.classes = {c["id"]: c for c in F.get("classes") or []}
|
|
104
|
+
self.by_name = {}
|
|
105
|
+
for c in self.classes.values():
|
|
106
|
+
if c.get("name"):
|
|
107
|
+
self.by_name.setdefault(c["name"], []).append(c["id"])
|
|
108
|
+
self.supers = {c["id"]: (c.get("extends") or {}).get("id") for c in self.classes.values()}
|
|
109
|
+
self._modules(st)
|
|
110
|
+
self._providers(st)
|
|
111
|
+
self._di(st)
|
|
112
|
+
self._routes(st)
|
|
113
|
+
self._entries(st)
|
|
114
|
+
st.update(finish(project, b, ctx, self.name))
|
|
115
|
+
if st["di_params"]:
|
|
116
|
+
st["di_resolution_rate"] = round(st["di_resolved"] / max(1, st["di_params"] - st["di_external"]), 3)
|
|
117
|
+
st["di_unresolved_samples"] = getattr(self, "unresolved_samples", [])[:15]
|
|
118
|
+
return st
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------ helpers
|
|
121
|
+
def method_of(self, cls: str | None, name: str):
|
|
122
|
+
for _ in range(8):
|
|
123
|
+
c = self.classes.get(cls)
|
|
124
|
+
if not c:
|
|
125
|
+
return None
|
|
126
|
+
for m in c.get("methods") or []:
|
|
127
|
+
if m["name"] == name and m.get("id"):
|
|
128
|
+
return m["id"]
|
|
129
|
+
for p in c.get("props") or []: # arrow-function class properties
|
|
130
|
+
if p["name"] == name and p.get("node"):
|
|
131
|
+
return p["node"]
|
|
132
|
+
cls = self.supers.get(cls)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def class_ids(self, d) -> list[str]:
|
|
136
|
+
"""Classes a described value names: X, forwardRef(() => X), X.forRoot(...) (the module class)."""
|
|
137
|
+
if not isinstance(d, dict):
|
|
138
|
+
return []
|
|
139
|
+
if d.get("node", "").startswith("class:"):
|
|
140
|
+
return [d["node"]]
|
|
141
|
+
if d.get("body"):
|
|
142
|
+
return self.class_ids(d["body"])
|
|
143
|
+
if "call" in d:
|
|
144
|
+
if last_name(d["call"]) == "forwardRef":
|
|
145
|
+
return [x for a in d.get("args") or [] for x in self.class_ids(a)]
|
|
146
|
+
if d.get("recv"):
|
|
147
|
+
return self.class_ids(d["recv"])
|
|
148
|
+
if "new" in d and (d.get("node") or "").startswith("class:"):
|
|
149
|
+
return [d["node"]]
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------ modules
|
|
153
|
+
def _modules(self, st):
|
|
154
|
+
b = self.b
|
|
155
|
+
self.module_of_controller = {}
|
|
156
|
+
self.module_paths = {} # module class -> RouterModule path prefix
|
|
157
|
+
self.module_providers = [] # (module class, provider desc)
|
|
158
|
+
self.client_tokens = set() # ClientsModule.register([{ name }]) tokens -> ClientProxy
|
|
159
|
+
self.modules = {}
|
|
160
|
+
for c in self.classes.values():
|
|
161
|
+
d = _decos(c).get("Module")
|
|
162
|
+
if not d or not _is_nest(d):
|
|
163
|
+
continue
|
|
164
|
+
o = obj((d.get("args") or [None])[0])
|
|
165
|
+
info = {}
|
|
166
|
+
for role in ("imports", "controllers", "providers", "exports"):
|
|
167
|
+
items = (o.get(role) or {}).get("arr") or []
|
|
168
|
+
info[role] = items
|
|
169
|
+
self.modules[c["id"]] = info
|
|
170
|
+
st["modules"] += 1
|
|
171
|
+
names = {}
|
|
172
|
+
for role, items in info.items():
|
|
173
|
+
ids = []
|
|
174
|
+
for it in items:
|
|
175
|
+
for cid in self.class_ids(it):
|
|
176
|
+
ids.append(cid)
|
|
177
|
+
if role == "controllers":
|
|
178
|
+
self.module_of_controller.setdefault(cid, c["id"])
|
|
179
|
+
if b.has(c["id"]) and b.has(cid):
|
|
180
|
+
b.add_edge(c["id"], cid, "BINDS" if role == "providers" else "REFERENCES", file=c["file"],
|
|
181
|
+
line=(it or {}).get("line") or c["line"], confidence="exact", role=role)
|
|
182
|
+
if role == "providers":
|
|
183
|
+
self.module_providers.append((c["id"], it))
|
|
184
|
+
if role == "imports" and "call" in (it or {}) and last_name(it["call"]) == "register" and "RouterModule" in it["call"]:
|
|
185
|
+
self._router_module(it.get("args") or [], "")
|
|
186
|
+
if role == "imports" and "call" in (it or {}) and "ClientsModule" in it["call"]:
|
|
187
|
+
for e in ((it.get("args") or [{}])[0] or {}).get("arr") or []: # ClientsModule.register([{ name: TOKEN }])
|
|
188
|
+
tok = _token(obj(e).get("name"))
|
|
189
|
+
if tok:
|
|
190
|
+
self.client_tokens.add(tok)
|
|
191
|
+
names[role] = [b.nodes[x].name for x in ids if b.has(x)]
|
|
192
|
+
if b.has(c["id"]):
|
|
193
|
+
b.nodes[c["id"]].attrs["nest_module"] = names
|
|
194
|
+
|
|
195
|
+
def _router_module(self, args, prefix):
|
|
196
|
+
for a in args:
|
|
197
|
+
for e in (a or {}).get("arr") or []:
|
|
198
|
+
o = obj(e)
|
|
199
|
+
if not o:
|
|
200
|
+
for cid in self.class_ids(e):
|
|
201
|
+
self.module_paths.setdefault(cid, prefix)
|
|
202
|
+
continue
|
|
203
|
+
p = join_path(prefix, sval(o.get("path")) or "")
|
|
204
|
+
for cid in self.class_ids(o.get("module")):
|
|
205
|
+
self.module_paths.setdefault(cid, p)
|
|
206
|
+
if o.get("children"):
|
|
207
|
+
self._router_module([o["children"]], p)
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------ providers + DI
|
|
210
|
+
def _providers(self, st):
|
|
211
|
+
self.providers = {} # token -> [(impl node, kind, desc)]
|
|
212
|
+
self.app_enhancers = [] # (decorator name, class id, evidence)
|
|
213
|
+
seen = set()
|
|
214
|
+
extra = [(None, po["desc"]) for po in self.F.get("provide_objs") or [] if isinstance(po.get("desc"), dict)]
|
|
215
|
+
for mod, it in self.module_providers + extra:
|
|
216
|
+
o = obj(it)
|
|
217
|
+
if o and (it.get("line"), _token(o.get("provide"))) in seen:
|
|
218
|
+
continue
|
|
219
|
+
if o:
|
|
220
|
+
seen.add((it.get("line"), _token(o.get("provide"))))
|
|
221
|
+
if not o:
|
|
222
|
+
for cid in self.class_ids(it):
|
|
223
|
+
self.providers.setdefault(cid, []).append((cid, "class", it))
|
|
224
|
+
st["providers"] += 1
|
|
225
|
+
continue
|
|
226
|
+
tok = _token(o.get("provide"))
|
|
227
|
+
if not tok:
|
|
228
|
+
continue
|
|
229
|
+
impls = []
|
|
230
|
+
if o.get("useClass"):
|
|
231
|
+
impls = [(x, "useClass") for x in self.class_ids(o["useClass"])]
|
|
232
|
+
elif o.get("useExisting"):
|
|
233
|
+
impls = [(x, "useExisting") for x in self.class_ids(o["useExisting"])]
|
|
234
|
+
elif o.get("useFactory"):
|
|
235
|
+
f = o["useFactory"]
|
|
236
|
+
news = self._news(f)
|
|
237
|
+
impls = [(x, "useFactory") for x in news] or [(f.get("fn") or f.get("node"), "useFactory")]
|
|
238
|
+
elif "useValue" in o:
|
|
239
|
+
v = o["useValue"]
|
|
240
|
+
impls = [(x, "useValue") for x in self.class_ids(v)] or [(None, "useValue")]
|
|
241
|
+
for x, k in impls:
|
|
242
|
+
self.providers.setdefault(tok, []).append((x, k, it))
|
|
243
|
+
st["providers"] += 1
|
|
244
|
+
ref = (o.get("provide") or {}).get("ref")
|
|
245
|
+
if ref in APP_TOKENS:
|
|
246
|
+
for x, _ in impls:
|
|
247
|
+
if x:
|
|
248
|
+
self.app_enhancers.append((APP_TOKENS[ref], x))
|
|
249
|
+
|
|
250
|
+
def _news(self, d, depth=0) -> list[str]:
|
|
251
|
+
if not isinstance(d, dict) or depth > 5:
|
|
252
|
+
return []
|
|
253
|
+
out = []
|
|
254
|
+
if "new" in d and (d.get("node") or "").startswith("class:"):
|
|
255
|
+
out.append(d["node"])
|
|
256
|
+
for k in ("body", "ret", "args", "arr", "cond"):
|
|
257
|
+
v = d.get(k)
|
|
258
|
+
for x in (v if isinstance(v, list) else [v]):
|
|
259
|
+
out += self._news(x, depth + 1)
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
def _di(self, st):
|
|
263
|
+
b = self.b
|
|
264
|
+
self.member_impl = {} # (class id, member name) -> [impl class or fn ids]
|
|
265
|
+
self.member_meta = {} # (class id, member name) -> {"queue": q, "client": bool, "emitter": bool}
|
|
266
|
+
self.unresolved_samples = []
|
|
267
|
+
for c in self.classes.values():
|
|
268
|
+
cdecos = _decos(c)
|
|
269
|
+
injectable = any(n in cdecos for n in INJECTABLE) or c["id"] in self.module_of_controller
|
|
270
|
+
if not injectable:
|
|
271
|
+
continue
|
|
272
|
+
members = [("ctor", p) for p in c.get("ctor") or []] + [("prop", p) for p in c.get("props") or [] if _decos(p).get("Inject")]
|
|
273
|
+
deps = (cdecos.get("Dependencies") or {}).get("args") or [] # JS: @Dependencies(A, B) -> constructor params
|
|
274
|
+
for idx, (where, p) in enumerate(members):
|
|
275
|
+
pd = _decos(p)
|
|
276
|
+
t = p.get("type") or {}
|
|
277
|
+
st["di_params"] += 1
|
|
278
|
+
tok, how = None, None
|
|
279
|
+
meta = {}
|
|
280
|
+
custom_inject = [n for n in pd if n.startswith("Inject") and n not in ("Inject", "InjectQueue", *ORM_INJECT)]
|
|
281
|
+
inj = (pd.get("Inject") or {}).get("args") or []
|
|
282
|
+
if inj and (inj[0] or {}).get("mod") and not str(inj[0]["mod"]).startswith(".") and not (inj[0] or {}).get("node"):
|
|
283
|
+
how = "custom-inject" # @Inject(LibraryClass): token imported from a package
|
|
284
|
+
elif inj:
|
|
285
|
+
tok, how = _token(inj[0]), "@Inject"
|
|
286
|
+
elif where == "ctor" and idx < len(deps) and _token(deps[idx]):
|
|
287
|
+
tok, how = _token(deps[idx]), "@Dependencies"
|
|
288
|
+
elif custom_inject:
|
|
289
|
+
how = "custom-inject" # @InjectDrizzle(), @InjectStripe(), ...: provided by a library module
|
|
290
|
+
elif pd.get("InjectQueue"):
|
|
291
|
+
meta["queue"] = sval((pd["InjectQueue"].get("args") or [{}])[0]) or "default"
|
|
292
|
+
how = "@InjectQueue"
|
|
293
|
+
elif any(pd.get(n) for n in ORM_INJECT):
|
|
294
|
+
how = "orm"
|
|
295
|
+
elif pd.get("Optional") and not t.get("id"):
|
|
296
|
+
how = "optional"
|
|
297
|
+
elif t.get("id"):
|
|
298
|
+
tok, how = t["id"], "type"
|
|
299
|
+
if t.get("name") == "EventEmitter2":
|
|
300
|
+
meta["emitter"] = True
|
|
301
|
+
if t.get("name") in ("ClientProxy", "ClientKafka", "ClientGrpc", "ClientRMQ", "ClientRedis", "ClientNats"):
|
|
302
|
+
meta["client"] = True
|
|
303
|
+
if meta:
|
|
304
|
+
self.member_meta[(c["id"], p["name"])] = meta
|
|
305
|
+
if how in ("@InjectQueue", "orm"):
|
|
306
|
+
st["di_resolved"] += 1
|
|
307
|
+
continue
|
|
308
|
+
if how == "custom-inject":
|
|
309
|
+
st["di_external"] += 1
|
|
310
|
+
continue
|
|
311
|
+
if tok and tok in self.client_tokens:
|
|
312
|
+
self.member_meta.setdefault((c["id"], p["name"]), {})["client"] = True
|
|
313
|
+
st["di_resolved"] += 1
|
|
314
|
+
continue
|
|
315
|
+
if not tok:
|
|
316
|
+
if t.get("mod") or (t.get("name") and not t.get("id")) or how == "optional":
|
|
317
|
+
st["di_external"] += 1 # library provider (ConfigService, JwtService, ...): not in this repo
|
|
318
|
+
else:
|
|
319
|
+
st["di_unresolved"] += 1
|
|
320
|
+
self.unresolved_samples.append(f"{c.get('name')}.{p['name']}: {t.get('text')}")
|
|
321
|
+
continue
|
|
322
|
+
impls = [x for x, _, _ in self.providers.get(tok, []) if x]
|
|
323
|
+
kind = "provider"
|
|
324
|
+
if not impls and tok.startswith("class:") and tok in self.classes:
|
|
325
|
+
impls, kind = [tok], "class-token"
|
|
326
|
+
if not impls and self.providers.get(tok):
|
|
327
|
+
st["di_resolved"] += 1 # useValue / factory without a class: resolved, nothing to link
|
|
328
|
+
continue
|
|
329
|
+
if not impls:
|
|
330
|
+
if tok.startswith("type:"):
|
|
331
|
+
st["di_unresolved"] += 1
|
|
332
|
+
self.unresolved_samples.append(f"{c.get('name')}.{p['name']}: interface {t.get('text')} without @Inject token")
|
|
333
|
+
elif tok.startswith(("str:", "key:", "ref:")):
|
|
334
|
+
st["di_unresolved"] += 1
|
|
335
|
+
self.unresolved_samples.append(f"{c.get('name')}.{p['name']}: token {tok} has no provider")
|
|
336
|
+
else:
|
|
337
|
+
st["di_external"] += 1
|
|
338
|
+
continue
|
|
339
|
+
st["di_resolved"] += 1
|
|
340
|
+
self.member_impl[(c["id"], p["name"])] = impls
|
|
341
|
+
for x in impls:
|
|
342
|
+
if b.has(c["id"]) and b.has(x) and x != c["id"]:
|
|
343
|
+
b.add_edge(c["id"], x, "INJECTS", file=c["file"], line=p.get("line") or c["line"],
|
|
344
|
+
confidence="exact" if kind == "class-token" and how == "type" else "resolved",
|
|
345
|
+
token=tok.split(":", 1)[1], member=p["name"], via=how)
|
|
346
|
+
# abstract class token bound to a concrete provider: dispatch its methods
|
|
347
|
+
if tok.startswith("class:") and tok in self.classes:
|
|
348
|
+
for x in impls:
|
|
349
|
+
if x == tok or x not in self.classes:
|
|
350
|
+
continue
|
|
351
|
+
for m in self.classes[tok].get("methods") or []:
|
|
352
|
+
impl_m = self.method_of(x, m["name"])
|
|
353
|
+
if m.get("id") and impl_m and impl_m != m["id"]:
|
|
354
|
+
b.add_edge(m["id"], impl_m, "BOUND_TO", file=c["file"], line=p.get("line") or c["line"], confidence="resolved", via="nest-provider")
|
|
355
|
+
# calls through injected members: this.<member>.<method>()
|
|
356
|
+
existing = {(e.src, e.dst, e.line) for e in b.edges.values() if e.kind == "CALLS"}
|
|
357
|
+
for mc in self.F.get("member_calls") or []:
|
|
358
|
+
if not mc.get("prop") or not mc.get("src") or not b.has(mc["src"]):
|
|
359
|
+
continue
|
|
360
|
+
cls = mc.get("cls")
|
|
361
|
+
impls = None
|
|
362
|
+
for _ in range(6):
|
|
363
|
+
if not cls:
|
|
364
|
+
break
|
|
365
|
+
impls = self.member_impl.get((cls, mc["prop"]))
|
|
366
|
+
if impls:
|
|
367
|
+
break
|
|
368
|
+
cls = self.supers.get(cls)
|
|
369
|
+
for x in impls or []:
|
|
370
|
+
tgt = self.method_of(x, mc["method"]) if x in self.classes else (x if mc["method"] in ("call", "apply") else None)
|
|
371
|
+
if tgt and (mc["src"], tgt, mc["line"]) not in existing:
|
|
372
|
+
b.add_edge(mc["src"], tgt, "CALLS", file=mc["file"], line=mc["line"], confidence="resolved", via=["nest-di"], member=mc["prop"])
|
|
373
|
+
st["di_calls"] += 1
|
|
374
|
+
|
|
375
|
+
# ------------------------------------------------------------ routes
|
|
376
|
+
def _app_config(self):
|
|
377
|
+
prefix, exclude, versioning = "", [], None
|
|
378
|
+
enhancers = list(self.app_enhancers)
|
|
379
|
+
for call in self.F.get("calls") or []:
|
|
380
|
+
m = call["method"]
|
|
381
|
+
args = call.get("args") or []
|
|
382
|
+
if m == "setGlobalPrefix" and args and sval(args[0]) is not None:
|
|
383
|
+
prefix = sval(args[0])
|
|
384
|
+
for e in (obj(args[1]).get("exclude") or {}).get("arr") or [] if len(args) > 1 else []:
|
|
385
|
+
p = sval(e) or sval(obj(e).get("path"))
|
|
386
|
+
if p is not None:
|
|
387
|
+
exclude.append(express_path(p))
|
|
388
|
+
elif m == "enableVersioning":
|
|
389
|
+
o = obj(args[0]) if args else {}
|
|
390
|
+
typ = (o.get("type") or {}).get("ref") or ""
|
|
391
|
+
versioning = {"type": "uri" if "URI" in typ or not typ else typ.split(".")[-1].lower(),
|
|
392
|
+
"prefix": "v" if "prefix" not in o else (sval(o["prefix"]) or ""),
|
|
393
|
+
"default": svals(o.get("defaultVersion")) or ([None] if o.get("defaultVersion") else [])}
|
|
394
|
+
elif m in GLOBAL_ENHANCERS:
|
|
395
|
+
for a in args:
|
|
396
|
+
for cid in self.class_ids(a):
|
|
397
|
+
enhancers.append((GLOBAL_ENHANCERS[m], cid))
|
|
398
|
+
return prefix, exclude, versioning, enhancers
|
|
399
|
+
|
|
400
|
+
def _enhancer_refs(self, decos: list) -> list[tuple[str, str, str | None]]:
|
|
401
|
+
"""[(role, name, class id)] from @UseGuards/... decorators."""
|
|
402
|
+
out = []
|
|
403
|
+
for d in decos:
|
|
404
|
+
n = last_name(d["name"])
|
|
405
|
+
if n not in ENHANCERS:
|
|
406
|
+
continue
|
|
407
|
+
for a in d.get("args") or []:
|
|
408
|
+
cids = self.class_ids(a)
|
|
409
|
+
name = (self.b.nodes[cids[0]].name if cids and self.b.has(cids[0]) else (a or {}).get("ref") or (a or {}).get("call") or (a or {}).get("new") or "?")
|
|
410
|
+
out.append((n, str(name).split("#")[-1], cids[0] if cids else None))
|
|
411
|
+
return out
|
|
412
|
+
|
|
413
|
+
def _dto_fields(self, cid: str | None) -> list[str] | None:
|
|
414
|
+
c = self.classes.get(cid)
|
|
415
|
+
if not c:
|
|
416
|
+
return None
|
|
417
|
+
out = []
|
|
418
|
+
seen = set()
|
|
419
|
+
while c and len(seen) < 6:
|
|
420
|
+
seen.add(c["id"])
|
|
421
|
+
for p in c.get("props") or []:
|
|
422
|
+
if not p.get("static") and p["name"] not in out:
|
|
423
|
+
out.append(p["name"] + ("?" if p.get("optional") else ""))
|
|
424
|
+
ext = c.get("extends") or {}
|
|
425
|
+
nxt = ext.get("id")
|
|
426
|
+
if not nxt and ext.get("call"): # PartialType(CreateDto) / PickType(...)
|
|
427
|
+
ids = [x for a in ext["call"].get("args") or [] for x in self.class_ids(a)]
|
|
428
|
+
nxt = ids[0] if ids else None
|
|
429
|
+
c = self.classes.get(nxt) if nxt not in seen else None
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
def _routes(self, st):
|
|
433
|
+
b = self.b
|
|
434
|
+
prefix, exclude, versioning, global_enh = self._app_config()
|
|
435
|
+
self.global_prefix = prefix
|
|
436
|
+
st["global_prefix"] = prefix or None
|
|
437
|
+
st["versioning"] = versioning
|
|
438
|
+
for c in self.classes.values():
|
|
439
|
+
cd = _decos(c)
|
|
440
|
+
ctl = cd.get("Controller")
|
|
441
|
+
if not ctl or not _is_nest(ctl):
|
|
442
|
+
if cd.get("Resolver") and _is_nest(cd["Resolver"]):
|
|
443
|
+
self._graphql(c, st)
|
|
444
|
+
continue
|
|
445
|
+
st["controllers"] += 1
|
|
446
|
+
a0 = (ctl.get("args") or [None])[0]
|
|
447
|
+
cpaths = svals(a0) or svals(obj(a0).get("path")) or [""]
|
|
448
|
+
cver = svals(obj(a0).get("version")) if a0 else []
|
|
449
|
+
mod = self.module_of_controller.get(c["id"])
|
|
450
|
+
mpath = self.module_paths.get(mod, "") if mod else ""
|
|
451
|
+
cenh = self._enhancer_refs(c.get("decorators") or [])
|
|
452
|
+
for m in c.get("methods") or []:
|
|
453
|
+
md = _decos(m)
|
|
454
|
+
hits = [(n, d) for n, d in md.items() if n in ROUTE_DECOS and _is_nest(d)]
|
|
455
|
+
if not hits or not m.get("id"):
|
|
456
|
+
continue
|
|
457
|
+
menh = self._enhancer_refs(m.get("decorators") or [])
|
|
458
|
+
mver = svals((md.get("Version") or {}).get("args", [None])[0]) if md.get("Version") else []
|
|
459
|
+
versions = mver or cver or ((versioning or {}).get("default") or [None])
|
|
460
|
+
enh = [(r, n, x) for r, n, x in global_enh_named(b, global_enh)] + cenh + menh
|
|
461
|
+
attrs = {"controller": c.get("name"), "action": m["name"]}
|
|
462
|
+
for role_deco, (role, _) in ENHANCERS.items():
|
|
463
|
+
names = [n for r, n, _ in enh if r == role_deco]
|
|
464
|
+
if names:
|
|
465
|
+
attrs[role] = names
|
|
466
|
+
custom = [d["name"] for d in m.get("decorators") or [] if last_name(d["name"]) not in ROUTE_DECOS and last_name(d["name"]) not in ENHANCERS and last_name(d["name"]) not in ("Version", "HttpCode", "Header", "Redirect", "Render")]
|
|
467
|
+
if custom:
|
|
468
|
+
attrs["decorators"] = custom
|
|
469
|
+
for p in m.get("params") or []:
|
|
470
|
+
pd = _decos(p)
|
|
471
|
+
if "Body" in pd:
|
|
472
|
+
key = sval((pd["Body"].get("args") or [None])[0])
|
|
473
|
+
if key:
|
|
474
|
+
attrs.setdefault("body_fields", []).append(key)
|
|
475
|
+
elif (p.get("type") or {}).get("id"):
|
|
476
|
+
attrs["body_dto"] = (p["type"].get("name") or p["type"]["text"])
|
|
477
|
+
attrs["body_fields"] = self._dto_fields(p["type"]["id"])
|
|
478
|
+
if "Query" in pd:
|
|
479
|
+
key = sval((pd["Query"].get("args") or [None])[0])
|
|
480
|
+
if key:
|
|
481
|
+
attrs.setdefault("query_fields", []).append(key)
|
|
482
|
+
elif (p.get("type") or {}).get("id"):
|
|
483
|
+
attrs["query_dto"] = p["type"].get("name")
|
|
484
|
+
attrs["query_fields"] = self._dto_fields(p["type"]["id"])
|
|
485
|
+
mw = []
|
|
486
|
+
for role_deco, name, cid in enh:
|
|
487
|
+
hook = ENHANCERS[role_deco][1]
|
|
488
|
+
tgt = self.method_of(cid, hook) if cid else None
|
|
489
|
+
mw.append((tgt, name, "resolved"))
|
|
490
|
+
for n, d in hits:
|
|
491
|
+
method = ROUTE_DECOS[n]
|
|
492
|
+
for mp in (svals((d.get("args") or [None])[0]) or [""]) if d.get("args") else [""]:
|
|
493
|
+
for cp in cpaths:
|
|
494
|
+
for v in versions:
|
|
495
|
+
vseg = ""
|
|
496
|
+
if versioning and versioning["type"] == "uri" and v and not str(v).startswith("VERSION_NEUTRAL"):
|
|
497
|
+
vseg = f"{versioning['prefix']}{v}"
|
|
498
|
+
local = join_path(mpath, cp, mp)
|
|
499
|
+
excluded = express_path(local) in exclude
|
|
500
|
+
uri = express_path(join_path("" if excluded else prefix, vseg, mpath, cp, mp))
|
|
501
|
+
ra = dict(attrs)
|
|
502
|
+
if v:
|
|
503
|
+
ra["version"] = v
|
|
504
|
+
if mpath:
|
|
505
|
+
ra["router_module_path"] = mpath
|
|
506
|
+
add_route(b, method, uri, [m["id"]], c["file"], d.get("line") or m["line"], "nest", "exact", ra, mw)
|
|
507
|
+
st["routes"] += 1
|
|
508
|
+
|
|
509
|
+
def _graphql(self, c, st):
|
|
510
|
+
for m in c.get("methods") or []:
|
|
511
|
+
md = _decos(m)
|
|
512
|
+
for n in ("Query", "Mutation", "Subscription", "ResolveField"):
|
|
513
|
+
d = md.get(n)
|
|
514
|
+
if not d or not (d.get("mod") or "").startswith("@nestjs/graphql") or not m.get("id"):
|
|
515
|
+
continue
|
|
516
|
+
a = d.get("args") or []
|
|
517
|
+
name = next((sval(obj(x).get("name")) for x in a if sval(obj(x).get("name"))), None) or (sval(a[0]) if a and sval(a[0]) else m["name"])
|
|
518
|
+
key = f"GRAPHQL {n}.{name}"
|
|
519
|
+
rid = self.b.add_node("route", key, name=key, file=c["file"], line=d.get("line") or m["line"], module=module_of(c["file"]), lang="ts",
|
|
520
|
+
entry_kind="http_route", attrs={"graphql": n, "field": name, "framework": "nest", "resolver": c.get("name")})
|
|
521
|
+
self.b.add_edge(rid, m["id"], "ROUTES_TO", file=c["file"], line=d.get("line") or m["line"], confidence="exact")
|
|
522
|
+
st["graphql_ops"] += 1
|
|
523
|
+
|
|
524
|
+
# ------------------------------------------------------------ other entry points
|
|
525
|
+
def _entries(self, st):
|
|
526
|
+
b = self.b
|
|
527
|
+
events, messages, jobs = {}, {}, {}
|
|
528
|
+
for c in self.classes.values():
|
|
529
|
+
cd = _decos(c)
|
|
530
|
+
proc = cd.get("Processor") if cd.get("Processor") and _is_nest(cd["Processor"]) and "microservices" not in (cd["Processor"].get("mod") or "") else None
|
|
531
|
+
queue = None
|
|
532
|
+
if proc:
|
|
533
|
+
a0 = (proc.get("args") or [None])[0]
|
|
534
|
+
queue = sval(a0) or sval(obj(a0).get("name")) or "default"
|
|
535
|
+
gw = cd.get("WebSocketGateway")
|
|
536
|
+
ns = None
|
|
537
|
+
if gw:
|
|
538
|
+
for a in gw.get("args") or []:
|
|
539
|
+
ns = ns or sval(obj(a).get("namespace"))
|
|
540
|
+
cmd = cd.get("Command") or cd.get("SubCommand")
|
|
541
|
+
if cmd and (cmd.get("mod") or "").startswith("nest-commander") or (cmd and cmd.get("mod") is None and any((x.get("text") or "").startswith("CommandRunner") for x in [c.get("extends") or {}])):
|
|
542
|
+
o = obj((cmd.get("args") or [None])[0])
|
|
543
|
+
name = sval(o.get("name")) or c.get("name")
|
|
544
|
+
run = self.method_of(c["id"], "run")
|
|
545
|
+
cid = b.add_node("command", f"nest:{name}", name=name, file=c["file"], line=cmd.get("line") or c["line"], module=module_of(c["file"]),
|
|
546
|
+
lang="ts", entry_kind="cli_command", attrs={"class": c.get("name"), "framework": "nest-commander", "description": sval(o.get("description"))})
|
|
547
|
+
if run:
|
|
548
|
+
b.add_edge(cid, run, "HANDLED_BY", file=c["file"], line=cmd.get("line") or c["line"], confidence="exact")
|
|
549
|
+
st["commands"] += 1
|
|
550
|
+
if proc and "WorkerHost" in ((c.get("extends") or {}).get("text") or ""):
|
|
551
|
+
h = self.method_of(c["id"], "process")
|
|
552
|
+
jid = b.add_node("job", f"{queue}", name=f"queue {queue}", file=c["file"], line=proc.get("line") or c["line"], module=module_of(c["file"]),
|
|
553
|
+
lang="ts", entry_kind="queue_job", attrs={"queue": queue, "framework": "bullmq", "class": c.get("name")})
|
|
554
|
+
if h:
|
|
555
|
+
b.add_edge(jid, h, "HANDLED_BY", file=c["file"], line=proc.get("line") or c["line"], confidence="exact")
|
|
556
|
+
jobs.setdefault(queue, {})[None] = jid
|
|
557
|
+
st["jobs"] += 1
|
|
558
|
+
for m in c.get("methods") or []:
|
|
559
|
+
if not m.get("id"):
|
|
560
|
+
continue
|
|
561
|
+
md = _decos(m)
|
|
562
|
+
line = m["line"]
|
|
563
|
+
for n in ("Cron", "Interval", "Timeout"):
|
|
564
|
+
d = md.get(n)
|
|
565
|
+
if d and _is_nest(d):
|
|
566
|
+
a = d.get("args") or []
|
|
567
|
+
expr = sval(a[0]) if a and sval(a[0]) is not None else ((a[0] or {}).get("ref") if a else None) or (str(a[0].get("n")) if a and a[0] and a[0].get("n") is not None else None)
|
|
568
|
+
if n != "Cron" and len(a) > 1:
|
|
569
|
+
expr = str((a[1] or {}).get("n") or (a[1] or {}).get("ref"))
|
|
570
|
+
sid = b.add_node("schedule", f"{m['id'].split(':', 1)[1]}@{n}", name=f"{n.lower()} {expr} {c.get('name')}.{m['name']}",
|
|
571
|
+
file=c["file"], line=d.get("line") or line, module=module_of(c["file"]), lang="ts", entry_kind="scheduled",
|
|
572
|
+
attrs={"trigger": n, "expr": expr, "framework": "nest"})
|
|
573
|
+
b.add_edge(sid, m["id"], "SCHEDULES", file=c["file"], line=d.get("line") or line, confidence="exact")
|
|
574
|
+
st["schedules"] += 1
|
|
575
|
+
if proc and md.get("Process"):
|
|
576
|
+
d = md["Process"]
|
|
577
|
+
a0 = (d.get("args") or [None])[0]
|
|
578
|
+
jname = sval(a0) or sval(obj(a0).get("name"))
|
|
579
|
+
jid = b.add_node("job", f"{queue}:{jname}" if jname else queue, name=f"queue {queue}" + (f" job {jname}" if jname else ""),
|
|
580
|
+
file=c["file"], line=d.get("line") or line, module=module_of(c["file"]), lang="ts", entry_kind="queue_job",
|
|
581
|
+
attrs={"queue": queue, "job": jname, "framework": "bull"})
|
|
582
|
+
b.add_edge(jid, m["id"], "HANDLED_BY", file=c["file"], line=d.get("line") or line, confidence="exact")
|
|
583
|
+
jobs.setdefault(queue, {})[jname] = jid
|
|
584
|
+
st["jobs"] += 1
|
|
585
|
+
if md.get("OnEvent"):
|
|
586
|
+
d = md["OnEvent"]
|
|
587
|
+
lid = b.add_node("listener", m["id"].split(":", 1)[1], name=f"{c.get('name')}.{m['name']}", file=c["file"], line=d.get("line") or line,
|
|
588
|
+
module=module_of(c["file"]), lang="ts", entry_kind="listener", attrs={"framework": "nest-event-emitter"})
|
|
589
|
+
b.add_edge(lid, m["id"], "HANDLED_BY", file=c["file"], line=d.get("line") or line, confidence="exact")
|
|
590
|
+
a0 = (d.get("args") or [None])[0]
|
|
591
|
+
# `@OnEvent('a.b')`, `@OnEvent(['a', 'b'])`, a custom `@OnEvent({ name: 'X', ... })` (SetMetadata)
|
|
592
|
+
for ev in svals(a0) or svals(obj(a0).get("name")) or [(a0 or {}).get("ref") or "?"]:
|
|
593
|
+
eid = b.add_node("event", ev, name=ev, lang="ts", attrs={"framework": "nest-event-emitter"})
|
|
594
|
+
b.add_edge(eid, m["id"], "LISTENED_BY", file=c["file"], line=d.get("line") or line, confidence="exact")
|
|
595
|
+
events.setdefault(ev, eid)
|
|
596
|
+
st["listeners"] += 1
|
|
597
|
+
for n, transport in (("MessagePattern", "rpc"), ("EventPattern", "event"), ("SubscribeMessage", "ws"), ("GrpcMethod", "grpc")):
|
|
598
|
+
d = md.get(n)
|
|
599
|
+
if not d or not _is_nest(d):
|
|
600
|
+
continue
|
|
601
|
+
# @UseGuards on the class / handler and APP_GUARD providers (#69); app.useGlobalGuards() binds the
|
|
602
|
+
# HTTP app only, not microservice / gateway handlers
|
|
603
|
+
guards = [nm for r, nm, _ in list(global_enh_named(b, self.app_enhancers))
|
|
604
|
+
+ self._enhancer_refs(c.get("decorators") or []) + self._enhancer_refs(m.get("decorators") or [])
|
|
605
|
+
if r == "UseGuards"]
|
|
606
|
+
a = d.get("args") or []
|
|
607
|
+
pat = _pattern(a[0]) if a else m["name"]
|
|
608
|
+
if n == "GrpcMethod":
|
|
609
|
+
pat = ".".join(x for x in [sval(a[0]) if a else c.get("name"), sval(a[1]) if len(a) > 1 else m["name"]] if x)
|
|
610
|
+
key = f"{transport}:{ns + ':' if ns and transport == 'ws' else ''}{pat}"
|
|
611
|
+
mid = b.add_node("message", key, name=f"{transport} {pat}", file=c["file"], line=d.get("line") or line, module=module_of(c["file"]),
|
|
612
|
+
lang="ts", entry_kind="message_handler", attrs={"transport": transport, "pattern": pat, "namespace": ns, "framework": "nest",
|
|
613
|
+
"guards": list(dict.fromkeys(guards))})
|
|
614
|
+
b.add_edge(mid, m["id"], "HANDLED_BY", file=c["file"], line=d.get("line") or line, confidence="exact")
|
|
615
|
+
messages.setdefault(pat, []).append(mid)
|
|
616
|
+
st["messages"] += 1
|
|
617
|
+
# producers
|
|
618
|
+
for mc in self.F.get("member_calls") or []:
|
|
619
|
+
if not mc.get("prop") or not mc.get("src") or not b.has(mc["src"]):
|
|
620
|
+
continue
|
|
621
|
+
meta = None
|
|
622
|
+
cls = mc.get("cls")
|
|
623
|
+
for _ in range(6):
|
|
624
|
+
if not cls:
|
|
625
|
+
break
|
|
626
|
+
meta = self.member_meta.get((cls, mc["prop"]))
|
|
627
|
+
if meta:
|
|
628
|
+
break
|
|
629
|
+
cls = self.supers.get(cls)
|
|
630
|
+
meta = meta or {}
|
|
631
|
+
a0 = mc.get("a0")
|
|
632
|
+
if meta.get("queue") and mc["method"] in ("add", "addBulk"):
|
|
633
|
+
q = jobs.get(meta["queue"], {})
|
|
634
|
+
tgt = q.get(sval(a0)) or q.get(None)
|
|
635
|
+
if tgt:
|
|
636
|
+
b.add_edge(mc["src"], tgt, "DISPATCHES", file=mc["file"], line=mc["line"], confidence="resolved", queue=meta["queue"], job=sval(a0))
|
|
637
|
+
elif mc["method"] in ("emit", "emitAsync") and sval(a0) and (meta.get("emitter") or re.search(r"event", mc["prop"], re.I)) and not meta.get("client"):
|
|
638
|
+
ev = sval(a0)
|
|
639
|
+
eid = events.get(ev) or b.add_node("event", ev, name=ev, lang="ts", attrs={"framework": "nest-event-emitter"})
|
|
640
|
+
b.add_edge(mc["src"], eid, "DISPATCHES", file=mc["file"], line=mc["line"], confidence="resolved")
|
|
641
|
+
elif mc["method"] in ("send", "emit") and (meta.get("client") or re.search(r"client|proxy", mc["prop"], re.I)) and a0:
|
|
642
|
+
pat = _pattern(a0)
|
|
643
|
+
targets = messages.get(pat, [])
|
|
644
|
+
if not targets and pat: # handled by another service: an outgoing message node (same id as a handler's)
|
|
645
|
+
transport = "event" if mc["method"] == "emit" else "rpc"
|
|
646
|
+
targets = [b.add_node("message", f"{transport}:{pat}", name=pat, lang="ts",
|
|
647
|
+
attrs={"transport": transport, "pattern": pat, "framework": "nest", "outgoing": True})]
|
|
648
|
+
for mid in targets:
|
|
649
|
+
b.add_edge(mc["src"], mid, "DISPATCHES", file=mc["file"], line=mc["line"], confidence="resolved", pattern=pat)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def global_enh_named(b, enh):
|
|
653
|
+
for role, cid in enh:
|
|
654
|
+
yield role, (b.nodes[cid].name.split("#")[-1] if b.has(cid) else cid), cid
|