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,428 @@
1
+ """Express / Koa (+ @koa/router) / Fastify / Hono router layer (on the TypeScript plugin; plain-JS projects too).
2
+
3
+ Router instances are declarations (variables, class properties, function parameters) whose initialiser is a known
4
+ factory (express(), express.Router(), Router(), new Router({prefix}), fastify(), new Koa(), new Hono(), app.basePath(p),
5
+ ...), identified across files through imports, require(), module.exports and factory functions that return them.
6
+
7
+ routes x.get|post|put|patch|delete|all|head|options(path, ...middleware, handler), x.route(path).get(h).post(h),
8
+ fastify.route({method, url, handler, preHandler}), fastify.get(path, {preHandler, schema}, handler),
9
+ hono.on(methods, path, h); paths `:id` -> `{id}`, `*` -> `{wildcard*}`
10
+ mounting x.use(prefix?, ...mw, sub) (Express, koa-router sub.routes()), fastify.register(plugin, {prefix}) (the
11
+ plugin's first parameter is the child instance), hono.route(prefix, sub), basePath; chains across files are
12
+ joined into full paths; a function called with a router argument (`routes(app)`) binds its parameter
13
+ middleware route-level middleware, x.use(mw) router-level middleware and middleware passed with a mount are recorded on
14
+ the route (attrs.middleware) with USES_MIDDLEWARE edges where the function resolves
15
+ confidence exact: literal path, known factory, direct handler; resolved: through wrappers / handler objects / aliases
16
+ / factory functions / params; heuristic: router never mounted from an app (prefix unknown) or receiver only
17
+ recognised by name
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from collections import defaultdict
23
+
24
+ from ...core.plugin import FrameworkPlugin, GraphBuilder, Project
25
+ from ..tsweb.common import (TEST_SKIP_RE, add_route, express_path, finish, fw_facts, has_server_framework, join_path,
26
+ last_name, merge_extractor_cfg, obj, ref_nodes, register, sval, svals)
27
+
28
+ VERBS = {"get": "GET", "post": "POST", "put": "PUT", "patch": "PATCH", "delete": "DELETE", "del": "DELETE", "head": "HEAD",
29
+ "options": "OPTIONS", "all": "ANY", "any": "ANY"}
30
+ MODS = {"express": "express", "koa": "koa", "@koa/router": "koa-router", "koa-router": "koa-router", "fastify": "fastify",
31
+ "hono": "hono", "elysia": "elysia", "polka": "polka", "restify": "restify", "h3": "h3", "@hono/zod-openapi": "hono"}
32
+ APP_FACTORIES = {"express": "express", "Fastify": "fastify", "fastify": "fastify", "Koa": "koa", "Hono": "hono", "OpenAPIHono": "hono",
33
+ "Elysia": "elysia", "polka": "polka", "createServer": "restify", "createApp": "h3"}
34
+ ROUTER_FACTORIES = {"Router": "express", "KoaRouter": "koa-router", "createRouter": "h3"}
35
+ HOOKS = ("onRequest", "preParsing", "preValidation", "preHandler")
36
+ MW_OPTS = ("preHandler", "onRequest", "preValidation", "preParsing", "beforeHandler", "middleware")
37
+ NAME_HINT = re.compile(r"(^|\.)(app|server|router|routes?|api|fastify|instance|r)$", re.I)
38
+
39
+
40
+ class ExpressPlugin(FrameworkPlugin):
41
+ name, language = "express", "typescript"
42
+
43
+ def detect(self, project: Project) -> bool:
44
+ return has_server_framework(project, "express")
45
+
46
+ def register_hooks(self, ctx) -> None:
47
+ root = ctx.project.root
48
+ dirs = [d for d in ("src", "server", "api", "app", "lib", "routes", "core") if (root / d).is_dir()]
49
+ merge_extractor_cfg(ctx.extractor_cfg, src_dirs=dirs or ["."], skip_re=TEST_SKIP_RE, walk_src=True, allow_js=True)
50
+ if dirs: # entry files at the root (index.js, app.js, server.ts)
51
+ merge_extractor_cfg(ctx.extractor_cfg, extra_files=[f for f in ("index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts", "main.ts", "main.js") if (root / f).exists()],
52
+ src_dirs=["."] if any((root / f).exists() for f in ("index.js", "app.js", "server.js", "index.ts", "app.ts", "server.ts")) else None)
53
+ register(ctx, self.name)
54
+
55
+ # ------------------------------------------------------------
56
+ def contribute(self, project: Project, b: GraphBuilder, ctx) -> dict:
57
+ st = {"routers": 0, "apps": 0, "mounts": 0, "routes": 0, "routes_unmounted": 0, "routes_unresolved_handler": 0}
58
+ F = fw_facts(ctx)
59
+ if not F:
60
+ return {**st, "status": "no facts"}
61
+ self.b, self.F = b, F
62
+ self._route_keys = None
63
+ self.I = F.get("instances") or {}
64
+ calls = F.get("calls") or []
65
+ self.kinds = {} # key -> (framework, "app"|"router"|"derived"|"param"|"name")
66
+ self.own_prefix = defaultdict(str)
67
+ self.mounts = defaultdict(list) # child -> [(parent, prefix, conf, mw)]
68
+ self.router_mw = defaultdict(list) # key -> [(prefix, (node, name, conf), file, line)]
69
+ for k, inst in self.I.items():
70
+ c = self.classify(k)
71
+ if c:
72
+ self.kinds[k] = c
73
+ route_calls, mount_calls = [], []
74
+ for c in calls:
75
+ m = c["method"]
76
+ keys = self.recv_keys(c)
77
+ if not keys:
78
+ continue
79
+ if m in VERBS or (m == "on" and len(c.get("args") or []) >= 3) or (m == "route" and len(c["args"]) == 1 and obj(c["args"][0]).get("method")):
80
+ route_calls.append((c, keys))
81
+ elif m in ("use", "lazyUse", "mount", "register", "route", "group"):
82
+ mount_calls.append((c, keys))
83
+ elif m == "prefix" and c.get("args") and sval(c["args"][0]):
84
+ for k, _ in keys:
85
+ self.own_prefix[k] = sval(c["args"][0])
86
+ elif m == "addHook" and len(c.get("args") or []) >= 2 and sval(c["args"][0]) in HOOKS:
87
+ for k, kp in keys: # fastify hooks apply to the instance's routes and its child plugins
88
+ self.router_mw[k].append((kp, self.mw_of(c["args"][1]), c["file"], c["line"]))
89
+ for k, inst in self.I.items(): # new Router({ prefix: '/x' }) (koa-router)
90
+ init = (inst or {}).get("init") or {}
91
+ if last_name(init.get("new") or init.get("call") or "") in ("Router", "KoaRouter") and init.get("args"):
92
+ pfx = sval(obj(init["args"][0]).get("prefix"))
93
+ if pfx:
94
+ self.own_prefix[k] = pfx
95
+ # routerish = known factories + keys that receive route calls (and the apps mounting them)
96
+ for c, keys in route_calls:
97
+ for k, _ in keys:
98
+ if k not in self.kinds:
99
+ inst = self.I.get(k) or {}
100
+ if inst.get("kind") == "param":
101
+ self.kinds[k] = ("?", "param")
102
+ elif NAME_HINT.search(inst.get("name") or c.get("recv_text") or ""):
103
+ self.kinds[k] = ("?", "name")
104
+ for c, keys in mount_calls:
105
+ self.mount(c, keys, st)
106
+ for bc in F.get("bind_calls") or []:
107
+ for k, inst in self.I.items():
108
+ if inst and inst.get("kind") == "param" and inst.get("fn") == bc["fn"] and inst.get("index") is not None and inst["index"] < len(bc["args"]):
109
+ for pk, pfx in self.inst_keys(bc["args"][inst["index"]]):
110
+ if pk in self.kinds and pk != k:
111
+ self.mounts[k].append((pk, pfx, "resolved", [], f"{bc['file']}:{bc['line']}"))
112
+ st["mounts"] += 1
113
+ st["apps"] = sum(1 for v in self.kinds.values() if v[1] == "app")
114
+ st["routers"] = len({k for k, v in self.kinds.items() if v[1] in ("router", "derived", "param", "name")})
115
+ for c, keys in route_calls:
116
+ self.route(c, keys, st)
117
+ st["frameworks"] = sorted({v[0] for v in self.kinds.values() if v[0] != "?"})
118
+ st.update(finish(project, b, ctx, self.name))
119
+ return st
120
+
121
+ # ------------------------------------------------------------ instances
122
+ def classify(self, k):
123
+ inst = self.I.get(k) or {}
124
+ init = inst.get("init") or {}
125
+ if inst.get("kind") == "param":
126
+ t = inst.get("type") or ""
127
+ for n, fw in (("FastifyInstance", "fastify"), ("Hono", "hono"), ("Router", "express"), ("Express", "express"), ("Application", "express")):
128
+ if re.search(rf"\b{n}\b", t):
129
+ return (fw, "param")
130
+ return None
131
+ return self._factory(init)
132
+
133
+ def _factory(self, d, depth=0):
134
+ if not isinstance(d, dict) or depth > 4:
135
+ return None
136
+ callee = d.get("call") or d.get("new")
137
+ if callee is None:
138
+ if d.get("cond"):
139
+ for x in d["cond"]:
140
+ r = self._factory(x, depth + 1)
141
+ if r:
142
+ return r
143
+ return None
144
+ ln = last_name(callee)
145
+ mod = d.get("mod") or ""
146
+ fw = MODS.get(mod) or (MODS.get(mod.split("/")[0]) if mod.startswith("hono/") else None)
147
+ if ln in ("basePath", "route", "use", "get", "post", "put", "patch", "delete", "on", "prefix") and d.get("recv"):
148
+ parent = self.inst_keys(d["recv"])
149
+ if parent:
150
+ return (self.kinds.get(parent[0][0], ("?", ""))[0], "derived")
151
+ inner = self._factory(d["recv"], depth + 1) # new Hono().basePath('/api')
152
+ if inner:
153
+ return inner
154
+ if ln in APP_FACTORIES and (fw or "new" in d or ln in ("express", "fastify", "Fastify", "polka")):
155
+ return (fw or APP_FACTORIES[ln], "app" if ln not in ("Router",) else "router")
156
+ if ln == "Router" or ln in ROUTER_FACTORIES:
157
+ if "new" in d:
158
+ return ("koa-router" if fw in (None, "koa-router", "koa") else fw, "router")
159
+ return (fw or ("express" if callee.startswith("express") or not mod else MODS.get(mod, "express")), "router")
160
+ if fw and ln not in ("json", "urlencoded", "static", "text", "raw"):
161
+ return (fw, "app")
162
+ return None
163
+
164
+ def recv_keys(self, c):
165
+ d = c.get("recv") or {}
166
+ keys = self.inst_keys(d)
167
+ if not keys and c.get("owner") and ("new" in d or "call" in d) and self._factory(d):
168
+ k = c["owner"] # chain owned by a declaration: const api = Router().use(...)
169
+ self.kinds.setdefault(k, self._factory(d))
170
+ keys = [(k, "")]
171
+ if not keys and ("new" in d or "call" in d) and self._factory(d):
172
+ k = f"inline:{c['file']}:{c['line']}"
173
+ self.kinds.setdefault(k, self._factory(d))
174
+ keys = [(k, "")]
175
+ out = []
176
+ for k, pfx in keys:
177
+ for ch in c.get("chain") or []:
178
+ if ch["method"] in ("basePath", "prefix") and ch.get("args") and sval(ch["args"][0]):
179
+ pfx = join_path(pfx, sval(ch["args"][0]))
180
+ out.append((k, pfx))
181
+ return out
182
+
183
+ def inst_keys(self, d, depth=0) -> list[tuple[str, str]]:
184
+ """Router instances a described value evaluates to: [(key, extra prefix)]."""
185
+ if not isinstance(d, dict) or depth > 6:
186
+ return []
187
+ if d.get("key"):
188
+ k = d["key"]
189
+ init = (self.I.get(k) or {}).get("init") or {}
190
+ if "call" in init and last_name(init["call"]) == "basePath" and init.get("recv"):
191
+ bp = sval((init.get("args") or [None])[0]) or ""
192
+ return [(pk, join_path(pp, bp)) for pk, pp in self.inst_keys(init["recv"], depth + 1)] or [(k, bp)]
193
+ return [(k, "")]
194
+ if "call" in d:
195
+ ln = last_name(d["call"])
196
+ if ln in ("routes", "middleware", "allowedMethods", "callback", "getRouter", "router", "fetch") and d.get("recv"):
197
+ return self.inst_keys(d["recv"], depth + 1)
198
+ if ln in ("basePath",) and d.get("recv"):
199
+ return [(k, join_path(p, sval((d.get("args") or [None])[0]) or "")) for k, p in self.inst_keys(d["recv"], depth + 1)]
200
+ out = []
201
+ for r in d.get("ret") or []:
202
+ out += self.inst_keys(r, depth + 1)
203
+ if not out and last_name(d["call"]) in ("fp", "fastifyPlugin") and d.get("args"):
204
+ out = self.inst_keys(d["args"][0], depth + 1)
205
+ return out
206
+ if "require" in d:
207
+ return self.inst_keys(d.get("exp"), depth + 1)
208
+ if d.get("fn"):
209
+ out = []
210
+ for r in (d.get("ret") or []) + ([d["body"]] if d.get("body") else []):
211
+ out += self.inst_keys(r, depth + 1)
212
+ return out
213
+ if d.get("of") and d.get("ref", "").endswith(".router"):
214
+ return []
215
+ return []
216
+
217
+ def plugin_params(self, d, depth=0) -> list[str]:
218
+ """fastify.register(plugin): the plugin function's first parameter instance."""
219
+ if not isinstance(d, dict) or depth > 4:
220
+ return []
221
+ fns = []
222
+ if d.get("fn"):
223
+ fns.append(d["fn"])
224
+ if d.get("node"):
225
+ fns.append(d["node"])
226
+ if "require" in d:
227
+ return self.plugin_params(d.get("exp"), depth + 1)
228
+ if "call" in d:
229
+ for a in d.get("args") or []:
230
+ fns += self.plugin_params(a, depth + 1)
231
+ return fns
232
+ out = [k for k, inst in self.I.items() if inst and inst.get("kind") == "param" and inst.get("index") == 0 and inst.get("fn") in fns]
233
+ return out or [x for x in fns if x.startswith("param:")]
234
+
235
+ # ------------------------------------------------------------ mounts
236
+ def mw_of(self, d):
237
+ """(node, name, confidence) for a described middleware argument."""
238
+ if not isinstance(d, dict):
239
+ return (None, "?", "heuristic")
240
+ if d.get("fn"):
241
+ return (d["fn"], "inline", "exact")
242
+ if d.get("ref") is not None:
243
+ return (d.get("node") or (d.get("obj_fns") or [None])[0], d["ref"], "resolved")
244
+ if "call" in d:
245
+ return (d.get("node"), d["call"] + "()", "resolved" if d.get("node") else "heuristic")
246
+ return (None, d.get("expr") or "?", "heuristic")
247
+
248
+ def mount(self, c, keys, st):
249
+ args = c.get("args") or []
250
+ m = c["method"]
251
+ if m == "register":
252
+ if not args:
253
+ return
254
+ pfx = sval(obj(args[1]).get("prefix")) if len(args) > 1 else None
255
+ for child in self.plugin_params(args[0]):
256
+ self.kinds.setdefault(child, (self.kinds.get(keys[0][0], ("fastify", ""))[0], "param"))
257
+ for k, kp in keys:
258
+ self.mounts[child].append((k, join_path(kp, pfx or ""), "resolved", [], f"{c['file']}:{c['line']}"))
259
+ st["mounts"] += 1
260
+ return
261
+ prefixes = [""]
262
+ rest = args
263
+ if args and (svals(args[0]) and "re" not in args[0]):
264
+ prefixes, rest = svals(args[0]), args[1:]
265
+ flat = []
266
+ for a in rest:
267
+ flat += a.get("arr") if isinstance(a, dict) and a.get("arr") is not None else [a]
268
+ children, mws = [], []
269
+ for a in flat:
270
+ ks = [x for x in self.inst_keys(a) if x[0] in self.kinds or self._has_routes(x[0])]
271
+ if ks:
272
+ children += ks
273
+ else:
274
+ mws.append(self.mw_of(a))
275
+ if children:
276
+ for ck, cp in children:
277
+ for k, kp in keys:
278
+ if ck == k:
279
+ continue
280
+ for p in prefixes:
281
+ self.mounts[ck].append((k, join_path(kp, p, cp), "exact" if self.kinds.get(ck, ("", ""))[1] in ("router", "app", "derived") else "resolved",
282
+ mws, f"{c['file']}:{c['line']}"))
283
+ st["mounts"] += 1
284
+ elif m in ("use", "lazyUse"):
285
+ for k, kp in keys:
286
+ for p in prefixes:
287
+ for mw in mws:
288
+ self.router_mw[k].append((join_path(kp, p), mw, c["file"], c["line"]))
289
+
290
+ def _has_routes(self, k) -> bool:
291
+ if self._route_keys is None:
292
+ self._route_keys = {kk for c in self.F.get("calls") or [] if c["method"] in VERBS for kk, _ in self.inst_keys(c.get("recv") or {})}
293
+ return k in self._route_keys
294
+
295
+ def _mw_before(self, k, at, pfx=None):
296
+ """Router-level middleware of k registered before `at` (file, line) (order known only within one file)."""
297
+ out = []
298
+ for p, x, f, ln in self.router_mw.get(k, []):
299
+ if pfx is None and p not in ("", "/"):
300
+ continue
301
+ if pfx is not None and (p in ("", "/") or not (pfx or "/").startswith(p)):
302
+ continue # prefix-scoped middleware only (global ones come through the parent's own walk)
303
+ if at and f == at[0] and ln > at[1]:
304
+ continue # registered after the route / mount: does not run for it
305
+ if x[1] in ("?", "inline") and not x[0]:
306
+ continue
307
+ out.append(x)
308
+ return out
309
+
310
+ def prefixes(self, k, depth=0, seen=frozenset(), at=None):
311
+ """[(prefix, confidence, middleware, mounted_from_app)] for router k (`at`: the route call's file, line)."""
312
+ own = self.own_prefix.get(k, "")
313
+ if k in seen or depth > 8:
314
+ return [(own, "heuristic", [], False)]
315
+ if k not in self.mounts:
316
+ kind = self.kinds.get(k, ("?", "name"))[1]
317
+ mw = self._mw_before(k, at)
318
+ return [(own, "exact" if kind == "app" else "heuristic", mw, kind == "app")]
319
+ out = []
320
+ for parent, pfx, conf, mws, ev in self.mounts[k][:8]:
321
+ f, _, ln = ev.rpartition(":")
322
+ at_ev = (f, int(ln)) if ln.isdigit() else None
323
+ for pp, pc, pmw, ok in self.prefixes(parent, depth + 1, seen | {k}, at=at_ev):
324
+ inherited = self._mw_before(parent, at_ev, pfx)
325
+ out.append((join_path(pp, pfx, own), _weaker(conf, pc), pmw + inherited + mws + self._mw_before(k, at), ok))
326
+ if len(out) >= 8:
327
+ return out
328
+ return out
329
+
330
+ # ------------------------------------------------------------ routes
331
+ def route(self, c, keys, st):
332
+ b = self.b
333
+ args = list(c.get("args") or [])
334
+ m = c["method"]
335
+ methods, paths, opts = [], [], {}
336
+ if m == "route": # fastify.route({ method, url, handler })
337
+ o = obj(args[0])
338
+ methods = [x.upper() for x in svals(o.get("method"))] or ["GET"]
339
+ paths = svals(o.get("url")) or svals(o.get("path"))
340
+ opts = o
341
+ handlers = [o.get("handler")]
342
+ mids = []
343
+ else:
344
+ if m == "on":
345
+ methods = [x.upper() for x in svals(args[0])] or ["ANY"]
346
+ args = args[1:]
347
+ else:
348
+ methods = [VERBS[m]]
349
+ chain_path = None
350
+ for ch in c.get("chain") or []:
351
+ if ch["method"] == "route" and ch.get("args") and svals(ch["args"][0]):
352
+ chain_path = svals(ch["args"][0])
353
+ if args and (svals(args[0]) or (isinstance(args[0], dict) and "re" in args[0])) and not (chain_path is not None and not svals(args[0])):
354
+ a0 = args.pop(0)
355
+ paths = svals(a0) or ["{regex}"]
356
+ elif chain_path is not None:
357
+ paths = chain_path
358
+ elif args and isinstance(args[0], dict) and args[0].get("ref") and not args[0].get("node"):
359
+ args.pop(0)
360
+ paths = ["{" + re.sub(r"\W", "_", args and "path" or "path") + "}"]
361
+ else:
362
+ paths = [""] if chain_path is None else chain_path
363
+ flat = []
364
+ for a in args:
365
+ flat += a.get("arr") if isinstance(a, dict) and a.get("arr") is not None else [a]
366
+ opt_objs = [a for a in flat if isinstance(a, dict) and a.get("obj") is not None]
367
+ for o in opt_objs:
368
+ opts.update(o["obj"])
369
+ fns = [a for a in flat if not (isinstance(a, dict) and a.get("obj") is not None)]
370
+ handlers = fns[-1:] or [opts.get("handler")]
371
+ mids = fns[:-1]
372
+ mw = [self.mw_of(x) for x in mids]
373
+ for k in MW_OPTS:
374
+ v = opts.get(k)
375
+ for x in (v.get("arr") if isinstance(v, dict) and v.get("arr") is not None else [v] if v else []):
376
+ mw.append(self.mw_of(x))
377
+ handler_nodes, hconf = [], "exact"
378
+ for h in handlers:
379
+ if not isinstance(h, dict):
380
+ continue
381
+ ns = [n for n in ref_nodes(h) if b.has(n)]
382
+ if not ns and h.get("node") and b.has(h["node"]):
383
+ ns = [h["node"]]
384
+ handler_nodes += ns
385
+ if not h.get("fn") and not (h.get("ref") is not None and h.get("node")):
386
+ hconf = "resolved"
387
+ attrs = {}
388
+ schema = obj(opts.get("schema"))
389
+ if schema:
390
+ props = obj(obj(schema.get("body")).get("properties"))
391
+ if props:
392
+ attrs["body_fields"] = sorted(props)
393
+ q = obj(obj(schema.get("querystring")).get("properties"))
394
+ if q:
395
+ attrs["query_fields"] = sorted(q)
396
+ if not handler_nodes:
397
+ attrs["handler_unresolved"] = True
398
+ counted = False
399
+ literal = any(p == "" or p.startswith(("/", "*")) for p in paths if "{" not in p[:1])
400
+ for k, kp in keys:
401
+ fw, kind = self.kinds.get(k, ("?", "name"))
402
+ if kind == "name" and not handler_nodes:
403
+ continue
404
+ if kind in ("name", "param") and fw == "?" and not literal:
405
+ continue # untyped receiver + non-literal first argument: a map/cache/http-client .get(key), not a route
406
+ for pfx, pconf, pmw, mounted in self.prefixes(k, at=(c["file"], c["line"])):
407
+ for p in paths:
408
+ uri = express_path(join_path(pfx, kp, p))
409
+ conf = _weaker(hconf, pconf if mounted else "heuristic")
410
+ if kind == "name":
411
+ conf = "heuristic"
412
+ if any("{" in s and not re.fullmatch(r"\{\w+\*?\??\}", s) for s in uri.split("/") if s) or "{regex}" in uri:
413
+ conf = "heuristic"
414
+ ra = {**attrs, "router": (self.I.get(k) or {}).get("name") or c.get("recv_text"), "router_framework": fw}
415
+ if not mounted:
416
+ ra["unmounted"] = True
417
+ st["routes_unmounted"] += 1
418
+ for method in methods:
419
+ add_route(b, method, uri, handler_nodes, c["file"], c["line"], fw if fw != "?" else "express", conf, ra, pmw + mw)
420
+ st["routes"] += 1
421
+ if not handler_nodes and not counted:
422
+ st["routes_unresolved_handler"] += 1 # once per route call that produced routes
423
+ counted = True
424
+
425
+
426
+ def _weaker(a, b):
427
+ order = {"exact": 3, "resolved": 2, "heuristic": 1}
428
+ return a if order[a] <= order[b] else b
File without changes