cg-code-graph 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,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
|