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,290 @@
1
+ """Shared helpers for the TypeScript server-framework layers (NestJS, Next.js, Express/Fastify/Koa/Hono).
2
+
3
+ All three sit on the TypeScript language plugin. The extractor's `fw` facts (extractor/fw.mjs) describe decorators,
4
+ router calls, exports and data-access calls as JSON values; these helpers turn them into graph nodes and edges:
5
+
6
+ * route paths in one notation (`{param}`, `{rest*}` catch-all, `{rest*?}` optional catch-all) so `link` can match
7
+ client calls against any backend,
8
+ * route nodes (`route:<METHOD> <uri>`, entry kind http_route) with ROUTES_TO / USES_MIDDLEWARE evidence,
9
+ * the post-pass shared by all TS server frameworks (env/config keys, ORM tables, in-repo client -> route links),
10
+ run once after the last framework contributed.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ from pathlib import Path
17
+
18
+ from ... import presets
19
+ from ...core.plugin import GraphBuilder, Project
20
+
21
+ # package.json dependencies that mark a TS/JS server framework (any of them enables the TS plugin, even without a
22
+ # tsconfig.json: plain-JS projects are indexed with allowJs)
23
+ SERVER_DEPS = {
24
+ "nest": ("@nestjs/core", "@nestjs/common"),
25
+ "next": ("next",),
26
+ "express": ("express", "koa", "@koa/router", "koa-router", "fastify", "hono", "@hono/node-server", "restify", "polka", "h3", "elysia"),
27
+ }
28
+ # test trees: anywhere for the unambiguous names; `test/`, `tests/`, `e2e/` only at the top (or under src/), since
29
+ # deeper directories with those names are often real route segments (app/api/test/route.ts)
30
+ # build output (codegraph/presets/typescript.yaml build_dirs) and test tooling directories
31
+ TEST_TOOL_DIRS = ("__tests__", "__mocks__", "__fixtures__", "cypress", "playwright", ".storybook")
32
+ TEST_SKIP_RE = (r"(^|/)(" + "|".join(re.escape(d) for d in sorted(presets.skip_dirs("typescript", "build_dirs"))
33
+ + list(TEST_TOOL_DIRS)) + r")(/|$)"
34
+ r"|^(src/)?(test|tests|e2e|spec)(/|$)"
35
+ r"|\.(test|spec|e2e-spec|stories)\.(t|j)sx?$")
36
+ HTTP_VERBS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
37
+
38
+
39
+ def pkg_deps(root: Path) -> dict:
40
+ p = Path(root) / "package.json"
41
+ if not p.exists():
42
+ return {}
43
+ try:
44
+ d = json.loads(p.read_text())
45
+ except Exception:
46
+ return {}
47
+ return {**(d.get("dependencies") or {}), **(d.get("devDependencies") or {}), **(d.get("peerDependencies") or {})}
48
+
49
+
50
+ def has_server_framework(project: Project, which: str | None = None) -> bool:
51
+ deps = pkg_deps(project.root)
52
+ names = SERVER_DEPS[which] if which else [d for v in SERVER_DEPS.values() for d in v]
53
+ return any(n in deps for n in names)
54
+
55
+
56
+ def merge_extractor_cfg(cfg: dict, src_dirs=None, skip_re: str | None = None, extra_files=None, allow_js=False, walk_src=False):
57
+ """Several TS framework plugins may configure the one extractor run: union of source dirs, OR of skip regexes."""
58
+ if src_dirs:
59
+ cur = cfg.get("src_dirs") or []
60
+ cfg["src_dirs"] = list(dict.fromkeys([*cur, *src_dirs]))
61
+ if skip_re:
62
+ cfg["skip_re"] = f"(?:{cfg['skip_re']})|(?:{skip_re})" if cfg.get("skip_re") and skip_re not in cfg["skip_re"] else skip_re
63
+ if extra_files:
64
+ cfg["extra_files"] = list(dict.fromkeys([*(cfg.get("extra_files") or []), *extra_files]))
65
+ if allow_js:
66
+ cfg["allow_js"] = True
67
+ if walk_src:
68
+ cfg["walk_src"] = True
69
+
70
+
71
+ # ---------------------------------------------------------------- paths
72
+ def join_path(*parts: str | None) -> str:
73
+ segs = []
74
+ for p in parts:
75
+ if not p:
76
+ continue
77
+ segs += [s for s in str(p).split("/") if s]
78
+ return "/" + "/".join(segs)
79
+
80
+
81
+ _COLON = re.compile(r":(\w+)(\([^)]*\))?([?*+])?")
82
+
83
+
84
+ def express_path(p: str) -> str:
85
+ """Express / Koa / Fastify / Hono / NestJS path -> uri template: `:id` -> `{id}`, `:id?` -> `{id?}`,
86
+ `:id(\\d+)` -> `{id}`, catch-alls `:p+` / `*p` -> `{p*}` (one or more segments), `*` / `:p*` / `{*p}` -> `{p*?}`
87
+ (zero or more)."""
88
+ if p is None:
89
+ return ""
90
+ out = []
91
+ for seg in str(p).split("/"):
92
+ if not seg:
93
+ continue
94
+ if seg in ("*", "(.*)", "(.*)*"):
95
+ out.append("{wildcard*?}")
96
+ continue
97
+ m = re.fullmatch(r"\{/?\*(\w+)\}", seg) # Express 5 optional wildcard {*rest}: zero or more
98
+ if m:
99
+ out.append("{%s*?}" % m.group(1))
100
+ continue
101
+ m = re.fullmatch(r"\*(\w+)", seg) # Express 5 wildcard *rest: one or more
102
+ if m:
103
+ out.append("{%s*}" % m.group(1))
104
+ continue
105
+ m = re.fullmatch(r"\{/?:(\w+)\}", seg) # Express 5 optional `{:id}`
106
+ if m:
107
+ out.append("{%s?}" % m.group(1))
108
+ continue
109
+
110
+ def sub(m):
111
+ name, mod = m.group(1), m.group(3)
112
+ if mod == "*":
113
+ return "{%s*?}" % name # path-to-regexp `:p*` zero or more, `:p+` one or more
114
+ if mod == "+":
115
+ return "{%s*}" % name
116
+ return "{%s?}" % name if mod == "?" else "{%s}" % name
117
+ out.append(_COLON.sub(sub, seg))
118
+ return "/" + "/".join(out)
119
+
120
+
121
+ def next_segment(seg: str) -> str | None:
122
+ """Next.js app/pages directory segment -> uri segment ('' = not part of the URL, None = private folder)."""
123
+ if seg.startswith("_"):
124
+ return None
125
+ if re.fullmatch(r"\(.*\)", seg) or seg.startswith("@"):
126
+ return "" # route group / parallel-route slot
127
+ m = re.fullmatch(r"\[\[\.\.\.(\w+)\]\]", seg)
128
+ if m:
129
+ return "{%s*?}" % m.group(1)
130
+ m = re.fullmatch(r"\[\.\.\.(\w+)\]", seg)
131
+ if m:
132
+ return "{%s*}" % m.group(1)
133
+ return re.sub(r"\[(\w+)\]", r"{\1}", seg)
134
+
135
+
136
+ # ---------------------------------------------------------------- values described by fw.mjs
137
+ def sval(d) -> str | None:
138
+ """String value of a described expression ({s}), else None."""
139
+ return d.get("s") if isinstance(d, dict) and isinstance(d.get("s"), str) else None
140
+
141
+
142
+ def svals(d) -> list[str]:
143
+ if not isinstance(d, dict):
144
+ return []
145
+ if isinstance(d.get("s"), str):
146
+ return [d["s"]]
147
+ if d.get("ss"):
148
+ return list(d["ss"])
149
+ if d.get("arr") is not None:
150
+ return [x for e in d["arr"] for x in svals(e)]
151
+ return []
152
+
153
+
154
+ def obj(d) -> dict:
155
+ return d.get("obj") or {} if isinstance(d, dict) else {}
156
+
157
+
158
+ def last_name(name: str | None) -> str:
159
+ return (name or "").split(".")[-1].split("(")[0]
160
+
161
+
162
+ def ref_nodes(d, depth=0) -> list[str]:
163
+ """Code nodes a described value points at (function/method/class), through wrappers and handler objects."""
164
+ if not isinstance(d, dict) or depth > 4:
165
+ return []
166
+ if d.get("fn"):
167
+ return [d["fn"]]
168
+ if d.get("node") and d.get("ref") is not None:
169
+ return [d["node"]]
170
+ if d.get("obj_fns"):
171
+ return list(d["obj_fns"])
172
+ if "call" in d:
173
+ out = []
174
+ if last_name(d["call"]) == "bind" and d.get("recv"):
175
+ return ref_nodes(d["recv"], depth + 1)
176
+ for a in d.get("args") or []:
177
+ out += ref_nodes(a, depth + 1)
178
+ return out
179
+ return []
180
+
181
+
182
+ def snake(name: str) -> str:
183
+ s = re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name)
184
+ s = re.sub(r"(?<=[A-Z])([A-Z][a-z])", r"_\1", s)
185
+ return s.lower()
186
+
187
+
188
+ def plural(word: str) -> str:
189
+ if re.search(r"(s|x|z|ch|sh)$", word):
190
+ return word + "es"
191
+ if re.search(r"[^aeiou]y$", word):
192
+ return word[:-1] + "ies"
193
+ return word + "s"
194
+
195
+
196
+ def module_of(path: str | None) -> str | None:
197
+ from ..ts.plugin import module_of as m
198
+ return m(path)
199
+
200
+
201
+ # ---------------------------------------------------------------- routes
202
+ def add_route(b: GraphBuilder, method: str, uri: str, handlers: list[str], file: str, line: int, framework: str,
203
+ confidence: str = "exact", attrs: dict | None = None, middleware: list[tuple[str, str, str]] | None = None) -> str:
204
+ """route:<METHOD> <uri>, entry kind http_route; ROUTES_TO each handler; USES_MIDDLEWARE (node, name, conf)."""
205
+ method = method.upper()
206
+ key = f"{method} {uri}"
207
+ a = {"uri": uri, "method": method, "framework": framework, **(attrs or {})}
208
+ if middleware:
209
+ a["middleware"] = list(dict.fromkeys([m[1] for m in middleware if m[1]]))
210
+ rid = b.add_node("route", key, name=key, file=file, line=line, module=module_of(file), lang="ts", entry_kind="http_route", attrs=a)
211
+ n = b.nodes[rid]
212
+ if n.entry_kind is None:
213
+ n.entry_kind = "http_route"
214
+ for h in handlers:
215
+ if h and b.has(h):
216
+ b.add_edge(rid, h, "ROUTES_TO", file=file, line=line, confidence=confidence)
217
+ hn = b.nodes[h]
218
+ if hn.file and not n.attrs.get("handler"):
219
+ n.attrs["handler"] = hn.name
220
+ n.module = module_of(hn.file)
221
+ for node, name, conf in middleware or []:
222
+ if node and b.has(node):
223
+ b.add_edge(rid, node, "USES_MIDDLEWARE", file=file, line=line, confidence=conf, name=name)
224
+ return rid
225
+
226
+
227
+ def route_specs(b: GraphBuilder) -> list[dict]:
228
+ """Route nodes in the shape link.match_endpoint expects."""
229
+ out = []
230
+ for n in b.nodes.values():
231
+ if n.kind != "route" or n.lang != "ts":
232
+ continue
233
+ uri, method = n.attrs.get("uri"), n.attrs.get("method")
234
+ if not uri or not method:
235
+ continue
236
+ uris = [("as-declared", uri)] + [(f"variant:{v}", v) for v in n.attrs.get("uri_variants") or []]
237
+ out.append({"id": n.id, "uri": uri, "method": method, "uris": uris, "file": n.file, "line": n.line})
238
+ return out
239
+
240
+
241
+ def link_in_repo(b: GraphBuilder, ctx) -> dict:
242
+ """Client HTTP calls in the same repo (Next.js pages/components, a bundled SPA) -> this repo's routes, with the
243
+ same deterministic matcher `cg link` uses across repos. Same-origin relative URLs are in scope here."""
244
+ from ...link import match_endpoint
245
+ from ...core.model import CONFIDENCE_RANK
246
+ routes = route_specs(b)
247
+ st = {"endpoints": 0, "matched": 0}
248
+ if not routes:
249
+ return st
250
+ rmap = {r["id"]: r for r in routes}
251
+ for n in list(b.nodes.values()):
252
+ if n.kind != "http" or n.lang != "ts":
253
+ continue
254
+ a = n.attrs
255
+ ok = a.get("origin_kind", "api")
256
+ if ok == "other":
257
+ continue
258
+ st["endpoints"] += 1
259
+ res = match_endpoint(a.get("method", "GET"), a.get("path", ""), routes, "api" if ok == "same-origin" else ok, a.get("origin"))
260
+ for m in res["matched"]:
261
+ r = rmap[m["route"]]
262
+ b.add_edge(n.id, m["route"], "MATCHES_ROUTE", file=r["file"], line=r["line"], confidence=m["confidence"],
263
+ client_path=a.get("path"), uri_variant=m["uri_variant"], segments=m["segments"], in_repo=True)
264
+ if res["matched"]:
265
+ st["matched"] += 1
266
+ return st
267
+
268
+
269
+ def finish(project: Project, b: GraphBuilder, ctx, name: str) -> dict:
270
+ """Called at the end of each TS server framework's contribute(); the shared post-pass runs once, after the last."""
271
+ pending = getattr(ctx, "tsweb_pending", None)
272
+ if pending is None:
273
+ return {}
274
+ pending.discard(name)
275
+ if pending:
276
+ return {}
277
+ from .data import contribute_data
278
+ st = contribute_data(project, b, ctx)
279
+ st["in_repo_links"] = link_in_repo(b, ctx)
280
+ return {"shared": st}
281
+
282
+
283
+ def register(ctx, name: str) -> None:
284
+ if not hasattr(ctx, "tsweb_pending"):
285
+ ctx.tsweb_pending = set()
286
+ ctx.tsweb_pending.add(name)
287
+
288
+
289
+ def fw_facts(ctx) -> dict:
290
+ return (ctx.facts or {}).get("fw") or {} if ctx else {}
@@ -0,0 +1,276 @@
1
+ """Env/config keys and ORM data access for TypeScript server code (shared by the NestJS, Next.js and Express layers).
2
+
3
+ env / config
4
+ process.env.X, process.env['X'], import.meta.env.X, `const { X } = process.env` -> READS_ENV env:X (exact)
5
+ ConfigService.get('ns.key') with a registerAs('ns', () => ({ key: process.env.X })) namespace
6
+ -> READS_CONFIG config:ns.key (resolved), config:ns.key READS_ENV env:X (exact)
7
+ ConfigService.get('X') (UPPER_SNAKE, no namespace) -> READS_ENV env:X (resolved)
8
+ NEXT_PUBLIC_ / VITE_ / NUXT_PUBLIC_ keys are marked attrs.public (inlined into client bundles)
9
+
10
+ tables (node kind `table`, collections too: attrs.collection)
11
+ TypeORM / MikroORM @Entity('t') | @Entity() (snake_case class name), sequelize-typescript @Table({tableName}),
12
+ Mongoose @Schema({collection}) / mongoose.model('Cat', s) (lower-case plural) -> MAPS_TO_TABLE, columns (CONTAINS)
13
+ Prisma schema.prisma models (@@map / @map) -> table + column nodes
14
+ Drizzle pgTable/mysqlTable/sqliteTable('t', ...) -> table for the variable
15
+ reads/writes:
16
+ this.<repo>.<op>() where <repo> is @InjectRepository(E) / Repository<E> / @InjectModel(E.name) / Model<E>
17
+ prisma.<model>.<op>(), <ds>.getRepository(E).<op>() / manager.<op>(E, ...)
18
+ Kysely selectFrom/insertInto/updateTable/deleteFrom('t'), knex('t'), Drizzle .from(t) / .insert(t) / ...
19
+ -> READS_TABLE / WRITES_TABLE (resolved: needed type/name resolution; heuristic for name-shaped receivers)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from pathlib import Path
25
+
26
+ from ...core.plugin import GraphBuilder, Project
27
+ from .common import fw_facts, last_name, obj, plural, ref_nodes, snake, sval, svals
28
+ from ... import presets
29
+
30
+ # dependency / VCS directories every walk skips (codegraph/presets/common.yaml skip_dirs)
31
+ COMMON_SKIP = presets.skip_dirs("common")
32
+
33
+ PUBLIC_ENV = re.compile(r"^(NEXT_PUBLIC_|VITE_|NUXT_PUBLIC_|REACT_APP_|EXPO_PUBLIC_|PUBLIC_)")
34
+ READ_OPS = {"findUnique", "findUniqueOrThrow", "findFirst", "findFirstOrThrow", "findMany", "count", "aggregate", "groupBy",
35
+ "find", "findOne", "findOneBy", "findBy", "findAndCount", "findAndCountBy", "findOneOrFail", "findOneByOrFail", "findByIds",
36
+ "exist", "exists", "existsBy", "createQueryBuilder", "findAll", "findByPk", "findById", "countDocuments",
37
+ "estimatedDocumentCount", "distinct", "selectFrom", "from", "innerJoin", "leftJoin", "rightJoin", "fullJoin", "query"}
38
+ KNEX_WRITES = {"insert", "update", "del", "delete", "upsert", "merge", "truncate", "increment", "decrement"}
39
+ WRITE_OPS = {"create", "createMany", "createManyAndReturn", "update", "updateMany", "updateManyAndReturn", "upsert", "delete",
40
+ "deleteMany", "save", "insert", "remove", "softDelete", "softRemove", "restore", "increment", "decrement",
41
+ "findOrCreate", "bulkCreate", "destroy", "findByIdAndUpdate", "findByIdAndDelete", "findOneAndUpdate",
42
+ "findOneAndDelete", "findOneAndReplace", "updateOne", "deleteOne", "insertMany", "replaceOne", "bulkWrite",
43
+ "insertInto", "updateTable", "deleteFrom", "replaceInto", "mergeInto", "into"}
44
+ ENTITY_DECOS = {"Entity", "ViewEntity", "Table", "Schema"}
45
+ COLUMN_DECOS = {"Column", "PrimaryColumn", "PrimaryGeneratedColumn", "CreateDateColumn", "UpdateDateColumn", "DeleteDateColumn",
46
+ "VersionColumn", "ObjectIdColumn", "Property", "PrimaryKey", "Prop", "ForeignKey", "Index"}
47
+ RELATION_DECOS = {"ManyToOne", "OneToMany", "OneToOne", "ManyToMany", "BelongsTo", "HasMany", "HasOne", "BelongsToMany"}
48
+ DRIZZLE_TABLES = {"pgTable", "mysqlTable", "sqliteTable", "singlestoreTable", "table"}
49
+
50
+
51
+ def _ensure_table(b: GraphBuilder, name: str, **attrs) -> str:
52
+ return b.add_node("table", name, name=name, lang="ts", attrs=attrs)
53
+
54
+
55
+ def _ensure_column(b: GraphBuilder, table: str, col: str, file=None, line=None) -> str:
56
+ tid = _ensure_table(b, table)
57
+ cid = b.add_node("column", f"{table}.{col}", name=f"{table}.{col}", file=file, line=line, lang="ts", attrs={"table": table})
58
+ b.add_edge(tid, cid, "CONTAINS", file=file, line=line, confidence="exact")
59
+ return cid
60
+
61
+
62
+ def parse_prisma(root: Path) -> dict:
63
+ """model Name { field Type @map("col") ... @@map("table") } -> {Name: {table, fields: {field: column}, file, line}}"""
64
+ models = {}
65
+ files = []
66
+ for pat in ("schema.prisma", "prisma/schema.prisma", "prisma/*.prisma", "prisma/schema/*.prisma", "src/prisma/*.prisma"):
67
+ files += sorted(root.glob(pat))
68
+ if not files:
69
+ files = [p for p in sorted(root.rglob("*.prisma")) if COMMON_SKIP.isdisjoint(p.parts)][:20]
70
+ for f in dict.fromkeys(files):
71
+ try:
72
+ txt = f.read_text()
73
+ except Exception:
74
+ continue
75
+ rel = str(f.relative_to(root))
76
+ for m in re.finditer(r"^model\s+(\w+)\s*\{(.*?)^\}", txt, re.S | re.M):
77
+ name, body = m.group(1), m.group(2)
78
+ line = txt[:m.start()].count("\n") + 1
79
+ mp = re.search(r"@@map\(\s*(?:name:\s*)?\"([^\"]+)\"", body)
80
+ fields = {}
81
+ for fm in re.finditer(r"^\s*(\w+)\s+(\w+)(\[\])?\??([^\n]*)$", body, re.M):
82
+ fname, ftype, rest = fm.group(1), fm.group(2), fm.group(4)
83
+ if fname.startswith("@@"):
84
+ continue
85
+ if "@relation" in rest or (ftype[0].isupper() and ftype not in ("String", "Int", "BigInt", "Float", "Decimal", "Boolean", "DateTime", "Json", "Bytes")):
86
+ continue
87
+ cm = re.search(r"@map\(\s*(?:name:\s*)?\"([^\"]+)\"", rest)
88
+ fields[fname] = cm.group(1) if cm else fname
89
+ models[name] = {"table": mp.group(1) if mp else name, "fields": fields, "file": rel, "line": line}
90
+ return models
91
+
92
+
93
+ def contribute_data(project: Project, b: GraphBuilder, ctx) -> dict:
94
+ F = fw_facts(ctx)
95
+ st = {"env_reads": 0, "config_reads": 0, "tables": 0, "table_reads": 0, "table_writes": 0}
96
+ if not F:
97
+ return st
98
+ classes = {c["id"]: c for c in F.get("classes") or []}
99
+ instances = F.get("instances") or {}
100
+
101
+ # ---- config namespaces (registerAs)
102
+ cfg_defs = {}
103
+ for d in F.get("config_defs") or []:
104
+ cid = b.add_node("config", d["key"], name=d["key"], file=d["file"], line=d["line"], lang="ts", attrs={"namespace": d["ns"]})
105
+ cfg_defs[d["key"]] = cid
106
+ for k in d["env"]:
107
+ b.add_edge(cid, _env(b, k), "READS_ENV", file=d["file"], line=d["line"], confidence="exact")
108
+ namespaces = {d["ns"] for d in F.get("config_defs") or []}
109
+ for e in F.get("env") or []:
110
+ if not e.get("src") or not b.has(e["src"]):
111
+ continue
112
+ key = e["key"]
113
+ if e["via"].startswith("ConfigService"):
114
+ if key in cfg_defs or key.split(".")[0] in namespaces:
115
+ cid = cfg_defs.get(key) or b.add_node("config", key, name=key, lang="ts", attrs={"namespace": key.split(".")[0]})
116
+ b.add_edge(e["src"], cid, "READS_CONFIG", file=e["file"], line=e["line"], confidence="resolved", via=e["via"])
117
+ st["config_reads"] += 1
118
+ elif re.fullmatch(r"[A-Z][A-Z0-9_]*", key):
119
+ b.add_edge(e["src"], _env(b, key), "READS_ENV", file=e["file"], line=e["line"], confidence="resolved", via=e["via"])
120
+ st["env_reads"] += 1
121
+ else:
122
+ cid = b.add_node("config", key, name=key, lang="ts")
123
+ b.add_edge(e["src"], cid, "READS_CONFIG", file=e["file"], line=e["line"], confidence="resolved", via=e["via"])
124
+ st["config_reads"] += 1
125
+ continue
126
+ b.add_edge(e["src"], _env(b, key), "READS_ENV", file=e["file"], line=e["line"], confidence="exact", via=e["via"])
127
+ st["env_reads"] += 1
128
+
129
+ # ---- entities -> tables
130
+ entity_table = {} # class node id -> table
131
+ model_name_table = {} # Mongoose model name -> collection
132
+ for c in classes.values():
133
+ decos = {last_name(d["name"]): d for d in c.get("decorators") or []}
134
+ hit = next((n for n in ENTITY_DECOS if n in decos), None)
135
+ if not hit or not c.get("name"):
136
+ continue
137
+ d = decos[hit]
138
+ a0 = (d.get("args") or [None])[0]
139
+ if hit == "Schema" and not (d.get("mod") or "").startswith("@nestjs/mongoose") and d.get("mod"):
140
+ continue
141
+ if hit == "Table" and d.get("mod") and "sequelize" not in d["mod"]:
142
+ continue
143
+ explicit = sval(a0) or sval(obj(a0).get("name")) or sval(obj(a0).get("tableName")) or sval(obj(a0).get("collection"))
144
+ if hit == "Schema":
145
+ table, kind = explicit or plural(c["name"].lower()), {"collection": True}
146
+ elif hit == "Table":
147
+ table, kind = explicit or plural(c["name"]), {}
148
+ else:
149
+ table, kind = explicit or snake(c["name"]), {}
150
+ tid = _ensure_table(b, table, orm=hit, **kind)
151
+ entity_table[c["id"]] = table
152
+ model_name_table[c["name"]] = table
153
+ if b.has(c["id"]):
154
+ b.add_edge(c["id"], tid, "MAPS_TO_TABLE", file=c["file"], line=c["line"], confidence="exact" if explicit else "resolved",
155
+ via=f"@{hit}" if explicit else "naming-convention")
156
+ for p in c.get("props") or []:
157
+ pd = {last_name(x["name"]): x for x in p.get("decorators") or []}
158
+ col = next((n for n in COLUMN_DECOS if n in pd), None)
159
+ rel = next((n for n in RELATION_DECOS if n in pd), None)
160
+ if col:
161
+ args = pd[col].get("args") or []
162
+ cname = next((sval(obj(a).get("name")) for a in args if sval(obj(a).get("name"))), None) or p["name"]
163
+ _ensure_column(b, table, cname, c["file"], p["line"])
164
+ if rel:
165
+ for a in pd[rel].get("args") or []:
166
+ for n in ref_nodes(a.get("body") or a if isinstance(a, dict) else None) + [((a or {}).get("body") or {}).get("node")]:
167
+ if n and n.startswith("class:") and b.has(c["id"]):
168
+ b.add_edge(c["id"], n, "HAS_RELATION", file=c["file"], line=p["line"], confidence="exact", relation=rel, property=p["name"])
169
+ break
170
+ else:
171
+ continue
172
+ break
173
+ st["entities"] = len(entity_table)
174
+
175
+ # mongoose.model('Cat', CatSchema)
176
+ for mc in F.get("member_calls") or []:
177
+ if mc["method"] == "model" and sval(mc.get("a0")):
178
+ pass
179
+ prisma = parse_prisma(project.root)
180
+ prisma_by_client = {}
181
+ for name, m in prisma.items():
182
+ tid = _ensure_table(b, m["table"], orm="prisma", model=name)
183
+ b.nodes[tid].file = b.nodes[tid].file or m["file"]
184
+ b.nodes[tid].line = b.nodes[tid].line or m["line"]
185
+ for f, col in m["fields"].items():
186
+ _ensure_column(b, m["table"], col, m["file"], m["line"])
187
+ prisma_by_client[name[0].lower() + name[1:]] = m["table"]
188
+ st["prisma_models"] = len(prisma)
189
+ drizzle = {} # instance key -> table
190
+ drizzle_names = {}
191
+ for k, inst in instances.items():
192
+ init = (inst or {}).get("init") or {}
193
+ if "call" in init and last_name(init["call"]) in DRIZZLE_TABLES and sval((init.get("args") or [None])[0]) and (init.get("mod") or "").startswith("drizzle"):
194
+ t = sval(init["args"][0])
195
+ drizzle[k] = t
196
+ drizzle_names[inst.get("name")] = t
197
+ _ensure_table(b, t, orm="drizzle")
198
+ st["drizzle_tables"] = len(drizzle)
199
+
200
+ # repositories / models injected into classes: (class id, prop name) -> table
201
+ repo_prop = {}
202
+ for c in classes.values():
203
+ members = [(p, p.get("decorators") or []) for p in c.get("ctor") or []] + [(p, p.get("decorators") or []) for p in c.get("props") or []]
204
+ for p, decos in members:
205
+ table = None
206
+ for d in decos:
207
+ n = last_name(d["name"])
208
+ a0 = (d.get("args") or [None])[0] or {}
209
+ if n == "InjectRepository":
210
+ table = entity_table.get(a0.get("node"))
211
+ elif n == "InjectModel":
212
+ ref = (a0.get("ref") or sval(a0) or "").replace(".name", "")
213
+ table = model_name_table.get(ref) or entity_table.get(a0.get("node"))
214
+ t = p.get("type") or {}
215
+ if not table and t.get("name") in ("Repository", "Model", "EntityRepository", "MongoRepository", "TreeRepository") and t.get("args"):
216
+ table = entity_table.get((t["args"][0] or {}).get("id"))
217
+ if table:
218
+ repo_prop[(c["id"], p["name"])] = table
219
+ supers = {c["id"]: (c.get("extends") or {}).get("id") for c in classes.values()}
220
+
221
+ def prop_table(cls, prop):
222
+ for _ in range(6):
223
+ if not cls:
224
+ return None
225
+ if (cls, prop) in repo_prop:
226
+ return repo_prop[(cls, prop)]
227
+ cls = supers.get(cls)
228
+ return None
229
+
230
+ for mc in F.get("member_calls") or []:
231
+ src, m = mc.get("src"), mc["method"]
232
+ if not src or not b.has(src):
233
+ continue
234
+ tables, conf, via = [], "resolved", None
235
+ a0 = mc.get("a0") or {}
236
+ if mc.get("prop") and (m in READ_OPS or m in WRITE_OPS):
237
+ t = prop_table(mc.get("cls"), mc["prop"])
238
+ if t:
239
+ tables, via = [t], "injected-repository"
240
+ elif m in ("selectFrom", "insertInto", "updateTable", "deleteFrom", "replaceInto", "mergeInto") and svals(a0):
241
+ tables, via = [s.split(" ")[0] for s in svals(a0)], "kysely"
242
+ elif mc.get("chain") and (m in READ_OPS or m in WRITE_OPS):
243
+ ch = mc["chain"]
244
+ if len(ch) >= 2 and ch[-1] in prisma_by_client and (re.search(r"prisma|db|tx|client|trx", ".".join(ch[:-1]), re.I) or "Prisma" in (mc.get("recv_type") or "")):
245
+ tables, via = [prisma_by_client[ch[-1]]], "prisma"
246
+ elif len(ch) >= 3 and ch[-2] == "query" and ch[-1] in drizzle_names:
247
+ tables, via = [drizzle_names[ch[-1]]], "drizzle-query"
248
+ elif m in ("selectFrom", "insertInto", "updateTable", "deleteFrom", "replaceInto", "mergeInto") and svals(a0):
249
+ tables, via = [s.split(" ")[0] for s in svals(a0)], "kysely"
250
+ elif a0.get("node") in entity_table:
251
+ tables, via = [entity_table[a0["node"]]], "entity-argument"
252
+ elif m == "knex" and svals(a0):
253
+ tables, via, conf = [s.split(" ")[0] for s in svals(a0)], "knex", "heuristic"
254
+ elif mc.get("chained") or mc.get("root"):
255
+ if a0.get("key") in drizzle:
256
+ tables, via = [drizzle[a0["key"]]], "drizzle"
257
+ elif m in ("selectFrom", "insertInto", "updateTable", "deleteFrom", "replaceInto", "mergeInto", "innerJoin", "leftJoin") and svals(a0):
258
+ tables, via = [s.split(" ")[0] for s in svals(a0)], "kysely"
259
+ elif m in ("from", "into", "table") and svals(a0) and re.search(r"knex|db|trx|qb|query|sql", mc.get("root") or "", re.I):
260
+ tables, via, conf = [s.split(" ")[0] for s in svals(a0)], "query-builder", "heuristic"
261
+ if not tables and a0.get("key") in drizzle and m in ("from", "insert", "update", "delete", "into", "innerJoin", "leftJoin"):
262
+ tables, via = [drizzle[a0["key"]]], "drizzle"
263
+ for t in tables:
264
+ if not t or "{" in t:
265
+ continue
266
+ tid = _ensure_table(b, t)
267
+ write = m in WRITE_OPS or (via == "drizzle" and m in ("insert", "update", "delete")) \
268
+ or (via == "knex" and bool(KNEX_WRITES & set(mc.get("chain") or ())))
269
+ b.add_edge(src, tid, "WRITES_TABLE" if write else "READS_TABLE", file=mc["file"], line=mc["line"], confidence=conf, via=via, op=m)
270
+ st["table_writes" if write else "table_reads"] += 1
271
+ st["tables"] = sum(1 for n in b.nodes.values() if n.kind == "table" and n.lang == "ts")
272
+ return st
273
+
274
+
275
+ def _env(b: GraphBuilder, key: str) -> str:
276
+ return b.add_node("env", key, name=key, lang="ts", attrs={"public": True} if PUBLIC_ENV.match(key) else {})