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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. 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