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,625 @@
|
|
|
1
|
+
"""Dart language plugin.
|
|
2
|
+
|
|
3
|
+
Facts come from extractor/bin/extract.dart (package:analyzer `parseString`, unresolved AST, no
|
|
4
|
+
pub get of the analysed project needed). Names/types are resolved by program.DartProgram.
|
|
5
|
+
|
|
6
|
+
Nodes: module:<file>, class:<file>#C, method:<file>#C.m (ctor = C.new / C.named,
|
|
7
|
+
bloc closure handlers = C.on<Event>@line), function:<file>#f, http:<METHOD> <path>, env:<KEY>.
|
|
8
|
+
Edges: CONTAINS, IMPORTS, EXTENDS, IMPLEMENTS, OVERRIDDEN_BY, IMPLEMENTED_BY, CALLS,
|
|
9
|
+
INSTANTIATES, HTTP_CALLS, PARSES_JSON, READS_ENV.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ... import presets
|
|
25
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED, CONFIDENCE_RANK
|
|
26
|
+
from ...core.paths import rules as path_rules
|
|
27
|
+
from ...core import cache, extractors, fsutil
|
|
28
|
+
from ...core.plugin import FrameworkPlugin, GraphBuilder, LanguagePlugin, Project
|
|
29
|
+
from .http import TOKEN, HttpExtractor, Tpl, UrlEval, bind_args, join, load_env_files, min_conf
|
|
30
|
+
from .models import ModelIndex
|
|
31
|
+
from .program import Ctx, DartProgram, DClass, DFunc, root_name, split_type, walk_repr
|
|
32
|
+
|
|
33
|
+
EXTRACTOR_DIR = Path(__file__).parent / "extractor"
|
|
34
|
+
EXTRACTOR = EXTRACTOR_DIR / "bin" / "extract.dart"
|
|
35
|
+
BIN = EXTRACTOR_DIR / ".bin" / "extract"
|
|
36
|
+
# pub / build output, CocoaPods, FVM, IDE state (codegraph/presets/dart.yaml), for the facts-cache walk; the extractor
|
|
37
|
+
# gets the same rules (PathRules.extractor_cfg) through its config
|
|
38
|
+
SKIP_DIRS = presets.skip_dirs("dart")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_dart() -> str | None:
|
|
42
|
+
for c in (os.environ.get("DART"), shutil.which("dart"), str(Path.home() / "opt/dart-sdk/bin/dart"),
|
|
43
|
+
str(Path.home() / "flutter/bin/dart"), "/usr/lib/dart/bin/dart"):
|
|
44
|
+
if c and Path(c).exists():
|
|
45
|
+
return c
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def module_of(path: str | None) -> str | None:
|
|
50
|
+
if not path:
|
|
51
|
+
return None
|
|
52
|
+
parts = path.split("/")
|
|
53
|
+
if "lib" in parts:
|
|
54
|
+
i = parts.index("lib")
|
|
55
|
+
return "/".join(parts[i + 1:-1]) or "lib"
|
|
56
|
+
return "/".join(parts[:-1]) or None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def extractor_stamp() -> str:
|
|
60
|
+
"""Hash of the extractor source and its lockfile; the compiled binary is reused only while it matches (an mtime
|
|
61
|
+
comparison would keep an outdated binary after a checkout that leaves the source older than the binary)."""
|
|
62
|
+
h = hashlib.sha256()
|
|
63
|
+
for f in (EXTRACTOR, EXTRACTOR_DIR / "pubspec.lock"):
|
|
64
|
+
h.update(fsutil.content_key(f).encode() + b"\n")
|
|
65
|
+
return h.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _stamp_path(bin_: Path | None = None) -> Path:
|
|
69
|
+
b = bin_ or BIN
|
|
70
|
+
return b.with_name(b.name + ".sha256")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extractor_binary_current(bin_: Path | None = None) -> bool:
|
|
74
|
+
b = bin_ or BIN
|
|
75
|
+
try:
|
|
76
|
+
return b.exists() and _stamp_path(b).read_text().strip() == extractor_stamp()
|
|
77
|
+
except OSError:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_extractor_stamp(bin_: Path | None = None) -> None:
|
|
82
|
+
_stamp_path(bin_).write_text(extractor_stamp() + "\n")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def facts_fingerprint(root: Path, cfg: dict, rules=None) -> str:
|
|
86
|
+
"""Cache key for extractor facts: cache version + extractor code + deps lock + config + (path, size, content hash)
|
|
87
|
+
of every .dart / .yaml / .env* file. Content, not mtime: a same-size edit with a restored mtime is a miss."""
|
|
88
|
+
h = hashlib.sha256(f"cg-cache-v{fsutil.CACHE_VERSION}\n".encode())
|
|
89
|
+
for f in (EXTRACTOR, EXTRACTOR_DIR / "pubspec.lock"):
|
|
90
|
+
h.update(f.read_bytes() if f.exists() else b"")
|
|
91
|
+
h.update(json.dumps(cfg, sort_keys=True).encode())
|
|
92
|
+
for dp, dns, fns in os.walk(root):
|
|
93
|
+
rd = os.path.relpath(dp, root).replace(os.sep, "/")
|
|
94
|
+
rd = "" if rd == "." else rd
|
|
95
|
+
dns[:] = rules.prune(rd, dns, dot=True) if rules is not None else sorted(d for d in dns if d not in SKIP_DIRS and not d.startswith("."))
|
|
96
|
+
for fn in sorted(fns):
|
|
97
|
+
if fn.endswith((".dart", ".yaml")) or fn.startswith(".env"):
|
|
98
|
+
p = os.path.join(dp, fn)
|
|
99
|
+
h.update(f"{os.path.relpath(p, root)}|{fsutil.content_key(p)}\n".encode())
|
|
100
|
+
return h.hexdigest()[:20]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def normalize_url(url: str) -> tuple[str, dict]:
|
|
104
|
+
"""URL template -> (path without trailing slash, info{origin, query, trailing_slash, relative})."""
|
|
105
|
+
info: dict = {}
|
|
106
|
+
u = url.strip()
|
|
107
|
+
m = re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]*", u)
|
|
108
|
+
if m:
|
|
109
|
+
info["origin"] = m.group(0)
|
|
110
|
+
u = u[m.end():]
|
|
111
|
+
else:
|
|
112
|
+
m = re.match(r"^\{[^{}/]*\}(?=/|$)", u)
|
|
113
|
+
if m:
|
|
114
|
+
info["origin"] = m.group(0)
|
|
115
|
+
u = u[m.end():]
|
|
116
|
+
q = re.search(r"\?(?![^{]*\})", u)
|
|
117
|
+
if q:
|
|
118
|
+
info["query"] = u[q.end():]
|
|
119
|
+
u = u[:q.start()]
|
|
120
|
+
u = re.sub(r"/{2,}", "/", u)
|
|
121
|
+
if not u.startswith("/"):
|
|
122
|
+
info["relative"] = True
|
|
123
|
+
u = "/" + u
|
|
124
|
+
info["trailing_slash"] = len(u) > 1 and u.endswith("/")
|
|
125
|
+
if len(u) > 1:
|
|
126
|
+
u = u.rstrip("/")
|
|
127
|
+
return u, info
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _error_spans(f: dict) -> list:
|
|
131
|
+
"""[[line, line]] per analyzer error of a file (`error_lines`, else the line of `first_error`), #73."""
|
|
132
|
+
lines = f.get("error_lines") or []
|
|
133
|
+
if not lines and isinstance(f.get("first_error"), str):
|
|
134
|
+
m = re.search(r"@(\d+)$", f["first_error"])
|
|
135
|
+
lines = [int(m.group(1))] if m else []
|
|
136
|
+
return [[ln, ln] for ln in lines]
|
|
137
|
+
|
|
138
|
+
class DartPlugin(LanguagePlugin):
|
|
139
|
+
name = "dart"
|
|
140
|
+
|
|
141
|
+
def __init__(self):
|
|
142
|
+
self.program: DartProgram | None = None
|
|
143
|
+
|
|
144
|
+
def detect(self, project: Project) -> bool:
|
|
145
|
+
if project.exists("pubspec.yaml"):
|
|
146
|
+
return True
|
|
147
|
+
for p in project.root.glob("*/pubspec.yaml"):
|
|
148
|
+
return True
|
|
149
|
+
for p in project.root.glob("*/*/pubspec.yaml"):
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def ensure_extractor(self, dart: str) -> str:
|
|
154
|
+
"""The compiled extractor: in the package's extractor directory when its dependencies are there (a checkout),
|
|
155
|
+
else in the per-user cache directory (codegraph/core/extractors.py)."""
|
|
156
|
+
work = extractors.workdir("dart")
|
|
157
|
+
bin_ = BIN if work == extractors.SPECS["dart"].pkg else work / ".bin" / "extract"
|
|
158
|
+
if extractor_binary_current(bin_):
|
|
159
|
+
return str(bin_)
|
|
160
|
+
work = extractors.ensure("dart", dart)
|
|
161
|
+
with extractors.install_lock(work): # two first-time compiles into one directory wait (#65)
|
|
162
|
+
if extractor_binary_current(bin_):
|
|
163
|
+
return str(bin_)
|
|
164
|
+
bin_.parent.mkdir(exist_ok=True)
|
|
165
|
+
r = subprocess.run([dart, "compile", "exe", str(work / "bin" / EXTRACTOR.name), "-o", str(bin_)], cwd=work,
|
|
166
|
+
capture_output=True, text=True)
|
|
167
|
+
if r.returncode != 0:
|
|
168
|
+
raise RuntimeError("dart extractor compile failed: " + r.stderr[-500:])
|
|
169
|
+
write_extractor_stamp(bin_)
|
|
170
|
+
return str(bin_)
|
|
171
|
+
|
|
172
|
+
def run_extractor(self, project: Project, cfg: dict, rules=None) -> tuple[dict, str]:
|
|
173
|
+
cache_file, status = None, "disabled"
|
|
174
|
+
if not os.environ.get("CODEGRAPH_NO_CACHE"):
|
|
175
|
+
fp = facts_fingerprint(project.root, cfg, rules)
|
|
176
|
+
cdir = cache.subdir("dart")
|
|
177
|
+
rkey = cache.root_key(project.root)
|
|
178
|
+
cache_file = cdir / f"{rkey}-v{fsutil.CACHE_VERSION}-{fp}.json"
|
|
179
|
+
if cache_file.exists():
|
|
180
|
+
return json.loads(cache_file.read_text()), "hit"
|
|
181
|
+
status = "miss"
|
|
182
|
+
dart = find_dart()
|
|
183
|
+
if not dart:
|
|
184
|
+
raise FileNotFoundError("dart SDK not found (set $DART or put dart on PATH)")
|
|
185
|
+
exe = self.ensure_extractor(dart)
|
|
186
|
+
with tempfile.TemporaryDirectory() as td:
|
|
187
|
+
out = Path(td) / "facts.json"
|
|
188
|
+
c = dict(cfg, root=str(project.root), out=str(out))
|
|
189
|
+
(Path(td) / "cfg.json").write_text(json.dumps(c))
|
|
190
|
+
r = subprocess.run([exe, "--config", str(Path(td) / "cfg.json")], capture_output=True, text=True)
|
|
191
|
+
if r.returncode != 0:
|
|
192
|
+
raise RuntimeError("dart extractor failed: " + r.stderr[-800:])
|
|
193
|
+
facts = json.loads(out.read_text())
|
|
194
|
+
if cache_file:
|
|
195
|
+
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
for old in cache_file.parent.glob(cache_file.name.split("-")[0] + "-*.json"):
|
|
197
|
+
old.unlink(missing_ok=True)
|
|
198
|
+
cache_file.write_text(json.dumps(facts))
|
|
199
|
+
cache.note_project(project.root)
|
|
200
|
+
return facts, status
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _conditional(b: GraphBuilder, imp: dict, how: str) -> None:
|
|
204
|
+
"""Conditional import / export: IMPORTS edges to each alternative library (attrs.condition) and the group for
|
|
205
|
+
the platform tags (codegraph/platforms.py: the default library and each `if (dart.library.x)` one)."""
|
|
206
|
+
if not imp.get("configs"):
|
|
207
|
+
return
|
|
208
|
+
for c in imp["configs"]:
|
|
209
|
+
if c["lib"] is not None:
|
|
210
|
+
b.add_edge(f"module:{imp['file']}", c["lib"].id, "IMPORTS", imp["file"], imp.get("l"), EXACT, conditional=True,
|
|
211
|
+
condition=c["name"] + ("" if c.get("value") in (None, "true") else f" == {c['value']!r}"),
|
|
212
|
+
**({"via": "export"} if how == "export" else {}))
|
|
213
|
+
b.platform_imports.append({"file": imp["file"], "line": imp.get("l"), "how": how,
|
|
214
|
+
"default": imp["lib"].file if imp["lib"] is not None else None,
|
|
215
|
+
"configs": [(c["name"], c.get("value"), c["lib"].file if c["lib"] is not None else None)
|
|
216
|
+
for c in imp["configs"]]})
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _variant_reexports(prog, b: GraphBuilder) -> None:
|
|
220
|
+
"""A library that is one alternative of a conditional import / export defines the names it re-exports
|
|
221
|
+
(`export 'src/x.dart' show f`) and its public top-level tear-offs (`const f = Impl.f`) too: recorded as
|
|
222
|
+
module attrs.reexports (name -> node id) for the platform divergence checks (codegraph/platforms.py)."""
|
|
223
|
+
files = set()
|
|
224
|
+
for pi in b.platform_imports:
|
|
225
|
+
if pi.get("default"):
|
|
226
|
+
files.add(pi["default"])
|
|
227
|
+
files |= {c[2] for c in pi.get("configs") or () if c[2]}
|
|
228
|
+
|
|
229
|
+
def tearoff(lib, e):
|
|
230
|
+
if not isinstance(e, dict):
|
|
231
|
+
return None
|
|
232
|
+
if e.get("k") == "id":
|
|
233
|
+
v = prog.lookup(lib, e.get("v") or "")
|
|
234
|
+
return getattr(v, "id", None) if isinstance(v, DFunc) else None
|
|
235
|
+
t = e.get("t") or {}
|
|
236
|
+
if e.get("k") == "prop" and t.get("k") == "id":
|
|
237
|
+
c = prog.lookup(lib, t.get("v") or "")
|
|
238
|
+
if isinstance(c, DClass):
|
|
239
|
+
m = c.methods.get(e.get("n"))
|
|
240
|
+
return m.id if m is not None else None
|
|
241
|
+
for imp in prog.prefix_imports(lib, t.get("v") or ""):
|
|
242
|
+
v = prog.namespace(imp["lib"]).get(e.get("n")) if imp["lib"] is not None else None
|
|
243
|
+
if isinstance(v, DFunc):
|
|
244
|
+
return v.id
|
|
245
|
+
return None
|
|
246
|
+
own_vars = defaultdict(list)
|
|
247
|
+
for v in prog.vars:
|
|
248
|
+
if v.cls is None and not v.name.startswith("_"):
|
|
249
|
+
own_vars[v.file].append(v)
|
|
250
|
+
for lib in prog.libs.values():
|
|
251
|
+
if lib.file not in files or lib.id not in b.nodes:
|
|
252
|
+
continue
|
|
253
|
+
rex = {}
|
|
254
|
+
for k, v in prog.namespace(lib).items():
|
|
255
|
+
vid = getattr(v, "id", None)
|
|
256
|
+
if getattr(v, "file", lib.file) != lib.file and vid in b.nodes:
|
|
257
|
+
rex[k] = vid
|
|
258
|
+
for v in own_vars.get(lib.file, ()):
|
|
259
|
+
tid = tearoff(lib, v.init)
|
|
260
|
+
if tid in b.nodes:
|
|
261
|
+
rex[v.name] = tid
|
|
262
|
+
if rex:
|
|
263
|
+
n = b.nodes[lib.id]
|
|
264
|
+
n.attrs = {**(n.attrs or {}), "reexports": rex}
|
|
265
|
+
|
|
266
|
+
# ------------------------------------------------------------------ index
|
|
267
|
+
def index(self, project: Project, builder: GraphBuilder, frameworks: list[FrameworkPlugin]) -> dict:
|
|
268
|
+
t0 = time.time()
|
|
269
|
+
# the walk's directory rules (common + dart preset skip_dirs, .cg.yaml skip_dirs.add / keep, include); the
|
|
270
|
+
# extractor keeps no skip list of its own
|
|
271
|
+
rules = path_rules(project, "dart")
|
|
272
|
+
cfg = {**rules.extractor_cfg(project.options.get("dart_skip_dirs") or []),
|
|
273
|
+
"skip_suffixes": list(presets.values("dart", "generated_suffixes", default=[]))
|
|
274
|
+
if not project.options.get("dart_keep_generated") else []}
|
|
275
|
+
try:
|
|
276
|
+
facts, cache = self.run_extractor(project, cfg, rules)
|
|
277
|
+
except (FileNotFoundError, RuntimeError, subprocess.CalledProcessError) as ex:
|
|
278
|
+
return {"status": "skipped", "reason": str(ex)[:300]}
|
|
279
|
+
t_ext = time.time() - t0
|
|
280
|
+
if rules.exclude: # .cg.yaml exclude globs: dropped from the extractor's output before the graph is built
|
|
281
|
+
# (generated files stay: *.g.dart holds the exact JSON keys; their nodes leave the graph in the final pass)
|
|
282
|
+
facts = dict(facts, files=[f for f in facts.get("files") or [] if not rules.excluded(f.get("file") or "", generated=False)])
|
|
283
|
+
prog = DartProgram(project.root, facts)
|
|
284
|
+
prog.load()
|
|
285
|
+
failed = [x.get("file") if isinstance(x, dict) else str(x) for x in facts.get("failures") or []]
|
|
286
|
+
self.file_report = {"seen": list(prog.files) + [f for f in failed if f], "parse_failed": [f for f in failed if f],
|
|
287
|
+
"syntax_errors": {rel: _error_spans(f) for rel, f in prog.files.items() if f.get("errors")}}
|
|
288
|
+
self.program = prog
|
|
289
|
+
prog.models = ModelIndex(prog)
|
|
290
|
+
prog.env_values = load_env_files(project.root, [p.dir for p in prog.pkg_dirs])
|
|
291
|
+
prog.builder = builder
|
|
292
|
+
for fw in frameworks:
|
|
293
|
+
fw.register_hooks(prog)
|
|
294
|
+
b = builder
|
|
295
|
+
st = {"files": len(prog.files), "parse_error_files": sum(1 for f in prog.files.values() if f.get("errors")),
|
|
296
|
+
"extract_failures": len(facts.get("failures") or []), "skipped_generated": facts.get("skipped"),
|
|
297
|
+
"packages": sorted(prog.pkgs), "facts_cache": cache, "extract_seconds": round(t_ext, 2)}
|
|
298
|
+
# ---- symbols
|
|
299
|
+
for rel, f in prog.files.items():
|
|
300
|
+
b.add_node("module", rel, name=rel, fqn=rel, file=rel, line=1, end_line=f.get("lines"), module=module_of(rel), lang="dart",
|
|
301
|
+
attrs={"part_of": prog.part_owner[rel]} if rel in prog.part_owner else None)
|
|
302
|
+
for c in prog.classes.values():
|
|
303
|
+
b.add_node("class", f"{c.file}#{c.name}", name=c.name, fqn=f"{c.file}#{c.name}", file=c.file, line=c.line,
|
|
304
|
+
end_line=c.raw.get("end"), module=module_of(c.file), lang="dart",
|
|
305
|
+
attrs={k: v for k, v in {"dart_kind": c.kind if c.kind != "class" else None,
|
|
306
|
+
"annotations": c.ann or None, "abstract": c.raw.get("abstract"),
|
|
307
|
+
"sealed": c.raw.get("sealed")}.items() if v})
|
|
308
|
+
b.add_edge(f"module:{c.file}", c.id, "CONTAINS", c.file, c.line, EXACT)
|
|
309
|
+
for fn in prog.funcs.values():
|
|
310
|
+
kind = "function" if fn.cls is None else "method"
|
|
311
|
+
attrs = {k: v for k, v in {"dart_kind": fn.kind if fn.kind not in ("function", "method") else None,
|
|
312
|
+
"static": fn.raw.get("static"), "abstract": fn.raw.get("abstract"), "async": fn.raw.get("async"),
|
|
313
|
+
"returns": fn.raw.get("ret"), "event": fn.raw.get("event"),
|
|
314
|
+
"annotations": [a["name"] for a in fn.raw.get("ann") or []] or None}.items() if v}
|
|
315
|
+
b.add_node(kind, fn.qual, name=fn.name, fqn=fn.qual, file=fn.file, line=fn.line, end_line=fn.raw.get("end"),
|
|
316
|
+
module=module_of(fn.file), lang="dart", attrs=attrs or None)
|
|
317
|
+
b.add_edge(fn.cls.id if fn.cls else f"module:{fn.file}", fn.id, "CONTAINS", fn.file, fn.line, EXACT)
|
|
318
|
+
# ---- imports
|
|
319
|
+
n_imp = 0
|
|
320
|
+
for lib in prog.libs.values():
|
|
321
|
+
for imp in lib.imports:
|
|
322
|
+
cond = {"conditional": True} if imp.get("configs") else {}
|
|
323
|
+
if imp["lib"] is not None:
|
|
324
|
+
b.add_edge(f"module:{imp['file']}", imp["lib"].id, "IMPORTS", imp["file"], imp.get("l"), EXACT, **cond); n_imp += 1
|
|
325
|
+
self._conditional(b, imp, "import")
|
|
326
|
+
for ex in lib.exports:
|
|
327
|
+
if ex.get("configs"):
|
|
328
|
+
if ex["lib"] is not None:
|
|
329
|
+
b.add_edge(f"module:{ex['file']}", ex["lib"].id, "IMPORTS", ex["file"], ex.get("l"), EXACT,
|
|
330
|
+
conditional=True, via="export")
|
|
331
|
+
self._conditional(b, ex, "export")
|
|
332
|
+
elif ex["lib"] is not None: # re-exported unconditionally (lib/x.dart: export 'src/x.dart'): built everywhere
|
|
333
|
+
b.platform_imports.append({"plain": ex["lib"].file})
|
|
334
|
+
b.add_edge(f"module:{ex['file']}", ex["lib"].id, "IMPORTS", ex["file"], ex.get("l"), EXACT, via="export")
|
|
335
|
+
for p in lib.parts:
|
|
336
|
+
b.add_edge(lib.id, f"module:{p}", "CONTAINS", lib.file, 1, EXACT, via="part")
|
|
337
|
+
self._variant_reexports(prog, b)
|
|
338
|
+
# ---- inheritance / dispatch
|
|
339
|
+
n_ext = 0
|
|
340
|
+
for c in prog.classes.values():
|
|
341
|
+
for rel, t in c.supers:
|
|
342
|
+
sc = prog.resolve_class(c.lib, split_type(t)[0])
|
|
343
|
+
if sc:
|
|
344
|
+
b.add_edge(c.id, sc.id, "IMPLEMENTS" if rel == "implements" else "EXTENDS", c.file, c.line, EXACT,
|
|
345
|
+
**({"via": rel} if rel in ("with", "on") else {}))
|
|
346
|
+
n_ext += 1
|
|
347
|
+
else:
|
|
348
|
+
b.nodes[c.id].attrs.setdefault("ext_bases", []).append(t)
|
|
349
|
+
for name, m in c.methods.items():
|
|
350
|
+
if m.raw.get("static"):
|
|
351
|
+
continue
|
|
352
|
+
for s in prog.mro(c):
|
|
353
|
+
if name in s.methods and not s.methods[name].raw.get("static"):
|
|
354
|
+
sm = s.methods[name]
|
|
355
|
+
kind = "IMPLEMENTED_BY" if (sm.raw.get("abstract") or s.kind == "mixin" and False) or any(
|
|
356
|
+
r == "implements" and split_type(t)[0].split(".")[-1] == s.name for r, t in c.supers) else "OVERRIDDEN_BY"
|
|
357
|
+
b.add_edge(sm.id, m.id, kind, m.file, m.line, RESOLVED)
|
|
358
|
+
break
|
|
359
|
+
# ---- calls
|
|
360
|
+
conf_ct = {EXACT: 0, RESOLVED: 0, HEURISTIC: 0}
|
|
361
|
+
n_env = 0
|
|
362
|
+
|
|
363
|
+
def emit_calls(src_id: str, file: str, facts_: list, ctx: Ctx, fn: DFunc | None):
|
|
364
|
+
nonlocal n_env
|
|
365
|
+
for f in facts_:
|
|
366
|
+
if f["ft"] in ("call", "new"):
|
|
367
|
+
for tgt, conf, via in prog.resolve_call(f, ctx):
|
|
368
|
+
if isinstance(tgt, DClass):
|
|
369
|
+
b.add_edge(src_id, tgt.id, "INSTANTIATES", file, f.get("l"), conf)
|
|
370
|
+
ct = tgt.ctors.get(via or "new") if via != "new" else tgt.ctors.get("new")
|
|
371
|
+
if ct:
|
|
372
|
+
b.add_edge(src_id, ct.id, "CALLS", file, f.get("l"), conf, via="constructor")
|
|
373
|
+
prog.callers.setdefault(ct.id, []).append((fn, f, ctx))
|
|
374
|
+
if via and via in tgt.methods: # static call through type
|
|
375
|
+
b.add_edge(src_id, tgt.methods[via].id, "CALLS", file, f.get("l"), conf)
|
|
376
|
+
prog.callers.setdefault(tgt.methods[via].id, []).append((fn, f, ctx))
|
|
377
|
+
else:
|
|
378
|
+
rc = getattr(prog, "last_recv", None)
|
|
379
|
+
b.add_edge(src_id, tgt.id, "CALLS", file, f.get("l"), conf, **({"via": via} if via else {}),
|
|
380
|
+
**({"recv": [rc.id]} if rc is not None and rc.kind != "extension" else {}))
|
|
381
|
+
prog.callers.setdefault(tgt.id, []).append((fn, f, ctx))
|
|
382
|
+
conf_ct[conf] += 1
|
|
383
|
+
k = env_key(f)
|
|
384
|
+
if k:
|
|
385
|
+
b.add_edge(src_id, b.add_node("env", k[0], lang="env"), "READS_ENV", file, f.get("l"), EXACT, via=k[1]); n_env += 1
|
|
386
|
+
elif f["ft"] == "index":
|
|
387
|
+
k = env_key_idx(f)
|
|
388
|
+
if k:
|
|
389
|
+
b.add_edge(src_id, b.add_node("env", k[0], lang="env"), "READS_ENV", file, f.get("l"), EXACT, via=k[1]); n_env += 1
|
|
390
|
+
|
|
391
|
+
for fn in prog.funcs.values():
|
|
392
|
+
emit_calls(fn.id, fn.file, fn.facts, prog.ctx_of(fn), fn)
|
|
393
|
+
if fn.kind == "ctor" and fn.raw.get("redirect"):
|
|
394
|
+
tname = fn.raw["redirect"].split(".")[0]
|
|
395
|
+
tc = prog.resolve_class(fn.lib, tname)
|
|
396
|
+
if tc:
|
|
397
|
+
b.add_edge(fn.id, tc.id, "INSTANTIATES", fn.file, fn.line, EXACT, via="redirecting factory")
|
|
398
|
+
for c in prog.classes.values(): # field initialisers run when the object is created
|
|
399
|
+
fs = []
|
|
400
|
+
for v in c.fields.values():
|
|
401
|
+
for x in walk_repr(v.init):
|
|
402
|
+
if x.get("k") in ("call", "new"):
|
|
403
|
+
y = dict(x)
|
|
404
|
+
y["ft"] = x["k"]
|
|
405
|
+
y["l"] = x.get("l") or v.line
|
|
406
|
+
fs.append(y)
|
|
407
|
+
if fs:
|
|
408
|
+
emit_calls(c.id, c.file, fs, Ctx(prog, c.lib, None, c), None)
|
|
409
|
+
for v in prog.vars:
|
|
410
|
+
ctx = Ctx(prog, v.lib, None, None)
|
|
411
|
+
emit_calls(f"module:{v.file}", v.file, v.facts, ctx, None)
|
|
412
|
+
# ---- JSON models
|
|
413
|
+
mi: ModelIndex = prog.models
|
|
414
|
+
n_models = 0
|
|
415
|
+
for c in prog.classes.values():
|
|
416
|
+
m = mi.model(c)
|
|
417
|
+
attrs = {}
|
|
418
|
+
if m.get("from"):
|
|
419
|
+
attrs["json_from"] = m["from"]
|
|
420
|
+
attrs["json_from_source"] = m.get("from_source")
|
|
421
|
+
if m.get("to"):
|
|
422
|
+
attrs["json_to"] = m["to"]
|
|
423
|
+
attrs["json_to_source"] = m.get("to_source")
|
|
424
|
+
if m.get("enum_values"):
|
|
425
|
+
attrs["enum_values"] = m["enum_values"]
|
|
426
|
+
if m.get("field_rename"):
|
|
427
|
+
attrs["field_rename"] = m["field_rename"]
|
|
428
|
+
if attrs.get("json_from") or attrs.get("json_to"):
|
|
429
|
+
n_models += 1
|
|
430
|
+
if attrs:
|
|
431
|
+
b.nodes[c.id].attrs.update(attrs)
|
|
432
|
+
for fn in prog.funcs.values():
|
|
433
|
+
ctx = prog.ctx_of(fn)
|
|
434
|
+
for f in fn.facts:
|
|
435
|
+
if f["ft"] == "call" and f.get("n") in ("fromJson", "fromMap") and (f.get("t") or {}).get("k") == "id":
|
|
436
|
+
tc = prog.resolve_class(fn.lib, f["t"]["v"])
|
|
437
|
+
if tc:
|
|
438
|
+
b.add_edge(fn.id, tc.id, "PARSES_JSON", fn.file, f.get("l"), EXACT, role="fromJson")
|
|
439
|
+
elif f["ft"] == "call" and f.get("n") in ("toJson", "toMap") and f.get("t"):
|
|
440
|
+
tt = prog.infer(f["t"], ctx)
|
|
441
|
+
if tt and tt[0] == "inst" and tt[1] is not None and mi.keys(tt[1], "to"):
|
|
442
|
+
b.add_edge(fn.id, tt[1].id, "PARSES_JSON", fn.file, f.get("l"), RESOLVED, role="toJson")
|
|
443
|
+
# ---- HTTP
|
|
444
|
+
n_http, http_stats = self.emit_http(project, prog, b)
|
|
445
|
+
# ---- Flutter platform channels (send side; codegraph/bridges.py adds the native receivers)
|
|
446
|
+
from .bridges import emit as emit_bridges
|
|
447
|
+
bst = emit_bridges(prog, UrlEval(prog, prog.env_values), b)
|
|
448
|
+
if bst["flutter"] or bst["flutter-event"] or bst["unresolved_channel"] or bst.get("dart_handlers"):
|
|
449
|
+
st["platform_channel_sends"] = bst
|
|
450
|
+
# ---- Pigeon APIs (definitions -> endpoint:pigeon:<Api>#<method>; bridges.py adds the native side)
|
|
451
|
+
from .bridges import emit_pigeon, pigeon_apis
|
|
452
|
+
apis = pigeon_apis(prog)
|
|
453
|
+
if apis:
|
|
454
|
+
b.pigeon_apis = apis
|
|
455
|
+
st["pigeon"] = {"apis": len(apis), "host_apis": sum(1 for v in apis.values() if v["kind"] == "host"),
|
|
456
|
+
"flutter_apis": sum(1 for v in apis.values() if v["kind"] == "flutter"),
|
|
457
|
+
**emit_pigeon(prog, b, apis)}
|
|
458
|
+
st.update({"libraries": len(prog.libs), "classes": len(prog.classes),
|
|
459
|
+
"functions": sum(1 for f in prog.funcs.values() if f.cls is None),
|
|
460
|
+
"methods": sum(1 for f in prog.funcs.values() if f.cls is not None), "imports": n_imp, "extends": n_ext,
|
|
461
|
+
"calls": conf_ct, "env_reads": n_env, "json_models": n_models, "http_calls": n_http, **http_stats,
|
|
462
|
+
"parse_error_sample": [f["file"] for f in prog.files.values() if f.get("errors")][:10],
|
|
463
|
+
"seconds": round(time.time() - t0, 2)})
|
|
464
|
+
return st
|
|
465
|
+
|
|
466
|
+
# ------------------------------------------------------------------ http
|
|
467
|
+
def emit_http(self, project: Project, prog: DartProgram, b: GraphBuilder):
|
|
468
|
+
ev = UrlEval(prog, prog.env_values)
|
|
469
|
+
hx = HttpExtractor(prog, ev, prog.models.keys)
|
|
470
|
+
prog.http = hx
|
|
471
|
+
calls = hx.scan()
|
|
472
|
+
api_hosts = set()
|
|
473
|
+
records = []
|
|
474
|
+
for c in calls:
|
|
475
|
+
for rec in self.expand(prog, hx, ev, c):
|
|
476
|
+
records.append(rec)
|
|
477
|
+
t = rec["tpl"]
|
|
478
|
+
m = re.match(r"^([a-zA-Z][a-zA-Z0-9+.-]*://[^/]*)", t.text)
|
|
479
|
+
if m and (t.named or t.env):
|
|
480
|
+
api_hosts.add(m.group(1))
|
|
481
|
+
n = 0
|
|
482
|
+
unresolved = 0
|
|
483
|
+
for rec in records:
|
|
484
|
+
t: Tpl = rec["tpl"]
|
|
485
|
+
path, info = normalize_url(t.text)
|
|
486
|
+
origin = info.get("origin")
|
|
487
|
+
if rec.get("dio_base_unknown"):
|
|
488
|
+
okind = "unknown"
|
|
489
|
+
origin = origin or "{dio.baseUrl}"
|
|
490
|
+
elif origin and origin.startswith("{env:"):
|
|
491
|
+
okind = "env" # configured server URL (env key) whose value is not in a .env file
|
|
492
|
+
elif origin and origin in api_hosts:
|
|
493
|
+
okind = "api"
|
|
494
|
+
elif origin and "://" in origin:
|
|
495
|
+
okind = "other"
|
|
496
|
+
elif origin and path == "/" and not info.get("query") and re.fullmatch(r"\{[^{}]*\}", origin):
|
|
497
|
+
okind = "dynamic" # the whole URL is one runtime value (`http.get(Uri.parse(photo.url))`)
|
|
498
|
+
elif origin:
|
|
499
|
+
okind = "unknown"
|
|
500
|
+
else:
|
|
501
|
+
okind = "unknown" if info.get("relative") or rec["client"] in ("http", "websocket", "dart:io") else "api"
|
|
502
|
+
if rec["method"] is None:
|
|
503
|
+
method = "ANY"
|
|
504
|
+
else:
|
|
505
|
+
method = rec["method"]
|
|
506
|
+
if "{?}" in path:
|
|
507
|
+
unresolved += 1
|
|
508
|
+
key = f"{method} {path}" if okind in ("api", "unknown") else (
|
|
509
|
+
f"{method} {origin}" if okind == "dynamic" else f"{method} {origin or ''}{path}")
|
|
510
|
+
nid = b.add_node("http", key, key, fqn=key, lang="dart",
|
|
511
|
+
attrs={"method": method, "path": path, "client": rec["client"], "origin": origin, "origin_kind": okind})
|
|
512
|
+
body = rec.get("body_keys") or []
|
|
513
|
+
resp = rec.get("response") or {}
|
|
514
|
+
b.add_edge(rec["src"], nid, "HTTP_CALLS", file=rec["file"], line=rec["line"], confidence=min_conf(rec["conf"], t.conf),
|
|
515
|
+
client=rec["client"], url=t.text, origin=origin, query=info.get("query"), trailing_slash=info.get("trailing_slash"),
|
|
516
|
+
env=t.env or None, via_helper=rec.get("via_helper"), helper_chain=rec.get("chain") or None,
|
|
517
|
+
body_keys=body or None, body_opaque=rec.get("body_opaque") or None, multipart=rec.get("multipart") or None,
|
|
518
|
+
response_keys=resp.get("keys") or None, response_models=resp.get("models") or None,
|
|
519
|
+
status_checks=resp.get("status") or None, declared=rec.get("declared") or None)
|
|
520
|
+
n += 1
|
|
521
|
+
return n, {"http_unresolved_paths": unresolved, "dio_base_urls": sorted({t.text for t, *_ in hx.dio_bases})[:5]}
|
|
522
|
+
|
|
523
|
+
def expand(self, prog: DartProgram, hx: HttpExtractor, ev: UrlEval, c: dict):
|
|
524
|
+
"""Evaluate a call's URL; helpers (URL from a parameter) are expanded at their call sites (<= 3 levels)."""
|
|
525
|
+
fn: DFunc = c["fn"]
|
|
526
|
+
|
|
527
|
+
def make(ctx: Ctx, site_fn: DFunc, site_file, site_line, chain, inner_fns, conf_extra=EXACT):
|
|
528
|
+
t = c["tpl"] if c.get("tpl") is not None else ev.eval(c["url"], ctx)
|
|
529
|
+
method = c["method"]
|
|
530
|
+
if method is None and c.get("method_repr") is not None:
|
|
531
|
+
mt = ev.eval(c["method_repr"], ctx)
|
|
532
|
+
method = mt.text.upper() if mt.text and "{" not in mt.text else None
|
|
533
|
+
recs = [(t, EXACT)]
|
|
534
|
+
if c["client"] in ("dio", "retrofit", "chopper") and not re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", t.text) \
|
|
535
|
+
and not t.text.startswith("{") and (c["client"] == "dio" or c.get("base_from_dio")):
|
|
536
|
+
bases = hx.dio_base_for(fn)
|
|
537
|
+
recs = [(join(bt, t), bconf) for bt, bconf in bases] if bases else [(t, "unknown-base")]
|
|
538
|
+
res = []
|
|
539
|
+
for tt, bconf in recs:
|
|
540
|
+
body_keys = list(c.get("body_keys") or [])
|
|
541
|
+
opaque = False
|
|
542
|
+
if c.get("body") is not None and not body_absent(c["body"], ctx):
|
|
543
|
+
ks = hx.keys_of(c["body"], ctx)
|
|
544
|
+
body_keys += ks
|
|
545
|
+
opaque = not ks
|
|
546
|
+
resp = {"keys": [], "models": [], "status": []}
|
|
547
|
+
for f_ in [site_fn] + [x for x in inner_fns if x is not site_fn]:
|
|
548
|
+
r_ = hx.response_facts(f_, prog.ctx_of(f_))
|
|
549
|
+
for k_ in resp:
|
|
550
|
+
resp[k_] += r_.get(k_) or []
|
|
551
|
+
res.append({"src": site_fn.id, "file": site_file, "line": site_line, "tpl": tt, "method": method, "client": c["client"],
|
|
552
|
+
"conf": min_conf(c["conf"], conf_extra, bconf if bconf in CONFIDENCE_RANK else EXACT),
|
|
553
|
+
"dio_base_unknown": bconf == "unknown-base", "body_keys": body_keys, "body_opaque": opaque,
|
|
554
|
+
"response": resp, "via_helper": fn.id if site_fn is not fn else None, "chain": chain,
|
|
555
|
+
"multipart": c.get("multipart"), "declared": c.get("declared")})
|
|
556
|
+
return res
|
|
557
|
+
|
|
558
|
+
def needs(recs):
|
|
559
|
+
return any(TOKEN.search(r["tpl"].text) for r in recs) or (c.get("method_repr") is not None and recs and recs[0]["method"] is None)
|
|
560
|
+
|
|
561
|
+
first = make(prog.ctx_of(fn), fn, c["file"], c["line"], [], [])
|
|
562
|
+
if not needs(first) or c.get("declared"):
|
|
563
|
+
return first
|
|
564
|
+
results, frontier = [], [[]]
|
|
565
|
+
for _level in range(3):
|
|
566
|
+
nxt = []
|
|
567
|
+
for chain in frontier:
|
|
568
|
+
callee = chain[-1][1] if chain else fn
|
|
569
|
+
for caller, fact, _cctx in prog.callers.get(callee.id, [])[:MAXSITES]:
|
|
570
|
+
if caller is None or caller is fn or any(caller is x[1] for x in chain):
|
|
571
|
+
continue
|
|
572
|
+
ch = chain + [(callee, caller, fact)]
|
|
573
|
+
ctx = prog.ctx_of(ch[-1][1])
|
|
574
|
+
for callee_, caller_, fact_ in reversed(ch):
|
|
575
|
+
ctx = Ctx(prog, callee_.lib, callee_, callee_.cls, bind_args(callee_, fact_, ctx))
|
|
576
|
+
recs = make(ctx, caller, caller.file, fact.get("l"), [f"{x[1].file}:{x[2].get('l')}" for x in ch],
|
|
577
|
+
[fn] + [x[1] for x in ch[:-1]], RESOLVED)
|
|
578
|
+
if needs(recs):
|
|
579
|
+
nxt.append(ch)
|
|
580
|
+
else:
|
|
581
|
+
results += recs
|
|
582
|
+
frontier = nxt[:100]
|
|
583
|
+
if not frontier:
|
|
584
|
+
break
|
|
585
|
+
if not results:
|
|
586
|
+
for r in first:
|
|
587
|
+
r["tpl"] = Tpl(TOKEN.sub(lambda m: "{" + m.group(1) + "}", r["tpl"].text), HEURISTIC, r["tpl"].named, r["tpl"].env)
|
|
588
|
+
r["helper_unexpanded"] = True
|
|
589
|
+
return first
|
|
590
|
+
return results
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
MAXSITES = 60
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def body_absent(r, ctx: Ctx) -> bool:
|
|
597
|
+
"""`body: jsonEncode(body)` in a helper whose optional `body` argument was not passed at this call site."""
|
|
598
|
+
while isinstance(r, dict) and r.get("k") in ("call", "nn", "as") and (r.get("k") != "call" or r.get("n") in ("jsonEncode", "encode", None)):
|
|
599
|
+
if r.get("k") == "call":
|
|
600
|
+
a = r.get("a") or []
|
|
601
|
+
if not a:
|
|
602
|
+
return False
|
|
603
|
+
r = a[0]
|
|
604
|
+
else:
|
|
605
|
+
r = r["e"]
|
|
606
|
+
if isinstance(r, dict) and r.get("k") == "id" and r["v"] in ctx.bindings:
|
|
607
|
+
return ctx.bindings[r["v"]][0] is None or (ctx.bindings[r["v"]][0] or {}).get("k") == "null"
|
|
608
|
+
return isinstance(r, dict) and r.get("k") == "null"
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def env_key(f: dict):
|
|
612
|
+
n, t = f.get("n"), f.get("t") or {}
|
|
613
|
+
a = f.get("a") or []
|
|
614
|
+
if n == "fromEnvironment" and root_name(t) in ("String", "bool", "int") and a and a[0].get("k") == "str":
|
|
615
|
+
return a[0]["v"], f"{root_name(t)}.fromEnvironment"
|
|
616
|
+
if n in ("get", "maybeGet", "getOrElse", "getBool", "getInt") and (root_name(t) or "").lower() in ("dotenv", "env") and a and a[0].get("k") == "str":
|
|
617
|
+
return a[0]["v"], "dotenv.get"
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def env_key_idx(f: dict):
|
|
622
|
+
tg = f.get("target") or {}
|
|
623
|
+
if tg.get("k") == "prop" and tg.get("n") in ("env", "environment") and (f.get("key") or {}).get("k") == "str":
|
|
624
|
+
return f["key"]["v"], "dotenv.env[]" if tg.get("n") == "env" else "Platform.environment[]"
|
|
625
|
+
return None
|