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