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,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 {})
|