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,907 @@
|
|
|
1
|
+
"""Dart program model built from the analyzer-based extractor's facts (bin/extract.dart).
|
|
2
|
+
|
|
3
|
+
Unresolved (syntactic) ASTs only, so names are resolved here:
|
|
4
|
+
* packages: every pubspec.yaml under the root (monorepos, example/ apps); `package:<name>/x.dart`
|
|
5
|
+
maps to `<pkg dir>/lib/x.dart`, other packages are external;
|
|
6
|
+
* libraries: a file plus its `part` files share one scope; exports (show/hide) are followed;
|
|
7
|
+
* lexical lookup: locals/params -> class members (incl. inherited) -> library -> imports/prefixes;
|
|
8
|
+
* flow-insensitive type inference from declarations, initialisers, constructor calls,
|
|
9
|
+
return types (Future<T> unwrapped by await), `context.read<T>()`, `BlocProvider.of<T>()`,
|
|
10
|
+
`GetIt.I<T>()`/`getIt<T>()` and fields initialised from constructor parameters.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Iterator
|
|
19
|
+
|
|
20
|
+
from ... import presets
|
|
21
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
22
|
+
|
|
23
|
+
# directories that never hold a Dart package of their own: build output, Flutter platform runners (presets/dart.yaml)
|
|
24
|
+
PACKAGE_SKIP_DIRS = presets.skip_dirs("dart", "package_skip_dirs")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# supertypes of well-known external classes (the analysed project's dependencies are not parsed)
|
|
28
|
+
EXTERNAL_SUPERS = {
|
|
29
|
+
"BaseClient": {"Client"}, "IOClient": {"BaseClient", "Client"}, "RetryClient": {"BaseClient", "Client"},
|
|
30
|
+
"BrowserClient": {"BaseClient", "Client"}, "MockClient": {"BaseClient", "Client"},
|
|
31
|
+
"DioForNative": {"Dio", "DioMixin"}, "DioForBrowser": {"Dio", "DioMixin"}, "DioMixin": {"Dio"},
|
|
32
|
+
"Bloc": {"BlocBase"}, "Cubit": {"BlocBase"}, "HydratedBloc": {"Bloc", "BlocBase"}, "HydratedCubit": {"Cubit", "BlocBase"},
|
|
33
|
+
"StatelessWidget": {"Widget"}, "StatefulWidget": {"Widget"}, "ConsumerWidget": {"StatelessWidget", "Widget"},
|
|
34
|
+
"ConsumerStatefulWidget": {"StatefulWidget", "Widget"}, "HookWidget": {"StatelessWidget", "Widget"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DLib:
|
|
40
|
+
file: str
|
|
41
|
+
pkg: "Pkg | None"
|
|
42
|
+
parts: list[str] = field(default_factory=list)
|
|
43
|
+
decls: dict[str, Any] = field(default_factory=dict) # top-level name -> DClass | DFunc | DVar
|
|
44
|
+
imports: list[dict] = field(default_factory=list) # {"lib": DLib|None, "uri", "prefix", "show", "hide", "ext": bool}
|
|
45
|
+
exports: list[dict] = field(default_factory=list)
|
|
46
|
+
_ns: dict | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def id(self) -> str:
|
|
50
|
+
return f"module:{self.file}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Pkg:
|
|
55
|
+
name: str
|
|
56
|
+
dir: str # relative to project root ('' for root)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class DClass:
|
|
61
|
+
name: str
|
|
62
|
+
file: str
|
|
63
|
+
lib: DLib
|
|
64
|
+
kind: str
|
|
65
|
+
raw: dict
|
|
66
|
+
methods: dict[str, "DFunc"] = field(default_factory=dict)
|
|
67
|
+
ctors: dict[str, "DFunc"] = field(default_factory=dict)
|
|
68
|
+
fields: dict[str, "DVar"] = field(default_factory=dict)
|
|
69
|
+
supers: list[tuple[str, str]] = field(default_factory=list) # (relation, type text)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def id(self) -> str:
|
|
73
|
+
return f"class:{self.file}#{self.name}"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def line(self) -> int:
|
|
77
|
+
return self.raw.get("line", 1)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def ann(self) -> list[str]:
|
|
81
|
+
return [a["name"] for a in self.raw.get("ann") or []]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class DFunc:
|
|
86
|
+
name: str
|
|
87
|
+
file: str
|
|
88
|
+
lib: DLib
|
|
89
|
+
cls: DClass | None
|
|
90
|
+
kind: str # function | method | getter | setter | ctor | handler | operator
|
|
91
|
+
raw: dict
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def id(self) -> str:
|
|
95
|
+
if self.cls is None:
|
|
96
|
+
return f"function:{self.file}#{self.name}"
|
|
97
|
+
return f"method:{self.file}#{self.cls.name}.{self.name}"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def qual(self) -> str:
|
|
101
|
+
return self.id.split(":", 1)[1]
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def line(self) -> int:
|
|
105
|
+
return self.raw.get("line", 1)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def facts(self) -> list[dict]:
|
|
109
|
+
return self.raw.get("facts") or []
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def params(self) -> list[dict]:
|
|
113
|
+
return self.raw.get("params") or []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class DVar:
|
|
118
|
+
name: str
|
|
119
|
+
file: str
|
|
120
|
+
lib: DLib
|
|
121
|
+
cls: DClass | None
|
|
122
|
+
type: str | None
|
|
123
|
+
init: dict | None
|
|
124
|
+
line: int
|
|
125
|
+
static: bool = False
|
|
126
|
+
const: bool = False
|
|
127
|
+
final: bool = False
|
|
128
|
+
ann: list = field(default_factory=list)
|
|
129
|
+
facts: list = field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def split_type(t: str | None) -> tuple[str, list[str], bool]:
|
|
133
|
+
"""'Future<List<Foo>>?' -> ('Future', ['List<Foo>'], True)."""
|
|
134
|
+
if not t:
|
|
135
|
+
return "", [], False
|
|
136
|
+
t = t.strip()
|
|
137
|
+
nullable = t.endswith("?")
|
|
138
|
+
if nullable:
|
|
139
|
+
t = t[:-1]
|
|
140
|
+
if "<" not in t:
|
|
141
|
+
return t, [], nullable
|
|
142
|
+
i = t.index("<")
|
|
143
|
+
base, inner = t[:i], t[i + 1: t.rindex(">")] if ">" in t else ""
|
|
144
|
+
args, depth, cur = [], 0, ""
|
|
145
|
+
for ch in inner:
|
|
146
|
+
if ch == "<" or ch == "(":
|
|
147
|
+
depth += 1
|
|
148
|
+
elif ch == ">" or ch == ")":
|
|
149
|
+
depth -= 1
|
|
150
|
+
if ch == "," and depth == 0:
|
|
151
|
+
args.append(cur.strip())
|
|
152
|
+
cur = ""
|
|
153
|
+
else:
|
|
154
|
+
cur += ch
|
|
155
|
+
if cur.strip():
|
|
156
|
+
args.append(cur.strip())
|
|
157
|
+
return base, args, nullable
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def walk_repr(r) -> Iterator[dict]:
|
|
161
|
+
"""All nested expression reprs (calls/new/closures) of an expression repr."""
|
|
162
|
+
if isinstance(r, dict):
|
|
163
|
+
yield r
|
|
164
|
+
for k, v in r.items():
|
|
165
|
+
if k in ("l",):
|
|
166
|
+
continue
|
|
167
|
+
if isinstance(v, (dict, list)):
|
|
168
|
+
yield from walk_repr(v)
|
|
169
|
+
elif isinstance(r, list):
|
|
170
|
+
for x in r:
|
|
171
|
+
yield from walk_repr(x)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def root_name(r) -> str | None:
|
|
175
|
+
while isinstance(r, dict):
|
|
176
|
+
k = r.get("k")
|
|
177
|
+
if k == "id":
|
|
178
|
+
return r["v"]
|
|
179
|
+
if k in ("prop", "idx", "call"):
|
|
180
|
+
r = r.get("t")
|
|
181
|
+
elif k in ("nn", "as", "await"):
|
|
182
|
+
r = r.get("e")
|
|
183
|
+
else:
|
|
184
|
+
return None
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def last_name(r) -> str | None:
|
|
189
|
+
"""Name a value would be called in a URL placeholder: $id -> id, ${p.id} -> id, ${m()} -> m."""
|
|
190
|
+
if not isinstance(r, dict):
|
|
191
|
+
return None
|
|
192
|
+
k = r.get("k")
|
|
193
|
+
if k == "id":
|
|
194
|
+
return r["v"]
|
|
195
|
+
if k == "prop":
|
|
196
|
+
return r["n"]
|
|
197
|
+
if k == "call":
|
|
198
|
+
if r.get("n") in ("toString", "toLowerCase", "toUpperCase", "trim") and r.get("t"):
|
|
199
|
+
return last_name(r["t"])
|
|
200
|
+
return r.get("n") or last_name(r.get("t"))
|
|
201
|
+
if k == "idx":
|
|
202
|
+
i = r.get("i") or {}
|
|
203
|
+
return i.get("v") if i.get("k") == "str" else last_name(r.get("t"))
|
|
204
|
+
if k in ("nn", "as", "await"):
|
|
205
|
+
return last_name(r.get("e"))
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ctor_type(r) -> str | None:
|
|
210
|
+
"""Type name of an instance creation. Without resolution `Foo(..)` parses as a call, so a capitalised
|
|
211
|
+
unqualified callee (or `prefix.Foo(..)`) is treated as a constructor call."""
|
|
212
|
+
if not isinstance(r, dict):
|
|
213
|
+
return None
|
|
214
|
+
if r.get("k") == "new":
|
|
215
|
+
return r.get("type")
|
|
216
|
+
if r.get("k") == "call" and r.get("n") and r["n"][:1].isupper():
|
|
217
|
+
t = r.get("t")
|
|
218
|
+
if t is None:
|
|
219
|
+
return r["n"]
|
|
220
|
+
if t.get("k") == "id" and t["v"][:1].islower():
|
|
221
|
+
return f"{t['v']}.{r['n']}"
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
STOP_METHODS = {"get", "set", "add", "remove", "map", "where", "toList", "toString", "toJson", "fromJson", "copyWith", "build",
|
|
226
|
+
"dispose", "init", "initState", "call", "then", "listen", "close", "clear", "update", "load", "fetch", "save",
|
|
227
|
+
"delete", "create", "put", "post", "send", "read", "write", "open", "start", "stop", "reset", "refresh", "of",
|
|
228
|
+
"contains", "forEach", "first", "last", "length", "isEmpty", "isNotEmpty", "addAll", "insert", "emit", "on",
|
|
229
|
+
"setState", "notifyListeners", "toMap", "fromMap", "parse", "format", "value", "submit", "cancel", "run", "execute",
|
|
230
|
+
"handle", "process", "validate", "dispatch", "show", "hide", "push", "pop", "go", "of", "watch", "select", "when",
|
|
231
|
+
"maybeWhen", "props", "hashCode", "noSuchMethod", "runtimeType", "toggle", "play", "pause", "seek", "error"}
|
|
232
|
+
|
|
233
|
+
PROVIDER_LOOKUPS = {"read", "watch", "select", "get", "of", "call", "getIt", "locator", "sl", "inject", "find", "put", "lazyPut",
|
|
234
|
+
"Get.find", "instance", "I", "getAsync", "readOrNull"}
|
|
235
|
+
LIST_TYPES = {"List", "Iterable", "Set", "Queue"}
|
|
236
|
+
FUTURE_TYPES = {"Future", "FutureOr"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Ctx:
|
|
240
|
+
def __init__(self, prog: "DartProgram", lib: DLib, fn: DFunc | None, cls: DClass | None, bindings: dict | None = None):
|
|
241
|
+
self.prog, self.lib, self.fn, self.cls = prog, lib, fn, cls
|
|
242
|
+
self.bindings = bindings or {}
|
|
243
|
+
self._locals: dict | None = None
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def locals(self) -> dict:
|
|
247
|
+
"""name -> ('param', pdict) | ('var', fact)"""
|
|
248
|
+
if self._locals is None:
|
|
249
|
+
loc = {}
|
|
250
|
+
if self.fn:
|
|
251
|
+
for p in self.fn.params:
|
|
252
|
+
loc[p["name"]] = ("param", p)
|
|
253
|
+
for f in self.fn.facts:
|
|
254
|
+
if f["ft"] == "var" and f["name"] not in loc:
|
|
255
|
+
loc[f["name"]] = ("var", f)
|
|
256
|
+
elif f["ft"] == "var":
|
|
257
|
+
loc.setdefault(f["name"] + "#2", ("var", f))
|
|
258
|
+
self._locals = loc
|
|
259
|
+
return self._locals
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class DartProgram:
|
|
263
|
+
def __init__(self, root: Path, facts: dict):
|
|
264
|
+
self.root = Path(root)
|
|
265
|
+
self.facts = facts
|
|
266
|
+
self.files: dict[str, dict] = {f["file"]: f for f in facts.get("files") or []}
|
|
267
|
+
self.pkgs: dict[str, Pkg] = {}
|
|
268
|
+
self.pkg_dirs: list[Pkg] = []
|
|
269
|
+
self.libs: dict[str, DLib] = {}
|
|
270
|
+
self.part_owner: dict[str, str] = {}
|
|
271
|
+
self.classes: dict[str, DClass] = {}
|
|
272
|
+
self.funcs: dict[str, DFunc] = {}
|
|
273
|
+
self.vars: list[DVar] = []
|
|
274
|
+
self.by_name: dict[str, list[DClass]] = {}
|
|
275
|
+
self.methods_by_name: dict[str, list[DFunc]] = {}
|
|
276
|
+
self.callers: dict[str, list[tuple[Ctx, dict]]] = {}
|
|
277
|
+
self.subs: dict[str, list[DClass]] = {}
|
|
278
|
+
self.type_rules: list = [] # framework hooks: f(prog, repr, ctx) -> type | None
|
|
279
|
+
self._infer_stack: set = set()
|
|
280
|
+
|
|
281
|
+
# ------------------------------------------------------------------ loading
|
|
282
|
+
def load(self) -> None:
|
|
283
|
+
self._packages()
|
|
284
|
+
for f in self.files.values():
|
|
285
|
+
self._norm(f)
|
|
286
|
+
for rel, f in self.files.items():
|
|
287
|
+
for p in f.get("parts") or []:
|
|
288
|
+
tgt = self._rel_uri(rel, p.get("uri") if isinstance(p, dict) else p)
|
|
289
|
+
if tgt:
|
|
290
|
+
self.part_owner[tgt] = rel
|
|
291
|
+
lib_names = {f["library"]: rel for rel, f in self.files.items() if f.get("library")}
|
|
292
|
+
for rel, f in self.files.items():
|
|
293
|
+
if f.get("part_of") and rel not in self.part_owner:
|
|
294
|
+
po = f["part_of"]
|
|
295
|
+
tgt = self._rel_uri(rel, po) if isinstance(po, str) and po.endswith(".dart") else lib_names.get(po)
|
|
296
|
+
if tgt:
|
|
297
|
+
self.part_owner[rel] = tgt
|
|
298
|
+
for rel in self.files:
|
|
299
|
+
if rel not in self.part_owner:
|
|
300
|
+
self.libs[rel] = DLib(file=rel, pkg=self.pkg_of(rel))
|
|
301
|
+
for rel, owner in self.part_owner.items():
|
|
302
|
+
if owner in self.libs:
|
|
303
|
+
self.libs[owner].parts.append(rel)
|
|
304
|
+
else:
|
|
305
|
+
self.libs[rel] = DLib(file=rel, pkg=self.pkg_of(rel))
|
|
306
|
+
for rel, f in self.files.items():
|
|
307
|
+
lib = self.lib_of(rel)
|
|
308
|
+
self._decls(lib, rel, f)
|
|
309
|
+
for lib in self.libs.values():
|
|
310
|
+
for rel in [lib.file] + lib.parts:
|
|
311
|
+
f = self.files.get(rel) or {}
|
|
312
|
+
for imp in f.get("imports") or []:
|
|
313
|
+
lib.imports.append(self._imp(rel, imp))
|
|
314
|
+
for ex in f.get("exports") or []:
|
|
315
|
+
lib.exports.append(self._imp(rel, ex))
|
|
316
|
+
for c in self.classes.values():
|
|
317
|
+
for rel_, t in c.supers:
|
|
318
|
+
sc = self.resolve_class(c.lib, split_type(t)[0])
|
|
319
|
+
if sc:
|
|
320
|
+
self.subs.setdefault(sc.id, []).append(c)
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _norm(f: dict) -> None:
|
|
324
|
+
"""Body facts: 'ft' is the fact type ('t' of a call fact is its receiver, as in expression reprs)."""
|
|
325
|
+
def facts_of(x):
|
|
326
|
+
for fact in x.get("facts") or []:
|
|
327
|
+
fact["ft"] = "call" if fact.get("k") == "call" else fact.get("t")
|
|
328
|
+
for c in f.get("classes") or []:
|
|
329
|
+
for m in c.get("members") or []:
|
|
330
|
+
facts_of(m)
|
|
331
|
+
for fn in f.get("functions") or []:
|
|
332
|
+
facts_of(fn)
|
|
333
|
+
for v in f.get("vars") or []:
|
|
334
|
+
facts_of(v)
|
|
335
|
+
|
|
336
|
+
def _packages(self) -> None:
|
|
337
|
+
for dp, dns, fns in os.walk(self.root):
|
|
338
|
+
dns[:] = [d for d in dns if not d.startswith(".") and d not in PACKAGE_SKIP_DIRS]
|
|
339
|
+
if "pubspec.yaml" in fns:
|
|
340
|
+
try:
|
|
341
|
+
txt = (Path(dp) / "pubspec.yaml").read_text(errors="replace")
|
|
342
|
+
except OSError:
|
|
343
|
+
continue
|
|
344
|
+
m = re.search(r"^name:\s*['\"]?([A-Za-z0-9_]+)", txt, re.M)
|
|
345
|
+
if m:
|
|
346
|
+
rel = os.path.relpath(dp, self.root)
|
|
347
|
+
pk = Pkg(m.group(1), "" if rel == "." else rel)
|
|
348
|
+
self.pkgs.setdefault(pk.name, pk)
|
|
349
|
+
self.pkg_dirs.append(pk)
|
|
350
|
+
self.pkg_dirs.sort(key=lambda p: -len(p.dir))
|
|
351
|
+
|
|
352
|
+
def pkg_of(self, rel: str) -> Pkg | None:
|
|
353
|
+
for p in self.pkg_dirs:
|
|
354
|
+
if p.dir == "" or rel == p.dir or rel.startswith(p.dir + "/"):
|
|
355
|
+
return p
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
def _rel_uri(self, frm: str, uri: str | None) -> str | None:
|
|
359
|
+
if not uri:
|
|
360
|
+
return None
|
|
361
|
+
if uri.startswith("package:"):
|
|
362
|
+
name, _, rest = uri[8:].partition("/")
|
|
363
|
+
pk = self.pkgs.get(name)
|
|
364
|
+
if not pk:
|
|
365
|
+
return None
|
|
366
|
+
return os.path.normpath(os.path.join(pk.dir, "lib", rest)) if pk.dir else os.path.normpath(os.path.join("lib", rest))
|
|
367
|
+
if uri.startswith("dart:") or "://" in uri:
|
|
368
|
+
return None
|
|
369
|
+
return os.path.normpath(os.path.join(os.path.dirname(frm), uri))
|
|
370
|
+
|
|
371
|
+
def _imp(self, frm: str, imp: dict) -> dict:
|
|
372
|
+
uri = imp.get("uri") or ""
|
|
373
|
+
tgt = self._rel_uri(frm, uri)
|
|
374
|
+
lib = self.libs.get(tgt) if tgt else None
|
|
375
|
+
out = {"lib": lib, "uri": uri, "prefix": imp.get("prefix"), "show": imp.get("show") or [], "hide": imp.get("hide") or [],
|
|
376
|
+
"ext": lib is None, "l": imp.get("l"), "file": frm}
|
|
377
|
+
if imp.get("configs"): # conditional import / export: per-platform alternatives of `lib`
|
|
378
|
+
out["configs"] = []
|
|
379
|
+
for c in imp["configs"]:
|
|
380
|
+
t = self._rel_uri(frm, c.get("uri") or "")
|
|
381
|
+
out["configs"].append({"name": c.get("name"), "value": c.get("value"), "uri": c.get("uri"),
|
|
382
|
+
"lib": self.libs.get(t) if t else None})
|
|
383
|
+
return out
|
|
384
|
+
|
|
385
|
+
def lib_of(self, rel: str) -> DLib:
|
|
386
|
+
return self.libs[self.part_owner.get(rel, rel)] if self.part_owner.get(rel, rel) in self.libs else self.libs[rel]
|
|
387
|
+
|
|
388
|
+
def _decls(self, lib: DLib, rel: str, f: dict) -> None:
|
|
389
|
+
for c in f.get("classes") or []:
|
|
390
|
+
dc = DClass(name=c["name"], file=rel, lib=lib, kind=c.get("kind", "class"), raw=c)
|
|
391
|
+
if c.get("extends"):
|
|
392
|
+
dc.supers.append(("extends", c["extends"]))
|
|
393
|
+
for w in c.get("with") or []:
|
|
394
|
+
dc.supers.append(("with", w))
|
|
395
|
+
for w in c.get("implements") or []:
|
|
396
|
+
dc.supers.append(("implements", w))
|
|
397
|
+
for w in c.get("on") or []:
|
|
398
|
+
dc.supers.append(("on", w))
|
|
399
|
+
for m in c.get("members") or []:
|
|
400
|
+
k = m.get("kind")
|
|
401
|
+
if k == "field":
|
|
402
|
+
for v in m.get("vars") or []:
|
|
403
|
+
dc.fields[v["name"]] = DVar(v["name"], rel, lib, dc, m.get("type"), v.get("init"), v.get("l", m.get("line", 1)),
|
|
404
|
+
bool(m.get("static")), bool(m.get("const")), bool(m.get("final")), m.get("ann") or [])
|
|
405
|
+
elif k == "ctor":
|
|
406
|
+
nm = m.get("name") or "new"
|
|
407
|
+
fn = DFunc(f"{dc.name}.{nm}" if nm != "new" else f"{dc.name}.new", rel, lib, dc, "ctor", m)
|
|
408
|
+
fn.name = nm if nm else "new"
|
|
409
|
+
dc.ctors[fn.name] = fn
|
|
410
|
+
self.funcs[fn.id] = fn
|
|
411
|
+
elif k in ("method", "getter", "setter", "operator", "handler"):
|
|
412
|
+
nm = m["name"] + ("=" if k == "setter" else "")
|
|
413
|
+
fn = DFunc(nm, rel, lib, dc, k, m)
|
|
414
|
+
dc.methods[nm] = fn
|
|
415
|
+
self.funcs[fn.id] = fn
|
|
416
|
+
self.methods_by_name.setdefault(nm, []).append(fn)
|
|
417
|
+
if c.get("kind") == "enum":
|
|
418
|
+
for v in c.get("values") or []:
|
|
419
|
+
dc.fields[v["name"]] = DVar(v["name"], rel, lib, dc, dc.name, None, v.get("l", 1), True, True, True, v.get("ann") or [])
|
|
420
|
+
lib.decls.setdefault(dc.name, dc)
|
|
421
|
+
self.classes[dc.id] = dc
|
|
422
|
+
self.by_name.setdefault(dc.name, []).append(dc)
|
|
423
|
+
for fn_ in f.get("functions") or []:
|
|
424
|
+
k = fn_.get("kind", "function")
|
|
425
|
+
fn = DFunc(fn_["name"], rel, lib, None, "getter" if k == "getter" else "function", fn_)
|
|
426
|
+
lib.decls.setdefault(fn.name, fn)
|
|
427
|
+
self.funcs[fn.id] = fn
|
|
428
|
+
for v in f.get("vars") or []:
|
|
429
|
+
dv = DVar(v["name"], rel, lib, None, v.get("type"), v.get("init"), v.get("l", 1), True, bool(v.get("const")),
|
|
430
|
+
bool(v.get("final")), [], v.get("facts") or [])
|
|
431
|
+
lib.decls.setdefault(dv.name, dv)
|
|
432
|
+
self.vars.append(dv)
|
|
433
|
+
|
|
434
|
+
# ------------------------------------------------------------------ scopes
|
|
435
|
+
def namespace(self, lib: DLib, depth=0) -> dict:
|
|
436
|
+
"""Exported namespace of a library: public own decls + re-exports."""
|
|
437
|
+
if lib._ns is not None:
|
|
438
|
+
return lib._ns
|
|
439
|
+
lib._ns = {}
|
|
440
|
+
ns = {k: v for k, v in lib.decls.items() if not k.startswith("_")}
|
|
441
|
+
if depth < 12:
|
|
442
|
+
for ex in lib.exports:
|
|
443
|
+
if ex["lib"] is None:
|
|
444
|
+
continue
|
|
445
|
+
for k, v in self.namespace(ex["lib"], depth + 1).items():
|
|
446
|
+
if (ex["show"] and k not in ex["show"]) or k in ex["hide"]:
|
|
447
|
+
continue
|
|
448
|
+
ns.setdefault(k, v)
|
|
449
|
+
lib._ns = ns
|
|
450
|
+
return ns
|
|
451
|
+
|
|
452
|
+
def lookup(self, lib: DLib, name: str):
|
|
453
|
+
if name in lib.decls:
|
|
454
|
+
return lib.decls[name]
|
|
455
|
+
for imp in lib.imports:
|
|
456
|
+
if imp["prefix"] or imp["lib"] is None:
|
|
457
|
+
continue
|
|
458
|
+
if (imp["show"] and name not in imp["show"]) or name in imp["hide"]:
|
|
459
|
+
continue
|
|
460
|
+
v = self.namespace(imp["lib"]).get(name)
|
|
461
|
+
if v is not None:
|
|
462
|
+
return v
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
def prefix_imports(self, lib: DLib, prefix: str) -> list[dict]:
|
|
466
|
+
return [i for i in lib.imports if i["prefix"] == prefix]
|
|
467
|
+
|
|
468
|
+
def lookup_prefixed(self, lib: DLib, prefix: str, name: str):
|
|
469
|
+
for imp in self.prefix_imports(lib, prefix):
|
|
470
|
+
if imp["lib"] is not None:
|
|
471
|
+
v = self.namespace(imp["lib"]).get(name)
|
|
472
|
+
if v is not None:
|
|
473
|
+
return v
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
def imports_ext(self, lib: DLib, pattern: str, prefix: str | None = "*", name: str | None = None) -> bool:
|
|
477
|
+
"""Library imports an external uri matching pattern (prefix='*': any; None: unprefixed only).
|
|
478
|
+
With `name`, the import must also make that name visible (show/hide combinators)."""
|
|
479
|
+
for imp in lib.imports:
|
|
480
|
+
if imp["ext"] and re.search(pattern, imp["uri"]) and (prefix == "*" or imp["prefix"] == prefix):
|
|
481
|
+
if name is not None and ((imp["show"] and name not in imp["show"]) or name in imp["hide"]):
|
|
482
|
+
continue
|
|
483
|
+
return True
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
def resolve_class(self, lib: DLib, tname: str) -> DClass | None:
|
|
487
|
+
if not tname:
|
|
488
|
+
return None
|
|
489
|
+
if "." in tname:
|
|
490
|
+
p, _, n = tname.partition(".")
|
|
491
|
+
v = self.lookup_prefixed(lib, p, n)
|
|
492
|
+
else:
|
|
493
|
+
v = self.lookup(lib, tname)
|
|
494
|
+
return v if isinstance(v, DClass) else None
|
|
495
|
+
|
|
496
|
+
# ------------------------------------------------------------------ class hierarchy
|
|
497
|
+
def supers(self, c: DClass, rels=("extends", "with", "implements", "on")) -> list:
|
|
498
|
+
out = []
|
|
499
|
+
for rel, t in c.supers:
|
|
500
|
+
if rel not in rels:
|
|
501
|
+
continue
|
|
502
|
+
base = split_type(t)[0]
|
|
503
|
+
sc = self.resolve_class(c.lib, base)
|
|
504
|
+
out.append(("type", sc) if sc else ("ext", t))
|
|
505
|
+
return out
|
|
506
|
+
|
|
507
|
+
def mro(self, c: DClass, depth=0, rels=("extends", "with", "implements", "on")) -> list[DClass]:
|
|
508
|
+
out, seen = [], {c.id}
|
|
509
|
+
stack = [c]
|
|
510
|
+
while stack and len(out) < 60:
|
|
511
|
+
x = stack.pop(0)
|
|
512
|
+
for kind, s in self.supers(x, rels):
|
|
513
|
+
if kind == "type" and s.id not in seen:
|
|
514
|
+
seen.add(s.id)
|
|
515
|
+
out.append(s)
|
|
516
|
+
stack.append(s)
|
|
517
|
+
return out
|
|
518
|
+
|
|
519
|
+
def lineage(self, c: DClass) -> list[str]:
|
|
520
|
+
"""External base type texts (generic args kept) of c and its project ancestors."""
|
|
521
|
+
out = []
|
|
522
|
+
for x in [c] + self.mro(c):
|
|
523
|
+
for kind, s in self.supers(x):
|
|
524
|
+
if kind == "ext":
|
|
525
|
+
out.append(s)
|
|
526
|
+
return out
|
|
527
|
+
|
|
528
|
+
def ext_base_names(self, c: DClass) -> set[str]:
|
|
529
|
+
names = {split_type(t)[0].split(".")[-1] for t in self.lineage(c)}
|
|
530
|
+
for n in list(names):
|
|
531
|
+
names |= EXTERNAL_SUPERS.get(n, set())
|
|
532
|
+
return names
|
|
533
|
+
|
|
534
|
+
def is_sub(self, c: DClass, *names: str) -> bool:
|
|
535
|
+
if c.name in names:
|
|
536
|
+
return True
|
|
537
|
+
if self.ext_base_names(c) & set(names):
|
|
538
|
+
return True
|
|
539
|
+
return any(x.name in names for x in self.mro(c))
|
|
540
|
+
|
|
541
|
+
def find_member(self, c: DClass, name: str) -> tuple[Any, bool]:
|
|
542
|
+
"""(member, inherited) — method/getter/field searched along extends/with/implements."""
|
|
543
|
+
for i, x in enumerate([c] + self.mro(c)):
|
|
544
|
+
if name in x.methods:
|
|
545
|
+
return x.methods[name], i > 0
|
|
546
|
+
if name in x.fields:
|
|
547
|
+
return x.fields[name], i > 0
|
|
548
|
+
return None, False
|
|
549
|
+
|
|
550
|
+
def ctor(self, c: DClass, name: str = "new") -> DFunc | None:
|
|
551
|
+
return c.ctors.get(name or "new")
|
|
552
|
+
|
|
553
|
+
# ------------------------------------------------------------------ types
|
|
554
|
+
def type_of_text(self, t: str | None, lib: DLib, depth=0):
|
|
555
|
+
if t and " Function(" in t:
|
|
556
|
+
return ("fnret", self.type_of_text(t.split(" Function(")[0], lib, depth + 1))
|
|
557
|
+
base, args, _ = split_type(t)
|
|
558
|
+
if not base or depth > 6:
|
|
559
|
+
return None
|
|
560
|
+
bname = base.split(".")[-1]
|
|
561
|
+
if bname in FUTURE_TYPES:
|
|
562
|
+
return ("fut", self.type_of_text(args[0], lib, depth + 1) if args else None)
|
|
563
|
+
if bname in LIST_TYPES:
|
|
564
|
+
return ("list", self.type_of_text(args[0], lib, depth + 1) if args else None)
|
|
565
|
+
if bname == "Stream":
|
|
566
|
+
return ("stream", self.type_of_text(args[0], lib, depth + 1) if args else None)
|
|
567
|
+
if bname == "Map":
|
|
568
|
+
return ("map", args)
|
|
569
|
+
c = self.resolve_class(lib, base)
|
|
570
|
+
if c:
|
|
571
|
+
return ("inst", c, args)
|
|
572
|
+
if bname in ("dynamic", "void", "Object", "var", "Never", "Null"):
|
|
573
|
+
return None
|
|
574
|
+
return ("einst", base, args)
|
|
575
|
+
|
|
576
|
+
def var_type(self, v: DVar, depth=0):
|
|
577
|
+
if v.type:
|
|
578
|
+
return self.type_of_text(v.type, v.lib)
|
|
579
|
+
if v.cls and v.cls.kind == "enum" and v.init is None and v.type == v.cls.name:
|
|
580
|
+
return ("inst", v.cls, [])
|
|
581
|
+
if v.init is not None and depth < 4:
|
|
582
|
+
key = ("dv", v.file, v.name, v.line)
|
|
583
|
+
if key in self._infer_stack:
|
|
584
|
+
return None
|
|
585
|
+
self._infer_stack.add(key)
|
|
586
|
+
try:
|
|
587
|
+
return self.infer(v.init, Ctx(self, v.lib, None, v.cls))
|
|
588
|
+
finally:
|
|
589
|
+
self._infer_stack.discard(key)
|
|
590
|
+
# field initialised from a constructor parameter (this.x / x = param)
|
|
591
|
+
if v.cls:
|
|
592
|
+
for ct in v.cls.ctors.values():
|
|
593
|
+
for p in ct.params:
|
|
594
|
+
if p["name"] == v.name and p.get("this") and p.get("type"):
|
|
595
|
+
return self.type_of_text(p["type"], v.lib)
|
|
596
|
+
for ini in ct.raw.get("inits") or []:
|
|
597
|
+
if ini.get("field") == v.name:
|
|
598
|
+
return self.infer(ini["v"], Ctx(self, v.lib, ct, v.cls))
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
def func_ret(self, f: DFunc, depth=0):
|
|
602
|
+
if f.kind == "ctor":
|
|
603
|
+
return ("inst", f.cls, [])
|
|
604
|
+
if f.raw.get("ret"):
|
|
605
|
+
t = self.type_of_text(f.raw["ret"], f.lib)
|
|
606
|
+
return t
|
|
607
|
+
if depth < 3:
|
|
608
|
+
for x in f.facts:
|
|
609
|
+
if x["ft"] == "return":
|
|
610
|
+
key = ("ret", f.id)
|
|
611
|
+
if key in self._infer_stack:
|
|
612
|
+
return None
|
|
613
|
+
self._infer_stack.add(key)
|
|
614
|
+
try:
|
|
615
|
+
t = self.infer(x["v"], Ctx(self, f.lib, f, f.cls))
|
|
616
|
+
finally:
|
|
617
|
+
self._infer_stack.discard(key)
|
|
618
|
+
if t:
|
|
619
|
+
return ("fut", t) if f.raw.get("async") else t
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
def infer(self, r, ctx: Ctx, depth=0):
|
|
623
|
+
if not isinstance(r, dict) or depth > 12:
|
|
624
|
+
return None
|
|
625
|
+
for rule in self.type_rules:
|
|
626
|
+
t = rule(self, r, ctx)
|
|
627
|
+
if t is not None:
|
|
628
|
+
return t
|
|
629
|
+
k = r.get("k")
|
|
630
|
+
if k == "id":
|
|
631
|
+
return self.infer_name(r["v"], ctx, depth)
|
|
632
|
+
if k == "this":
|
|
633
|
+
return ("inst", ctx.cls, []) if ctx.cls else None
|
|
634
|
+
if k == "super":
|
|
635
|
+
if ctx.cls:
|
|
636
|
+
for kind, s in self.supers(ctx.cls, ("extends",)):
|
|
637
|
+
return ("inst", s, []) if kind == "type" else ("einst", s, [])
|
|
638
|
+
return None
|
|
639
|
+
if k == "prop":
|
|
640
|
+
if r["t"].get("k") == "id":
|
|
641
|
+
# prefixed class / top-level: p.X
|
|
642
|
+
pfx = r["t"]["v"]
|
|
643
|
+
if pfx not in ctx.locals and self.prefix_imports(ctx.lib, pfx):
|
|
644
|
+
v = self.lookup_prefixed(ctx.lib, pfx, r["n"])
|
|
645
|
+
return self._decl_type(v)
|
|
646
|
+
tt = self.infer(r["t"], ctx, depth + 1)
|
|
647
|
+
return self.member_type(tt, r["n"])
|
|
648
|
+
if k == "call":
|
|
649
|
+
for tgt, conf, via in self.resolve_call(r, ctx):
|
|
650
|
+
if isinstance(tgt, DClass):
|
|
651
|
+
return ("inst", tgt, [])
|
|
652
|
+
t = self.func_ret(tgt)
|
|
653
|
+
if t:
|
|
654
|
+
return t
|
|
655
|
+
if r.get("t") is None and r.get("n"):
|
|
656
|
+
ft = self.infer_name(r["n"], ctx, depth + 1)
|
|
657
|
+
if ft and ft[0] == "fnret":
|
|
658
|
+
return ft[1]
|
|
659
|
+
elif r.get("n") is None and isinstance(r.get("t"), dict):
|
|
660
|
+
ft = self.infer(r["t"], ctx, depth + 1)
|
|
661
|
+
if ft and ft[0] == "fnret":
|
|
662
|
+
return ft[1]
|
|
663
|
+
ta = r.get("ta")
|
|
664
|
+
if ta and r.get("n") in PROVIDER_LOOKUPS | {"getIt", "locator", "sl"}:
|
|
665
|
+
return self.type_of_text(ta[0], ctx.lib)
|
|
666
|
+
if ta and r.get("n") is None and root_name(r.get("t")) in ("getIt", "locator", "sl", "GetIt", "injector", "inject"):
|
|
667
|
+
return self.type_of_text(ta[0], ctx.lib)
|
|
668
|
+
if r.get("n") is None and isinstance(r.get("t"), dict):
|
|
669
|
+
return None
|
|
670
|
+
ct = ctor_type(r)
|
|
671
|
+
if ct:
|
|
672
|
+
return ("einst", ct, r.get("ta") or [])
|
|
673
|
+
# external method of an external type: Response.data etc. are handled by callers
|
|
674
|
+
return None
|
|
675
|
+
if k == "new":
|
|
676
|
+
c = self.resolve_class(ctx.lib, r["type"])
|
|
677
|
+
if c:
|
|
678
|
+
return ("inst", c, r.get("ta") or [])
|
|
679
|
+
return ("einst", r["type"], r.get("ta") or [])
|
|
680
|
+
if k == "await":
|
|
681
|
+
t = self.infer(r["e"], ctx, depth + 1)
|
|
682
|
+
return t[1] if t and t[0] == "fut" else t
|
|
683
|
+
if k == "as":
|
|
684
|
+
return self.type_of_text(r["type"], ctx.lib)
|
|
685
|
+
if k == "nn":
|
|
686
|
+
return self.infer(r["e"], ctx, depth + 1)
|
|
687
|
+
if k == "cond":
|
|
688
|
+
return self.infer(r["a"], ctx, depth + 1) or self.infer(r["b"], ctx, depth + 1)
|
|
689
|
+
if k == "bin" and r.get("op") == "??":
|
|
690
|
+
return self.infer(r["l"], ctx, depth + 1) or self.infer(r["r"], ctx, depth + 1)
|
|
691
|
+
if k == "cascade":
|
|
692
|
+
return self.infer(r["t"], ctx, depth + 1)
|
|
693
|
+
if k in ("str", "tpl", "cat"):
|
|
694
|
+
return ("einst", "String", [])
|
|
695
|
+
if k == "idx":
|
|
696
|
+
tt = self.infer(r["t"], ctx, depth + 1)
|
|
697
|
+
if tt and tt[0] == "list":
|
|
698
|
+
return tt[1]
|
|
699
|
+
return None
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
def _decl_type(self, v):
|
|
703
|
+
if isinstance(v, DClass):
|
|
704
|
+
return ("type", v)
|
|
705
|
+
if isinstance(v, DFunc):
|
|
706
|
+
return self.func_ret(v) if v.kind == "getter" else ("func", v)
|
|
707
|
+
if isinstance(v, DVar):
|
|
708
|
+
return self.var_type(v)
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
def infer_name(self, name: str, ctx: Ctx, depth=0):
|
|
712
|
+
if name in ctx.bindings:
|
|
713
|
+
br, bctx = ctx.bindings[name]
|
|
714
|
+
return self.infer(br, bctx, depth + 1) if br is not None else None
|
|
715
|
+
loc = ctx.locals.get(name)
|
|
716
|
+
if loc:
|
|
717
|
+
kind, d = loc
|
|
718
|
+
if kind == "param":
|
|
719
|
+
if d.get("type"):
|
|
720
|
+
return self.type_of_text(d["type"], ctx.lib)
|
|
721
|
+
if d.get("this") and ctx.cls and name in ctx.cls.fields:
|
|
722
|
+
return self.var_type(ctx.cls.fields[name])
|
|
723
|
+
return None
|
|
724
|
+
if d.get("type"):
|
|
725
|
+
return self.type_of_text(d["type"], ctx.lib)
|
|
726
|
+
if d.get("init") is not None:
|
|
727
|
+
key = ("loc", ctx.fn.id if ctx.fn else "", name)
|
|
728
|
+
if key in self._infer_stack:
|
|
729
|
+
return None
|
|
730
|
+
self._infer_stack.add(key)
|
|
731
|
+
try:
|
|
732
|
+
return self.infer(d["init"], ctx, depth + 1)
|
|
733
|
+
finally:
|
|
734
|
+
self._infer_stack.discard(key)
|
|
735
|
+
return None
|
|
736
|
+
if ctx.cls:
|
|
737
|
+
m, _ = self.find_member(ctx.cls, name)
|
|
738
|
+
if isinstance(m, DVar):
|
|
739
|
+
return self.var_type(m)
|
|
740
|
+
if isinstance(m, DFunc):
|
|
741
|
+
return self.func_ret(m) if m.kind == "getter" else ("func", m)
|
|
742
|
+
if name == "widget" and ctx.cls:
|
|
743
|
+
w = self.state_widget(ctx.cls)
|
|
744
|
+
if w:
|
|
745
|
+
return ("inst", w, [])
|
|
746
|
+
v = self.lookup(ctx.lib, name)
|
|
747
|
+
if v is not None:
|
|
748
|
+
return self._decl_type(v)
|
|
749
|
+
if name[:1].isupper():
|
|
750
|
+
return ("etype", name)
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
def state_widget(self, c: DClass) -> DClass | None:
|
|
754
|
+
"""State<MyWidget> -> MyWidget (for `widget.x`)."""
|
|
755
|
+
for t in self.lineage(c):
|
|
756
|
+
base, args, _ = split_type(t)
|
|
757
|
+
if base.split(".")[-1] == "State" and args:
|
|
758
|
+
return self.resolve_class(c.lib, split_type(args[0])[0])
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
def member_type(self, tt, name: str):
|
|
762
|
+
if not tt:
|
|
763
|
+
return None
|
|
764
|
+
if tt[0] == "inst" and tt[1] is not None:
|
|
765
|
+
m, _ = self.find_member(tt[1], name)
|
|
766
|
+
if isinstance(m, DVar):
|
|
767
|
+
return self.var_type(m)
|
|
768
|
+
if isinstance(m, DFunc):
|
|
769
|
+
return self.func_ret(m) if m.kind == "getter" else ("bound", m)
|
|
770
|
+
return None
|
|
771
|
+
if tt[0] == "type":
|
|
772
|
+
c = tt[1]
|
|
773
|
+
if name in c.fields:
|
|
774
|
+
f = c.fields[name]
|
|
775
|
+
return ("inst", c, []) if c.kind == "enum" and f.init is None and f.type == c.name else self.var_type(f)
|
|
776
|
+
if name in c.methods:
|
|
777
|
+
m = c.methods[name]
|
|
778
|
+
return self.func_ret(m) if m.kind == "getter" else ("func", m)
|
|
779
|
+
if name in c.ctors:
|
|
780
|
+
return ("ctor", c.ctors[name])
|
|
781
|
+
if tt[0] == "einst" and name == "data":
|
|
782
|
+
return ("json", tt[1])
|
|
783
|
+
return None
|
|
784
|
+
|
|
785
|
+
# ------------------------------------------------------------------ calls
|
|
786
|
+
def resolve_call(self, r: dict, ctx: Ctx) -> list[tuple[Any, str, str | None]]:
|
|
787
|
+
"""Targets of a call/new repr: [(DFunc | DClass, confidence, via)]. `self.last_recv` is the receiver's class
|
|
788
|
+
when the method was found on one of its ancestors (edge attrs.recv, #62)."""
|
|
789
|
+
self.last_recv = None
|
|
790
|
+
k = r.get("k")
|
|
791
|
+
if k == "new":
|
|
792
|
+
c = self.resolve_class(ctx.lib, r["type"])
|
|
793
|
+
if not c:
|
|
794
|
+
return []
|
|
795
|
+
return [(c, EXACT, r.get("ctor"))]
|
|
796
|
+
if k != "call":
|
|
797
|
+
return []
|
|
798
|
+
n, t = r.get("n"), r.get("t")
|
|
799
|
+
if n is None: # f(...) where f is an expression
|
|
800
|
+
if isinstance(t, dict) and t.get("k") == "id":
|
|
801
|
+
return self._unqualified(t["v"], ctx)
|
|
802
|
+
if isinstance(t, dict) and t.get("k") == "prop":
|
|
803
|
+
return self.resolve_call({"k": "call", "t": t["t"], "n": t["n"], "a": r.get("a"), "na": r.get("na")}, ctx)
|
|
804
|
+
return []
|
|
805
|
+
if t is None:
|
|
806
|
+
return self._unqualified(n, ctx)
|
|
807
|
+
if t.get("k") == "super" and ctx.cls:
|
|
808
|
+
for s in self.mro(ctx.cls, rels=("extends", "with")):
|
|
809
|
+
if n in s.methods:
|
|
810
|
+
return [(s.methods[n], EXACT, "super")]
|
|
811
|
+
return []
|
|
812
|
+
if t.get("k") == "this" and ctx.cls:
|
|
813
|
+
m, inh = self.find_member(ctx.cls, n)
|
|
814
|
+
if inh and isinstance(m, DFunc):
|
|
815
|
+
self.last_recv = ctx.cls
|
|
816
|
+
return [(m, EXACT if not inh else RESOLVED, None)] if isinstance(m, DFunc) else []
|
|
817
|
+
if t.get("k") == "id" and t["v"] not in ctx.locals and t["v"] not in ctx.bindings and self.prefix_imports(ctx.lib, t["v"]):
|
|
818
|
+
v = self.lookup_prefixed(ctx.lib, t["v"], n)
|
|
819
|
+
if isinstance(v, DFunc):
|
|
820
|
+
return [(v, EXACT, None)]
|
|
821
|
+
if isinstance(v, DClass):
|
|
822
|
+
return [(v, EXACT, None)]
|
|
823
|
+
return []
|
|
824
|
+
tt = self.infer(t, ctx)
|
|
825
|
+
if tt and tt[0] == "type":
|
|
826
|
+
c = tt[1]
|
|
827
|
+
if n in c.ctors:
|
|
828
|
+
return [(c, EXACT, n)]
|
|
829
|
+
if n in c.methods:
|
|
830
|
+
return [(c.methods[n], EXACT, None)]
|
|
831
|
+
for s in self.mro(c):
|
|
832
|
+
if n in s.methods and s.methods[n].raw.get("static"):
|
|
833
|
+
return [(s.methods[n], RESOLVED, None)]
|
|
834
|
+
return []
|
|
835
|
+
if tt and tt[0] == "inst" and tt[1] is not None:
|
|
836
|
+
m, inh = self.find_member(tt[1], n)
|
|
837
|
+
if isinstance(m, DFunc):
|
|
838
|
+
if inh:
|
|
839
|
+
self.last_recv = tt[1]
|
|
840
|
+
return [(m, RESOLVED, None)]
|
|
841
|
+
if isinstance(m, DVar):
|
|
842
|
+
return [] # calling a function-typed field
|
|
843
|
+
# extension methods on a project type or any of its supertypes (incl. well-known external ones)
|
|
844
|
+
names = {tt[1].name} | {x.name for x in self.mro(tt[1])} | self.ext_base_names(tt[1])
|
|
845
|
+
return self._extension(names, n, ctx)
|
|
846
|
+
if tt and tt[0] in ("einst", "etype", "list", "map", "fut", "json", "stream"):
|
|
847
|
+
return self._extension(tt[1] if tt[0] in ("einst", "etype") else tt[0], n, ctx) if tt[0] in ("einst", "etype") else []
|
|
848
|
+
# unknown receiver: unique project method name (heuristic)
|
|
849
|
+
if n in STOP_METHODS or n.startswith("_") is False and len(n) < 4:
|
|
850
|
+
return []
|
|
851
|
+
rn = root_name(t)
|
|
852
|
+
if rn and rn not in ctx.locals and not (ctx.cls and self.find_member(ctx.cls, rn)[0]) and self.prefix_imports(ctx.lib, rn):
|
|
853
|
+
return []
|
|
854
|
+
cands = [m for m in self.methods_by_name.get(n, []) if not m.raw.get("static") and m.kind in ("method", "getter")]
|
|
855
|
+
if len(cands) == 1:
|
|
856
|
+
return [(cands[0], HEURISTIC, "unique-method-name")]
|
|
857
|
+
# abstract + implementations: prefer the abstract declaration
|
|
858
|
+
abstract = [m for m in cands if m.raw.get("abstract")]
|
|
859
|
+
if len(abstract) == 1:
|
|
860
|
+
return [(abstract[0], HEURISTIC, "unique-abstract-method-name")]
|
|
861
|
+
return []
|
|
862
|
+
|
|
863
|
+
def _extension(self, tname, n: str, ctx: Ctx):
|
|
864
|
+
if getattr(self, "_ext_index", None) is None:
|
|
865
|
+
self._ext_index = {}
|
|
866
|
+
for lib_c in self.classes.values():
|
|
867
|
+
if lib_c.kind == "extension":
|
|
868
|
+
on = split_type(lib_c.raw.get("on_type") or "")[0].split(".")[-1]
|
|
869
|
+
for mn, m in lib_c.methods.items():
|
|
870
|
+
self._ext_index.setdefault(mn, []).append((on, m))
|
|
871
|
+
names = {x.split(".")[-1] for x in tname} if isinstance(tname, (set, frozenset, list, tuple)) else {tname.split(".")[-1]}
|
|
872
|
+
for on, m in self._ext_index.get(n, []):
|
|
873
|
+
if on in names:
|
|
874
|
+
return [(m, RESOLVED, "extension")]
|
|
875
|
+
return []
|
|
876
|
+
|
|
877
|
+
def _unqualified(self, n: str, ctx: Ctx):
|
|
878
|
+
if n in ctx.locals or n in ctx.bindings:
|
|
879
|
+
return []
|
|
880
|
+
if ctx.cls:
|
|
881
|
+
m, inh = self.find_member(ctx.cls, n)
|
|
882
|
+
if isinstance(m, DFunc):
|
|
883
|
+
if inh:
|
|
884
|
+
self.last_recv = ctx.cls
|
|
885
|
+
return [(m, EXACT if not inh else RESOLVED, None)]
|
|
886
|
+
if isinstance(m, DVar):
|
|
887
|
+
return []
|
|
888
|
+
if ctx.cls.kind == "extension":
|
|
889
|
+
pass
|
|
890
|
+
v = self.lookup(ctx.lib, n)
|
|
891
|
+
if isinstance(v, DFunc):
|
|
892
|
+
return [(v, EXACT, None)]
|
|
893
|
+
if isinstance(v, DClass):
|
|
894
|
+
return [(v, EXACT, None)]
|
|
895
|
+
return []
|
|
896
|
+
|
|
897
|
+
# ------------------------------------------------------------------ iteration
|
|
898
|
+
def all_funcs(self) -> Iterator[DFunc]:
|
|
899
|
+
yield from self.funcs.values()
|
|
900
|
+
|
|
901
|
+
def ctx_of(self, f: DFunc, bindings=None) -> Ctx:
|
|
902
|
+
return Ctx(self, f.lib, f, f.cls, bindings)
|
|
903
|
+
|
|
904
|
+
def calls_in(self, f: DFunc) -> Iterator[dict]:
|
|
905
|
+
for x in f.facts:
|
|
906
|
+
if x["ft"] in ("call", "new"):
|
|
907
|
+
yield x
|