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,643 @@
1
+ """Platform conditions in source text: runtime and compile-time platform checks in brace languages.
2
+
3
+ Finds the code regions that run only on some targets, for the platform tags (codegraph/platforms.py):
4
+
5
+ TypeScript / JavaScript `if (Platform.OS === 'ios') {...} else {...}`, `Platform.OS == 'web' ? a : b`,
6
+ `Platform.select({ios: ..., android: ..., native: ..., default: ...})`,
7
+ `switch (Platform.OS) { case 'ios': ... }`, Node / Electron `process.platform === 'win32'`
8
+ Dart / Flutter `Platform.isIOS` / `isAndroid` / `isMacOS` / `isWindows` / `isLinux`, `kIsWeb`,
9
+ `defaultTargetPlatform == TargetPlatform.iOS`, `switch (defaultTargetPlatform)` statements
10
+ and expressions
11
+ Rust `if cfg!(windows) {...} else {...}`
12
+
13
+ Each region is (start_line, start_col, end_line, end_col, cond, text, line): `cond` is a cfg-style tree
14
+ (codegraph.plugins.native.gates.parse_cfg shape) over the atoms of codegraph/platforms.py, `text` the source
15
+ condition, `line` the line of the check. Comments are skipped; string contents are kept only where a check needs
16
+ them ('ios'). The scan is textual (no parser), so it reads only the shapes above and leaves anything else untagged.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import bisect
21
+ import re
22
+
23
+ # ------------------------------------------------------------------ platform tests -> atoms
24
+ _TS_OS = r"Platform\s*\.\s*OS"
25
+ _NODE_OS = r"(?:process\s*\.\s*platform|os\s*\.\s*platform\s*\(\s*\))"
26
+ NODE_PLATFORMS = {"win32": "windows", "darwin": "macos", "linux": "linux", "android": "android", "freebsd": "freebsd",
27
+ "openbsd": "openbsd", "sunos": "sunos", "aix": "aix", "cygwin": "windows"}
28
+ DART_IS = {"isAndroid": "android", "isIOS": "ios", "isMacOS": "macos", "isWindows": "windows", "isLinux": "linux",
29
+ "isFuchsia": "fuchsia"}
30
+ DART_TARGET = {"android": "android", "iOS": "ios", "macOS": "macos", "windows": "windows", "linux": "linux",
31
+ "fuchsia": "fuchsia"}
32
+
33
+ TRIGGERS = {
34
+ "ts": re.compile(r"Platform\s*\.\s*(?:OS|select)\b|process\s*\.\s*platform|os\s*\.\s*platform\s*\("),
35
+ "dart": re.compile(r"\bPlatform\s*\.\s*is[A-Z]|\bkIsWeb\b|TargetPlatform\s*\."),
36
+ "rust": re.compile(r"\bcfg!\s*\("),
37
+ }
38
+
39
+
40
+ def _cmp_atoms(lang: str):
41
+ """(regex, fn(match) -> atom tree) for one-token platform tests of a language."""
42
+ q = r"""(?:'([^'\\]*)'|"([^"\\]*)")"""
43
+ if lang == "ts":
44
+ def os_val(m, neg):
45
+ v = (m.group(2) or m.group(3) or "").lower()
46
+ a = ("atom", "platform", v)
47
+ return ("not", a) if neg else a
48
+
49
+ def node_val(m, neg):
50
+ v = m.group(2) or m.group(3) or ""
51
+ a = ("atom", "platform", NODE_PLATFORMS.get(v, v))
52
+ return ("not", a) if neg else a
53
+ return [
54
+ (re.compile(rf"{_TS_OS}\s*(===|==|!==|!=)\s*{q}"), lambda m: os_val(m, m.group(1).startswith("!"))),
55
+ (re.compile(rf"{q}\s*(===|==|!==|!=)\s*{_TS_OS}"),
56
+ lambda m: ("not", ("atom", "platform", (m.group(1) or m.group(2)).lower())) if m.group(3).startswith("!")
57
+ else ("atom", "platform", (m.group(1) or m.group(2)).lower())),
58
+ (re.compile(rf"{_NODE_OS}\s*(===|==|!==|!=)\s*{q}"), lambda m: node_val(m, m.group(1).startswith("!"))),
59
+ ]
60
+ if lang == "dart":
61
+ return [
62
+ (re.compile(r"\b(?:io\.|universal_io\.)?Platform\s*\.\s*(is[A-Z]\w*)"),
63
+ lambda m: ("atom", "platform", DART_IS[m.group(1)]) if m.group(1) in DART_IS else ("atom", "?", m.group(0))),
64
+ (re.compile(r"\bkIsWeb\b"), lambda m: ("atom", "platform", "web")),
65
+ (re.compile(r"(?:\bdefaultTargetPlatform|[\w.()]*\.platform)\s*(==|!=)\s*TargetPlatform\s*\.\s*(\w+)"),
66
+ lambda m: _neg(m.group(1) == "!=", ("atom", "target_platform", DART_TARGET.get(m.group(2), m.group(2).lower())))),
67
+ (re.compile(r"TargetPlatform\s*\.\s*(\w+)\s*(==|!=)\s*(?:\bdefaultTargetPlatform|[\w.()]*\.platform)\b"),
68
+ lambda m: _neg(m.group(2) == "!=", ("atom", "target_platform", DART_TARGET.get(m.group(1), m.group(1).lower())))),
69
+ ]
70
+ if lang == "rust":
71
+ return [(re.compile(r"\bcfg!\s*\(((?:[^()]|\([^()]*(?:\([^()]*\))*[^()]*\))*)\)"), lambda m: ("cfg", m.group(1)))]
72
+ return []
73
+
74
+
75
+ def _neg(neg: bool, a):
76
+ return ("not", a) if neg else a
77
+
78
+
79
+ # ------------------------------------------------------------------ masking
80
+ def mask(src: str, lang: str) -> str:
81
+ """Same-length copy with comments blanked and string contents replaced by '_' (quotes kept), so brackets and
82
+ operators inside them don't count. Newlines are kept."""
83
+ out = list(src)
84
+ i, n = 0, len(src)
85
+ quotes = ('"', "'", "`") if lang in ("ts", "dart") else ('"',)
86
+ while i < n:
87
+ c = src[i]
88
+ if c == "/" and i + 1 < n and src[i + 1] == "/":
89
+ j = src.find("\n", i)
90
+ j = n if j < 0 else j
91
+ for k in range(i, j):
92
+ out[k] = " "
93
+ i = j
94
+ continue
95
+ if c == "/" and i + 1 < n and src[i + 1] == "*":
96
+ j = src.find("*/", i + 2)
97
+ j = n if j < 0 else j + 2
98
+ for k in range(i, j):
99
+ if out[k] != "\n":
100
+ out[k] = " "
101
+ i = j
102
+ continue
103
+ if c in quotes:
104
+ triple = lang == "dart" and src.startswith(c * 3, i)
105
+ end_tok = c * 3 if triple else c
106
+ j = i + len(end_tok)
107
+ while j < n:
108
+ if src[j] == "\\":
109
+ j += 2
110
+ continue
111
+ if src.startswith(end_tok, j):
112
+ break
113
+ if src[j] == "\n" and c != "`" and not triple:
114
+ break
115
+ j += 1
116
+ for k in range(i + len(end_tok), min(j, n)):
117
+ if out[k] != "\n":
118
+ out[k] = "_"
119
+ i = j + len(end_tok)
120
+ continue
121
+ i += 1
122
+ return "".join(out)
123
+
124
+
125
+ class _Text:
126
+ def __init__(self, src: str, lang: str):
127
+ self.src, self.lang = src, lang
128
+ self.m = mask(src, lang)
129
+ self.nl = [i for i, c in enumerate(src) if c == "\n"]
130
+
131
+ def pos(self, off: int) -> tuple[int, int]:
132
+ """offset -> (1-based line, 0-based col)."""
133
+ k = bisect.bisect_left(self.nl, off)
134
+ start = self.nl[k - 1] + 1 if k else 0
135
+ return k + 1, off - start
136
+
137
+ def match_close(self, i: int) -> int:
138
+ """Index of the bracket closing the one at i (or len)."""
139
+ pairs = {"(": ")", "{": "}", "[": "]"}
140
+ stack = [pairs[self.m[i]]]
141
+ j = i + 1
142
+ while j < len(self.m) and stack:
143
+ c = self.m[j]
144
+ if c in pairs:
145
+ stack.append(pairs[c])
146
+ elif c in ")}]":
147
+ if c == stack[-1]:
148
+ stack.pop()
149
+ else:
150
+ return j
151
+ j += 1
152
+ return j - 1 if not stack else len(self.m)
153
+
154
+ def skip_ws(self, i: int) -> int:
155
+ while i < len(self.m) and self.m[i].isspace():
156
+ i += 1
157
+ return i
158
+
159
+
160
+ # ------------------------------------------------------------------ condition parsing
161
+ def parse_condition(text: str, lang: str):
162
+ """Boolean expression of platform tests -> cfg tree, or None when no platform test occurs in it. Other operands
163
+ become unknown atoms ('?'), so `Platform.OS === 'ios' && featureOn` is ios-only with an unknown part."""
164
+ atoms = []
165
+ s = text
166
+ for rx, fn in _cmp_atoms(lang):
167
+ def sub(m):
168
+ atoms.append(fn(m))
169
+ return f" \x00{len(atoms) - 1}\x00 "
170
+ s = rx.sub(sub, s)
171
+ if not atoms:
172
+ return None
173
+ toks = re.findall(r"\x00\d+\x00|&&|\|\||!(?!=)|\(|\)|[^\s()&|!\x00]+|[&|]", s)
174
+ pos = [0]
175
+
176
+ def peek():
177
+ return toks[pos[0]] if pos[0] < len(toks) else None
178
+
179
+ def take():
180
+ pos[0] += 1
181
+ return toks[pos[0] - 1]
182
+
183
+ def primary():
184
+ t = peek()
185
+ if t is None:
186
+ return ("atom", "?", None)
187
+ if t == "!":
188
+ take()
189
+ return ("not", primary())
190
+ if t == "(":
191
+ take()
192
+ v = lor()
193
+ if peek() == ")":
194
+ take()
195
+ return v
196
+ take()
197
+ if t.startswith("\x00"):
198
+ a = atoms[int(t.strip("\x00"))]
199
+ if a[0] == "cfg":
200
+ from .plugins.native.gates import parse_cfg
201
+ return parse_cfg(a[1])
202
+ return a
203
+ # any other operand (a call, a flag, a comparison): consume up to the next boolean operator
204
+ while peek() not in (None, "&&", "||", ")"):
205
+ take()
206
+ return ("atom", "?", t)
207
+
208
+ def land():
209
+ xs = [primary()]
210
+ while peek() == "&&":
211
+ take()
212
+ xs.append(primary())
213
+ return xs[0] if len(xs) == 1 else ("all", xs)
214
+
215
+ def lor():
216
+ xs = [land()]
217
+ while peek() == "||":
218
+ take()
219
+ xs.append(land())
220
+ return xs[0] if len(xs) == 1 else ("any", xs)
221
+
222
+ try:
223
+ return lor()
224
+ except Exception: # noqa: BLE001
225
+ return None
226
+
227
+
228
+ def _all(*xs):
229
+ xs = [x for x in xs if x is not None]
230
+ return xs[0] if len(xs) == 1 else ("all", list(xs))
231
+
232
+
233
+ # ------------------------------------------------------------------ region scan
234
+ def scan(src: str, lang: str) -> list[tuple]:
235
+ """Platform-conditional regions of one file (see the module docstring)."""
236
+ trig = TRIGGERS.get(lang)
237
+ if trig is None or not trig.search(src):
238
+ return []
239
+ T = _Text(src, lang)
240
+ out: list[tuple] = []
241
+
242
+ def region(a: int, z: int, cond, text: str, at: int):
243
+ if z <= a:
244
+ return
245
+ sl, sc = T.pos(a)
246
+ el, ec = T.pos(z)
247
+ out.append((sl, sc, el, ec, cond, " ".join(text.split())[:160], T.pos(at)[0]))
248
+
249
+ m = T.m
250
+ # if / else if / else chains
251
+ rx_if = re.compile(r"\bif\s*\(" if lang != "rust" else r"\bif\s+")
252
+ for hit in rx_if.finditer(m):
253
+ if lang != "rust":
254
+ o = hit.end() - 1
255
+ c = T.match_close(o)
256
+ ctext = src[o + 1:c]
257
+ body = T.skip_ws(c + 1)
258
+ else:
259
+ o = hit.end()
260
+ j = o
261
+ while j < len(m) and m[j] != "{":
262
+ if m[j] in "([":
263
+ j = T.match_close(j)
264
+ j += 1
265
+ ctext, body = src[o:j], j
266
+ cond = parse_condition(ctext, lang)
267
+ if cond is None or (cond[0] == "atom" and cond[1] == "?"):
268
+ continue
269
+ # not the `else if` of a chain already handled: that one is scanned from its head
270
+ before = m[max(0, hit.start() - 12):hit.start()].rstrip()
271
+ if before.endswith("else"):
272
+ continue
273
+ prev = []
274
+ cur_cond, cur_text = cond, ctext
275
+ start = hit.start()
276
+ while True:
277
+ end = _stmt_end(T, body, lang)
278
+ region(body, end, _all(*[("not", p) for p in prev], cur_cond) if prev else cur_cond,
279
+ cur_text if not prev else f"else if ({cur_text})", start)
280
+ prev.append(cur_cond)
281
+ k = T.skip_ws(end)
282
+ if not m.startswith("else", k) or (k + 4 < len(m) and (m[k + 4].isalnum() or m[k + 4] == "_")):
283
+ # guard clause `if (Platform.OS !== 'ios') return`: the rest of the enclosing block runs only where
284
+ # the condition is false
285
+ if len(prev) == 1 and _exits(m, body, end):
286
+ close = _enclosing_close(T, hit.start())
287
+ if close is not None:
288
+ region(end, close, ("not", cond), f"after if ({ctext}) return", start)
289
+ break
290
+ k = T.skip_ws(k + 4)
291
+ if m.startswith("if", k) and not (m[k + 2].isalnum() or m[k + 2] == "_"):
292
+ k2 = T.skip_ws(k + 2)
293
+ if lang != "rust" and k2 < len(m) and m[k2] == "(":
294
+ c2 = T.match_close(k2)
295
+ cur_text = src[k2 + 1:c2]
296
+ body = T.skip_ws(c2 + 1)
297
+ elif lang == "rust":
298
+ j = k2
299
+ while j < len(m) and m[j] != "{":
300
+ j += 1
301
+ cur_text, body = src[k2:j], j
302
+ else:
303
+ break
304
+ cur_cond = parse_condition(cur_text, lang) or ("atom", "?", cur_text.strip()[:40])
305
+ start = k
306
+ continue
307
+ region(k, _stmt_end(T, k, lang), _all(*[("not", p) for p in prev]), "else of " + " / ".join(
308
+ " ".join(x.split())[:60] for x in [ctext]), start)
309
+ break
310
+ # ternaries: COND ? a : b
311
+ if lang != "rust":
312
+ for q in re.finditer(r"\?(?![.?:\[)\],;=])", m):
313
+ i = q.start()
314
+ a = _cond_start(T, i)
315
+ ctext = src[a:i]
316
+ cond = parse_condition(ctext, lang)
317
+ if cond is None or (cond[0] == "atom" and cond[1] == "?"):
318
+ continue
319
+ colon = _ternary_colon(T, i + 1)
320
+ if colon is None:
321
+ continue
322
+ end = _ternary_end(T, colon + 1)
323
+ region(i + 1, colon, cond, ctext, a)
324
+ region(colon + 1, end, ("not", cond), "not " + ctext.strip(), a)
325
+ # Platform.select({ ios: ..., android: ..., default: ... })
326
+ if lang == "ts":
327
+ for s in re.finditer(r"Platform\s*\.\s*select\s*(?:<[^()]*>)?\s*\(\s*\{", m):
328
+ o = s.end() - 1
329
+ close = T.match_close(o)
330
+ props = _split_props(T, o + 1, close)
331
+ keys = [k for k, _, _ in props]
332
+ for k, a, z in props:
333
+ if k == "default":
334
+ cond = _all(*[("not", _rn_key(x)) for x in keys if x != "default"]) if len(keys) > 1 else None
335
+ else:
336
+ cond = _rn_key(k)
337
+ if cond is not None:
338
+ region(a, z, cond, f"Platform.select {k}", s.start())
339
+ # switch (platform) { case X: ... } / Dart switch expressions { X => ..., _ => ... }
340
+ if lang in ("ts", "dart"):
341
+ sw = re.compile(r"\bswitch\s*\(")
342
+ for s in sw.finditer(m):
343
+ o = s.end() - 1
344
+ c = T.match_close(o)
345
+ subj = src[o + 1:c]
346
+ kind = _switch_kind(subj, lang)
347
+ if not kind:
348
+ continue
349
+ b = T.skip_ws(c + 1)
350
+ if b >= len(m) or m[b] != "{":
351
+ continue
352
+ bend = T.match_close(b)
353
+ _switch_regions(T, b + 1, bend, kind, lang, region, s.start())
354
+ return out
355
+
356
+
357
+ def _rn_key(k: str):
358
+ k = k.strip("'\"")
359
+ if k == "native":
360
+ return ("atom", "native", None)
361
+ return ("atom", "platform", k.lower())
362
+
363
+
364
+ def _switch_kind(subject: str, lang: str) -> str | None:
365
+ if lang == "ts" and re.search(_TS_OS, subject):
366
+ return "rn"
367
+ if lang == "ts" and re.search(_NODE_OS, subject):
368
+ return "node"
369
+ if lang == "dart" and re.search(r"\bdefaultTargetPlatform\b|\.platform\b", subject):
370
+ return "target"
371
+ return None
372
+
373
+
374
+ def _case_atom(label: str, kind: str):
375
+ label = label.strip()
376
+ if kind == "target":
377
+ m = re.fullmatch(r"TargetPlatform\s*\.\s*(\w+)", label)
378
+ return ("atom", "target_platform", DART_TARGET.get(m.group(1), m.group(1).lower())) if m else None
379
+ m = re.fullmatch(r"""['"]([^'"]*)['"]""", label)
380
+ if not m:
381
+ return None
382
+ v = m.group(1)
383
+ return ("atom", "platform", NODE_PLATFORMS.get(v, v) if kind == "node" else v.lower())
384
+
385
+
386
+ def _switch_regions(T: _Text, a: int, z: int, kind: str, lang: str, region, at: int):
387
+ m, src = T.m, T.src
388
+ body = m[a:z]
389
+ # statement form: case X: (case Y:) ... ; default:
390
+ labels = [(x.start() + a, x.end() + a, x.group(1)) for x in
391
+ re.finditer(r"\b(?:case\s+((?:[^:]|::)+?)|default)\s*:(?!:)", body) if _depth(T, a, x.start() + a) == 0]
392
+ if labels:
393
+ groups, cur = [], None
394
+ seen = []
395
+ for i, (s, e, lab) in enumerate(labels):
396
+ nxt = labels[i + 1][0] if i + 1 < len(labels) else z
397
+ text_between = m[e:nxt].strip()
398
+ atom = None if lab is None else _case_atom(src[s:e].split("case", 1)[1].rstrip(":").strip(), kind)
399
+ if cur is None:
400
+ cur = {"atoms": [], "default": False, "start": s}
401
+ if lab is None:
402
+ cur["default"] = True
403
+ elif atom is not None:
404
+ cur["atoms"].append(atom)
405
+ else:
406
+ cur["unknown"] = True
407
+ if text_between:
408
+ cur["end"] = nxt
409
+ cur["body"] = e
410
+ groups.append(cur)
411
+ cur = None
412
+ for g in groups:
413
+ if g.get("unknown") and not g["atoms"]:
414
+ continue
415
+ if g["default"]:
416
+ cond = _all(*[("not", x) for x in seen]) if seen else None
417
+ else:
418
+ cond = g["atoms"][0] if len(g["atoms"]) == 1 else ("any", g["atoms"])
419
+ if cond is not None:
420
+ region(g["body"], g["end"], cond, src[g["start"]:g["body"]].strip(), at)
421
+ seen += g["atoms"]
422
+ return
423
+ # expression form (Dart 3): PATTERN => expr, ...
424
+ if lang != "dart":
425
+ return
426
+ props = _split_top(T, a, z)
427
+ seen = []
428
+ for s, e in props:
429
+ seg = m[s:e]
430
+ k = seg.find("=>")
431
+ if k < 0:
432
+ continue
433
+ pat = src[s:s + k].strip()
434
+ if pat == "_" or pat.startswith("_ "):
435
+ cond = _all(*[("not", x) for x in seen]) if seen else None
436
+ atoms = []
437
+ else:
438
+ atoms = [x for x in (_case_atom(p, kind) for p in pat.split("||")) if x is not None]
439
+ if not atoms:
440
+ continue
441
+ cond = atoms[0] if len(atoms) == 1 else ("any", atoms)
442
+ if cond is not None:
443
+ region(s + k + 2, e, cond, pat + " =>", at)
444
+ seen += atoms
445
+
446
+
447
+ def _depth(T: _Text, a: int, i: int) -> int:
448
+ d = 0
449
+ for c in T.m[a:i]:
450
+ if c in "([{":
451
+ d += 1
452
+ elif c in ")]}":
453
+ d -= 1
454
+ return d
455
+
456
+
457
+ def _split_top(T: _Text, a: int, z: int) -> list[tuple[int, int]]:
458
+ out, d, s = [], 0, a
459
+ for i in range(a, z):
460
+ c = T.m[i]
461
+ if c in "([{":
462
+ d += 1
463
+ elif c in ")]}":
464
+ d -= 1
465
+ elif c == "," and d == 0:
466
+ out.append((s, i))
467
+ s = i + 1
468
+ if T.m[s:z].strip():
469
+ out.append((s, z))
470
+ return out
471
+
472
+
473
+ def _split_props(T: _Text, a: int, z: int) -> list[tuple[str, int, int]]:
474
+ """Object literal properties `key: value` -> (key, start of key, end of value)."""
475
+ out = []
476
+ for s, e in _split_top(T, a, z):
477
+ seg = T.src[s:e]
478
+ mm = re.match(r"""\s*(['"]?)(\w+)\1\s*:""", seg)
479
+ if mm:
480
+ out.append((mm.group(2), s + mm.start(2), e))
481
+ return out
482
+
483
+
484
+ def _stmt_end(T: _Text, i: int, lang: str) -> int:
485
+ """End offset (exclusive) of the block or single statement starting at i."""
486
+ m = T.m
487
+ if i < len(m) and m[i] == "{":
488
+ return T.match_close(i) + 1
489
+ d, j = 0, i
490
+ while j < len(m):
491
+ c = m[j]
492
+ if c in "([{":
493
+ d += 1
494
+ elif c in ")]}":
495
+ if d == 0:
496
+ return j
497
+ d -= 1
498
+ elif c == ";" and d == 0:
499
+ return j + 1
500
+ elif c == "\n" and d == 0 and lang == "ts" and _asi_break(m, i, j):
501
+ return j
502
+ j += 1
503
+ return j
504
+
505
+
506
+ _EXIT_RX = re.compile(r"\{?\s*(?:return|throw|continue|break)\b[^{};]*;?\s*\}?\s*$")
507
+
508
+
509
+ def _exits(m: str, a: int, z: int) -> bool:
510
+ """The body [a, z) is a single return / throw / continue / break (optionally in braces)."""
511
+ return bool(_EXIT_RX.fullmatch(m[a:z].strip()))
512
+
513
+
514
+ def _enclosing_close(T: _Text, i: int) -> int | None:
515
+ """Offset of the `}` closing the innermost block around offset i (None at top level)."""
516
+ m, d = T.m, 0
517
+ for j in range(i - 1, -1, -1):
518
+ c = m[j]
519
+ if c in ")]}":
520
+ d += 1
521
+ elif c in "([{":
522
+ if d == 0:
523
+ return T.match_close(j) if c == "{" else None
524
+ d -= 1
525
+ return None
526
+
527
+
528
+ _CONT_END = set("=+-*/%&|^<>?:,.!~({[")
529
+ _CONT_START = set(".?:+-*/%&|^,=)]}")
530
+
531
+
532
+ def _asi_break(m: str, i: int, j: int) -> bool:
533
+ """A JS / TS statement without a semicolon ends at this newline (automatic semicolon insertion): `if (x) return`
534
+ on its own line does not swallow the next statement."""
535
+ k = j - 1
536
+ while k >= i and m[k] in " \t\r":
537
+ k -= 1
538
+ if k < i:
539
+ return False
540
+ n = j + 1
541
+ while n < len(m) and m[n] in " \t\r\n":
542
+ n += 1
543
+ if n >= len(m):
544
+ return False
545
+ word = re.search(r"(\w+)$", m[i:k + 1])
546
+ if word and word.group(1) in ("return", "break", "continue", "throw") and m[i:k + 1].strip() == word.group(1):
547
+ return True # restricted productions: `return` + newline always ends the statement
548
+ return m[k] not in _CONT_END and m[n] not in _CONT_START
549
+
550
+
551
+ _STOP_WORDS = ("return", "await", "yield", "final", "var", "const", "let")
552
+
553
+
554
+ def _cond_start(T: _Text, i: int) -> int:
555
+ """Start offset of the ternary condition that ends at i (the '?')."""
556
+ m = T.m
557
+ j = i - 1
558
+ while j >= 0:
559
+ c = m[j]
560
+ if c in ")]}":
561
+ # skip a balanced group backwards
562
+ pairs = {")": "(", "]": "[", "}": "{"}
563
+ d, k = 0, j
564
+ while k >= 0:
565
+ if m[k] in ")]}":
566
+ d += 1
567
+ elif m[k] in "([{":
568
+ d -= 1
569
+ if d == 0:
570
+ break
571
+ k -= 1
572
+ if c == "}":
573
+ return j + 1
574
+ j = k - 1
575
+ continue
576
+ if c in "([{,;?:":
577
+ return j + 1
578
+ if c == "=" and (j == 0 or m[j - 1] not in "=!<>") and (j + 1 >= len(m) or m[j + 1] not in "="):
579
+ return j + 1
580
+ if c == ">" and j > 0 and m[j - 1] == "=":
581
+ return j + 1
582
+ if c == "\n" and m[j + 1:i].strip() == "":
583
+ pass
584
+ if c.isalpha():
585
+ k = j
586
+ while k >= 0 and (m[k].isalnum() or m[k] == "_"):
587
+ k -= 1
588
+ if m[k + 1:j + 1] in _STOP_WORDS:
589
+ return j + 1
590
+ j = k
591
+ continue
592
+ j -= 1
593
+ return 0
594
+
595
+
596
+ def _ternary_colon(T: _Text, i: int) -> int | None:
597
+ m = T.m
598
+ d, pend = 0, 0
599
+ j = i
600
+ while j < len(m):
601
+ c = m[j]
602
+ if c in "([{":
603
+ d += 1
604
+ elif c in ")]}":
605
+ if d == 0:
606
+ return None
607
+ d -= 1
608
+ elif d == 0:
609
+ if c == "?" and not (j + 1 < len(m) and m[j + 1] in ".?:[") and not (j > 0 and m[j - 1] == "?"):
610
+ pend += 1
611
+ elif c == ":":
612
+ if pend == 0:
613
+ return j
614
+ pend -= 1
615
+ elif c in ";,":
616
+ return None
617
+ j += 1
618
+ return None
619
+
620
+
621
+ def _ternary_end(T: _Text, i: int) -> int:
622
+ m = T.m
623
+ d, pend = 0, 0
624
+ j = i
625
+ while j < len(m):
626
+ c = m[j]
627
+ if c in "([{":
628
+ d += 1
629
+ elif c in ")]}":
630
+ if d == 0:
631
+ return j
632
+ d -= 1
633
+ elif d == 0:
634
+ if c == "?" and not (j + 1 < len(m) and m[j + 1] in ".?:[") and not (j > 0 and m[j - 1] == "?"):
635
+ pend += 1
636
+ elif c == ":":
637
+ if pend == 0:
638
+ return j
639
+ pend -= 1
640
+ elif c in ";,":
641
+ return j
642
+ j += 1
643
+ return j