cg-code-graph 0.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,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