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,904 @@
1
+ """HTTP client calls in Dart: package:http, Dio (+ BaseOptions.baseUrl), Retrofit / Chopper
2
+ annotations, dart:io HttpClient and WebSocket channels.
3
+
4
+ URLs are evaluated to templates: string interpolation becomes a {name} placeholder; constants,
5
+ static fields, getters, local variables, `Uri.parse/https/http`, `dotenv.env['X']` /
6
+ `String.fromEnvironment('X')` (resolved through .env files when present) and small URL-building
7
+ helpers are followed. A URL that depends on a parameter of the enclosing function marks a helper:
8
+ it is expanded at its call sites (up to three levels), like the TS plugin does.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+
16
+ from ...core.fsutil import is_real_file
17
+ from ...core.model import EXACT, HEURISTIC, RESOLVED, CONFIDENCE_RANK
18
+ from .program import Ctx, DartProgram, DClass, DFunc, DVar, ctor_type, last_name, root_name, split_type, walk_repr
19
+
20
+ HTTP_PKG_METHODS = {"get": "GET", "post": "POST", "put": "PUT", "patch": "PATCH", "delete": "DELETE", "head": "HEAD",
21
+ "read": "GET", "readBytes": "GET"}
22
+ DIO_METHODS = {"get": "GET", "post": "POST", "put": "PUT", "patch": "PATCH", "delete": "DELETE", "head": "HEAD",
23
+ "getUri": "GET", "postUri": "POST", "putUri": "PUT", "patchUri": "PATCH", "deleteUri": "DELETE",
24
+ "download": "GET", "request": None, "requestUri": None}
25
+ IO_METHODS = {"getUrl": "GET", "postUrl": "POST", "putUrl": "PUT", "patchUrl": "PATCH", "deleteUrl": "DELETE", "openUrl": None}
26
+ IO_HOST_METHODS = {"get": "GET", "post": "POST", "put": "PUT", "patch": "PATCH", "delete": "DELETE", "head": "HEAD", "open": None}
27
+ HTTP_CLIENT_TYPES = {"Client", "BaseClient", "IOClient", "RetryClient", "BrowserClient", "MockClient", "InterceptedClient"}
28
+ DIO_TYPES = {"Dio", "DioMixin", "DioForNative", "DioForBrowser"}
29
+ WS_TYPES = {"WebSocketChannel", "IOWebSocketChannel", "HtmlWebSocketChannel", "WebSocket"}
30
+ RETROFIT_VERBS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}
31
+ CHOPPER_VERBS = {"Get": "GET", "Post": "POST", "Put": "PUT", "Patch": "PATCH", "Delete": "DELETE", "Head": "HEAD"}
32
+ TOKEN = re.compile(r"\{@param:(\w+)\}")
33
+ MAX_CTX = 40
34
+ HELPER_PARAM = re.compile(r"(?i)(path|url|uri|endpoint|route|resource|segment|base|prefix|api)s?$")
35
+
36
+
37
+ def min_conf(*cs):
38
+ return min((c for c in cs if c), key=lambda c: CONFIDENCE_RANK[c])
39
+
40
+
41
+ @dataclass
42
+ class Tpl:
43
+ text: str = ""
44
+ conf: str = EXACT
45
+ named: bool = False # leftmost part came through a named constant/variable/getter
46
+ env: list = field(default_factory=list)
47
+ unknown: bool = False
48
+
49
+ def __add__(self, o: "Tpl") -> "Tpl":
50
+ return Tpl(self.text + o.text, min_conf(self.conf, o.conf), self.named if self.text else o.named,
51
+ self.env + o.env, self.unknown or o.unknown)
52
+
53
+
54
+ def load_env_files(root: Path, pkg_dirs: list[str]) -> dict[str, tuple[str, str]]:
55
+ """KEY -> (value, file) from .env files of the Flutter packages (first definition wins; .env before .env.*)."""
56
+ out: dict[str, tuple[str, str]] = {}
57
+ for d in pkg_dirs:
58
+ base = root / d if d else root
59
+ cands = sorted(set(list(base.glob(".env")) + list(base.glob(".env*")) + list(base.glob("assets/.env*")) + list(base.glob("env/.env*"))),
60
+ key=lambda p: (p.name != ".env", p.name.endswith((".example", ".sample", ".template")), str(p)))
61
+ for p in cands:
62
+ if not is_real_file(p) or p.stat().st_size > 200_000:
63
+ continue
64
+ try:
65
+ txt = p.read_text(errors="replace")
66
+ except OSError:
67
+ continue
68
+ for line in txt.splitlines():
69
+ m = re.match(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$", line)
70
+ if m:
71
+ v = m.group(2).strip()
72
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"":
73
+ v = v[1:-1]
74
+ else:
75
+ v = v.split(" #")[0].strip()
76
+ out.setdefault(m.group(1), (v, str(p.relative_to(root))))
77
+ return out
78
+
79
+
80
+ class UrlEval:
81
+ def __init__(self, prog: DartProgram, env_values: dict):
82
+ self.prog = prog
83
+ self.env_values = env_values
84
+ self.stack: set = set()
85
+
86
+ def ph(self, r, conf=EXACT) -> Tpl:
87
+ n = last_name(r) or "?"
88
+ return Tpl("{" + n + "}", conf, unknown=False)
89
+
90
+ def eval(self, r, ctx: Ctx, depth=0) -> Tpl:
91
+ if not isinstance(r, dict) or depth > 14:
92
+ return Tpl("{?}", HEURISTIC, unknown=True)
93
+ k = r.get("k")
94
+ if k == "str":
95
+ return Tpl(r["v"])
96
+ if k in ("num", "bool"):
97
+ return Tpl(str(r["v"]).lower() if k == "bool" else str(r["v"]))
98
+ if k in ("tpl",):
99
+ out = Tpl()
100
+ for p in r["parts"]:
101
+ if isinstance(p, str):
102
+ out = out + Tpl(p)
103
+ elif isinstance(p, dict) and "e" in p:
104
+ out = out + self.interp(p["e"], ctx, depth)
105
+ elif isinstance(p, dict) and p.get("k") == "str":
106
+ out = out + Tpl(p["v"])
107
+ return out
108
+ if k == "cat":
109
+ out = Tpl()
110
+ for p in r["parts"]:
111
+ out = out + self.eval(p, ctx, depth + 1)
112
+ return out
113
+ if k == "bin" and r.get("op") == "+":
114
+ return self.interp(r["l"], ctx, depth) + self.interp(r["r"], ctx, depth)
115
+ if k == "bin" and r.get("op") == "??":
116
+ t = self.eval(r["l"], ctx, depth + 1)
117
+ return t if not t.unknown and "{" not in t.text[:1] else self.eval(r["r"], ctx, depth + 1)
118
+ if k in ("nn", "as", "await"):
119
+ return self.eval(r["e"], ctx, depth + 1)
120
+ if k == "cond":
121
+ t = self.eval(r["a"], ctx, depth + 1)
122
+ t.conf = min_conf(t.conf, HEURISTIC)
123
+ return t
124
+ if k == "id":
125
+ return self.name(r["v"], r, ctx, depth)
126
+ if k == "prop":
127
+ return self.prop(r, ctx, depth)
128
+ if k == "idx":
129
+ src = self.env_key(r)
130
+ if src:
131
+ return self.env(src)
132
+ return self.ph(r)
133
+ if k == "call" and ctor_type(r) != "Uri":
134
+ return self.call(r, ctx, depth)
135
+ if k == "new" or (k == "call" and ctor_type(r) == "Uri"):
136
+ base = (ctor_type(r) or "").split(".")[-1]
137
+ if base == "Uri":
138
+ na = r.get("na") or {}
139
+ out = Tpl()
140
+ if "scheme" in na and "host" in na:
141
+ out = self.eval(na["scheme"], ctx, depth + 1) + Tpl("://") + self.eval(na["host"], ctx, depth + 1)
142
+ if "port" in na:
143
+ out = out + Tpl(":") + self.interp(na["port"], ctx, depth + 1)
144
+ if "path" in na:
145
+ p = self.eval(na["path"], ctx, depth + 1)
146
+ out = out + (p if p.text.startswith("/") or not out.text else Tpl("/") + p)
147
+ return out if out.text else Tpl("{?}", HEURISTIC, unknown=True)
148
+ return Tpl("{?}", HEURISTIC, unknown=True)
149
+ return Tpl("{?}", HEURISTIC, unknown=True)
150
+
151
+ def interp(self, e, ctx: Ctx, depth) -> Tpl:
152
+ """Value used inside a template: a resolved string is inlined, anything else is a {name} placeholder."""
153
+ t = self.eval(e, ctx, depth + 1)
154
+ m = TOKEN.fullmatch(t.text)
155
+ if m and not HELPER_PARAM.search(m.group(1)):
156
+ return Tpl("{" + m.group(1) + "}", t.conf) # `/items/$id/`: a path parameter, not a URL-building helper
157
+ if t.unknown or (t.text.startswith("{") and t.text.endswith("}") and t.text.count("{") == 1 and not t.text.startswith(("{env:", "{@param:"))):
158
+ return self.ph(e)
159
+ return t
160
+
161
+ def env_key(self, r) -> str | None:
162
+ if r.get("k") == "idx" and (r.get("i") or {}).get("k") == "str":
163
+ t = r.get("t") or {}
164
+ if t.get("k") == "prop" and t.get("n") in ("env", "environment"):
165
+ return r["i"]["v"]
166
+ return None
167
+
168
+ def env(self, key: str) -> Tpl:
169
+ if key in self.env_values:
170
+ v = self.env_values[key][0]
171
+ return Tpl(v, RESOLVED, named=True, env=[key])
172
+ return Tpl("{env:" + key + "}", EXACT, named=True, env=[key])
173
+
174
+ def name(self, n: str, r, ctx: Ctx, depth) -> Tpl:
175
+ if n in ctx.bindings:
176
+ br, bctx = ctx.bindings[n]
177
+ if br is None:
178
+ return Tpl("{" + n + "}")
179
+ t = self.eval(br, bctx, depth + 1)
180
+ return t
181
+ loc = ctx.locals.get(n)
182
+ if loc:
183
+ kind, d = loc
184
+ if kind == "param":
185
+ return Tpl("{@param:" + n + "}")
186
+ init = d.get("init")
187
+ if init is None and ctx.fn:
188
+ for f in ctx.fn.facts:
189
+ if f["ft"] == "assign" and (f.get("lhs") or {}).get("k") == "id" and f["lhs"]["v"] == n:
190
+ init = f["rhs"]
191
+ break
192
+ if init is not None:
193
+ key = ("loc", id(ctx), n)
194
+ if key in self.stack:
195
+ return self.ph(r)
196
+ self.stack.add(key)
197
+ try:
198
+ t = self.eval(init, ctx, depth + 1)
199
+ finally:
200
+ self.stack.discard(key)
201
+ return t
202
+ return self.ph(r)
203
+ if ctx.cls:
204
+ m, _ = self.prog.find_member(ctx.cls, n)
205
+ if m is not None:
206
+ return self.member_value(m, r, depth)
207
+ v = self.prog.lookup(ctx.lib, n)
208
+ if v is not None:
209
+ return self.member_value(v, r, depth)
210
+ return self.ph(r)
211
+
212
+ def member_value(self, m, r, depth) -> Tpl:
213
+ key = ("m", id(m))
214
+ if key in self.stack:
215
+ return self.ph(r)
216
+ self.stack.add(key)
217
+ try:
218
+ if isinstance(m, DVar):
219
+ if m.init is not None:
220
+ t = self.eval(m.init, Ctx(self.prog, m.lib, None, m.cls), depth + 1)
221
+ t.named = True
222
+ return t
223
+ # field set from a constructor parameter with a default / initializer list
224
+ if m.cls:
225
+ for ct in m.cls.ctors.values():
226
+ for p in ct.params:
227
+ if p["name"] == m.name and p.get("this") and p.get("default") is not None:
228
+ t = self.eval(p["default"], Ctx(self.prog, m.lib, ct, m.cls), depth + 1)
229
+ t.named, t.conf = True, min_conf(t.conf, RESOLVED)
230
+ return t
231
+ for ini in ct.raw.get("inits") or []:
232
+ if ini.get("field") == m.name:
233
+ t = self.eval(ini["v"], Ctx(self.prog, m.lib, ct, m.cls), depth + 1)
234
+ if TOKEN.search(t.text):
235
+ continue
236
+ t.named, t.conf = True, min_conf(t.conf, RESOLVED)
237
+ return t
238
+ v = self.from_construction_sites(m, depth)
239
+ if v is not None:
240
+ return v
241
+ return self.ph(r)
242
+ if isinstance(m, DFunc) and m.kind == "getter":
243
+ for f in m.facts:
244
+ if f["ft"] == "return":
245
+ t = self.eval(f["v"], Ctx(self.prog, m.lib, m, m.cls), depth + 1)
246
+ t.named = True
247
+ return t
248
+ return self.ph(r)
249
+ finally:
250
+ self.stack.discard(key)
251
+
252
+ def from_construction_sites(self, m: DVar, depth) -> Tpl | None:
253
+ """Field initialised by a constructor parameter (`this.baseUrl`): value passed at the construction sites."""
254
+ vals = {}
255
+ for ct in m.cls.ctors.values():
256
+ pos = [p for p in ct.params if not p.get("named")]
257
+ for i, p in enumerate(ct.params):
258
+ if p["name"] != m.name or not (p.get("this") or any(ini.get("field") == m.name for ini in ct.raw.get("inits") or [])):
259
+ continue
260
+ for caller, fact, cctx in self.prog.callers.get(ct.id, [])[:30]:
261
+ arg = (fact.get("na") or {}).get(p["name"]) if p.get("named") else (
262
+ (fact.get("a") or [])[pos.index(p)] if p in pos and pos.index(p) < len(fact.get("a") or []) else None)
263
+ if arg is None:
264
+ continue
265
+ t = self.eval(arg, cctx, depth + 1)
266
+ if t.unknown or TOKEN.search(t.text):
267
+ continue
268
+ vals.setdefault(t.text, (t, f"{caller.file if caller else '?'}:{fact.get('l')}"))
269
+ if not vals:
270
+ return None
271
+ t, at = next(iter(vals.values()))
272
+ t.named = True
273
+ t.conf = min_conf(t.conf, RESOLVED if len(vals) == 1 else HEURISTIC)
274
+ return t
275
+
276
+ def prop(self, r, ctx: Ctx, depth) -> Tpl:
277
+ t, n = r["t"], r["n"]
278
+ if n in ("path", "toString") and self.prog.infer(t, ctx) == ("einst", "Uri", []):
279
+ return self.eval(t, ctx, depth + 1)
280
+ if t.get("k") == "id":
281
+ tv = t["v"]
282
+ if tv not in ctx.locals and tv not in ctx.bindings:
283
+ if self.prog.prefix_imports(ctx.lib, tv):
284
+ v = self.prog.lookup_prefixed(ctx.lib, tv, n)
285
+ return self.member_value(v, r, depth) if v is not None else self.ph(r)
286
+ c = self.prog.resolve_class(ctx.lib, tv)
287
+ if c is None and ctx.cls:
288
+ c = None
289
+ if c is not None:
290
+ m, _ = self.prog.find_member(c, n)
291
+ return self.member_value(m, r, depth) if m is not None else self.ph(r)
292
+ if t.get("k") == "this" and ctx.cls:
293
+ m, _ = self.prog.find_member(ctx.cls, n)
294
+ return self.member_value(m, r, depth) if m is not None else self.ph(r)
295
+ tt = self.prog.infer(t, ctx)
296
+ if tt and tt[0] == "inst" and tt[1] is not None:
297
+ m, _ = self.prog.find_member(tt[1], n)
298
+ if m is not None:
299
+ v = self.member_value(m, r, depth)
300
+ v.conf = min_conf(v.conf, RESOLVED)
301
+ return v
302
+ return self.ph(r)
303
+
304
+ def call(self, r, ctx: Ctx, depth) -> Tpl:
305
+ n, t = r.get("n"), r.get("t")
306
+ a, na = r.get("a") or [], r.get("na") or {}
307
+ rn = root_name(t) if t else None
308
+ if n in ("parse", "tryParse") and rn == "Uri" and a:
309
+ return self.eval(a[0], ctx, depth + 1)
310
+ if n in ("https", "http") and rn == "Uri" and a:
311
+ out = Tpl(n + "://") + self.eval(a[0], ctx, depth + 1)
312
+ if len(a) > 1:
313
+ p = self.eval(a[1], ctx, depth + 1)
314
+ out = out + (p if p.text.startswith("/") else Tpl("/") + p)
315
+ return out
316
+ if n in ("toString", "trim", "toLowerCase") and t:
317
+ return self.eval(t, ctx, depth + 1)
318
+ if n == "fromEnvironment" and a and a[0].get("k") == "str":
319
+ return self.env(a[0]["v"])
320
+ if n in ("get", "maybeGet", "getOrElse") and t and (rn or "").lower() in ("dotenv", "env") and a and a[0].get("k") == "str":
321
+ return self.env(a[0]["v"])
322
+ if n == "replace" and t and "path" in na:
323
+ base = self.eval(t, ctx, depth + 1)
324
+ m = re.match(r"^([a-zA-Z][a-zA-Z0-9+.-]*://[^/]*)", base.text)
325
+ return Tpl(m.group(1) if m else "", base.conf, base.named) + self.eval(na["path"], ctx, depth + 1)
326
+ if n == "resolve" and t and a:
327
+ return join(self.eval(t, ctx, depth + 1), self.eval(a[0], ctx, depth + 1))
328
+ if n == "join" and a:
329
+ if a[0].get("k") == "list" and t: # [...].join('/')? (t is the list)
330
+ pass
331
+ out = None
332
+ for x in a:
333
+ v = self.eval(x, ctx, depth + 1)
334
+ out = v if out is None else join(out, v)
335
+ if out is not None and rn in ("p", "path", "posix", "url"):
336
+ return out
337
+ if t and t.get("k") == "list" and n == "join":
338
+ sep = a[0]["v"] if a and a[0].get("k") == "str" else ""
339
+ out = Tpl()
340
+ for i, x in enumerate(t.get("items") or []):
341
+ out = out + (Tpl(sep) if i else Tpl()) + self.interp(x, ctx, depth)
342
+ return out
343
+ if depth < 10:
344
+ for tgt, conf, via in self.prog.resolve_call(r, ctx):
345
+ if isinstance(tgt, DFunc) and tgt.kind in ("function", "method", "getter"):
346
+ rets = [f for f in tgt.facts if f["ft"] == "return"]
347
+ if not rets:
348
+ continue
349
+ key = ("call", tgt.id, id(ctx))
350
+ if key in self.stack:
351
+ continue
352
+ self.stack.add(key)
353
+ try:
354
+ b = bind_args(tgt, r, ctx)
355
+ v = self.eval(rets[0]["v"], Ctx(self.prog, tgt.lib, tgt, tgt.cls, b), depth + 1)
356
+ finally:
357
+ self.stack.discard(key)
358
+ v.conf = min_conf(v.conf, conf)
359
+ v.named = True
360
+ return v
361
+ return self.ph(r)
362
+
363
+
364
+ def join(a: Tpl, b: Tpl) -> Tpl:
365
+ if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", b.text):
366
+ return b
367
+ if not a.text:
368
+ return b
369
+ return Tpl(a.text.rstrip("/") + "/" + b.text.lstrip("/"), min_conf(a.conf, b.conf), a.named, a.env + b.env, a.unknown or b.unknown)
370
+
371
+
372
+ def bind_args(fn: DFunc, call: dict, cctx: Ctx) -> dict:
373
+ """Parameter name -> (argument repr, caller ctx)."""
374
+ b = {}
375
+ pos = [p for p in fn.params if not p.get("named")]
376
+ a = call.get("a") or []
377
+ na = call.get("na") or {}
378
+ for i, p in enumerate(pos):
379
+ if i < len(a):
380
+ b[p["name"]] = (a[i], cctx)
381
+ elif p.get("default") is not None:
382
+ b[p["name"]] = (p["default"], Ctx(cctx.prog, fn.lib, fn, fn.cls))
383
+ for p in fn.params:
384
+ if p.get("named"):
385
+ if p["name"] in na:
386
+ b[p["name"]] = (na[p["name"]], cctx)
387
+ elif p.get("default") is not None:
388
+ b[p["name"]] = (p["default"], Ctx(cctx.prog, fn.lib, fn, fn.cls))
389
+ else:
390
+ b[p["name"]] = (None, cctx) # omitted optional named argument
391
+ return b
392
+
393
+
394
+ def type_label(r, ctx: Ctx) -> str | None:
395
+ if not isinstance(r, dict):
396
+ return None
397
+ k = r.get("k")
398
+ if k in ("str", "tpl", "cat"):
399
+ return "String"
400
+ if k == "num":
401
+ return "int" if isinstance(r.get("v"), int) else "double"
402
+ if k == "bool":
403
+ return "bool"
404
+ if k == "null":
405
+ return "Null"
406
+ if k == "cond":
407
+ a, b = type_label(r.get("a"), ctx), type_label(r.get("b"), ctx)
408
+ if a == "Null" and b:
409
+ return b.rstrip("?") + "?"
410
+ if b == "Null" and a:
411
+ return a.rstrip("?") + "?"
412
+ return a if a == b else None
413
+ if k in ("map",):
414
+ return "Map"
415
+ if k == "list":
416
+ return "List"
417
+ if k == "id":
418
+ loc = ctx.locals.get(r["v"])
419
+ if loc and loc[1].get("type"):
420
+ return loc[1]["type"]
421
+ if r["v"] in ctx.bindings and ctx.bindings[r["v"]][0] is not None:
422
+ br, bctx = ctx.bindings[r["v"]]
423
+ return type_label(br, bctx)
424
+ if k == "prop" or k == "id":
425
+ tt = ctx.prog.infer(r["t"], ctx) if k == "prop" else None
426
+ if k == "prop" and tt and tt[0] == "inst" and tt[1] is not None:
427
+ m, _ = ctx.prog.find_member(tt[1], r["n"])
428
+ if isinstance(m, DVar) and m.type:
429
+ return m.type
430
+ if k == "id" and ctx.cls:
431
+ m, _ = ctx.prog.find_member(ctx.cls, r["v"])
432
+ if isinstance(m, DVar) and m.type:
433
+ return m.type
434
+ if k == "call" and r.get("n") in ("toString", "toIso8601String"):
435
+ return "String"
436
+ if k == "call" and r.get("n") in ("toJson", "toMap"):
437
+ return "Map"
438
+ if k == "nn":
439
+ t = type_label(r["e"], ctx)
440
+ return t[:-1] if t and t.endswith("?") else t
441
+ t = ctx.prog.infer(r, ctx)
442
+ if t and t[0] == "inst" and t[1] is not None:
443
+ return t[1].name
444
+ if t and t[0] == "einst":
445
+ return t[1]
446
+ if t and t[0] == "list":
447
+ return "List"
448
+ return None
449
+
450
+
451
+ class HttpExtractor:
452
+ def __init__(self, prog: DartProgram, ev: UrlEval, model_keys):
453
+ self.prog, self.ev = prog, ev
454
+ self.model_keys = model_keys # (DClass, "to"|"from") -> list[dict]
455
+ self.calls: list[dict] = []
456
+ self.dio_bases: list[tuple[Tpl, DFunc | None, DClass | None, str]] = []
457
+ self._returns_json: dict[str, bool] = {}
458
+
459
+ # ---------------------------------------------------------------- receivers
460
+ def client_of(self, t, ctx: Ctx):
461
+ """('http'|'dio'|'io', confidence) for a call receiver, or None."""
462
+ if t is None:
463
+ return None
464
+ if t.get("k") == "id" and t["v"] not in ctx.locals and t["v"] not in ctx.bindings:
465
+ for imp in self.prog.prefix_imports(ctx.lib, t["v"]):
466
+ if imp["ext"] and imp["uri"].startswith("package:http/"):
467
+ return ("http", EXACT)
468
+ if imp["ext"] and imp["uri"].startswith("package:dio/"):
469
+ return None
470
+ tt = self.prog.infer(t, ctx)
471
+ if tt and tt[0] == "einst":
472
+ b = tt[1].split(".")[-1]
473
+ if b in HTTP_CLIENT_TYPES and self.prog.imports_ext(ctx.lib, r"^package:http") or b in ("IOClient", "RetryClient"):
474
+ return ("http", RESOLVED)
475
+ if b in DIO_TYPES:
476
+ return ("dio", RESOLVED)
477
+ if b == "HttpClient":
478
+ return ("io", RESOLVED)
479
+ if tt and tt[0] == "inst" and tt[1] is not None:
480
+ names = self.prog.ext_base_names(tt[1])
481
+ if names & DIO_TYPES:
482
+ return ("dio", RESOLVED)
483
+ if names & HTTP_CLIENT_TYPES:
484
+ return ("http", RESOLVED)
485
+ return None
486
+ if tt is None:
487
+ nm = (last_name(t) or "")
488
+ if re.search(r"(?i)dio$", nm) and self.prog.imports_ext(ctx.lib, r"^package:dio"):
489
+ return ("dio", HEURISTIC)
490
+ if re.search(r"(?i)(http|client)$", nm) and self.prog.imports_ext(ctx.lib, r"^package:http/"):
491
+ return ("http", HEURISTIC)
492
+ return None
493
+
494
+ def returns_json(self, f: DFunc, depth=0) -> bool:
495
+ if f.id in self._returns_json:
496
+ return self._returns_json[f.id]
497
+ self._returns_json[f.id] = False
498
+ ctx = self.prog.ctx_of(f)
499
+ out = False
500
+ for x in f.facts:
501
+ if x["ft"] == "return" and self.is_json_value(x["v"], ctx, depth + 1):
502
+ out = True
503
+ break
504
+ self._returns_json[f.id] = out
505
+ return out
506
+
507
+ def wrap_key(self, f: DFunc, depth=0):
508
+ """'' when f returns decoded JSON, 'body' when it returns {'statusCode': .., 'body': <json>}, else None."""
509
+ key = ("wrap", f.id)
510
+ if key in self._returns_json:
511
+ return self._returns_json[key]
512
+ self._returns_json[key] = None
513
+ ctx = self.prog.ctx_of(f)
514
+ out = None
515
+ for x in f.facts:
516
+ if x["ft"] != "return":
517
+ continue
518
+ v = x["v"]
519
+ while v.get("k") in ("await", "nn", "as"):
520
+ v = v["e"]
521
+ if self.is_json_value(v, ctx, 0):
522
+ out = ""
523
+ break
524
+ if v.get("k") == "map":
525
+ for el in v.get("entries") or []:
526
+ if (el.get("key") or {}).get("k") == "str" and self.is_json_value(el.get("value"), ctx, 0):
527
+ out = el["key"]["v"]
528
+ if out is not None:
529
+ break
530
+ if v.get("k") == "call":
531
+ for tgt, conf, via in self.prog.resolve_call(v, ctx):
532
+ if isinstance(tgt, DFunc):
533
+ k = self.wrap_key(tgt)
534
+ if k is not None:
535
+ out = k
536
+ break
537
+ if out is not None:
538
+ break
539
+ self._returns_json[key] = out
540
+ return out
541
+
542
+ def wrapper_var(self, r, ctx: Ctx):
543
+ """Wrapper key if r is (a variable holding) the result of a helper returning {'body': json, ...}."""
544
+ while isinstance(r, dict) and r.get("k") in ("await", "nn", "as"):
545
+ r = r["e"]
546
+ if not isinstance(r, dict):
547
+ return None
548
+ if r.get("k") == "id":
549
+ loc = ctx.locals.get(r["v"])
550
+ if loc and loc[0] == "var" and loc[1].get("init") is not None:
551
+ return self.wrapper_var(loc[1]["init"], ctx)
552
+ return None
553
+ if r.get("k") == "call":
554
+ for tgt, conf, via in self.prog.resolve_call(r, ctx):
555
+ if isinstance(tgt, DFunc):
556
+ k = self.wrap_key(tgt)
557
+ if k:
558
+ return k
559
+ return None
560
+
561
+ def is_json_value(self, r, ctx: Ctx, depth=0) -> bool:
562
+ if not isinstance(r, dict) or depth > 4:
563
+ return False
564
+ k = r.get("k")
565
+ if k in ("await", "nn", "as"):
566
+ return self.is_json_value(r["e"], ctx, depth)
567
+ if k == "call" and r.get("n") in ("jsonDecode", "decode") and (r.get("n") == "jsonDecode" or root_name(r.get("t")) in ("json", "jsonCodec", "convert")):
568
+ return True
569
+ if k == "call" and r.get("n") is None and root_name(r.get("t")) == "jsonDecode":
570
+ return True
571
+ if k == "idx" and (r.get("i") or {}).get("k") == "str" and depth < 4:
572
+ wk = self.wrapper_var(r.get("t"), ctx)
573
+ if wk and wk == r["i"]["v"]:
574
+ return True
575
+ if k == "prop" and r["n"] == "data":
576
+ tt = self.prog.infer(r["t"], ctx)
577
+ return tt is None or (tt[0] == "einst" and "Response" in tt[1])
578
+ if k == "id":
579
+ loc = ctx.locals.get(r["v"])
580
+ if loc and loc[0] == "var" and loc[1].get("init") is not None:
581
+ return self.is_json_value(loc[1]["init"], ctx, depth + 1)
582
+ if k == "call":
583
+ for tgt, conf, via in self.prog.resolve_call(r, ctx):
584
+ if isinstance(tgt, DFunc) and self.wrap_key(tgt) == "":
585
+ return True
586
+ return False
587
+
588
+ # ---------------------------------------------------------------- bodies
589
+ def keys_of(self, r, ctx: Ctx, depth=0, cond=False) -> list[dict]:
590
+ if not isinstance(r, dict) or depth > 6:
591
+ return []
592
+ k = r.get("k")
593
+ out: list[dict] = []
594
+ if k == "map":
595
+ for el in r.get("entries") or []:
596
+ if "key" in el:
597
+ kk = el["key"]
598
+ if kk.get("k") == "str":
599
+ out.append({"key": kk["v"], "line": el.get("l") or r.get("l"), "file": ctx.fn.file if ctx.fn else ctx.lib.file,
600
+ "type": type_label(el.get("value"), ctx), "conditional": cond or None,
601
+ "value": (el.get("value") or {}).get("v") if (el.get("value") or {}).get("k") in ("str", "num", "bool") else None})
602
+ elif el.get("k") == "spread":
603
+ out += self.keys_of(el["e"], ctx, depth + 1, cond)
604
+ elif el.get("k") == "if":
605
+ then = self.keys_of({"k": "map", "entries": [el["then"]], "l": r.get("l")}, ctx, depth + 1, True)
606
+ # `if (x != null) 'k': x` - flow promotion makes the value non-null
607
+ m = re.fullmatch(r"\s*([\w.]+)\s*!=\s*null\s*", el.get("c") or "")
608
+ v = (el.get("then") or {}).get("value") or {}
609
+ if m and v.get("k") in ("id", "prop") and then and (v.get("v") == m.group(1) or v.get("n") == m.group(1).split(".")[-1]):
610
+ for kd in then:
611
+ if kd.get("type"):
612
+ kd["type"] = kd["type"].rstrip("?")
613
+ out += then
614
+ if el.get("else"):
615
+ out += self.keys_of({"k": "map", "entries": [el["else"]], "l": r.get("l")}, ctx, depth + 1, True)
616
+ return out
617
+ if k in ("await", "nn", "as"):
618
+ return self.keys_of(r["e"], ctx, depth + 1, cond)
619
+ if k == "cond":
620
+ return self.keys_of(r["a"], ctx, depth + 1, True) + self.keys_of(r["b"], ctx, depth + 1, True)
621
+ if k == "call":
622
+ n = r.get("n")
623
+ if n in ("jsonEncode", "encode", "fromMap") or (n is None and root_name(r.get("t")) == "jsonEncode"):
624
+ a = r.get("a") or []
625
+ return self.keys_of(a[0], ctx, depth + 1, cond) if a else []
626
+ if n in ("toJson", "toMap") and r.get("t"):
627
+ tt = self.prog.infer(r["t"], ctx)
628
+ if tt and tt[0] == "inst" and tt[1] is not None:
629
+ return [dict(x, model=tt[1].id) for x in self.model_keys(tt[1], "to")]
630
+ for tgt, conf, via in self.prog.resolve_call(r, ctx):
631
+ if isinstance(tgt, DFunc) and depth < 4:
632
+ for x in tgt.facts:
633
+ if x["ft"] == "return":
634
+ return self.keys_of(x["v"], Ctx(self.prog, tgt.lib, tgt, tgt.cls, bind_args(tgt, r, ctx)), depth + 1, cond)
635
+ return []
636
+ if k == "id":
637
+ n = r["v"]
638
+ if n in ctx.bindings:
639
+ br, bctx = ctx.bindings[n]
640
+ return self.keys_of(br, bctx, depth + 1, cond) if br is not None else []
641
+ loc = ctx.locals.get(n)
642
+ if loc and loc[0] == "var":
643
+ out = self.keys_of(loc[1].get("init"), ctx, depth + 1, cond) if loc[1].get("init") is not None else []
644
+ for f in (ctx.fn.facts if ctx.fn else []):
645
+ if f["ft"] == "index" and f.get("write") and (f.get("target") or {}).get("k") == "id" and f["target"]["v"] == n \
646
+ and (f.get("key") or {}).get("k") == "str":
647
+ out.append({"key": f["key"]["v"], "line": f["l"], "file": ctx.fn.file, "type": None, "conditional": True})
648
+ if f["ft"] == "call" and f.get("n") in ("addAll", "putIfAbsent") and (f.get("t") or {}).get("k") == "id" and f["t"]["v"] == n:
649
+ out += self.keys_of((f.get("a") or [None])[0], ctx, depth + 1, True)
650
+ return out
651
+ if loc and loc[0] == "param":
652
+ tt = self.prog.infer(r, ctx)
653
+ if tt and tt[0] == "inst" and tt[1] is not None:
654
+ return [dict(x, model=tt[1].id) for x in self.model_keys(tt[1], "to")]
655
+ return []
656
+ if k == "cascade":
657
+ out = self.keys_of(r["t"], ctx, depth + 1, cond)
658
+ for s in r.get("sections") or []:
659
+ if s.get("k") == "assign" and (s.get("lhs") or {}).get("k") == "idx" and (s["lhs"].get("i") or {}).get("k") == "str":
660
+ out.append({"key": s["lhs"]["i"]["v"], "line": s["lhs"].get("l"), "file": ctx.fn.file if ctx.fn else None,
661
+ "type": type_label(s.get("rhs"), ctx), "conditional": cond or None})
662
+ return out
663
+ if (ctor_type(r) or "").split(".")[-1] == "FormData":
664
+ return []
665
+ tt = self.prog.infer(r, ctx)
666
+ if tt and tt[0] == "inst" and tt[1] is not None:
667
+ return [dict(x, model=tt[1].id) for x in self.model_keys(tt[1], "to")]
668
+ return []
669
+
670
+ # ---------------------------------------------------------------- responses
671
+ def response_facts(self, fn: DFunc, ctx: Ctx) -> dict:
672
+ """JSON keys read, models parsed (X.fromJson) and status codes checked in the function handling the response."""
673
+ roots: dict[str, str] = {} # var -> json path
674
+ for f in fn.facts:
675
+ if f["ft"] == "var" and f.get("init") is not None:
676
+ if self.is_json_value(f["init"], ctx):
677
+ roots[f["name"]] = ""
678
+ else:
679
+ init = f["init"]
680
+ while init.get("k") in ("as", "nn", "await"):
681
+ init = init["e"]
682
+ if init.get("k") == "idx" and (init.get("t") or {}).get("k") == "id" and init["t"]["v"] in roots \
683
+ and (init.get("i") or {}).get("k") == "str":
684
+ p = roots[init["t"]["v"]]
685
+ roots[f["name"]] = (p + "." if p else "") + init["i"]["v"]
686
+ # `(json['items'] as List).map((e) => X.fromJson(e))`: closure parameter e = items[]
687
+ clos: dict[str, str] = {}
688
+ for f in fn.facts:
689
+ if f["ft"] == "call" and f.get("n") in ("map", "forEach", "where", "expand") and f.get("a") and (f["a"][0] or {}).get("k") == "fn":
690
+ tg = f.get("t") or {}
691
+ while tg.get("k") in ("as", "nn", "await"):
692
+ tg = tg["e"]
693
+ path = None
694
+ if tg.get("k") == "id" and tg["v"] in roots:
695
+ path = roots[tg["v"]]
696
+ elif tg.get("k") == "idx" and (tg.get("i") or {}).get("k") == "str":
697
+ base = tg.get("t") or {}
698
+ while base.get("k") in ("as", "nn"):
699
+ base = base["e"]
700
+ if base.get("k") == "id" and base["v"] in roots:
701
+ path = ((roots[base["v"]] + ".") if roots[base["v"]] else "") + tg["i"]["v"]
702
+ elif self.is_json_value(base, ctx):
703
+ path = tg["i"]["v"]
704
+ if path is not None and f["a"][0].get("params"):
705
+ clos.setdefault(f["a"][0]["params"][0], []).append((f["a"][0].get("l") or f.get("l") or 0, (path + "[]") if path else "[]"))
706
+ keys, models, status = [], [], []
707
+ for f in fn.facts:
708
+ if f["ft"] == "index" and not f.get("write") and (f.get("key") or {}).get("k") == "str":
709
+ tg = f.get("target") or {}
710
+ while tg.get("k") in ("as", "nn", "await"):
711
+ tg = tg["e"]
712
+ path = None
713
+ if tg.get("k") == "id" and tg["v"] in roots:
714
+ path = roots[tg["v"]]
715
+ elif self.is_json_value(tg, ctx):
716
+ path = ""
717
+ if path is not None:
718
+ keys.append({"key": f["key"]["v"], "path": path or None, "line": f["l"], "file": fn.file, "cast": f.get("cast"),
719
+ "coalesce": f.get("coalesce"), "post": f.get("post"), "wrap": f.get("wrap")})
720
+ elif f["ft"] == "call" and f.get("n") in ("fromJson", "fromMap") and (f.get("t") or {}).get("k") == "id":
721
+ c = self.prog.resolve_class(ctx.lib, f["t"]["v"])
722
+ if c:
723
+ arg = (f.get("a") or [None])[0]
724
+ p = None
725
+ if isinstance(arg, dict):
726
+ a2 = arg
727
+ while a2.get("k") in ("as", "nn"):
728
+ a2 = a2["e"]
729
+ if a2.get("k") == "id" and a2["v"] in clos and a2["v"] not in roots:
730
+ prior = [c_ for c_ in clos[a2["v"]] if c_[0] <= (f.get("l") or 0)]
731
+ p = max(prior)[1] if prior else clos[a2["v"]][0][1]
732
+ elif a2.get("k") == "id" and a2["v"] in roots:
733
+ p = roots[a2["v"]] or ""
734
+ elif a2.get("k") == "idx" and (a2.get("t") or {}).get("k") == "id" and a2["t"]["v"] in roots and (a2.get("i") or {}).get("k") == "str":
735
+ p = ((roots[a2["t"]["v"]] + ".") if roots[a2["t"]["v"]] else "") + a2["i"]["v"]
736
+ elif self.is_json_value(a2, ctx):
737
+ p = ""
738
+ models.append({"model": c.id, "name": c.name, "path": p, "line": f["l"], "file": fn.file})
739
+ elif f["ft"] == "status":
740
+ status.append({"op": f["op"], "v": f["v"], "line": f["l"], "file": fn.file, "expr": f.get("e")})
741
+ return {"keys": keys, "models": models, "status": status}
742
+
743
+ # ---------------------------------------------------------------- scanning
744
+ def collect_dio_bases(self):
745
+ for fn in self.prog.all_funcs():
746
+ ctx = self.prog.ctx_of(fn)
747
+ for f in fn.facts:
748
+ if (ctor_type(f) or "").split(".")[-1] == "BaseOptions" and "baseUrl" in (f.get("na") or {}):
749
+ self.dio_bases.append((self.ev.eval(f["na"]["baseUrl"], ctx), fn, fn.cls, fn.file))
750
+ if f["ft"] == "assign" and (f.get("lhs") or {}).get("k") == "prop" and f["lhs"]["n"] == "baseUrl":
751
+ self.dio_bases.append((self.ev.eval(f["rhs"], ctx), fn, fn.cls, fn.file))
752
+ for c in self.prog.classes.values():
753
+ for v in c.fields.values():
754
+ for x in walk_repr(v.init):
755
+ if (ctor_type(x) or "").split(".")[-1] == "BaseOptions" and "baseUrl" in (x.get("na") or {}):
756
+ self.dio_bases.append((self.ev.eval(x["na"]["baseUrl"], Ctx(self.prog, c.lib, None, c)), None, c, c.file))
757
+ for v in self.prog.vars:
758
+ for x in walk_repr(v.init):
759
+ if (ctor_type(x) or "").split(".")[-1] == "BaseOptions" and "baseUrl" in (x.get("na") or {}):
760
+ self.dio_bases.append((self.ev.eval(x["na"]["baseUrl"], Ctx(self.prog, v.lib, None, None)), None, None, v.file))
761
+
762
+ def dio_base_for(self, fn: DFunc) -> list[tuple[Tpl, str]]:
763
+ if not self.dio_bases:
764
+ return []
765
+ same = [b for b in self.dio_bases if fn.cls is not None and b[2] is fn.cls] or [b for b in self.dio_bases if b[3] == fn.file]
766
+ cands = same or self.dio_bases
767
+ uniq = {}
768
+ for t, *_ in cands:
769
+ uniq.setdefault(t.text, t)
770
+ conf = RESOLVED if len(uniq) == 1 else HEURISTIC
771
+ if len(uniq) > 3:
772
+ return []
773
+ return [(t, conf) for t in uniq.values()]
774
+
775
+ def scan(self):
776
+ self.collect_dio_bases()
777
+ for fn in list(self.prog.all_funcs()):
778
+ ctx = self.prog.ctx_of(fn)
779
+ for f in fn.facts:
780
+ ct = ctor_type(f)
781
+ if ct and f["ft"] in ("new", "call"):
782
+ self.http_new(fn, ctx, f, ct)
783
+ elif f["ft"] == "call":
784
+ self.http_call(fn, ctx, f)
785
+ self.retrofit()
786
+ return self.calls
787
+
788
+ def http_call(self, fn, ctx, f):
789
+ n, t = f.get("n"), f.get("t")
790
+ a, na = f.get("a") or [], f.get("na") or {}
791
+ if n is None:
792
+ return
793
+ rn = root_name(t) if t else None
794
+ if n == "connect" and t and t.get("k") == "id" and t["v"] in WS_TYPES and a:
795
+ return self.add(fn, ctx, f, "WS", a[0], "websocket", EXACT)
796
+ if t is None:
797
+ if n in HTTP_PKG_METHODS and a and not self.prog._unqualified(n, ctx) and n not in ctx.locals \
798
+ and self.prog.imports_ext(ctx.lib, r"^package:http/", None, name=n):
799
+ return self.add(fn, ctx, f, HTTP_PKG_METHODS[n], a[0], "http", EXACT, body=na.get("body"))
800
+ return
801
+ cl = None
802
+ if n in HTTP_PKG_METHODS or n in DIO_METHODS or n in IO_METHODS or n in IO_HOST_METHODS or n in ("send", "fetch"):
803
+ cl = self.client_of(t, ctx)
804
+ if not cl:
805
+ return
806
+ kind, conf = cl
807
+ if kind == "http" and n in HTTP_PKG_METHODS and a:
808
+ return self.add(fn, ctx, f, HTTP_PKG_METHODS[n], a[0], "http", conf, body=na.get("body"))
809
+ if kind == "dio" and n in DIO_METHODS and a:
810
+ method = DIO_METHODS[n]
811
+ if method is None:
812
+ opts = na.get("options") or {}
813
+ m = ((opts.get("na") or {}).get("method") or {}) if isinstance(opts, dict) else {}
814
+ method = m.get("v", "GET").upper() if m.get("k") == "str" else "GET"
815
+ return self.add(fn, ctx, f, method, a[0], "dio", conf, body=na.get("data"), query=na.get("queryParameters"))
816
+ if kind == "io" and n in IO_HOST_METHODS and len(a) >= 3:
817
+ method = IO_HOST_METHODS[n]
818
+ off = 0
819
+ if method is None:
820
+ method = a[0].get("v", "GET").upper() if a[0].get("k") == "str" else "GET"
821
+ off = 1
822
+ if len(a) >= off + 3:
823
+ url = {"k": "tpl", "parts": ["http://", {"e": a[off]}, ":", {"e": a[off + 1]}, {"e": a[off + 2]}]}
824
+ return self.add(fn, ctx, f, method, url, "dart:io", conf)
825
+ if kind == "io" and n in IO_METHODS and a:
826
+ method = IO_METHODS[n]
827
+ uarg = a[0]
828
+ if method is None and len(a) > 1:
829
+ method = a[0].get("v", "GET").upper() if a[0].get("k") == "str" else "GET"
830
+ uarg = a[1]
831
+ return self.add(fn, ctx, f, method or "GET", uarg, "dart:io", conf)
832
+
833
+ def http_new(self, fn, ctx, f, ctype):
834
+ base = ctype.split(".")[-1]
835
+ a = f.get("a") or []
836
+ if base in ("Request", "MultipartRequest", "StreamedRequest") and len(a) >= 2 and (
837
+ "." in ctype or self.prog.imports_ext(ctx.lib, r"^package:http/")):
838
+ mt = self.ev.eval(a[0], ctx)
839
+ method = mt.text.upper() if mt.text and "{" not in mt.text else None
840
+ body_keys = []
841
+ # request.fields['k'] = v / request.body = jsonEncode(...) on the variable holding the request
842
+ var = None
843
+ for x in fn.facts:
844
+ if x["ft"] == "var" and isinstance(x.get("init"), dict) and x["init"].get("l") == f["l"] and ctor_type(x["init"]):
845
+ var = x["name"]
846
+ if var:
847
+ for x in fn.facts:
848
+ if x["ft"] == "index" and x.get("write") and (x.get("key") or {}).get("k") == "str":
849
+ tg = x.get("target") or {}
850
+ if tg.get("k") == "prop" and tg.get("n") == "fields" and root_name(tg) == var:
851
+ body_keys.append({"key": x["key"]["v"], "line": x["l"], "file": fn.file, "type": None, "form": True})
852
+ if x["ft"] == "assign" and (x.get("lhs") or {}).get("k") == "prop" and x["lhs"]["n"] in ("body", "bodyFields") \
853
+ and root_name(x["lhs"]) == var:
854
+ body_keys += self.keys_of(x["rhs"], ctx)
855
+ if x["ft"] == "call" and x.get("n") == "addAll" and (x.get("t") or {}).get("k") == "prop" and x["t"]["n"] == "fields" \
856
+ and root_name(x["t"]) == var:
857
+ body_keys += [dict(k, form=True) for k in self.keys_of((x.get("a") or [None])[0], ctx)]
858
+ self.add(fn, ctx, f, method, a[1], "http", EXACT, body_keys=body_keys, method_repr=a[0] if method is None else None,
859
+ multipart=base == "MultipartRequest")
860
+
861
+ def add(self, fn, ctx, f, method, url_repr, client, conf, body=None, query=None, body_keys=None, method_repr=None, multipart=False):
862
+ self.calls.append({"fn": fn, "ctx": ctx, "fact": f, "method": method, "url": url_repr, "client": client, "conf": conf,
863
+ "body": body, "query": query, "body_keys": body_keys, "method_repr": method_repr, "multipart": multipart,
864
+ "line": f.get("l"), "file": fn.file})
865
+
866
+ def retrofit(self):
867
+ for c in self.prog.classes.values():
868
+ anns = {a["name"].split(".")[-1]: a for a in c.raw.get("ann") or []}
869
+ rest = anns.get("RestApi")
870
+ chop = anns.get("ChopperApi")
871
+ if not rest and not chop:
872
+ continue
873
+ ann = rest or chop
874
+ base = (ann.get("na") or {}).get("baseUrl")
875
+ ctx0 = Ctx(self.prog, c.lib, None, c)
876
+ base_t = self.ev.eval(base, ctx0) if base else None
877
+ for m in c.methods.values():
878
+ for a in m.raw.get("ann") or []:
879
+ an = a["name"].split(".")[-1]
880
+ verb = an if (rest and an in RETROFIT_VERBS) else (CHOPPER_VERBS.get(an) if chop else None)
881
+ if not verb:
882
+ continue
883
+ pr = (a.get("a") or [None])[0] if rest else (a.get("na") or {}).get("path")
884
+ path = self.ev.eval(pr, ctx0) if pr else Tpl("")
885
+ body_keys, query = [], []
886
+ for p in m.params:
887
+ for pa in p.get("ann") or []:
888
+ pn = pa["name"].split(".")[-1]
889
+ if pn == "Body":
890
+ pt = self.prog.type_of_text(p.get("type"), m.lib)
891
+ if pt and pt[0] == "inst":
892
+ body_keys += [dict(x, model=pt[1].id) for x in self.model_keys(pt[1], "to")]
893
+ if pn in ("Field", "Part"):
894
+ kk = (pa.get("a") or [{}])[0] or {}
895
+ body_keys.append({"key": kk.get("v") or p["name"], "line": pa.get("l"), "file": m.file, "type": p.get("type"),
896
+ "form": True})
897
+ if pn == "Query":
898
+ kk = (pa.get("a") or [{}])[0] or {}
899
+ query.append(kk.get("v") or p["name"])
900
+ full = join(base_t, path) if base_t else path
901
+ self.calls.append({"fn": m, "ctx": self.prog.ctx_of(m), "fact": a, "method": verb, "url": None, "tpl": full,
902
+ "client": "retrofit" if rest else "chopper", "conf": EXACT, "body": None, "body_keys": body_keys,
903
+ "query_list": query, "line": a.get("l"), "file": m.file, "declared": True,
904
+ "base_from_dio": base_t is None})