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