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,538 @@
1
+ """Flutter framework plugin (on top of the Dart plugin).
2
+
3
+ contribute():
4
+ * widgets: StatelessWidget/StatefulWidget/State/Consumer*/Hook* classes get lifecycle CALLS
5
+ (createState/initState/didChangeDependencies/didUpdateWidget/build/dispose); creating a widget
6
+ (`X(...)`) is a RENDERS edge; StatefulWidget -> its State class via createState;
7
+ * bloc/cubit: `on<E>(handler)` registrations -> LISTENED_BY event class -> handler (subclasses of
8
+ E included), `bloc.add(E(...))` / `context.read<B>().add(..)` -> DISPATCHES; `emit(S(..))` /
9
+ `emit(state.copyWith(status: Enum.v))` -> EMITS_STATE; UI `is S`, `case S()`,
10
+ `state.status == Enum.v`, switch cases -> HANDLES_STATE. Per bloc a state-flow summary is kept on
11
+ the class node (attrs.state_flow): events dispatched without a handler, handlers never dispatched,
12
+ emitted states/status values no UI code checks;
13
+ * navigation: MaterialApp/CupertinoApp `routes:`/`home:`, go_router GoRoute trees (nested paths,
14
+ :param -> {param}), auto_route @RoutePage, `Navigator.push(MaterialPageRoute(builder: ..))`,
15
+ `pushNamed('/x')`, `context.go/push('/x')` -> page nodes (entry ui_page) + NAVIGATES_TO;
16
+ `main()` in lib/ is ui_global.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ from ...core.model import EXACT, HEURISTIC, RESOLVED
23
+ from ...core.plugin import FrameworkPlugin, GraphBuilder, Project
24
+ from ..dart.http import UrlEval
25
+ from ..dart.program import Ctx, DartProgram, DClass, DFunc, ctor_type, root_name, split_type, walk_repr
26
+
27
+ WIDGET_BASES = {"StatelessWidget", "StatefulWidget", "Widget", "HookWidget", "StatefulHookWidget", "ConsumerWidget",
28
+ "ConsumerStatefulWidget", "HookConsumerWidget", "InheritedWidget", "InheritedNotifier", "InheritedModel",
29
+ "RenderObjectWidget", "LeafRenderObjectWidget", "SingleChildRenderObjectWidget", "MultiChildRenderObjectWidget",
30
+ "PreferredSizeWidget", "ProxyWidget", "ImplicitlyAnimatedWidget", "AnimatedWidget", "StatelessComponent",
31
+ "StatefulComponent", "GetView", "GetWidget"}
32
+ STATE_BASES = {"State", "ConsumerState"}
33
+ LIFECYCLE = ("createState", "initState", "didChangeDependencies", "didUpdateWidget", "build", "dispose", "deactivate", "activate")
34
+ BLOC_BASES = {"Bloc", "HydratedBloc", "ReplayBloc"}
35
+ CUBIT_BASES = {"Cubit", "HydratedCubit", "ReplayCubit"}
36
+ ROUTE_BASES = {"Route", "PageRoute", "ModalRoute", "MaterialPageRoute", "CupertinoPageRoute", "PageRouteBuilder",
37
+ "TransitionRoute", "PopupRoute"}
38
+ ROUTE_TYPES = {"MaterialPageRoute", "CupertinoPageRoute", "PageRouteBuilder", "MaterialWithModalsPageRoute", "DialogRoute",
39
+ "ModalBottomSheetRoute", "FadeRoute", "SlideRoute"}
40
+ PUSH = {"push", "pushReplacement", "pushAndRemoveUntil", "pushAndRemoveAll", "popAndPush", "restorablePush", "to", "off", "offAll"}
41
+ PUSH_NAMED = {"pushNamed", "pushReplacementNamed", "popAndPushNamed", "pushNamedAndRemoveUntil", "restorablePushNamed", "toNamed",
42
+ "offNamed", "offAllNamed"}
43
+ GO = {"go", "push", "replace", "pushReplacement", "goNamed", "pushNamed", "replaceNamed", "pushReplacementNamed"}
44
+ APP_TYPES = {"MaterialApp", "CupertinoApp", "WidgetsApp", "GetMaterialApp", "GetCupertinoApp"}
45
+
46
+
47
+ def gopath(p: str) -> str:
48
+ return re.sub(r":(\w+)", r"{\1}", p)
49
+
50
+
51
+ class FlutterPlugin(FrameworkPlugin):
52
+ name, language = "flutter", "dart"
53
+
54
+ def detect(self, project: Project) -> bool:
55
+ for p in [project.root / "pubspec.yaml"] + list(project.root.glob("*/pubspec.yaml")) + list(project.root.glob("*/*/pubspec.yaml")):
56
+ try:
57
+ if re.search(r"^\s+flutter:\s*$|sdk:\s*flutter", p.read_text(errors="replace"), re.M):
58
+ return True
59
+ except OSError:
60
+ continue
61
+ return False
62
+
63
+ def register_hooks(self, prog: DartProgram) -> None:
64
+ def state_rule(prog_, r, ctx: Ctx):
65
+ if r.get("k") == "id" and r["v"] == "state" and ctx.cls is not None and "state" not in ctx.locals:
66
+ s = self.state_type(prog_, ctx.cls)
67
+ if s is not None:
68
+ return ("inst", s, [])
69
+ return None
70
+ prog.type_rules.append(state_rule)
71
+
72
+ @staticmethod
73
+ def bloc_kind(prog: DartProgram, c: DClass) -> str | None:
74
+ names = prog.ext_base_names(c)
75
+ if names & BLOC_BASES:
76
+ return "bloc"
77
+ if names & CUBIT_BASES:
78
+ return "cubit"
79
+ return None
80
+
81
+ @staticmethod
82
+ def bloc_args(prog: DartProgram, c: DClass) -> list[str]:
83
+ for t in prog.lineage(c):
84
+ base, args, _ = split_type(t)
85
+ if base.split(".")[-1] in BLOC_BASES | CUBIT_BASES:
86
+ return args
87
+ return []
88
+
89
+ def state_type(self, prog: DartProgram, c: DClass) -> DClass | None:
90
+ if self.bloc_kind(prog, c) is None:
91
+ return None
92
+ args = self.bloc_args(prog, c)
93
+ if args:
94
+ return prog.resolve_class(c.lib, split_type(args[-1])[0])
95
+ return None
96
+
97
+ # ------------------------------------------------------------------
98
+ def contribute(self, project: Project, builder: GraphBuilder, prog: DartProgram) -> dict:
99
+ if prog is None:
100
+ return {"status": "no dart program"}
101
+ b = builder
102
+ st = {"widgets": 0, "states": 0, "renders": 0, "blocs": 0, "cubits": 0, "event_handlers": 0, "dispatches": 0,
103
+ "dispatch_unhandled": 0, "emits": 0, "handles_state": 0, "pages": 0, "navigations": 0, "ui_global": 0}
104
+ widget = {}
105
+ for c in prog.classes.values():
106
+ names = prog.ext_base_names(c) | {x.name for x in prog.mro(c)}
107
+ if names & WIDGET_BASES:
108
+ widget[c.id] = c
109
+ b.nodes[c.id].attrs["flutter"] = "widget"
110
+ st["widgets"] += 1
111
+ elif names & STATE_BASES:
112
+ b.nodes[c.id].attrs["flutter"] = "state"
113
+ st["states"] += 1
114
+ else:
115
+ continue
116
+ for m in LIFECYCLE:
117
+ fm, inh = prog.find_member(c, m)
118
+ if isinstance(fm, DFunc):
119
+ b.add_edge(c.id, fm.id, "CALLS", c.file, fm.line, RESOLVED, via="flutter-lifecycle")
120
+ # creating a widget renders it; StatefulWidget -> State through createState
121
+ for e in list(b.edges.values()):
122
+ if e.kind == "INSTANTIATES" and e.dst in widget:
123
+ b.add_edge(e.src, e.dst, "RENDERS", e.file, e.line, e.confidence)
124
+ st["renders"] += 1
125
+ if e.kind == "INSTANTIATES" and e.src.endswith(".createState") and b.nodes.get(e.dst) is not None:
126
+ owner = e.src.rsplit(".", 1)[0].replace("method:", "class:", 1)
127
+ b.add_edge(owner, e.dst, "RENDERS", e.file, e.line, EXACT, via="createState")
128
+ self.blocs(prog, b, st)
129
+ self.navigation(prog, b, st)
130
+ for fn in prog.funcs.values():
131
+ if fn.cls is None and fn.name == "main" and "/lib/" in "/" + fn.file and "test" not in fn.file.split("/")[0]:
132
+ b.nodes[fn.id].entry_kind = "ui_global"
133
+ st["ui_global"] += 1
134
+ return st
135
+
136
+ # ------------------------------------------------------------------ bloc
137
+ def blocs(self, prog: DartProgram, b: GraphBuilder, st: dict) -> None:
138
+ blocs: dict[str, dict] = {}
139
+ for c in prog.classes.values():
140
+ kind = self.bloc_kind(prog, c)
141
+ if not kind:
142
+ continue
143
+ st["blocs" if kind == "bloc" else "cubits"] += 1
144
+ args = self.bloc_args(prog, c)
145
+ ev = prog.resolve_class(c.lib, split_type(args[0])[0]) if kind == "bloc" and len(args) >= 2 else None
146
+ sc = self.state_type(prog, c)
147
+ info = {"cls": c, "kind": kind, "event_base": ev, "state": sc, "handlers": [], "emits": [], "dispatched": []}
148
+ blocs[c.id] = info
149
+ b.nodes[c.id].attrs["flutter"] = kind
150
+ for ct in c.ctors.values():
151
+ for f in ct.facts:
152
+ if f["ft"] == "call" and f.get("n") == "on" and f.get("t") is None and f.get("ta"):
153
+ et = f["ta"][0]
154
+ ecls = prog.resolve_class(c.lib, split_type(et)[0])
155
+ a0 = (f.get("a") or [None])[0] or {}
156
+ h = None
157
+ if a0.get("k") == "id":
158
+ h = c.methods.get(a0["v"])
159
+ if h is None:
160
+ m, _ = prog.find_member(c, a0["v"])
161
+ h = m if isinstance(m, DFunc) else None
162
+ elif a0.get("k") == "fn":
163
+ h = c.methods.get(f"on<{et}>@{f.get('l')}")
164
+ if h is not None:
165
+ info["handlers"].append((ecls, et, h, f.get("l"), ct.file))
166
+ if "mapEventToState" in c.methods:
167
+ info["handlers"].append((ev, args[0] if args else "?", c.methods["mapEventToState"], c.methods["mapEventToState"].line, c.file))
168
+ for ecls, et, h, line, file in info["handlers"]:
169
+ st["event_handlers"] += 1
170
+ if ecls is None:
171
+ continue
172
+ b.add_edge(ecls.id, h.id, "LISTENED_BY", file, line, EXACT, via=f"on<{et}>", bloc=c.id)
173
+ for sub in self.subclasses(prog, ecls):
174
+ b.add_edge(sub.id, h.id, "LISTENED_BY", file, line, RESOLVED, via=f"on<{et}> (subtype)", bloc=c.id)
175
+ self._blocs = blocs
176
+ event_owner: dict[str, list[str]] = {}
177
+ for bid, info in blocs.items():
178
+ for ecls, et, h, line, file in info["handlers"]:
179
+ if ecls is not None:
180
+ for x in [ecls] + self.subclasses(prog, ecls):
181
+ event_owner.setdefault(x.id, []).append(bid)
182
+ if info["event_base"] is not None:
183
+ for x in [info["event_base"]] + self.subclasses(prog, info["event_base"]):
184
+ event_owner.setdefault(x.id, []).append(bid)
185
+ # dispatch: bloc.add(Event(...))
186
+ for fn in prog.funcs.values():
187
+ ctx = prog.ctx_of(fn)
188
+ for f in fn.facts:
189
+ if f["ft"] != "call" or f.get("n") != "add" or not f.get("a"):
190
+ continue
191
+ ev = None
192
+ for tgt, conf, via in prog.resolve_call(f["a"][0], ctx) if (f["a"][0] or {}).get("k") in ("call", "new") else []:
193
+ if isinstance(tgt, DClass):
194
+ ev = tgt
195
+ if ev is None:
196
+ continue
197
+ bloc, conf = None, RESOLVED
198
+ t = f.get("t")
199
+ if t is None and fn.cls is not None and fn.cls.id in blocs:
200
+ bloc, conf = blocs[fn.cls.id], EXACT
201
+ elif t is not None:
202
+ tt = prog.infer(t, ctx)
203
+ if tt and tt[0] == "inst" and tt[1] is not None and tt[1].id in blocs:
204
+ bloc = blocs[tt[1].id]
205
+ if bloc is None:
206
+ owners = sorted(set(event_owner.get(ev.id, [])))
207
+ if len(owners) == 1:
208
+ bloc, conf = blocs[owners[0]], HEURISTIC
209
+ else:
210
+ continue
211
+ handled = any(ecls is not None and (ecls is ev or ev in self.subclasses(prog, ecls)) for ecls, *_ in bloc["handlers"])
212
+ if bloc["kind"] == "cubit":
213
+ continue
214
+ b.add_edge(fn.id, ev.id, "DISPATCHES", fn.file, f.get("l"), conf, bloc=bloc["cls"].id,
215
+ **({"unhandled": True} if not handled else {}))
216
+ bloc["dispatched"].append((ev, fn, f.get("l"), handled))
217
+ st["dispatches"] += 1
218
+ if not handled:
219
+ st["dispatch_unhandled"] += 1
220
+ # emits
221
+ for bid, info in blocs.items():
222
+ c = info["cls"]
223
+ for m in list(c.methods.values()) + list(c.ctors.values()):
224
+ ctx = prog.ctx_of(m)
225
+ for f in m.facts:
226
+ if f["ft"] != "call" or f.get("n") not in ("emit",) or not f.get("a"):
227
+ continue
228
+ if f.get("t") is not None and root_name(f.get("t")) not in ("emit", "emitter"):
229
+ continue
230
+ a0 = f["a"][0] or {}
231
+ cls, values = None, {}
232
+ if a0.get("k") in ("new", "call"):
233
+ for tgt, conf, via in prog.resolve_call(a0, ctx):
234
+ if isinstance(tgt, DClass):
235
+ cls = tgt
236
+ if cls is None and a0.get("k") == "call" and a0.get("n") == "copyWith":
237
+ tt = prog.infer(a0.get("t"), ctx)
238
+ cls = tt[1] if tt and tt[0] == "inst" else info["state"]
239
+ for k_, v in (a0.get("na") or {}).items():
240
+ if v and v.get("k") == "prop" and (v.get("t") or {}).get("k") == "id" and v["t"]["v"][:1].isupper():
241
+ values[k_] = f"{v['t']['v']}.{v['n']}"
242
+ if cls is None:
243
+ continue
244
+ b.add_edge(m.id, cls.id, "EMITS_STATE", m.file, f.get("l"), EXACT, **({"values": values} if values else {}))
245
+ info["emits"].append((cls, values, m, f.get("l")))
246
+ st["emits"] += 1
247
+ # UI handling of states
248
+ state_classes: dict[str, str] = {}
249
+ status_enums: dict[str, str] = {}
250
+ for bid, info in blocs.items():
251
+ s = info["state"]
252
+ if s is None:
253
+ continue
254
+ for x in [s] + self.subclasses(prog, s):
255
+ state_classes[x.name] = bid
256
+ for fv in x.fields.values():
257
+ ft = prog.resolve_class(x.lib, split_type(fv.type)[0]) if fv.type else None
258
+ if ft is not None and ft.kind == "enum":
259
+ status_enums[ft.name] = bid
260
+ for cls, values, m, line in info["emits"]:
261
+ for v in values.values():
262
+ status_enums.setdefault(v.split(".")[0], bid)
263
+ handled: dict[str, set] = {bid: set() for bid in blocs}
264
+ handled_at: dict[str, list] = {}
265
+ for fn in prog.funcs.values():
266
+ in_bloc = fn.cls is not None and fn.cls.id in blocs
267
+ for f in fn.facts:
268
+ names = []
269
+ if f["ft"] == "is":
270
+ names = [split_type(f["type"])[0]]
271
+ elif f["ft"] == "switch":
272
+ names = [re.match(r"^[\w.]+", c_).group(0) for c_ in f.get("cases") or [] if re.match(r"^[\w.]+", c_)]
273
+ elif f["ft"] == "cmp":
274
+ names = [x for x in (f.get("a"), f.get("b")) if x and re.match(r"^[A-Z]\w*\.\w+$", x)]
275
+ for nm in names:
276
+ base = nm.split(".")[0]
277
+ if nm in state_classes or base in state_classes and "." not in nm:
278
+ bid = state_classes.get(nm) or state_classes.get(base)
279
+ cls = prog.resolve_class(fn.lib, nm)
280
+ if cls is not None and not in_bloc:
281
+ b.add_edge(fn.id, cls.id, "HANDLES_STATE", fn.file, f.get("l"), RESOLVED, how=f["ft"])
282
+ handled[bid].add(cls.name)
283
+ handled_at.setdefault(cls.name, []).append(f"{fn.file}:{f.get('l')}")
284
+ st["handles_state"] += 1
285
+ elif base in status_enums and "." in nm:
286
+ bid = status_enums[base]
287
+ cls = prog.resolve_class(fn.lib, base)
288
+ if cls is not None and not in_bloc:
289
+ b.add_edge(fn.id, cls.id, "HANDLES_STATE", fn.file, f.get("l"), RESOLVED, how=f["ft"], value=nm)
290
+ handled[bid].add(nm)
291
+ handled_at.setdefault(nm, []).append(f"{fn.file}:{f.get('l')}")
292
+ st["handles_state"] += 1
293
+ # per-bloc state flow summary
294
+ for bid, info in blocs.items():
295
+ c = info["cls"]
296
+ emitted = {}
297
+ for cls, values, m, line in info["emits"]:
298
+ if values:
299
+ for v in values.values():
300
+ if v.split(".")[0] in status_enums:
301
+ emitted.setdefault(v, []).append(f"{m.file}:{line}")
302
+ if cls is not info["state"] or not values:
303
+ emitted.setdefault(cls.name, []).append(f"{m.file}:{line}")
304
+ unhandled_states = sorted(k for k in emitted if k not in handled[bid] and not (info["state"] is not None and k == info["state"].name))
305
+ dispatched_ids = {ev.id for ev, *_ in info["dispatched"]}
306
+ never = []
307
+ for ecls, et, h, line, file in info["handlers"]:
308
+ if ecls is None:
309
+ continue
310
+ if not ({ecls.id} | {s.id for s in self.subclasses(prog, ecls)}) & dispatched_ids:
311
+ never.append({"event": et, "handler": h.id, "at": f"{file}:{line}"})
312
+ flow = {"kind": info["kind"], "state": info["state"].id if info["state"] else None,
313
+ "events_dispatched_unhandled": [{"event": ev.name, "at": f"{fn.file}:{l}"} for ev, fn, l, h in info["dispatched"] if not h],
314
+ "handlers_never_dispatched": never,
315
+ "emitted": {k: v[:4] for k, v in emitted.items()},
316
+ "emitted_not_handled_by_ui": unhandled_states,
317
+ "handled_by_ui": {k: handled_at.get(k, [])[:4] for k in sorted(handled[bid])}}
318
+ b.nodes[c.id].attrs["state_flow"] = flow
319
+
320
+ @staticmethod
321
+ def subclasses(prog: DartProgram, c: DClass) -> list[DClass]:
322
+ out, seen, stack = [], {c.id}, [c]
323
+ while stack:
324
+ x = stack.pop()
325
+ for s in prog.subs.get(x.id, []):
326
+ if s.id not in seen:
327
+ seen.add(s.id)
328
+ out.append(s)
329
+ stack.append(s)
330
+ return out
331
+
332
+ # ------------------------------------------------------------------ navigation
333
+ def page(self, b: GraphBuilder, prog: DartProgram, cls: DClass | None, route: str | None, file, line, via: str) -> str:
334
+ pk = cls.lib.pkg if cls is not None else (prog.pkg_of(file) if file else None)
335
+ pkg = pk.name if pk else None
336
+ key = f"dart:{pkg or ''}:{route}" if route else f"dart:{cls.file}#{cls.name}"
337
+ name = route or cls.name
338
+ pid = b.add_node("page", key, name, fqn=key, file=cls.file if cls else file, line=cls.line if cls else line, lang="dart",
339
+ entry_kind="ui_page", attrs={"route": route, "widget": cls.id if cls else None, "via": via})
340
+ if cls is not None:
341
+ b.add_edge(pid, cls.id, "RENDERS", file, line, EXACT, via=via)
342
+ return pid
343
+
344
+ PAGE_WRAPPERS = {"Page", "MaterialPage", "CupertinoPage", "CustomTransitionPage", "NoTransitionPage"}
345
+
346
+ def widgets_in(self, prog: DartProgram, r, ctx: Ctx) -> list[DClass]:
347
+ out = []
348
+ for x in walk_repr(r):
349
+ if x.get("k") == "fn":
350
+ for ret in x.get("ret") or []:
351
+ out.extend(self._screen_of(prog, ret, ctx))
352
+ return out
353
+
354
+ def _screen_of(self, prog: DartProgram, ret, ctx: Ctx, depth=0) -> list[DClass]:
355
+ """The widget a route builder returns; a Page wrapper (`MaterialPage(child: X)`, a custom
356
+ `CustomTransitionPage` subclass) is looked through to its `child:`."""
357
+ if not isinstance(ret, dict) or ret.get("k") not in ("new", "call") or depth > 4:
358
+ return []
359
+ name = (ctor_type(ret) or "").split(".")[-1]
360
+ tg = [t for t, conf, via in prog.resolve_call(ret, ctx) if isinstance(t, DClass)]
361
+ is_page = name in self.PAGE_WRAPPERS or any(prog.ext_base_names(c) & self.PAGE_WRAPPERS for c in tg)
362
+ child = (ret.get("na") or {}).get("child")
363
+ if is_page and child is not None:
364
+ inner = self._screen_of(prog, child, ctx, depth + 1)
365
+ if not inner and isinstance(child, dict) and child.get("k") in ("new", "call") and \
366
+ (ctor_type(child) or "").split(".")[-1] == "Builder":
367
+ inner = self.widgets_in(prog, (child.get("na") or {}).get("builder"), ctx)
368
+ if inner:
369
+ return inner
370
+ return tg
371
+
372
+ def navigation(self, prog: DartProgram, b: GraphBuilder, st: dict) -> None:
373
+ widget_ids = {c.id for c in prog.classes.values()
374
+ if (prog.ext_base_names(c) | {x.name for x in prog.mro(c)}) & WIDGET_BASES}
375
+ ev = UrlEval(prog, {})
376
+ named_pages: dict[str, str] = {}
377
+ go_names: dict[str, str] = {}
378
+
379
+ # the extractor caps expression depth, so a deep route tree is truncated inside its root's repr;
380
+ # every call is also emitted as its own fact, so swap each nested call for its own full repr.
381
+ full_calls: dict = {}
382
+
383
+ def go_tree(r, prefix: str, ctx: Ctx, file, depth=0):
384
+ if not isinstance(r, dict) or depth > 60:
385
+ return
386
+ if r.get("k") in ("new", "call") and r.get("l"):
387
+ r = full_calls.get((file, r.get("l"), r.get("n")), r)
388
+ if (ctor_type(r) or "").split(".")[-1] == "GoRoute":
389
+ na = r.get("na") or {}
390
+ p = ev.eval(na["path"], ctx).text if na.get("path") else ""
391
+ full = p if p.startswith("/") else (prefix.rstrip("/") + "/" + p if p else prefix)
392
+ full = gopath(full or "/")
393
+ ws = self.widgets_in(prog, {"k": "list", "items": [na.get("builder"), na.get("pageBuilder")]}, ctx)
394
+ pid = None
395
+ for w in ws[:1] or [None]:
396
+ pid = self.page(b, prog, w, full, file, r.get("l"), "GoRoute")
397
+ st["pages"] += 1
398
+ named_pages[full] = pid
399
+ if na.get("name") and (na["name"].get("k") == "str"):
400
+ go_names[na["name"]["v"]] = pid
401
+ for ch in (na.get("routes") or {}).get("items") or []:
402
+ go_tree(ch, full, ctx, file, depth + 1)
403
+ return
404
+ for k_, v in r.items():
405
+ if k_ in ("l", "k"):
406
+ continue
407
+ if isinstance(v, dict):
408
+ go_tree(v, prefix, ctx, file, depth + 1)
409
+ elif isinstance(v, list):
410
+ for x in v:
411
+ go_tree(x, prefix, ctx, file, depth + 1)
412
+
413
+ # route tables (functions, field initialisers, top-level vars)
414
+ sources = []
415
+ for fn in prog.funcs.values():
416
+ sources.append((fn.facts, prog.ctx_of(fn), fn.file, fn))
417
+ for c in prog.classes.values():
418
+ sources.append(([{"ft": "init", "v": v.init} for v in c.fields.values() if v.init], Ctx(prog, c.lib, None, c), c.file, None))
419
+ for v in prog.vars:
420
+ sources.append(([{"ft": "init", "v": v.init}], Ctx(prog, v.lib, None, None), v.file, None))
421
+ for facts, ctx, file, fn in sources:
422
+ for f in facts:
423
+ if f.get("ft") in ("new", "call") and f.get("l"):
424
+ full_calls.setdefault((file, f.get("l"), f.get("n")), f)
425
+ GO_TYPES = ("GoRouter", "ShellRoute", "StatefulShellRoute", "StatefulShellBranch", "GoRoute")
426
+ # go_router trees: facts can list an inner GoRoute before the router that contains it, so first
427
+ # collect every route constructor nested inside another one and only walk the outermost trees.
428
+ nested_go: set = set()
429
+ for facts, ctx, file, fn in sources:
430
+ for f in facts:
431
+ r = f["v"] if f["ft"] == "init" else (f if f["ft"] in ("new", "call") else None)
432
+ if not isinstance(r, dict):
433
+ continue
434
+ for x in walk_repr(r):
435
+ if (ctor_type(x) or "").split(".")[-1] in GO_TYPES:
436
+ for y in walk_repr(x):
437
+ if y is not x and (ctor_type(y) or "").split(".")[-1] in GO_TYPES and y.get("l"):
438
+ nested_go.add((file, y.get("l"), y.get("c")))
439
+ seen_go = set()
440
+ for facts, ctx, file, fn in sources:
441
+ for f in facts:
442
+ roots = [f["v"]] if f["ft"] == "init" else ([f] if f["ft"] in ("new", "call") else [])
443
+ for r in roots:
444
+ if not isinstance(r, dict):
445
+ continue
446
+ for x in walk_repr(r):
447
+ k = x.get("k")
448
+ tname = (ctor_type(x) or "").split(".")[-1] or None
449
+ if tname in GO_TYPES:
450
+ key = (file, x.get("l"), x.get("c"))
451
+ if key in seen_go or key in nested_go:
452
+ continue
453
+ seen_go.add(key)
454
+ for y in walk_repr(x):
455
+ if y.get("k") in ("new", "call") and y.get("l"):
456
+ seen_go.add((file, y.get("l"), y.get("c")))
457
+ go_tree(x, "", ctx, file)
458
+ if tname in APP_TYPES:
459
+ na = x.get("na") or {}
460
+ routes = na.get("routes") or {}
461
+ for el in routes.get("entries") or []:
462
+ kk = el.get("key") or {}
463
+ path = kk.get("v") if kk.get("k") == "str" else ev.eval(kk, ctx).text if kk else None
464
+ ws = self.widgets_in(prog, el.get("value"), ctx)
465
+ pid = self.page(b, prog, ws[0] if ws else None, path, file, el.get("l"), "routes") if (ws or path) else None
466
+ if pid:
467
+ named_pages[path] = pid
468
+ st["pages"] += 1
469
+ if na.get("home") and na["home"].get("k") in ("new", "call"):
470
+ for tgt, conf, via in prog.resolve_call(na["home"], ctx):
471
+ if isinstance(tgt, DClass):
472
+ named_pages.setdefault("/", self.page(b, prog, tgt, "/", file, x.get("l"), "home"))
473
+ st["pages"] += 1
474
+ for c in prog.classes.values():
475
+ if any(a.split(".")[-1] in ("RoutePage", "AutoRoutePage") for a in c.ann):
476
+ self.page(b, prog, c, None, c.file, c.line, "@RoutePage")
477
+ st["pages"] += 1
478
+ # navigation calls
479
+ for fn in prog.funcs.values():
480
+ ctx = prog.ctx_of(fn)
481
+ for f in fn.facts:
482
+ k = f.get("k")
483
+ ct = (ctor_type(f) or "").split(".")[-1]
484
+ if ct not in ROUTE_TYPES and f.get("ft") in ("new", "call"):
485
+ # project route classes (`class ScreenRoute extends MaterialPageRoute`) and route helpers
486
+ # (`Route<T> buildScreenRoute({required Widget screen})`) construct a route as well
487
+ for tgt, conf, via in prog.resolve_call(f, ctx):
488
+ if isinstance(tgt, DClass) and prog.ext_base_names(tgt) & ROUTE_BASES:
489
+ ct = tgt.name
490
+ elif isinstance(tgt, DFunc) and split_type(tgt.raw.get("ret") or "")[0].split(".")[-1] in ROUTE_BASES:
491
+ ct = tgt.name
492
+ else:
493
+ continue
494
+ break
495
+ else:
496
+ ct = None
497
+ if ct:
498
+ na_vals = list((f.get("na") or {}).values()) + list(f.get("a") or [])
499
+ ws = self.widgets_in(prog, {"k": "list", "items": na_vals}, ctx)
500
+ if ct not in ROUTE_TYPES:
501
+ # a widget passed directly (`screen: GameScreen(...)`)
502
+ for v in na_vals:
503
+ if isinstance(v, dict) and v.get("k") in ("new", "call"):
504
+ ws += [t for t, _c, _v in prog.resolve_call(v, ctx) if isinstance(t, DClass) and t.id in widget_ids]
505
+ for w in ws:
506
+ pid = self.page(b, prog, w, None, fn.file, f.get("l"), ct)
507
+ b.add_edge(fn.id, pid, "NAVIGATES_TO", fn.file, f.get("l"), EXACT, via=ct)
508
+ st["navigations"] += 1
509
+ continue
510
+ if f["ft"] != "call":
511
+ continue
512
+ n, t, a = f.get("n"), f.get("t"), f.get("a") or []
513
+ rn = root_name(t) if t else None
514
+ path_r = None
515
+ if n in PUSH_NAMED and (rn == "Navigator" or rn == "Get"):
516
+ path_r = a[1] if rn == "Navigator" and len(a) > 1 and t.get("k") == "id" else (a[0] if a else None)
517
+ elif n in GO and t is not None and (rn in ("context", "ctx", "GoRouter", "router", "_router", "appRouter") or
518
+ (t.get("k") == "call" and t.get("n") == "of" and root_name(t.get("t")) == "GoRouter")):
519
+ path_r = a[0] if a else None
520
+ if path_r is None:
521
+ continue
522
+ if n.endswith("Named") and rn != "Navigator" and rn != "Get":
523
+ nm = path_r.get("v") if path_r.get("k") == "str" else None
524
+ pid = go_names.get(nm) if nm else None
525
+ route = None
526
+ else:
527
+ route = gopath(ev.eval(path_r, ctx).text).split("?")[0]
528
+ pid = named_pages.get(route)
529
+ if pid is None:
530
+ for pth, p_ in named_pages.items():
531
+ if pth and re.fullmatch(re.sub(r"\\\{\w+\\\}", "[^/]+", re.escape(pth)), route):
532
+ pid = p_
533
+ break
534
+ if pid is None and route and re.fullmatch(r"/[\w{}\-./:]*", route):
535
+ pid = b.add_node("page", f"dart::{route}", route, lang="dart", attrs={"route": route, "unresolved": True})
536
+ if pid:
537
+ b.add_edge(fn.id, pid, "NAVIGATES_TO", fn.file, f.get("l"), EXACT if route in named_pages else HEURISTIC, via=n)
538
+ st["navigations"] += 1
File without changes