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
codegraph/bridges.py
ADDED
|
@@ -0,0 +1,1701 @@
|
|
|
1
|
+
"""Web / native bridges: Capacitor plugins, React Native (and Expo) native modules, Flutter platform channels, and the
|
|
2
|
+
process boundaries of desktop apps: Electron IPC / context bridge, Tauri commands.
|
|
3
|
+
|
|
4
|
+
A bridge call is local message passing between the JS / Dart side of an app and its Kotlin / Java / Swift / ObjC side.
|
|
5
|
+
It is modelled with the protocol endpoint model of #31 (epic #29), so the bridge path is
|
|
6
|
+
|
|
7
|
+
JS / Dart call site -SENDS_TO-> endpoint:<protocol>:<namespace>#<method> -RECEIVED_BY-> native method
|
|
8
|
+
|
|
9
|
+
protocol capacitor | react-native (NativeModules, TurboModules, Expo Modules: attrs.api = expo-modules)
|
|
10
|
+
| flutter (MethodChannel) | flutter-event (EventChannel, no method part: endpoint:flutter-event:<channel>)
|
|
11
|
+
| pigeon (@HostApi / @FlutterApi classes of the Pigeon definition files)
|
|
12
|
+
namespace Capacitor plugin name, React Native module name, Flutter channel name, Pigeon API class
|
|
13
|
+
direction to_native (app code sends, native receives) or to_app: a native `invokeMethod` received by a Dart
|
|
14
|
+
`setMethodCallHandler`, a native @FlutterApi call received by the Dart class implementing it
|
|
15
|
+
RECEIVED_BY one edge per native implementation, in the native file: the platform conventions of platforms.py tag it
|
|
16
|
+
(android/ ios/ macos/ directories, else Kotlin / Java = android, Swift / ObjC = ios), so `--platform ios`
|
|
17
|
+
drops the Android receivers and `impact` / `reaches` cross the bridge in both directions
|
|
18
|
+
|
|
19
|
+
The send side comes from the language plugins (TypeScript extractor facts, codegraph/plugins/dart/bridges.py); the
|
|
20
|
+
receive side is a source-text pass over the native files run here, after the language plugins (Java and ObjC have no
|
|
21
|
+
language plugin: their receivers become stub method nodes with attrs.bridge_stub). `finalize` then records per
|
|
22
|
+
endpoint which platforms receive it and the checks: missing_on (sent, received on some mobile targets of the project
|
|
23
|
+
but not on these), no_receiver (the namespace is implemented in this repo, the method is not), no_sender, external
|
|
24
|
+
(nothing in this repo implements the namespace: an npm / pub plugin package).
|
|
25
|
+
|
|
26
|
+
Process protocols (one app, two processes; receivers carry attrs.process instead of a platform, no missing_on):
|
|
27
|
+
electron-ipc endpoint:electron-ipc:<channel>; ipcRenderer.invoke / send / sendSync / postMessage and
|
|
28
|
+
webContents.send -> ipcMain.handle / handleOnce / on / once and ipcRenderer.on / once handlers
|
|
29
|
+
electron-preload endpoint:electron-preload:<key>#<member>; window.<key>.<member>() in the renderer ->
|
|
30
|
+
contextBridge.exposeInMainWorld('<key>', {member: ...}) in the preload script
|
|
31
|
+
tauri endpoint:tauri:<command> (plugin commands: endpoint:tauri:plugin:<name>|<command>, external
|
|
32
|
+
tauri-plugin-<name> unless the plugin crate is in the repo); invoke('cmd') from @tauri-apps/api ->
|
|
33
|
+
#[tauri::command] fn in the Rust core; check unregistered: not listed in generate_handler!
|
|
34
|
+
no_receiver for these: the other side is in the repo (some channel / command is received) but not this one.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import bisect
|
|
39
|
+
import fnmatch
|
|
40
|
+
import json
|
|
41
|
+
import os
|
|
42
|
+
import re
|
|
43
|
+
from collections import defaultdict
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
PROTOCOLS = {
|
|
47
|
+
"capacitor": "Capacitor plugin method (registerPlugin / Plugins.X -> @CapacitorPlugin / CAPPlugin)",
|
|
48
|
+
"react-native": "React Native native module method (NativeModules / TurboModuleRegistry / requireNativeModule -> "
|
|
49
|
+
"@ReactMethod / RCT_EXPORT_METHOD / Expo Function)",
|
|
50
|
+
"flutter": "Flutter MethodChannel method (invokeMethod -> setMethodCallHandler)",
|
|
51
|
+
"flutter-event": "Flutter EventChannel stream (receiveBroadcastStream -> setStreamHandler)",
|
|
52
|
+
"pigeon": "Pigeon API method (@HostApi: Dart call -> Kotlin / Swift / Java implementation of the generated "
|
|
53
|
+
"interface; @FlutterApi: native call -> Dart class extending the generated API)",
|
|
54
|
+
"electron-ipc": "Electron IPC channel (ipcRenderer.invoke / send -> ipcMain.handle / on; webContents.send -> "
|
|
55
|
+
"ipcRenderer.on)",
|
|
56
|
+
"electron-preload": "Electron context bridge member (window.<key>.<member>() -> contextBridge.exposeInMainWorld)",
|
|
57
|
+
"tauri": "Tauri command (invoke('cmd') -> #[tauri::command] fn cmd registered in generate_handler!)",
|
|
58
|
+
"react-native-event": "React Native event (native sendEvent / RCTDeviceEventEmitter.emit / sendEventWithName / Expo "
|
|
59
|
+
"sendEvent -> NativeEventEmitter / DeviceEventEmitter addListener in JS)",
|
|
60
|
+
"capacitor-event": "Capacitor plugin event (native notifyListeners('evt') -> Plugin.addListener('evt') in JS)",
|
|
61
|
+
"cordova": "Cordova plugin action (cordova.exec(ok, fail, 'Service', 'action') -> CordovaPlugin.execute / CDVPlugin "
|
|
62
|
+
"method; services from plugin.xml / config.xml <feature>)",
|
|
63
|
+
}
|
|
64
|
+
# native -> app events: the JS listener is the receiver; no_sender only where the repo sends some event natively
|
|
65
|
+
EVENT_PROTOCOLS = ("react-native-event", "capacitor-event")
|
|
66
|
+
# protocols between processes of one app (no per-platform receivers)
|
|
67
|
+
PROCESS_PROTOCOLS = ("electron-ipc", "electron-preload", "tauri")
|
|
68
|
+
TRANSPORT = {"electron-ipc": "ipc", "tauri": "ipc", "electron-preload": "local"}
|
|
69
|
+
MOBILE = ("android", "ios", "macos")
|
|
70
|
+
APP_LANGS = ("dart", "ts") # the app side of a bridge (receivers there: a native -> app call)
|
|
71
|
+
# Capacitor Plugin / CAPPlugin base-class methods: handled by the bridge on every platform unless a plugin overrides them
|
|
72
|
+
CAP_BASE_METHODS = {"checkPermissions", "requestPermissions", "addListener", "removeAllListeners", "removeListener"}
|
|
73
|
+
# NativeEventEmitter plumbing (RCTEventEmitter implements it on iOS) and the TurboModule constants getter
|
|
74
|
+
RN_BASE_METHODS = {"addListener", "removeListeners", "getConstants"}
|
|
75
|
+
BASE_METHODS = {"capacitor": CAP_BASE_METHODS, "react-native": RN_BASE_METHODS}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _flutter_folders(root: Path, rel: str | None, cache: dict) -> set | None:
|
|
79
|
+
"""Platform folders (android/, ios/, macos/) next to the nearest pubspec.yaml above a Dart file."""
|
|
80
|
+
if not rel:
|
|
81
|
+
return None
|
|
82
|
+
d = Path(rel).parent
|
|
83
|
+
while True:
|
|
84
|
+
key = ("pubspec", str(d))
|
|
85
|
+
if key not in cache:
|
|
86
|
+
cache[key] = ({p for p in ("android", "ios", "macos") if (root / d / p).is_dir()}
|
|
87
|
+
if (root / d / "pubspec.yaml").is_file() else None)
|
|
88
|
+
if cache[key] is not None:
|
|
89
|
+
return cache[key]
|
|
90
|
+
if d == d.parent or str(d) in ("", "."):
|
|
91
|
+
return None
|
|
92
|
+
d = d.parent
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _codegen_platforms(root: Path, rel: str, cache: dict) -> list[str] | None:
|
|
96
|
+
"""platforms of the nearest package.json's React Native codegenConfig (a module declared for android only)."""
|
|
97
|
+
d = Path(rel).parent
|
|
98
|
+
while True:
|
|
99
|
+
key = str(d)
|
|
100
|
+
if key not in cache:
|
|
101
|
+
cache[key] = None
|
|
102
|
+
pj = root / d / "package.json"
|
|
103
|
+
if pj.is_file():
|
|
104
|
+
try:
|
|
105
|
+
cfg = json.loads(pj.read_text(encoding="utf-8", errors="replace")).get("codegenConfig") or {}
|
|
106
|
+
plats = cfg.get("platforms") if isinstance(cfg, dict) else None
|
|
107
|
+
cache[key] = [p for p in plats if isinstance(p, str)] if isinstance(plats, list) else []
|
|
108
|
+
except (OSError, ValueError, AttributeError):
|
|
109
|
+
cache[key] = []
|
|
110
|
+
if cache[key] is not None:
|
|
111
|
+
return cache[key] or None
|
|
112
|
+
if d == d.parent or str(d) in ("", "."):
|
|
113
|
+
return None
|
|
114
|
+
d = d.parent
|
|
115
|
+
NATIVE_EXTS = (".kt", ".java", ".swift", ".m", ".mm")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------ protocol helpers (the #31 builder helper shape)
|
|
119
|
+
def endpoint_key(protocol: str, namespace: str, method: str | None = None) -> str:
|
|
120
|
+
"""Key of endpoint:<protocol>:<name>, name = <namespace>#<method> (or the namespace alone for streams)."""
|
|
121
|
+
return f"{protocol}:{namespace}#{method}" if method else f"{protocol}:{namespace}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _endpoint(builder, protocol: str, namespace: str, method: str | None) -> str:
|
|
125
|
+
key = endpoint_key(protocol, namespace, method)
|
|
126
|
+
name = key.split(":", 1)[1]
|
|
127
|
+
return builder.add_node("endpoint", key, name, fqn=name,
|
|
128
|
+
attrs={"protocol": protocol, "transport": TRANSPORT.get(protocol, "local"), "namespace": namespace,
|
|
129
|
+
"method": method})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def protocol_send(builder, protocol: str, namespace: str, method: str | None, src: str, file: str | None,
|
|
133
|
+
line: int | None, confidence: str, test: bool = False, **attrs) -> str:
|
|
134
|
+
"""Code `src` sends to an endpoint (SENDS_TO; test code: TEST_CALLS with orig SENDS_TO)."""
|
|
135
|
+
nid = _endpoint(builder, protocol, namespace, method)
|
|
136
|
+
n = builder.nodes[nid]
|
|
137
|
+
if test:
|
|
138
|
+
n.attrs.setdefault("test_only", True)
|
|
139
|
+
else:
|
|
140
|
+
n.attrs["test_only"] = False
|
|
141
|
+
a = {k: v for k, v in attrs.items() if v not in (None, [], "")}
|
|
142
|
+
if test:
|
|
143
|
+
builder.add_edge(src, nid, "TEST_CALLS", file=file, line=line, confidence=confidence, orig="SENDS_TO", **a)
|
|
144
|
+
else:
|
|
145
|
+
builder.add_edge(src, nid, "SENDS_TO", file=file, line=line, confidence=confidence, role="invoke", **a)
|
|
146
|
+
return nid
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def protocol_receive(builder, protocol: str, namespace: str, method: str | None, handler: str, file: str | None,
|
|
150
|
+
line: int | None, confidence: str, **attrs) -> str:
|
|
151
|
+
"""An endpoint is handled by `handler` (RECEIVED_BY, endpoint -> handler)."""
|
|
152
|
+
nid = _endpoint(builder, protocol, namespace, method)
|
|
153
|
+
builder.add_edge(nid, handler, "RECEIVED_BY", file=file, line=line, confidence=confidence,
|
|
154
|
+
**{k: v for k, v in attrs.items() if v not in (None, [], "")})
|
|
155
|
+
return nid
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------ source text helpers
|
|
159
|
+
_TOKEN = re.compile(r'"(?:\\.|[^"\\\n])*"|//[^\n]*|/\*.*?\*/', re.S)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def strip_comments(src: str) -> str:
|
|
163
|
+
"""Comments blanked (newlines kept, so offsets and line numbers stay), string literals kept."""
|
|
164
|
+
def rep(m):
|
|
165
|
+
t = m.group(0)
|
|
166
|
+
if t.startswith('"'):
|
|
167
|
+
return t
|
|
168
|
+
return re.sub(r"[^\n]", " ", t)
|
|
169
|
+
return _TOKEN.sub(rep, src)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _blank_strings(src: str) -> str:
|
|
173
|
+
return re.sub(r'"(?:\\.|[^"\\\n])*"', lambda m: '"' + " " * (len(m.group(0)) - 2) + '"', src)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Text:
|
|
177
|
+
def __init__(self, src: str):
|
|
178
|
+
self.src = strip_comments(src)
|
|
179
|
+
self.code = _blank_strings(self.src) # for brace matching: braces inside strings do not count
|
|
180
|
+
self.nl = [i for i, c in enumerate(self.src) if c == "\n"]
|
|
181
|
+
|
|
182
|
+
def line(self, pos: int) -> int:
|
|
183
|
+
return bisect.bisect_right(self.nl, pos - 1) + 1
|
|
184
|
+
|
|
185
|
+
def match(self, pos: int, open_: str = "{", close: str = "}") -> int:
|
|
186
|
+
"""Offset of the bracket closing the one at pos (or the first `open_` at / after pos); len(src) if unbalanced."""
|
|
187
|
+
i = self.code.find(open_, pos)
|
|
188
|
+
if i < 0:
|
|
189
|
+
return len(self.code)
|
|
190
|
+
depth = 0
|
|
191
|
+
for j in range(i, len(self.code)):
|
|
192
|
+
c = self.code[j]
|
|
193
|
+
if c == open_:
|
|
194
|
+
depth += 1
|
|
195
|
+
elif c == close:
|
|
196
|
+
depth -= 1
|
|
197
|
+
if depth == 0:
|
|
198
|
+
return j
|
|
199
|
+
return len(self.code)
|
|
200
|
+
|
|
201
|
+
def args(self, open_pos: int) -> list[tuple[int, str]]:
|
|
202
|
+
"""Top-level comma-separated arguments of the call whose `(` is at open_pos: [(offset, text)]."""
|
|
203
|
+
end = self.match(open_pos, "(", ")")
|
|
204
|
+
out, depth, start = [], 0, open_pos + 1
|
|
205
|
+
for j in range(open_pos + 1, end):
|
|
206
|
+
c = self.code[j]
|
|
207
|
+
if c in "([{":
|
|
208
|
+
depth += 1
|
|
209
|
+
elif c in ")]}":
|
|
210
|
+
depth -= 1
|
|
211
|
+
elif c == "," and depth == 0:
|
|
212
|
+
out.append((start, self.src[start:j].strip()))
|
|
213
|
+
start = j + 1
|
|
214
|
+
tail = self.src[start:end].strip()
|
|
215
|
+
if tail:
|
|
216
|
+
out.append((start, tail))
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
CLASS_RE = re.compile(r"\b(?:class|object|struct|actor)\s+(\w+)\s*(?:<[^{;()]*?>)?\s*"
|
|
221
|
+
r"(?:\((?:[^()]|\([^()]*\))*\))?[^{;=]*\{") # a primary constructor may hold `= default`
|
|
222
|
+
OBJC_IMPL_RE = re.compile(r"@implementation\s+(\w+)")
|
|
223
|
+
CONST_RE = re.compile(r"\b(?:val|var|let|const\s+val|static\s+let|static\s+var|(?:public\s+|private\s+|protected\s+)?"
|
|
224
|
+
r"(?:static\s+)?final\s+String|String)\s+(\w+)\s*(?::\s*String\??)?\s*=\s*\"([^\"\\\n]*)\"")
|
|
225
|
+
OBJC_CONST_RE = re.compile(r"NSString\s*\*\s*(?:const\s+)?(\w+)\s*=\s*@\"([^\"\n]*)\"|#define\s+(\w+)\s+@\"([^\"\n]*)\"")
|
|
226
|
+
PACKAGE_RE = re.compile(r"^\s*package\s+([\w.]+)", re.M)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class NativeFile:
|
|
230
|
+
def __init__(self, rel: str, src: str):
|
|
231
|
+
self.rel, self.t = rel, Text(src)
|
|
232
|
+
self.ext = os.path.splitext(rel)[1]
|
|
233
|
+
self.lang = {".kt": "kotlin", ".java": "java", ".swift": "swift", ".m": "objc", ".mm": "objc"}[self.ext]
|
|
234
|
+
m = PACKAGE_RE.search(self.t.src)
|
|
235
|
+
self.package = m.group(1) if m and self.lang in ("kotlin", "java") else None
|
|
236
|
+
self.classes: list[tuple[str, int, int]] = [] # (name, start, end) offsets
|
|
237
|
+
for m in CLASS_RE.finditer(self.t.code):
|
|
238
|
+
self.classes.append((m.group(1), m.start(), self.t.match(m.end() - 1)))
|
|
239
|
+
if self.lang == "objc":
|
|
240
|
+
for m in OBJC_IMPL_RE.finditer(self.t.code):
|
|
241
|
+
e = self.t.code.find("@end", m.end())
|
|
242
|
+
self.classes.append((m.group(1), m.start(), e if e >= 0 else len(self.t.code)))
|
|
243
|
+
self.consts = {}
|
|
244
|
+
for m in CONST_RE.finditer(self.t.src):
|
|
245
|
+
self.consts.setdefault(m.group(1), m.group(2))
|
|
246
|
+
for m in OBJC_CONST_RE.finditer(self.t.src):
|
|
247
|
+
if m.group(1):
|
|
248
|
+
self.consts.setdefault(m.group(1), m.group(2))
|
|
249
|
+
else:
|
|
250
|
+
self.consts.setdefault(m.group(3), m.group(4))
|
|
251
|
+
|
|
252
|
+
def class_at(self, pos: int) -> tuple[str, int, int] | None:
|
|
253
|
+
best = None
|
|
254
|
+
for c in self.classes:
|
|
255
|
+
if c[1] <= pos <= c[2] and (best is None or c[1] > best[1]):
|
|
256
|
+
best = c
|
|
257
|
+
return best
|
|
258
|
+
|
|
259
|
+
def class_named(self, name: str):
|
|
260
|
+
return next((c for c in self.classes if c[0] == name), None)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
TRIGGER = re.compile(r"CapacitorPlugin|CAPPlugin|CAP_PLUGIN|ReactMethod|RCT_(?:EXPORT|EXTERN|REMAP)|ReactContextBaseJavaModule|"
|
|
264
|
+
r"BaseJavaModule|Native\w+Spec\b|ModuleDefinition|MethodChannel|EventChannel|MethodCallHandler|"
|
|
265
|
+
r"StreamHandler|isEqualToString:\s*call\.method|call\.method|@objc\s*\(|"
|
|
266
|
+
r"RCTEventEmitter|RCTDeviceEventEmitter|DeviceEventManagerModule|sendEventWithName|sendEvent\s*\(|emitDeviceEvent|"
|
|
267
|
+
r"notifyListeners|CordovaPlugin|CDVPlugin|CDVInvokedUrlCommand")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------ receivers found in native source text
|
|
271
|
+
class Receiver:
|
|
272
|
+
__slots__ = ("protocol", "namespace", "method", "file", "pos", "fn", "cls", "conf", "via", "api", "lookup")
|
|
273
|
+
|
|
274
|
+
def __init__(self, protocol, namespace, method, nf: NativeFile, pos, fn=None, cls=None, conf="resolved", via=None,
|
|
275
|
+
api=None, lookup=None):
|
|
276
|
+
self.protocol, self.namespace, self.method = protocol, namespace, method
|
|
277
|
+
self.file, self.pos, self.fn, self.cls, self.conf, self.via, self.api = nf, pos, fn, cls, conf, via, api
|
|
278
|
+
self.lookup = lookup # (swift / objc class name, method name): implemented in another file
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
ANN_TAIL = r"\s*(?:@\w+(?:\.\w+)*(?:\([^)]*\))?\s*)*(?:(?:public|private|protected|internal|static|final|override|open|" \
|
|
282
|
+
r"suspend|synchronized|native)\s+)*(?:fun\s+(?:<[^>]*>\s*)?(\w+)|[\w<>\[\],.?]+(?:\s*<[^>]*>)?\s+(\w+))\s*\("
|
|
283
|
+
CAP_PLUGIN_ANN = re.compile(r"@CapacitorPlugin\b\s*(\()?")
|
|
284
|
+
PLUGIN_METHOD = re.compile(r"@PluginMethod\b(?:\s*\([^)]*\))?" + ANN_TAIL)
|
|
285
|
+
REACT_METHOD = re.compile(r"@ReactMethod\b(?:\s*\([^)]*\))?" + ANN_TAIL)
|
|
286
|
+
JAVA_CLASS = re.compile(r"\bclass\s+(\w+)\s*(?:\([^)]*\))?\s*(?::|extends)\s*([^{]*)\{")
|
|
287
|
+
GET_NAME = re.compile(r"\bgetName\s*\(\s*\)\s*(?::\s*String\s*)?(?:=\s*|\{\s*return\s+)(\"([^\"]*)\"|([\w.]+))")
|
|
288
|
+
OVERRIDE_FUN = re.compile(r"(?:\boverride\s+fun\s+(\w+)\s*\(|@Override\s+(?:public\s+)?(?:synchronized\s+)?[\w<>\[\],.?]+\s+(\w+)\s*\()")
|
|
289
|
+
SPEC_SKIP = {"getName", "getConstants", "getTypedExportedConstants", "initialize", "invalidate", "onCatalystInstanceDestroy",
|
|
290
|
+
"canOverrideExistingModule", "hasConstants", "getExportedConstants", "toString", "equals", "hashCode",
|
|
291
|
+
"onHostResume", "onHostPause", "onHostDestroy", "onActivityResult", "onNewIntent", "createNativeModules",
|
|
292
|
+
"createViewManagers", "getReactModuleInfoProvider", "getModule"}
|
|
293
|
+
SWIFT_CLASS = re.compile(r"(?:@objc\s*\(\s*(\w+)\s*\)\s*)?(?:(?:public|open|final|internal|private)\s+)*class\s+(\w+)\s*:\s*([^{]*)\{")
|
|
294
|
+
CAP_METHOD_NAME = re.compile(r"CAPPluginMethod\s*\(\s*name:\s*\"(\w+)\"|CAPPluginMethod\s*\(\s*\"(\w+)\"")
|
|
295
|
+
JS_NAME = re.compile(r"\bjsName\s*=\s*\"([^\"]+)\"")
|
|
296
|
+
OBJC_FUNC = re.compile(r"@objc(?:\s*\([^)]*\))?\s+(?:(?:public|open|internal|private|final|override|dynamic)\s+)*func\s+(\w+)\s*\(")
|
|
297
|
+
SWIFT_FUNC = re.compile(r"\bfunc\s+(\w+)\s*(?:<[^>]*>)?\s*\(")
|
|
298
|
+
RCT_MODULE = re.compile(r"\bRCT_EXPORT_MODULE\s*\(\s*(\w*)\s*\)")
|
|
299
|
+
RCT_EXPORT = re.compile(r"\bRCT_(?:EXPORT_METHOD|EXPORT_BLOCKING_SYNCHRONOUS_METHOD)\s*\(\s*(\w+)|"
|
|
300
|
+
r"\bRCT_(?:REMAP_METHOD|REMAP_BLOCKING_SYNCHRONOUS_METHOD)\s*\(\s*(\w+)\s*,")
|
|
301
|
+
RCT_EXTERN_MODULE = re.compile(r"\bRCT_EXTERN_MODULE\s*\(\s*(\w+)\s*,|\bRCT_EXTERN_REMAP_MODULE\s*\(\s*(\w*)\s*,\s*(\w+)\s*,")
|
|
302
|
+
RCT_EXTERN_METHOD = re.compile(r"\bRCT_EXTERN_(?:_BLOCKING_SYNCHRONOUS_)?METHOD\s*\(\s*(\w+)")
|
|
303
|
+
EXPO_NAME = re.compile(r"\bName\s*\(\s*\"([^\"]+)\"\s*\)")
|
|
304
|
+
EXPO_FN = re.compile(r"\b(?:AsyncFunction|Function)\s*\(\s*\"(\w+)\"")
|
|
305
|
+
CHANNEL_NEW = re.compile(r"(?<![\w.])(?:new\s+)?(?:io\.flutter\.plugin\.common\.)?(Method|Event|OptionalMethod)Channel\s*\(")
|
|
306
|
+
SWIFT_CHANNEL_NEW = re.compile(r"\bFlutter(Method|Event)Channel\s*\(\s*name:\s*")
|
|
307
|
+
OBJC_CHANNEL_NEW = re.compile(r"\[\s*Flutter(Method|Event)Channel\s+(?:methodChannel|eventChannel)WithName:\s*(@\"[^\"]*\"|\w+)")
|
|
308
|
+
METHOD_EQ = re.compile(r"\b\w+\.method\s*==\s*\"([^\"]+)\"|\"([^\"]+)\"\s*==\s*\w+\.method\b|"
|
|
309
|
+
r"\"([^\"]+)\"\.equals\s*\(\s*\w+\.method\s*\)|\b\w+\.method\.equals\s*\(\s*\"([^\"]+)\"\s*\)|"
|
|
310
|
+
r"\[\s*@\"([^\"]+)\"\s+isEqualToString:\s*\w+\.method\s*\]|\[\s*\w+\.method\s+isEqualToString:\s*@\"([^\"]+)\"\s*\]")
|
|
311
|
+
SWITCH_ON = re.compile(r"\b(?:when|switch)\s*\(?\s*\w+\.method\s*\)?\s*\{")
|
|
312
|
+
CASE_STR = re.compile(r"(?:\bcase\s+((?:\"[^\"]+\"\s*,\s*)*\"[^\"]+\")\s*:|^\s*((?:\"[^\"]+\"\s*,\s*)*\"[^\"]+\")\s*->)", re.M)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _str_value(nf: NativeFile, expr: str, consts: dict) -> str | None:
|
|
316
|
+
e = expr.strip()
|
|
317
|
+
if e.startswith("name:"):
|
|
318
|
+
e = e[5:].strip()
|
|
319
|
+
m = re.fullmatch(r'@?"([^"\\\n]*)"', e)
|
|
320
|
+
if m:
|
|
321
|
+
return m.group(1)
|
|
322
|
+
if re.fullmatch(r"[\w.]+", e):
|
|
323
|
+
last = e.split(".")[-1]
|
|
324
|
+
if last in nf.consts:
|
|
325
|
+
return nf.consts[last]
|
|
326
|
+
if e in consts:
|
|
327
|
+
return consts[e]
|
|
328
|
+
if last in consts:
|
|
329
|
+
return consts[last]
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def scan_capacitor(nf: NativeFile, out: list, objc_plugins: dict) -> None:
|
|
334
|
+
t = nf.t
|
|
335
|
+
if nf.lang in ("kotlin", "java"):
|
|
336
|
+
for m in CAP_PLUGIN_ANN.finditer(t.code):
|
|
337
|
+
name = None
|
|
338
|
+
if m.group(1):
|
|
339
|
+
end = t.match(m.end() - 1, "(", ")")
|
|
340
|
+
nm = re.search(r"\bname\s*=\s*\"([^\"]+)\"", t.src[m.end():end])
|
|
341
|
+
name = nm.group(1) if nm else None
|
|
342
|
+
cm = re.compile(r"\bclass\s+(\w+)").search(t.code, m.end())
|
|
343
|
+
if not cm:
|
|
344
|
+
continue
|
|
345
|
+
cls = nf.class_named(cm.group(1))
|
|
346
|
+
ns = name or cm.group(1)
|
|
347
|
+
lo, hi = (cls[1], cls[2]) if cls else (cm.start(), len(t.code))
|
|
348
|
+
for pm in PLUGIN_METHOD.finditer(t.code, lo, hi):
|
|
349
|
+
fn = pm.group(1) or pm.group(2)
|
|
350
|
+
out.append(Receiver("capacitor", ns, fn, nf, pm.start(pm.lastindex), fn=fn, cls=cm.group(1), conf="exact",
|
|
351
|
+
via="@PluginMethod"))
|
|
352
|
+
elif nf.lang == "swift":
|
|
353
|
+
for m in SWIFT_CLASS.finditer(t.code):
|
|
354
|
+
bases = m.group(3)
|
|
355
|
+
if not re.search(r"\bCAPPlugin\b|\bCAPBridgedPlugin\b", bases):
|
|
356
|
+
continue
|
|
357
|
+
cls = m.group(2)
|
|
358
|
+
end = t.match(m.end() - 1)
|
|
359
|
+
body = t.src[m.start():end]
|
|
360
|
+
jm = JS_NAME.search(body)
|
|
361
|
+
listed = [a or b for a, b in CAP_METHOD_NAME.findall(body)]
|
|
362
|
+
objc_name = m.group(1) or cls
|
|
363
|
+
reg = objc_plugins.get(objc_name) or objc_plugins.get(cls)
|
|
364
|
+
ns = jm.group(1) if jm else (reg[0] if reg else None)
|
|
365
|
+
if not ns:
|
|
366
|
+
continue
|
|
367
|
+
names = set(listed) | set(reg[1] if reg else ())
|
|
368
|
+
funcs = {f.group(1): f.start(1) for f in OBJC_FUNC.finditer(t.code, m.start(), end)}
|
|
369
|
+
for fn in sorted(names) if names else sorted(funcs):
|
|
370
|
+
if fn in funcs:
|
|
371
|
+
out.append(Receiver("capacitor", ns, fn, nf, funcs[fn], fn=fn, cls=cls, conf="exact",
|
|
372
|
+
via="CAPPluginMethod" if fn in listed else "CAP_PLUGIN_METHOD" if names else "@objc func"))
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def scan_objc_capacitor(nf: NativeFile, objc_plugins: dict) -> None:
|
|
376
|
+
"""CAP_PLUGIN(Class, "JsName", CAP_PLUGIN_METHOD(m, ...); ...) registrations (old-style Capacitor iOS plugins)."""
|
|
377
|
+
t = nf.t
|
|
378
|
+
for m in re.finditer(r"\bCAP_PLUGIN\s*\(", t.code):
|
|
379
|
+
args = t.args(m.end() - 1)
|
|
380
|
+
if len(args) < 2:
|
|
381
|
+
continue
|
|
382
|
+
cls, js = args[0][1], re.fullmatch(r'"([^"]+)"', args[1][1])
|
|
383
|
+
if not js:
|
|
384
|
+
continue
|
|
385
|
+
rest = t.src[args[1][0]:t.match(m.end() - 1, "(", ")")]
|
|
386
|
+
methods = [x.group(1) for x in re.finditer(r"CAP_PLUGIN_METHOD\s*\(\s*(\w+)", rest)]
|
|
387
|
+
objc_plugins[cls] = (js.group(1), methods, nf, m.start())
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def scan_react_native(nf: NativeFile, out: list, spec_names: dict, extern: dict) -> None:
|
|
391
|
+
t = nf.t
|
|
392
|
+
if nf.lang in ("kotlin", "java"):
|
|
393
|
+
for m in JAVA_CLASS.finditer(t.code):
|
|
394
|
+
bases = m.group(2)
|
|
395
|
+
spec = re.search(r"\b(Native\w+Spec)\b", bases)
|
|
396
|
+
if not (spec or re.search(r"\b(?:ReactContextBaseJavaModule|BaseJavaModule)\b", bases)):
|
|
397
|
+
continue
|
|
398
|
+
cls = m.group(1)
|
|
399
|
+
end = t.match(m.end() - 1)
|
|
400
|
+
body = t.src[m.start():end]
|
|
401
|
+
ns = None
|
|
402
|
+
gm = GET_NAME.search(body)
|
|
403
|
+
if gm:
|
|
404
|
+
ns = gm.group(2) if gm.group(2) is not None else _str_value(nf, gm.group(3), _GLOBAL_CONSTS)
|
|
405
|
+
if not ns and spec:
|
|
406
|
+
ns = spec_names.get(spec.group(1)) or spec.group(1)[len("Native"):-len("Spec")]
|
|
407
|
+
if not ns:
|
|
408
|
+
ns = nf.consts.get("NAME")
|
|
409
|
+
if not ns:
|
|
410
|
+
continue
|
|
411
|
+
seen = set()
|
|
412
|
+
for rm in REACT_METHOD.finditer(t.code, m.start(), end):
|
|
413
|
+
fn = rm.group(1) or rm.group(2)
|
|
414
|
+
seen.add(fn)
|
|
415
|
+
out.append(Receiver("react-native", ns, fn, nf, rm.start(rm.lastindex), fn=fn, cls=cls, conf="exact",
|
|
416
|
+
via="@ReactMethod"))
|
|
417
|
+
if spec:
|
|
418
|
+
for om in OVERRIDE_FUN.finditer(t.code, m.start(), end):
|
|
419
|
+
fn = om.group(1) or om.group(2)
|
|
420
|
+
if fn in seen or fn in SPEC_SKIP:
|
|
421
|
+
continue
|
|
422
|
+
seen.add(fn)
|
|
423
|
+
out.append(Receiver("react-native", ns, fn, nf, om.start(om.lastindex), fn=fn, cls=cls, conf="exact",
|
|
424
|
+
via=spec.group(1)))
|
|
425
|
+
elif nf.lang == "objc":
|
|
426
|
+
for m in RCT_MODULE.finditer(t.code):
|
|
427
|
+
c = nf.class_at(m.start())
|
|
428
|
+
cls = c[0] if c else None
|
|
429
|
+
ns = m.group(1) or (re.sub(r"^RCT", "", cls) if cls else None)
|
|
430
|
+
if not ns:
|
|
431
|
+
continue
|
|
432
|
+
lo, hi = (c[1], c[2]) if c else (0, len(t.code))
|
|
433
|
+
for em in RCT_EXPORT.finditer(t.code, lo, hi):
|
|
434
|
+
fn = em.group(1) or em.group(2)
|
|
435
|
+
out.append(Receiver("react-native", ns, fn, nf, em.start(), fn=fn, cls=cls, conf="exact",
|
|
436
|
+
via="RCT_EXPORT_METHOD"))
|
|
437
|
+
for m in RCT_EXTERN_MODULE.finditer(t.code):
|
|
438
|
+
cls = m.group(1) or m.group(3)
|
|
439
|
+
ns = m.group(2) or cls
|
|
440
|
+
e = t.code.find("@end", m.end())
|
|
441
|
+
hi = e if e >= 0 else len(t.code)
|
|
442
|
+
for em in RCT_EXTERN_METHOD.finditer(t.code, m.end(), hi):
|
|
443
|
+
extern.setdefault(cls, []).append((ns, em.group(1), nf, em.start()))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def scan_expo(nf: NativeFile, out: list) -> None:
|
|
447
|
+
t = nf.t
|
|
448
|
+
if nf.lang not in ("kotlin", "swift") or "ModuleDefinition" not in t.code:
|
|
449
|
+
return
|
|
450
|
+
for m in re.finditer(r"\bdefinition\s*\(\s*\)", t.code):
|
|
451
|
+
end = t.match(m.end())
|
|
452
|
+
body_lo = t.code.find("{", m.end())
|
|
453
|
+
nm = EXPO_NAME.search(t.src, m.end(), end)
|
|
454
|
+
c = nf.class_at(m.start())
|
|
455
|
+
ns = nm.group(1) if nm else (c[0] if c else None)
|
|
456
|
+
if not ns or body_lo < 0:
|
|
457
|
+
continue
|
|
458
|
+
for fm in EXPO_FN.finditer(t.src, body_lo, end):
|
|
459
|
+
out.append(Receiver("react-native", ns, fm.group(1), nf, m.start(), fn="definition", cls=c[0] if c else None,
|
|
460
|
+
conf="exact", via="Expo " + fm.group(0).split("(")[0].strip(), api="expo-modules"))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def scan_flutter(nf: NativeFile, out: list, handler_classes: dict, pending_sites: list) -> None:
|
|
464
|
+
"""MethodChannel / EventChannel registrations with their handler regions, and `call.method` sites."""
|
|
465
|
+
t = nf.t
|
|
466
|
+
regs = [] # (kind 'method'|'event', name, pos, region (lo, hi) or None, handler class name or None)
|
|
467
|
+
news = []
|
|
468
|
+
if nf.lang in ("kotlin", "java"):
|
|
469
|
+
for m in CHANNEL_NEW.finditer(t.code):
|
|
470
|
+
args = t.args(m.end() - 1)
|
|
471
|
+
if len(args) >= 2:
|
|
472
|
+
news.append(("event" if m.group(1) == "Event" else "method", args[1][1], m.start(), t.match(m.end() - 1, "(", ")")))
|
|
473
|
+
elif nf.lang == "swift":
|
|
474
|
+
for m in SWIFT_CHANNEL_NEW.finditer(t.code):
|
|
475
|
+
args = t.args(t.code.rfind("(", 0, m.end()))
|
|
476
|
+
if args:
|
|
477
|
+
news.append(("event" if m.group(1) == "Event" else "method", args[0][1], m.start(),
|
|
478
|
+
t.match(t.code.rfind("(", 0, m.end()), "(", ")")))
|
|
479
|
+
else:
|
|
480
|
+
for m in OBJC_CHANNEL_NEW.finditer(t.src):
|
|
481
|
+
news.append(("event" if m.group(1) == "Event" else "method", m.group(2), m.start(), t.match(m.start(), "[", "]")))
|
|
482
|
+
for kind, expr, pos, close in news:
|
|
483
|
+
name = _str_value(nf, expr, _GLOBAL_CONSTS)
|
|
484
|
+
if not name:
|
|
485
|
+
continue
|
|
486
|
+
setter = "setStreamHandler" if kind == "event" else "setMethodCallHandler"
|
|
487
|
+
region, hcls = None, None
|
|
488
|
+
# chained: MethodChannel(...).setMethodCallHandler(...) / held in a variable: x = MethodChannel(...); x.set...(...)
|
|
489
|
+
after = t.code[close + 1:close + 200]
|
|
490
|
+
sm = re.match(r"\s*\??\.\s*" + setter + r"\b", after)
|
|
491
|
+
hpos = close + 1 + sm.end() if sm else None
|
|
492
|
+
if hpos is None:
|
|
493
|
+
head = t.code[max(0, pos - 160):pos]
|
|
494
|
+
vm = re.search(r"(?:\b(?:val|var|let|final\s+\w+|\w+)\s+)?(?:self\.|this\.)?(\w+)\s*(?::\s*[\w.<>?]+\s*)?=\s*(?:new\s+)?$",
|
|
495
|
+
head)
|
|
496
|
+
if vm:
|
|
497
|
+
hm = re.compile(r"\b" + re.escape(vm.group(1)) + r"\s*[?!]?\s*\.\s*" + setter + r"\b").search(t.code, close)
|
|
498
|
+
if hm:
|
|
499
|
+
hpos = hm.end()
|
|
500
|
+
if hpos is not None:
|
|
501
|
+
rest = t.code[hpos:hpos + 400]
|
|
502
|
+
lm = re.match(r"\s*\(?\s*(\{)", rest) # trailing lambda / closure
|
|
503
|
+
cm = re.match(r"\s*\(\s*(?:new\s+)?(\w+)\s*(?:\(|\))", rest)
|
|
504
|
+
if lm:
|
|
505
|
+
lo = hpos + lm.start(1)
|
|
506
|
+
region = (lo, t.match(lo))
|
|
507
|
+
elif cm and cm.group(1) in ("this", "self"):
|
|
508
|
+
c = nf.class_at(pos)
|
|
509
|
+
region = (c[1], c[2]) if c else None
|
|
510
|
+
elif cm and cm.group(1)[:1].isupper():
|
|
511
|
+
hcls = cm.group(1)
|
|
512
|
+
c = nf.class_named(hcls)
|
|
513
|
+
region = (c[1], c[2]) if c else None
|
|
514
|
+
regs.append((kind, name, pos, region, hcls))
|
|
515
|
+
sites = []
|
|
516
|
+
for m in METHOD_EQ.finditer(t.src):
|
|
517
|
+
sites.append((next(g for g in m.groups() if g is not None), m.start()))
|
|
518
|
+
for m in SWITCH_ON.finditer(t.code):
|
|
519
|
+
end = t.match(m.end() - 1)
|
|
520
|
+
for cm in CASE_STR.finditer(t.src, m.end(), end):
|
|
521
|
+
for s in re.findall(r'"([^"]+)"', cm.group(1) or cm.group(2)):
|
|
522
|
+
sites.append((s, cm.start()))
|
|
523
|
+
mregs = [r for r in regs if r[0] == "method"]
|
|
524
|
+
for kind, name, pos, region, hcls in regs:
|
|
525
|
+
if hcls:
|
|
526
|
+
handler_classes.setdefault(hcls, []).append((kind, name, nf, pos))
|
|
527
|
+
if kind == "event":
|
|
528
|
+
# the stream handler: onListen of the handler class / region, else the registering function
|
|
529
|
+
if region:
|
|
530
|
+
lm = re.compile(r"\bonListen\b").search(t.code, region[0], region[1])
|
|
531
|
+
out.append(Receiver("flutter-event", name, None, nf, lm.start() if lm else pos, conf="resolved",
|
|
532
|
+
via="setStreamHandler"))
|
|
533
|
+
elif hcls:
|
|
534
|
+
pending_sites.append(("event", name, hcls, nf, pos))
|
|
535
|
+
else:
|
|
536
|
+
out.append(Receiver("flutter-event", name, None, nf, pos, conf="heuristic", via="EventChannel"))
|
|
537
|
+
for meth, spos in sites:
|
|
538
|
+
owners = [r for r in mregs if r[3] and r[3][0] <= spos <= r[3][1]]
|
|
539
|
+
if owners:
|
|
540
|
+
for r in owners[-1:]:
|
|
541
|
+
out.append(Receiver("flutter", r[1], meth, nf, spos, conf="resolved", via="setMethodCallHandler"))
|
|
542
|
+
continue
|
|
543
|
+
c = nf.class_at(spos)
|
|
544
|
+
if c:
|
|
545
|
+
pending_sites.append(("method", meth, c[0], nf, spos)) # a handler class registered elsewhere
|
|
546
|
+
if len(mregs) == 1 and not (c and any(r[4] == c[0] for r in mregs)):
|
|
547
|
+
out.append(Receiver("flutter", mregs[0][1], meth, nf, spos, conf="heuristic", via="call.method"))
|
|
548
|
+
elif len(mregs) > 1:
|
|
549
|
+
for r in mregs:
|
|
550
|
+
out.append(Receiver("flutter", r[1], meth, nf, spos, conf="heuristic", via="call.method"))
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
_GLOBAL_CONSTS: dict[str, str] = {}
|
|
554
|
+
# string-valued enum cases (#95): Swift `enum Event: String { case a, b = "x" }` -> `Event.a.rawValue`, Kotlin
|
|
555
|
+
# `enum class Events(val event: String) { SAVED("Saved") }` -> `Events.SAVED.event`
|
|
556
|
+
SWIFT_STR_ENUM = re.compile(r"\benum\s+(\w+)\s*:\s*String\b[^{]*\{")
|
|
557
|
+
KT_STR_ENUM = re.compile(r"\benum\s+class\s+(\w+)\s*\(\s*(?:(?:private|internal|public)\s+)?val\s+(\w+)\s*:\s*String\s*\)[^{]*\{")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _enum_consts(rel: str, src: str) -> dict[str, str]:
|
|
561
|
+
out: dict[str, str] = {}
|
|
562
|
+
if rel.endswith(".swift") and "enum" in src:
|
|
563
|
+
for m in SWIFT_STR_ENUM.finditer(src):
|
|
564
|
+
depth, i = 1, m.end()
|
|
565
|
+
while i < len(src) and depth:
|
|
566
|
+
depth += {"{": 1, "}": -1}.get(src[i], 0)
|
|
567
|
+
i += 1
|
|
568
|
+
body = src[m.end():i - 1]
|
|
569
|
+
for cm in re.finditer(r"^\s*case\s+([^\n{(]+)$", body, re.M):
|
|
570
|
+
for part in cm.group(1).split(","):
|
|
571
|
+
pm = re.fullmatch(r'\s*(\w+)\s*(?:=\s*"([^"\\\n]*)")?\s*(?://.*)?', part)
|
|
572
|
+
if pm:
|
|
573
|
+
out[f"{m.group(1)}.{pm.group(1)}.rawValue"] = pm.group(2) if pm.group(2) is not None else pm.group(1)
|
|
574
|
+
elif rel.endswith(".kt") and "enum" in src:
|
|
575
|
+
for m in KT_STR_ENUM.finditer(src):
|
|
576
|
+
body = src[m.end():src.find("}", m.end())]
|
|
577
|
+
for em in re.finditer(r'\b([A-Z_][A-Z0-9_]*)\s*\(\s*"([^"\\\n]*)"\s*\)', body):
|
|
578
|
+
out[f"{m.group(1)}.{em.group(1)}.{m.group(2)}"] = em.group(2)
|
|
579
|
+
return out
|
|
580
|
+
|
|
581
|
+
# ------------------------------------------------------------------ Flutter MethodChannel: native -> Dart
|
|
582
|
+
NATIVE_INVOKE = re.compile(r"(?:\b(?:self|this)\s*\.\s*)?(\w+)\s*[?!]*\s*\.\s*invokeMethod\s*(?:<[^>]*>)?\s*\(\s*")
|
|
583
|
+
OBJC_INVOKE = re.compile(r"\[\s*(?:self\.|_)?(\w+)\s+invokeMethod:\s*")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def flutter_native_sends(builder, by_file, nfs: list) -> int:
|
|
587
|
+
"""`channel.invokeMethod("m", args)` in Kotlin / Java / Swift / ObjC on a MethodChannel whose name is known
|
|
588
|
+
(the variable the channel was assigned to, or the file's only channel) -> SENDS_TO endpoint:flutter:<channel>#m
|
|
589
|
+
from the enclosing native method; the Dart `setMethodCallHandler` that tests `call.method == 'm'` receives it."""
|
|
590
|
+
n = 0
|
|
591
|
+
for nf in nfs:
|
|
592
|
+
t = nf.t
|
|
593
|
+
if "invokeMethod" not in t.code:
|
|
594
|
+
continue
|
|
595
|
+
chans = {} # variable -> channel name
|
|
596
|
+
names = []
|
|
597
|
+
if nf.lang in ("kotlin", "java"):
|
|
598
|
+
news = [(m.start(), t.args(m.end() - 1)) for m in CHANNEL_NEW.finditer(t.code) if m.group(1) != "Event"]
|
|
599
|
+
news = [(p, a[1][1]) for p, a in news if len(a) >= 2]
|
|
600
|
+
elif nf.lang == "swift":
|
|
601
|
+
news = []
|
|
602
|
+
for m in SWIFT_CHANNEL_NEW.finditer(t.code):
|
|
603
|
+
if m.group(1) == "Event":
|
|
604
|
+
continue
|
|
605
|
+
a = t.args(t.code.rfind("(", 0, m.end()))
|
|
606
|
+
if a:
|
|
607
|
+
news.append((m.start(), a[0][1]))
|
|
608
|
+
else:
|
|
609
|
+
news = [(m.start(), m.group(2)) for m in OBJC_CHANNEL_NEW.finditer(t.src) if m.group(1) != "Event"]
|
|
610
|
+
for pos, expr in news:
|
|
611
|
+
name = _str_value(nf, expr, _GLOBAL_CONSTS)
|
|
612
|
+
if not name:
|
|
613
|
+
continue
|
|
614
|
+
names.append(name)
|
|
615
|
+
head = t.code[max(0, pos - 160):pos]
|
|
616
|
+
vm = re.search(r"(?:self\.|this\.|_)?(\w+)\s*(?::\s*[\w.<>?]+\s*)?=\s*(?:new\s+)?(?:\[\s*)?$", head)
|
|
617
|
+
if vm:
|
|
618
|
+
chans.setdefault(vm.group(1), name)
|
|
619
|
+
if not names:
|
|
620
|
+
continue
|
|
621
|
+
rx = OBJC_INVOKE if nf.lang == "objc" else NATIVE_INVOKE
|
|
622
|
+
for m in rx.finditer(t.code):
|
|
623
|
+
var = m.group(1).lstrip("_")
|
|
624
|
+
name = chans.get(var) or chans.get(m.group(1)) or (names[0] if len(set(names)) == 1 else None)
|
|
625
|
+
if not name:
|
|
626
|
+
continue
|
|
627
|
+
sm = re.match(r'@?"([^"\n]+)"', t.src[m.end():m.end() + 200])
|
|
628
|
+
if not sm:
|
|
629
|
+
continue
|
|
630
|
+
conf = "resolved" if (var in chans or m.group(1) in chans) else "heuristic"
|
|
631
|
+
line = t.line(m.start())
|
|
632
|
+
hid = _handler(builder, by_file, Receiver("flutter", name, sm.group(1), nf, m.start()), nf, line)
|
|
633
|
+
if hid is None:
|
|
634
|
+
continue
|
|
635
|
+
plat, _why = _platform_of(nf.rel, nf.lang)
|
|
636
|
+
protocol_send(builder, "flutter", name, sm.group(1), hid, nf.rel, line, conf, via="invokeMethod (native)",
|
|
637
|
+
platform=plat, direction="to_app")
|
|
638
|
+
n += 1
|
|
639
|
+
return n
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
# ------------------------------------------------------------------ native -> JS events (#61)
|
|
643
|
+
RN_EMIT = re.compile(r"\.\s*emit\s*\(\s*")
|
|
644
|
+
RN_SEND_EVENT = re.compile(r"\bsendEvent\s*\(\s*(?:withName:\s*)?")
|
|
645
|
+
RN_EMIT_DEVICE = re.compile(r"\.\s*emitDeviceEvent\s*\(\s*")
|
|
646
|
+
JVM_FUN = re.compile(r"\bfun\s+(?:<[^>]*>\s*)?(\w+)\s*\(|\b(\w+)\s*\([^()]*\)\s*(?:throws\s+[\w.,\s]+)?\{")
|
|
647
|
+
OBJC_SEND_EVENT = re.compile(r"\[\s*(?:self|_?\w+)\s+sendEventWithName:\s*")
|
|
648
|
+
CAP_NOTIFY = re.compile(r"\bnotifyListeners\s*\(\s*")
|
|
649
|
+
OBJC_CAP_NOTIFY = re.compile(r"\[\s*(?:self|_?\w+)\s+notifyListeners:\s*")
|
|
650
|
+
RN_EMITTER_HINT = re.compile(r"RCTDeviceEventEmitter|DeviceEventManagerModule|RCTNativeAppEventEmitter|RCTEventEmitter|"
|
|
651
|
+
r"ModuleDefinition|ReactContext|ReactApplicationContext")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _first_value(nf: NativeFile, text: str, after: int, args: bool) -> tuple[str | None, str]:
|
|
655
|
+
"""The event name at `after` (the first call argument, or an ObjC message argument): (value, raw text)."""
|
|
656
|
+
t = nf.t
|
|
657
|
+
if args:
|
|
658
|
+
a = t.args(after - 1) if after > 0 and t.code[after - 1] == "(" else t.args(t.code.rfind("(", 0, after))
|
|
659
|
+
if not a:
|
|
660
|
+
return None, ""
|
|
661
|
+
parts = [x[1].strip() for x in a]
|
|
662
|
+
# sendEvent(reactContext, "evt", params): the first string-valued argument among the first two
|
|
663
|
+
for raw in parts[:2]:
|
|
664
|
+
v = _str_value(nf, raw.split(":", 1)[1] if re.match(r"^\w+:", raw) else raw, _GLOBAL_CONSTS)
|
|
665
|
+
if v:
|
|
666
|
+
return v, raw
|
|
667
|
+
return None, parts[0] if parts else ""
|
|
668
|
+
m = re.match(r'(@?"[^"\n]*"|[\w.]+)', t.src[after:after + 200])
|
|
669
|
+
if not m:
|
|
670
|
+
return None, ""
|
|
671
|
+
return _str_value(nf, m.group(1), _GLOBAL_CONSTS), m.group(1)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
SWIFT_STR_PROP = re.compile(r"\bvar\s+(\w+)\s*:\s*String\s*\{")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _str_props(nf: NativeFile) -> dict:
|
|
678
|
+
"""Swift computed `var name: String { switch self { case .a: return "x" ... } }` whose every return is a string
|
|
679
|
+
literal -> name -> [values] (`event.listenerEvent` is one of them)."""
|
|
680
|
+
if nf.lang != "swift" or "String" not in nf.t.code:
|
|
681
|
+
return {}
|
|
682
|
+
out, t = {}, nf.t
|
|
683
|
+
for m in SWIFT_STR_PROP.finditer(t.code):
|
|
684
|
+
ob = m.end() - 1
|
|
685
|
+
body = t.src[ob:t.match(ob) + 1]
|
|
686
|
+
lits = re.findall(r'\breturn\s+"([^"\\\n]*)"', body) + re.findall(r'\bcase\s+[^:\n]+:\s*"([^"\\\n]*)"', body)
|
|
687
|
+
if lits and len(re.findall(r"\breturn\b", t.code[ob:t.match(ob) + 1])) == len(re.findall(r'\breturn\s+"', body)):
|
|
688
|
+
out[m.group(1)] = sorted(set(lits))
|
|
689
|
+
return out
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _event_values(nf: NativeFile, pos: int, raw: str, props: dict) -> list[str]:
|
|
693
|
+
"""Event names a non-literal argument stands for: `self?.name` (a constant), a local `let event = self?.name` /
|
|
694
|
+
`val event = NAME` declared before the call, or `event.listenerEvent` (a Swift computed string property)."""
|
|
695
|
+
e = re.sub(r"[?!](?=\.)", "", (raw or "").strip())
|
|
696
|
+
if e.startswith("name:"):
|
|
697
|
+
e = e[5:].strip()
|
|
698
|
+
if not e:
|
|
699
|
+
return []
|
|
700
|
+
v = _str_value(nf, e, _GLOBAL_CONSTS)
|
|
701
|
+
if v:
|
|
702
|
+
return [v]
|
|
703
|
+
if re.fullmatch(r"[a-z]\w*", e):
|
|
704
|
+
src = nf.t.src[max(0, pos - 3000):pos]
|
|
705
|
+
last = None
|
|
706
|
+
for m in re.finditer(r"\b(?:let|var|val|final\s+String|String)\s+" + re.escape(e) +
|
|
707
|
+
r"\s*(?::\s*String\??)?\s*=\s*([^\n,;{]+)", src):
|
|
708
|
+
last = m
|
|
709
|
+
if last:
|
|
710
|
+
x = re.sub(r"[?!](?=\.)", "", re.split(r"\s+else\b", last.group(1))[0].strip())
|
|
711
|
+
v = _str_value(nf, x, _GLOBAL_CONSTS)
|
|
712
|
+
if v:
|
|
713
|
+
return [v]
|
|
714
|
+
if "." in x and x.split(".")[-1] in props:
|
|
715
|
+
return props[x.split(".")[-1]]
|
|
716
|
+
return []
|
|
717
|
+
if re.fullmatch(r"[\w.]+", e) and "." in e and e.split(".")[-1] in props:
|
|
718
|
+
return props[e.split(".")[-1]]
|
|
719
|
+
return []
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def event_native_sends(builder, by_file, nfs: list, recs: list, dynamic: list, jvm_other: list = ()) -> dict:
|
|
723
|
+
"""Native code emitting an event to JS -> SENDS_TO endpoint (direction to_app) from the enclosing method:
|
|
724
|
+
React Native `RCTDeviceEventEmitter.emit("evt", ...)`, `sendEvent(ctx, "evt", ...)`, ObjC `sendEventWithName:`,
|
|
725
|
+
Swift `sendEvent(withName:)`, Expo `sendEvent("evt", ...)`; Capacitor `notifyListeners("evt", data)` in a plugin
|
|
726
|
+
class (the plugin name from its @CapacitorPlugin / CAPBridgedPlugin receivers in the same file)."""
|
|
727
|
+
cap_ns = defaultdict(set)
|
|
728
|
+
for r in recs:
|
|
729
|
+
if r.protocol == "capacitor":
|
|
730
|
+
cap_ns[r.file.rel].add(r.namespace)
|
|
731
|
+
n = defaultdict(int)
|
|
732
|
+
helpers: dict = {} # Kotlin / Java helper forwarding its parameter as the event name: name -> (arg, via)
|
|
733
|
+
found_by: dict = {}
|
|
734
|
+
for nf in nfs:
|
|
735
|
+
t = nf.t
|
|
736
|
+
found = found_by.setdefault(nf.rel, []) # (protocol, namespace, method, pos, via)
|
|
737
|
+
props = _str_props(nf)
|
|
738
|
+
if "notifyListeners" in t.code:
|
|
739
|
+
ns = cap_ns.get(nf.rel)
|
|
740
|
+
rx = OBJC_CAP_NOTIFY if nf.lang == "objc" else CAP_NOTIFY
|
|
741
|
+
for m in rx.finditer(t.code):
|
|
742
|
+
v, raw = _first_value(nf, t.code, m.end(), nf.lang != "objc")
|
|
743
|
+
if not ns or len(ns) != 1:
|
|
744
|
+
continue
|
|
745
|
+
vals = [v] if v is not None else _event_values(nf, m.start(), raw, props)
|
|
746
|
+
if not vals:
|
|
747
|
+
dynamic.append({"file": nf.rel, "line": t.line(m.start()), "protocol": "capacitor-event",
|
|
748
|
+
"what": f"notifyListeners({raw[:40]})"})
|
|
749
|
+
continue
|
|
750
|
+
for v in vals:
|
|
751
|
+
found.append(("capacitor-event", next(iter(ns)), v, m.start(), "notifyListeners"))
|
|
752
|
+
swift_named = nf.lang == "swift" and re.search(r"\bsendEvent\s*\(\s*(?:withName|name)\s*:", t.code)
|
|
753
|
+
if RN_EMITTER_HINT.search(t.code) or swift_named:
|
|
754
|
+
sites = []
|
|
755
|
+
if nf.lang == "objc":
|
|
756
|
+
sites = [(m, False, "sendEventWithName") for m in OBJC_SEND_EVENT.finditer(t.code)]
|
|
757
|
+
else:
|
|
758
|
+
hint = bool(RN_EMITTER_HINT.search(t.code))
|
|
759
|
+
sites = [(m, True, "sendEvent") for m in RN_SEND_EVENT.finditer(t.code)
|
|
760
|
+
if hint or "withName" in m.group(0) or re.match(r"name\s*:", t.code[m.end():])]
|
|
761
|
+
if re.search(r"RCTDeviceEventEmitter|RCTNativeAppEventEmitter", t.code):
|
|
762
|
+
sites += [(m, True, "RCTDeviceEventEmitter.emit") for m in RN_EMIT.finditer(t.code)]
|
|
763
|
+
if nf.lang in ("kotlin", "java"):
|
|
764
|
+
sites += [(m, True, "emitDeviceEvent") for m in RN_EMIT_DEVICE.finditer(t.code)]
|
|
765
|
+
for m, is_args, via in sites:
|
|
766
|
+
# `fun sendEvent(name: String, ...)` / `private void sendEvent(...)`: the helper's own declaration
|
|
767
|
+
if re.search(r"\b(?:fun|void|func)\s+$", t.code[max(0, m.start() - 12):m.start() + 1]) or \
|
|
768
|
+
re.search(r"\b(?:fun|void|func)\s+sendEvent\s*\($", t.code[max(0, m.start() - 20):m.end()]):
|
|
769
|
+
continue
|
|
770
|
+
v, raw = _first_value(nf, t.code, m.end(), is_args)
|
|
771
|
+
if v is None and (vals := _event_values(nf, m.start(), raw, props)):
|
|
772
|
+
for v in vals:
|
|
773
|
+
found.append(("react-native-event", v, None, m.start(), via))
|
|
774
|
+
continue
|
|
775
|
+
if v is None:
|
|
776
|
+
h = _jvm_helper(nf, m.start(), raw) if nf.lang in ("kotlin", "java") else None
|
|
777
|
+
if h: # `fun sendJSEvent(eventName: String, ..)`
|
|
778
|
+
helpers.setdefault(h[0], (h[1], via))
|
|
779
|
+
continue
|
|
780
|
+
if raw and not re.match(r"^[a-z]\w*$", raw): # a parameter (`eventName`) is the helper itself
|
|
781
|
+
dynamic.append({"file": nf.rel, "line": t.line(m.start()), "protocol": "react-native-event",
|
|
782
|
+
"what": f"{via}({raw[:40]})"})
|
|
783
|
+
continue
|
|
784
|
+
found.append(("react-native-event", v, None, m.start(), via))
|
|
785
|
+
extra = []
|
|
786
|
+
if helpers and jvm_other: # Kotlin / Java files without bridge code calling a helper
|
|
787
|
+
hx = re.compile(r"(?<![\w$])(?:" + "|".join(map(re.escape, sorted(helpers))) + r")\s*\(")
|
|
788
|
+
for rel, path in jvm_other:
|
|
789
|
+
try:
|
|
790
|
+
src = path.read_text(encoding="utf-8", errors="replace")
|
|
791
|
+
except OSError:
|
|
792
|
+
continue
|
|
793
|
+
if hx.search(src):
|
|
794
|
+
extra.append(NativeFile(rel, src))
|
|
795
|
+
for nf in list(nfs) + extra: # calls of the helpers: `RNUtilsModuleImpl.sendJSEvent(Events.X.event, map)`
|
|
796
|
+
if not helpers or nf.lang not in ("kotlin", "java"):
|
|
797
|
+
continue
|
|
798
|
+
t = nf.t
|
|
799
|
+
for name, (idx, via) in helpers.items():
|
|
800
|
+
if name not in t.code:
|
|
801
|
+
continue
|
|
802
|
+
for m in re.finditer(r"(?<![\w$])" + re.escape(name) + r"\s*\(", t.code):
|
|
803
|
+
pre = t.code[t.code.rfind("\n", 0, m.start()) + 1:m.start()]
|
|
804
|
+
if re.search(r"\bfun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?$", pre) or (
|
|
805
|
+
re.match(r"^\s*(?:@\w+\s+)*(?:(?:public|private|protected|static|final|synchronized)\s+)*[\w.<>\[\]]+\s+$", pre)
|
|
806
|
+
and not re.search(r"\b(?:return|else|throw|new)\s+$", pre)):
|
|
807
|
+
continue # the declaration (`fun sendJSEvent(` / `void sendJSEvent(`)
|
|
808
|
+
a = t.args(m.end() - 1)
|
|
809
|
+
if len(a) <= idx:
|
|
810
|
+
continue
|
|
811
|
+
raw = a[idx][1].strip()
|
|
812
|
+
v = _str_value(nf, raw, _GLOBAL_CONSTS)
|
|
813
|
+
if v is None:
|
|
814
|
+
if raw and not re.match(r"^[a-z]\w*$", raw) and not any(
|
|
815
|
+
d["file"] == nf.rel and d["line"] == t.line(m.start()) for d in dynamic):
|
|
816
|
+
dynamic.append({"file": nf.rel, "line": t.line(m.start()), "protocol": "react-native-event",
|
|
817
|
+
"what": f"{name}({raw[:40]})"})
|
|
818
|
+
continue
|
|
819
|
+
if any(f[3] == m.start() for f in found_by.get(nf.rel, ())):
|
|
820
|
+
continue # already a send site (`sendEvent(..)` itself)
|
|
821
|
+
found_by.setdefault(nf.rel, []).append(("react-native-event", v, None, m.start(), f"{name} -> {via}"))
|
|
822
|
+
for nf in list(nfs) + extra:
|
|
823
|
+
t = nf.t
|
|
824
|
+
for proto, ns, meth, pos, via in found_by.get(nf.rel, ()):
|
|
825
|
+
line = t.line(pos)
|
|
826
|
+
fn = None
|
|
827
|
+
if nf.lang == "objc": # inside RCT_EXPORT_METHOD(start:(NSString *)url) { ... }
|
|
828
|
+
for em in re.finditer(r"RCT_(?:EXPORT|REMAP)_METHOD\s*\(\s*(?:\w+\s*,\s*)?(\w+)", t.code[:pos]):
|
|
829
|
+
ob = t.code.find("{", em.end())
|
|
830
|
+
if 0 <= ob < pos <= t.match(ob):
|
|
831
|
+
fn = em.group(1)
|
|
832
|
+
hid = _handler(builder, by_file, Receiver(proto, ns, meth, nf, pos, fn=fn), nf, line)
|
|
833
|
+
if hid is None:
|
|
834
|
+
continue
|
|
835
|
+
plat, _why = _platform_of(nf.rel, nf.lang)
|
|
836
|
+
protocol_send(builder, proto, ns, meth, hid, nf.rel, line, "resolved", via=f"{via} (native)", platform=plat,
|
|
837
|
+
direction="to_app")
|
|
838
|
+
n[proto] += 1
|
|
839
|
+
return dict(n)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _jvm_helper(nf: NativeFile, pos: int, raw: str):
|
|
843
|
+
"""The Kotlin / Java function enclosing `pos` when `raw` is one of its parameters: (name, parameter index)."""
|
|
844
|
+
if not re.match(r"^[a-z]\w*$", raw or ""):
|
|
845
|
+
return None
|
|
846
|
+
t = nf.t
|
|
847
|
+
best = None
|
|
848
|
+
for m in JVM_FUN.finditer(t.code, 0, pos):
|
|
849
|
+
name = m.group(1) or m.group(2)
|
|
850
|
+
if name in ("if", "for", "while", "when", "switch", "catch", "synchronized", "return"):
|
|
851
|
+
continue
|
|
852
|
+
op = t.code.find("(", m.start())
|
|
853
|
+
cl = t.match(op, "(", ")")
|
|
854
|
+
ob = t.code.find("{", cl) if cl > 0 else -1
|
|
855
|
+
if ob < 0 or not (ob < pos <= t.match(ob)):
|
|
856
|
+
continue
|
|
857
|
+
best = (name, t.src[op + 1:cl])
|
|
858
|
+
if not best:
|
|
859
|
+
return None
|
|
860
|
+
for i, prm in enumerate(x for x in best[1].split(",")):
|
|
861
|
+
if re.match(r"^\s*(?:@\w+\s+)*(?:final\s+)?" + re.escape(raw) + r"\s*:", prm) or \
|
|
862
|
+
re.search(r"\bString\s+" + re.escape(raw) + r"\s*$", prm):
|
|
863
|
+
return best[0], i
|
|
864
|
+
return None
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
# ------------------------------------------------------------------ Cordova: native side (#61)
|
|
868
|
+
CORDOVA_JAVA_CLASS = re.compile(r"\bclass\s+(\w+)\s*(?:\([^)]*\))?\s*(?:extends|:)\s*(?:[\w.]+\.)?CordovaPlugin\b")
|
|
869
|
+
_CDV_V = r'("[^"]+"|[A-Z_][\w.]*)'
|
|
870
|
+
CORDOVA_ACTION = re.compile(r'\baction\s*\.\s*equals(?:IgnoreCase)?\s*\(\s*' + _CDV_V + r'\s*\)|' + _CDV_V +
|
|
871
|
+
r'\s*\.\s*equals(?:IgnoreCase)?\s*\(\s*action\s*\)|\baction\s*==\s*' + _CDV_V +
|
|
872
|
+
r'|^\s*case\s+' + _CDV_V + r'\s*:|^\s*' + _CDV_V + r'\s*->', re.M)
|
|
873
|
+
CORDOVA_OBJC_IFACE = re.compile(r"@interface\s+(\w+)\s*:\s*CDVPlugin\b")
|
|
874
|
+
CORDOVA_SWIFT_CLASS = re.compile(r"(?:@objc\s*\(\s*(\w+)\s*\)\s*)?(?:(?:public|open|final|internal)\s+)*class\s+(\w+)\s*:\s*CDVPlugin\b")
|
|
875
|
+
CORDOVA_OBJC_METHOD = re.compile(r"^\s*-\s*\(\s*void\s*\)\s*(\w+)\s*:\s*\(\s*CDVInvokedUrlCommand\s*\*\s*\)", re.M)
|
|
876
|
+
CORDOVA_SWIFT_METHOD = re.compile(r"(?:@objc\s*\(\s*(\w+):?\s*\)\s*)?(?:(?:public|open|internal|private|final|override|dynamic)\s+)*"
|
|
877
|
+
r"func\s+(\w+)\s*\(\s*_?\s*\w+\s*:\s*CDVInvokedUrlCommand")
|
|
878
|
+
FEATURE_RE = re.compile(r"<feature\s+name\s*=\s*\"([^\"]+)\"\s*>(.*?)</feature>", re.S)
|
|
879
|
+
FEATURE_PARAM = re.compile(r"<param\s+name\s*=\s*\"(android-package|ios-package)\"\s+value\s*=\s*\"([^\"]+)\"")
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def cordova_services(root: Path) -> dict:
|
|
883
|
+
"""plugin.xml / config.xml <feature name="Service"><param name="android-package" value="pkg.Cls"/> ->
|
|
884
|
+
{class simple name: service}; skips node_modules and platform build copies."""
|
|
885
|
+
out = {}
|
|
886
|
+
for dp, dns, fns in os.walk(root):
|
|
887
|
+
dns[:] = [d for d in dns if d not in ("node_modules", ".git", "build", "Pods") and not d.startswith(".")]
|
|
888
|
+
for fn in fns:
|
|
889
|
+
if fn not in ("plugin.xml", "config.xml"):
|
|
890
|
+
continue
|
|
891
|
+
try:
|
|
892
|
+
txt = (Path(dp) / fn).read_text(encoding="utf-8", errors="replace")
|
|
893
|
+
except OSError:
|
|
894
|
+
continue
|
|
895
|
+
for m in FEATURE_RE.finditer(txt):
|
|
896
|
+
for pm in FEATURE_PARAM.finditer(m.group(2)):
|
|
897
|
+
out.setdefault(pm.group(2).rsplit(".", 1)[-1], m.group(1))
|
|
898
|
+
return out
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def scan_cordova(nf: NativeFile, out: list, services: dict) -> None:
|
|
902
|
+
t = nf.t
|
|
903
|
+
if nf.lang in ("java", "kotlin"):
|
|
904
|
+
for m in CORDOVA_JAVA_CLASS.finditer(t.code):
|
|
905
|
+
cls = m.group(1)
|
|
906
|
+
c = nf.class_named(cls)
|
|
907
|
+
lo, hi = (c[1], c[2]) if c else (m.start(), len(t.code))
|
|
908
|
+
ex = re.compile(r"\bexecute\s*\(").search(t.code, lo, hi)
|
|
909
|
+
if not ex:
|
|
910
|
+
continue
|
|
911
|
+
svc = services.get(cls, cls)
|
|
912
|
+
seen = set()
|
|
913
|
+
for am in CORDOVA_ACTION.finditer(t.src, ex.start(), hi):
|
|
914
|
+
act = _str_value(nf, next(g for g in am.groups() if g), _GLOBAL_CONSTS)
|
|
915
|
+
if not act or act in seen:
|
|
916
|
+
continue
|
|
917
|
+
seen.add(act)
|
|
918
|
+
out.append(Receiver("cordova", svc, act, nf, am.start(), fn="execute", cls=cls,
|
|
919
|
+
conf="resolved" if cls in services else "heuristic", via="CordovaPlugin.execute"))
|
|
920
|
+
elif nf.lang == "objc":
|
|
921
|
+
for m in CORDOVA_OBJC_IFACE.finditer(t.code):
|
|
922
|
+
cls = m.group(1)
|
|
923
|
+
svc = services.get(cls, cls)
|
|
924
|
+
for mm in CORDOVA_OBJC_METHOD.finditer(t.code):
|
|
925
|
+
c = nf.class_at(mm.start())
|
|
926
|
+
if c and c[0] != cls:
|
|
927
|
+
continue
|
|
928
|
+
out.append(Receiver("cordova", svc, mm.group(1), nf, mm.start(1), fn=mm.group(1), cls=cls,
|
|
929
|
+
conf="resolved" if cls in services else "heuristic", via="CDVPlugin method"))
|
|
930
|
+
if "@implementation" in t.code and not CORDOVA_OBJC_IFACE.search(t.code):
|
|
931
|
+
for im in OBJC_IMPL_RE.finditer(t.code): # the @interface is in the .h file
|
|
932
|
+
cls = im.group(1)
|
|
933
|
+
if cls not in services:
|
|
934
|
+
continue
|
|
935
|
+
for mm in CORDOVA_OBJC_METHOD.finditer(t.code, im.end()):
|
|
936
|
+
out.append(Receiver("cordova", services[cls], mm.group(1), nf, mm.start(1), fn=mm.group(1), cls=cls,
|
|
937
|
+
conf="resolved", via="CDVPlugin method"))
|
|
938
|
+
elif nf.lang == "swift":
|
|
939
|
+
for m in CORDOVA_SWIFT_CLASS.finditer(t.code):
|
|
940
|
+
objc_name, cls = m.group(1), m.group(2)
|
|
941
|
+
svc = services.get(objc_name or cls, services.get(cls, objc_name or cls))
|
|
942
|
+
end = t.match(t.code.find("{", m.end()))
|
|
943
|
+
for mm in CORDOVA_SWIFT_METHOD.finditer(t.code, m.end(), end):
|
|
944
|
+
out.append(Receiver("cordova", svc, mm.group(1) or mm.group(2), nf, mm.start(2), fn=mm.group(2), cls=cls,
|
|
945
|
+
conf="resolved" if (objc_name or cls) in services else "heuristic",
|
|
946
|
+
via="CDVPlugin method"))
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
# ------------------------------------------------------------------ Pigeon: native side
|
|
950
|
+
SWIFT_EXT = re.compile(r"\bextension\s+(\w+)\s*:\s*([^{]*)\{")
|
|
951
|
+
SUPER_NAME = re.compile(r"(?<![\w.])(?:\w+\.)*([A-Z]\w*)(\s*\()?")
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _decl_rx(lang: str, m: str):
|
|
955
|
+
if lang == "kotlin":
|
|
956
|
+
return re.compile(r"\bfun\s+(?:<[^>]*>\s*)?" + re.escape(m) + r"\s*\(")
|
|
957
|
+
if lang == "swift":
|
|
958
|
+
return re.compile(r"\bfunc\s+" + re.escape(m) + r"\s*[(<]")
|
|
959
|
+
if lang == "java":
|
|
960
|
+
return re.compile(r"[\w<>\[\],.?]+\s+" + re.escape(m) + r"\s*\([^;{)]*\)\s*(?:throws\s+[\w., ]+)?\{")
|
|
961
|
+
return re.compile(r"^\s*-\s*\([^)]*\)\s*" + re.escape(m) + r"\b", re.M)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _supers(nf: NativeFile, start: int) -> list[tuple[str, bool]]:
|
|
965
|
+
"""Supertype names of the class / extension declared at `start`: (name, called-as-constructor)."""
|
|
966
|
+
code = nf.t.code
|
|
967
|
+
head = code[start:code.find("{", start)]
|
|
968
|
+
if nf.lang == "java":
|
|
969
|
+
m = re.search(r"\b(?:extends|implements)\b(.*)", head, re.S)
|
|
970
|
+
tail = m.group(1) if m else ""
|
|
971
|
+
else:
|
|
972
|
+
i, depth = -1, 0 # the ':' outside a Kotlin primary constructor `class X(ctx: Context) : Base(ctx), Api`
|
|
973
|
+
for j, ch in enumerate(head):
|
|
974
|
+
if ch in "(<":
|
|
975
|
+
depth += 1
|
|
976
|
+
elif ch in ")>":
|
|
977
|
+
depth -= 1
|
|
978
|
+
elif ch == ":" and depth == 0:
|
|
979
|
+
i = j
|
|
980
|
+
break
|
|
981
|
+
tail = head[i + 1:] if i >= 0 else ""
|
|
982
|
+
tail = re.sub(r"<[^<>]*>", "", tail)
|
|
983
|
+
tail = re.sub(r"\bwhere\b.*", "", tail, flags=re.S)
|
|
984
|
+
return [(m.group(1), bool(m.group(2))) for m in SUPER_NAME.finditer(tail)]
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def scan_pigeon_hosts(nfs: list, apis: dict, out: list) -> None:
|
|
988
|
+
"""@HostApi implementations: a Kotlin / Swift / Java class (or Swift extension) that implements the generated
|
|
989
|
+
interface / protocol `Api`; each API method is looked up in the class, then in its superclasses (an
|
|
990
|
+
`ApiImplBase` holding the shared methods)."""
|
|
991
|
+
host = {k: v for k, v in apis.items() if v["kind"] == "host"}
|
|
992
|
+
if not host:
|
|
993
|
+
return
|
|
994
|
+
decls = defaultdict(list) # class name -> [(nf, start, end)]
|
|
995
|
+
for nf in nfs:
|
|
996
|
+
for name, lo, hi in nf.classes:
|
|
997
|
+
decls[name].append((nf, lo, hi))
|
|
998
|
+
if nf.lang == "swift":
|
|
999
|
+
for m in SWIFT_EXT.finditer(nf.t.code):
|
|
1000
|
+
decls[m.group(1)].append((nf, m.start(), nf.t.match(m.end() - 1)))
|
|
1001
|
+
|
|
1002
|
+
def find(cls, meth, lang_fam, depth=0):
|
|
1003
|
+
for nf, lo, hi in decls.get(cls, ()):
|
|
1004
|
+
if FAM[nf.lang] != lang_fam:
|
|
1005
|
+
continue
|
|
1006
|
+
hit = _decl_rx(nf.lang, meth).search(nf.t.code, lo, hi)
|
|
1007
|
+
if hit:
|
|
1008
|
+
return nf, hit.start(), cls
|
|
1009
|
+
if depth < 3:
|
|
1010
|
+
for nf, lo, _hi in decls.get(cls, ()):
|
|
1011
|
+
for sup, _call in _supers(nf, lo):
|
|
1012
|
+
if sup != cls and sup not in host:
|
|
1013
|
+
got = find(sup, meth, lang_fam, depth + 1)
|
|
1014
|
+
if got:
|
|
1015
|
+
return got
|
|
1016
|
+
return None
|
|
1017
|
+
|
|
1018
|
+
seen = set()
|
|
1019
|
+
for cls, items in decls.items():
|
|
1020
|
+
for nf, lo, _hi in items:
|
|
1021
|
+
for sup, called in _supers(nf, lo):
|
|
1022
|
+
if sup not in host or called:
|
|
1023
|
+
continue
|
|
1024
|
+
for meth in host[sup]["methods"]:
|
|
1025
|
+
got = find(cls, meth, FAM[nf.lang])
|
|
1026
|
+
if not got:
|
|
1027
|
+
continue
|
|
1028
|
+
rnf, pos, rcls = got
|
|
1029
|
+
key = (sup, meth, rnf.rel, pos)
|
|
1030
|
+
if key in seen:
|
|
1031
|
+
continue
|
|
1032
|
+
seen.add(key)
|
|
1033
|
+
out.append(Receiver("pigeon", sup, meth, rnf, pos, fn=meth, cls=rcls, conf="resolved",
|
|
1034
|
+
via=f"Pigeon HostApi ({cls} : {sup})"))
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
FAM = {"kotlin": "jvm", "java": "jvm", "swift": "apple", "objc": "apple"}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def pigeon_native_sends(builder, by_file, nfs: list, apis: dict) -> int:
|
|
1041
|
+
"""@FlutterApi calls from native code: a variable / property holding the generated class
|
|
1042
|
+
(`flutterApi = BackgroundWorkerFlutterApi(binaryMessenger)`, `var api: XFlutterApi?`) and its method calls
|
|
1043
|
+
(`flutterApi?.onAndroidUpload(...)`) -> SENDS_TO endpoint:pigeon:<Api>#<method> from the enclosing native method."""
|
|
1044
|
+
flutter = {k: v for k, v in apis.items() if v["kind"] == "flutter"}
|
|
1045
|
+
n = 0
|
|
1046
|
+
for nf in nfs:
|
|
1047
|
+
code = nf.t.code
|
|
1048
|
+
for api, info in flutter.items():
|
|
1049
|
+
if api not in code:
|
|
1050
|
+
continue
|
|
1051
|
+
vars_ = set()
|
|
1052
|
+
for m in re.finditer(r"(\w+)\s*:\s*(?:[\w.]+\.)?" + re.escape(api) + r"\b\s*[?!]?", code):
|
|
1053
|
+
vars_.add(m.group(1))
|
|
1054
|
+
for m in re.finditer(r"(\w+)\s*=\s*(?:new\s+)?(?:[\w.]+\.)?" + re.escape(api) + r"\s*\(", code):
|
|
1055
|
+
vars_.add(m.group(1))
|
|
1056
|
+
meths = "|".join(map(re.escape, info["methods"]))
|
|
1057
|
+
if not meths:
|
|
1058
|
+
continue
|
|
1059
|
+
pats = []
|
|
1060
|
+
if vars_:
|
|
1061
|
+
pats.append(re.compile(r"(?<![\w])(?:(?:self|this)\s*\.\s*)?(?:" + "|".join(map(re.escape, sorted(vars_)))
|
|
1062
|
+
+ r")\s*[?!]*\s*\.\s*(" + meths + r")\s*[({]"))
|
|
1063
|
+
pats.append(re.compile(re.escape(api) + r"\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*[?!]*\s*\.\s*(" + meths + r")\s*[({]"))
|
|
1064
|
+
for rx in pats:
|
|
1065
|
+
for m in rx.finditer(code):
|
|
1066
|
+
line = nf.t.line(m.start(1))
|
|
1067
|
+
hid = _handler(builder, by_file, Receiver("pigeon", api, m.group(1), nf, m.start(1)), nf, line)
|
|
1068
|
+
if hid is None:
|
|
1069
|
+
continue
|
|
1070
|
+
plat, _why = _platform_of(nf.rel, nf.lang)
|
|
1071
|
+
protocol_send(builder, "pigeon", api, m.group(1), hid, nf.rel, line, "resolved",
|
|
1072
|
+
via="Pigeon FlutterApi call", platform=plat, direction="to_app")
|
|
1073
|
+
n += 1
|
|
1074
|
+
return n
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
# ------------------------------------------------------------------ the index pass
|
|
1078
|
+
def _platform_of(rel: str, lang: str) -> tuple[str, str]:
|
|
1079
|
+
parts = [p.lower() for p in rel.split("/")[:-1]]
|
|
1080
|
+
for p in ("android", "ios", "macos"):
|
|
1081
|
+
if p in parts:
|
|
1082
|
+
return p, f"{p}/ directory"
|
|
1083
|
+
for p in parts:
|
|
1084
|
+
if p in ("androidmain", "androidtest"):
|
|
1085
|
+
return "android", f"{p}/ source set"
|
|
1086
|
+
if p in ("iosmain",):
|
|
1087
|
+
return "ios", f"{p}/ source set"
|
|
1088
|
+
if lang in ("kotlin", "java"):
|
|
1089
|
+
return "android", f"{lang} native module"
|
|
1090
|
+
return "ios", f"{lang} native module"
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _native_files(project, scanned, builder) -> list[str]:
|
|
1094
|
+
out = []
|
|
1095
|
+
if scanned is not None:
|
|
1096
|
+
for e in (".kt", ".swift"):
|
|
1097
|
+
out += scanned.paths.get(e, [])
|
|
1098
|
+
out += getattr(scanned, "bridge_paths", [])
|
|
1099
|
+
return sorted(set(out))
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
TAURI_CMD = re.compile(r"#\[\s*(tauri::)?command\b[^\]]*\]\s*(?:#\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?"
|
|
1103
|
+
r"(?:async\s+)?(?:unsafe\s+)?fn\s+([A-Za-z_]\w*)")
|
|
1104
|
+
TAURI_HANDLER = re.compile(r"generate_handler!\s*\[((?:[^\[\]]|\[[^\]]*\])*)\]") # `#![plugin(x)]` inside
|
|
1105
|
+
TAURI_PLUGIN = re.compile(r"\bBuilder\s*(?:::\s*<[^>]*>\s*)?::\s*new\s*\(\s*\"([\w-]+)\"")
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _common_dirs(a: str, b: str) -> int:
|
|
1109
|
+
pa, pb = a.split("/")[:-1], b.split("/")[:-1]
|
|
1110
|
+
n = 0
|
|
1111
|
+
for x, y in zip(pa, pb):
|
|
1112
|
+
if x != y:
|
|
1113
|
+
break
|
|
1114
|
+
n += 1
|
|
1115
|
+
return n
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def tauri_receivers(project, builder) -> dict:
|
|
1119
|
+
"""Tauri commands: `#[tauri::command] fn cmd` in the Rust core -> RECEIVED_BY from endpoint:tauri:<cmd> (a command
|
|
1120
|
+
registered by a plugin crate, `tauri::plugin::Builder::new("x")...generate_handler![cmd]`, is
|
|
1121
|
+
endpoint:tauri:plugin:x|cmd). attrs.registered: listed in a generate_handler! (an unregistered command cannot be
|
|
1122
|
+
invoked)."""
|
|
1123
|
+
root = Path(project.root)
|
|
1124
|
+
by_file = defaultdict(dict)
|
|
1125
|
+
for n in builder.nodes.values():
|
|
1126
|
+
if n.lang == "rust" and n.kind in ("function", "method") and n.file:
|
|
1127
|
+
by_file[n.file].setdefault(n.name.rsplit("::", 1)[-1], n.id)
|
|
1128
|
+
cmds, registered = [], {}
|
|
1129
|
+
for rel in sorted(by_file):
|
|
1130
|
+
try:
|
|
1131
|
+
src = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
1132
|
+
except OSError:
|
|
1133
|
+
continue
|
|
1134
|
+
if "command" not in src and "generate_handler" not in src:
|
|
1135
|
+
continue
|
|
1136
|
+
src = strip_comments(src)
|
|
1137
|
+
bare_ok = bool(re.search(r"\buse\s+tauri::(?:\{[^}]*\bcommand\b|command\b)", src))
|
|
1138
|
+
for m in TAURI_CMD.finditer(src):
|
|
1139
|
+
if m.group(1) or bare_ok:
|
|
1140
|
+
cmds.append((rel, m.group(2), src.count("\n", 0, m.start(2)) + 1))
|
|
1141
|
+
plugin = TAURI_PLUGIN.search(src)
|
|
1142
|
+
for h in TAURI_HANDLER.finditer(src):
|
|
1143
|
+
for item in re.sub(r"#!?\[[^\]]*\]", " ", h.group(1)).split(","):
|
|
1144
|
+
name = item.strip().rsplit("::", 1)[-1].strip()
|
|
1145
|
+
if re.fullmatch(r"[A-Za-z_]\w*", name):
|
|
1146
|
+
registered.setdefault(name, []).append((rel, plugin.group(1) if plugin else None))
|
|
1147
|
+
n = 0
|
|
1148
|
+
for rel, name, line in cmds:
|
|
1149
|
+
nid = by_file[rel].get(name)
|
|
1150
|
+
if not nid:
|
|
1151
|
+
continue
|
|
1152
|
+
# the same command name registered by several plugins (Tauri's own `window` / `webview` / `menu` plugins all
|
|
1153
|
+
# have `new`, `set_icon`, ...): the registration closest to the command's file (longest common directory)
|
|
1154
|
+
regs = registered.get(name) or []
|
|
1155
|
+
plugin = max(regs, key=lambda r: (_common_dirs(r[0], rel), r[0] == rel))[1] if regs else None
|
|
1156
|
+
ns = f"plugin:{plugin}|{name}" if plugin else name
|
|
1157
|
+
ep = protocol_receive(builder, "tauri", ns, None, nid, rel, line, "exact", via="#[tauri::command]",
|
|
1158
|
+
process="core", registered=name in registered)
|
|
1159
|
+
if name not in registered:
|
|
1160
|
+
builder.nodes[ep].attrs["unregistered"] = True
|
|
1161
|
+
n += 1
|
|
1162
|
+
return {"commands": n, "registered": len(registered)} if n else {}
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def apply(project, builder, scanned=None, sends_only: bool = False) -> dict:
|
|
1166
|
+
"""Index pass: native receivers of every bridge protocol -> RECEIVED_BY edges (+ stub nodes for Java / ObjC) and
|
|
1167
|
+
platform marks on their files. Returns the bridges stats (empty when there is no bridge code)."""
|
|
1168
|
+
from .platforms import BIG, Cond, _plat_atom, mark
|
|
1169
|
+
tauri = tauri_receivers(project, builder)
|
|
1170
|
+
st0 = {"tauri": tauri} if tauri else {}
|
|
1171
|
+
st = _apply(project, builder, scanned, sends_only, mark, BIG, Cond, _plat_atom)
|
|
1172
|
+
if st0 or st:
|
|
1173
|
+
st = {**(st or {}), **st0}
|
|
1174
|
+
return st or {}
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _apply(project, builder, scanned, sends_only, mark, BIG, Cond, _plat_atom) -> dict:
|
|
1178
|
+
sends = [e for e in builder.edges.values() if e.kind == "SENDS_TO" or (e.kind == "TEST_CALLS" and e.attrs.get("orig") == "SENDS_TO")]
|
|
1179
|
+
root = Path(project.root)
|
|
1180
|
+
files = _native_files(project, scanned, builder)
|
|
1181
|
+
if not sends and not files:
|
|
1182
|
+
return {}
|
|
1183
|
+
clf = project.options.get("generated")
|
|
1184
|
+
apis = getattr(builder, "pigeon_apis", None) or {}
|
|
1185
|
+
# an API name or a name starting with it (`NativeSyncApiImplBase` holds methods its subclasses inherit)
|
|
1186
|
+
pigeon_rx = re.compile(r"\b(?:" + "|".join(map(re.escape, sorted(apis))) + r")") if apis else None
|
|
1187
|
+
nfs: list[NativeFile] = []
|
|
1188
|
+
other_consts: dict[str, str] = {}
|
|
1189
|
+
jvm_other: list = []
|
|
1190
|
+
_GLOBAL_CONSTS.clear()
|
|
1191
|
+
for rel in files:
|
|
1192
|
+
if clf is not None and clf.excludes(rel):
|
|
1193
|
+
continue
|
|
1194
|
+
try:
|
|
1195
|
+
src = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
1196
|
+
except OSError:
|
|
1197
|
+
continue
|
|
1198
|
+
if len(src) > 2_000_000:
|
|
1199
|
+
continue
|
|
1200
|
+
for k, v in _enum_consts(rel, src).items():
|
|
1201
|
+
other_consts.setdefault(k, v)
|
|
1202
|
+
if not (TRIGGER.search(src) or (pigeon_rx and pigeon_rx.search(src))):
|
|
1203
|
+
if rel.endswith((".kt", ".java")):
|
|
1204
|
+
jvm_other.append((rel, root / rel)) # may call an event helper of a scanned file
|
|
1205
|
+
# not scanned, but its class constants name modules elsewhere (`getName() = FooImpl.NAME`): qualified only
|
|
1206
|
+
if "String" in src and (cm := CLASS_RE.search(src)):
|
|
1207
|
+
for m in CONST_RE.finditer(src):
|
|
1208
|
+
other_consts.setdefault(f"{cm.group(1)}.{m.group(1)}", m.group(2))
|
|
1209
|
+
continue
|
|
1210
|
+
nf = NativeFile(rel, src)
|
|
1211
|
+
nfs.append(nf)
|
|
1212
|
+
for nf in nfs: # NAME constants of other classes (getName() = FooImpl.NAME, channel names in a Constants file)
|
|
1213
|
+
cls = nf.classes[0][0] if nf.classes else None
|
|
1214
|
+
for k, v in nf.consts.items():
|
|
1215
|
+
_GLOBAL_CONSTS.setdefault(k, v)
|
|
1216
|
+
if cls:
|
|
1217
|
+
_GLOBAL_CONSTS.setdefault(f"{cls}.{k}", v)
|
|
1218
|
+
for k, v in other_consts.items():
|
|
1219
|
+
_GLOBAL_CONSTS.setdefault(k, v)
|
|
1220
|
+
if not nfs and not sends:
|
|
1221
|
+
return {}
|
|
1222
|
+
# TurboModule codegen spec classes: NativeFooSpec <- the JS spec file NativeFoo.ts naming the module
|
|
1223
|
+
spec_names = {}
|
|
1224
|
+
for e in sends:
|
|
1225
|
+
at = e.attrs.get("module_at")
|
|
1226
|
+
if at and e.attrs.get("via") and "TurboModuleRegistry" in e.attrs["via"]:
|
|
1227
|
+
stem = os.path.splitext(at.split(":")[0].rsplit("/", 1)[-1])[0]
|
|
1228
|
+
nid = e.dst
|
|
1229
|
+
spec_names.setdefault(stem + "Spec", nid.split(":", 2)[2].split("#")[0])
|
|
1230
|
+
objc_plugins, extern, handler_classes, pending = {}, {}, {}, []
|
|
1231
|
+
for nf in nfs:
|
|
1232
|
+
if nf.lang == "objc":
|
|
1233
|
+
scan_objc_capacitor(nf, objc_plugins)
|
|
1234
|
+
recs: list[Receiver] = []
|
|
1235
|
+
cdv = cordova_services(root) if any(re.search(r"CordovaPlugin|CDVPlugin", nf.t.code) for nf in nfs) else {}
|
|
1236
|
+
for nf in nfs:
|
|
1237
|
+
scan_cordova(nf, recs, cdv)
|
|
1238
|
+
scan_capacitor(nf, recs, objc_plugins)
|
|
1239
|
+
scan_react_native(nf, recs, spec_names, extern)
|
|
1240
|
+
scan_expo(nf, recs)
|
|
1241
|
+
scan_flutter(nf, recs, handler_classes, pending)
|
|
1242
|
+
# CAP_PLUGIN / RCT_EXTERN_MODULE registrations whose Swift class was not found: ObjC stubs at the macro
|
|
1243
|
+
swift_cls = {}
|
|
1244
|
+
for nf in nfs:
|
|
1245
|
+
if nf.lang == "swift":
|
|
1246
|
+
for m in SWIFT_CLASS.finditer(nf.t.code):
|
|
1247
|
+
end = nf.t.match(m.end() - 1)
|
|
1248
|
+
funcs = {f.group(1): f.start(1) for f in SWIFT_FUNC.finditer(nf.t.code, m.start(), end)}
|
|
1249
|
+
for nm in {m.group(1), m.group(2)} - {None}:
|
|
1250
|
+
swift_cls.setdefault(nm, (nf, m.group(2), funcs))
|
|
1251
|
+
have = {(r.protocol, r.namespace, r.method, r.file.rel) for r in recs}
|
|
1252
|
+
for cls, (js, methods, nf, pos) in objc_plugins.items():
|
|
1253
|
+
sw = swift_cls.get(cls)
|
|
1254
|
+
for meth in methods:
|
|
1255
|
+
if sw and meth in sw[2]:
|
|
1256
|
+
if ("capacitor", js, meth, sw[0].rel) not in have:
|
|
1257
|
+
recs.append(Receiver("capacitor", js, meth, sw[0], sw[2][meth], fn=meth, cls=sw[1], conf="exact",
|
|
1258
|
+
via="CAP_PLUGIN_METHOD"))
|
|
1259
|
+
else:
|
|
1260
|
+
recs.append(Receiver("capacitor", js, meth, nf, pos, fn=meth, cls=cls, conf="resolved", via="CAP_PLUGIN_METHOD"))
|
|
1261
|
+
for cls, items in extern.items():
|
|
1262
|
+
sw = swift_cls.get(cls)
|
|
1263
|
+
for ns, meth, nf, pos in items:
|
|
1264
|
+
node = None if sw and meth in sw[2] else builder.nodes.get(f"method:{cls}.{meth}")
|
|
1265
|
+
if node is not None and node.file:
|
|
1266
|
+
recs.append(Receiver("react-native", ns, meth, NativeFile(node.file, ""), 0, fn=meth, cls=cls, conf="exact",
|
|
1267
|
+
via="RCT_EXTERN_METHOD", lookup=(node.id, node.line)))
|
|
1268
|
+
elif sw and meth in sw[2]:
|
|
1269
|
+
recs.append(Receiver("react-native", ns, meth, sw[0], sw[2][meth], fn=meth, cls=sw[1], conf="exact",
|
|
1270
|
+
via="RCT_EXTERN_METHOD"))
|
|
1271
|
+
else:
|
|
1272
|
+
recs.append(Receiver("react-native", ns, meth, nf, pos, fn=meth, cls=cls, conf="resolved", via="RCT_EXTERN_METHOD"))
|
|
1273
|
+
# method sites / stream handlers in handler classes registered from another place
|
|
1274
|
+
fam = {"kotlin": "jvm", "java": "jvm", "swift": "apple", "objc": "apple"}
|
|
1275
|
+
for kind, meth, cls, nf, spos in pending:
|
|
1276
|
+
for hk, name, rnf, rpos in handler_classes.get(cls, ()):
|
|
1277
|
+
if kind == "method" and hk == "method" and fam[nf.lang] == fam[rnf.lang]:
|
|
1278
|
+
recs.append(Receiver("flutter", name, meth, nf, spos, conf="resolved", via="setMethodCallHandler(handler class)"))
|
|
1279
|
+
for cls, regs in handler_classes.items():
|
|
1280
|
+
for hk, name, rnf, rpos in regs:
|
|
1281
|
+
if hk != "event":
|
|
1282
|
+
continue
|
|
1283
|
+
target = next(((nf, c) for nf in nfs for c in nf.classes if c[0] == cls and fam[nf.lang] == fam[rnf.lang]), None)
|
|
1284
|
+
if target:
|
|
1285
|
+
nf, c = target
|
|
1286
|
+
lm = re.compile(r"\bonListen\b").search(nf.t.code, c[1], c[2])
|
|
1287
|
+
recs.append(Receiver("flutter-event", name, None, nf, lm.start() if lm else c[1], conf="resolved",
|
|
1288
|
+
via="setStreamHandler(handler class)"))
|
|
1289
|
+
else:
|
|
1290
|
+
recs.append(Receiver("flutter-event", name, None, rnf, rpos, conf="heuristic", via="setStreamHandler"))
|
|
1291
|
+
if apis:
|
|
1292
|
+
scan_pigeon_hosts(nfs, apis, recs)
|
|
1293
|
+
# receivers -> handler nodes
|
|
1294
|
+
by_file = defaultdict(list)
|
|
1295
|
+
for n in builder.nodes.values():
|
|
1296
|
+
if n.file and n.kind in ("method", "function") and n.line:
|
|
1297
|
+
by_file[n.file].append(n)
|
|
1298
|
+
pigeon_sends = pigeon_native_sends(builder, by_file, nfs, apis) if apis else 0
|
|
1299
|
+
flutter_sends = flutter_native_sends(builder, by_file, nfs)
|
|
1300
|
+
dynamic = list(getattr(builder, "bridge_dynamic", None) or [])
|
|
1301
|
+
event_sends = event_native_sends(builder, by_file, nfs, recs, dynamic, jvm_other)
|
|
1302
|
+
n_recv, n_stub, marked = 0, 0, set()
|
|
1303
|
+
stats = defaultdict(lambda: defaultdict(int))
|
|
1304
|
+
seen = set()
|
|
1305
|
+
for r in recs:
|
|
1306
|
+
nf = r.file
|
|
1307
|
+
if r.lookup: # resolved to a language plugin node directly
|
|
1308
|
+
hid, line = r.lookup
|
|
1309
|
+
else:
|
|
1310
|
+
line = nf.t.line(r.pos)
|
|
1311
|
+
hid = _handler(builder, by_file, r, nf, line)
|
|
1312
|
+
if hid is None:
|
|
1313
|
+
continue
|
|
1314
|
+
if builder.nodes[hid].attrs.get("bridge_stub"):
|
|
1315
|
+
n_stub += 1
|
|
1316
|
+
plat, why = _platform_of(nf.rel, nf.lang)
|
|
1317
|
+
key = (r.protocol, r.namespace, r.method, hid)
|
|
1318
|
+
if key in seen:
|
|
1319
|
+
continue
|
|
1320
|
+
seen.add(key)
|
|
1321
|
+
protocol_receive(builder, r.protocol, r.namespace, r.method, hid, nf.rel, line, r.conf, platform=plat, via=r.via,
|
|
1322
|
+
api=r.api)
|
|
1323
|
+
if r.api:
|
|
1324
|
+
builder.nodes["endpoint:" + endpoint_key(r.protocol, r.namespace, r.method)].attrs["api"] = r.api
|
|
1325
|
+
stats[r.protocol]["receivers"] += 1
|
|
1326
|
+
n_recv += 1
|
|
1327
|
+
if nf.rel not in marked:
|
|
1328
|
+
marked.add(nf.rel)
|
|
1329
|
+
mark(builder, nf.rel, 1, BIG, Cond("tree", _plat_atom(plat), why), line=1)
|
|
1330
|
+
builder.platform_marks[-1]["bridge"] = True
|
|
1331
|
+
for e in sends:
|
|
1332
|
+
stats[e.attrs.get("protocol") or e.dst.split(":", 2)[1]]["sends" if e.kind == "SENDS_TO" else "test_sends"] += 1
|
|
1333
|
+
if pigeon_sends:
|
|
1334
|
+
stats["pigeon"]["native_sends"] += pigeon_sends
|
|
1335
|
+
if flutter_sends:
|
|
1336
|
+
stats["flutter"]["native_sends"] += flutter_sends
|
|
1337
|
+
for proto, k in event_sends.items():
|
|
1338
|
+
stats[proto]["native_sends"] += k
|
|
1339
|
+
out = {"native_files_scanned": len(nfs), "receivers": n_recv, "stub_nodes": n_stub,
|
|
1340
|
+
"per_protocol": {k: dict(v) for k, v in sorted(stats.items())}}
|
|
1341
|
+
if dynamic:
|
|
1342
|
+
out["dynamic"] = {"count": len(dynamic), "samples": dynamic[:20]}
|
|
1343
|
+
return out
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
_DECL_RX = {
|
|
1347
|
+
"java": re.compile(r"(?:(?:public|private|protected|static|final|synchronized|native|abstract)\s+)+[\w<>\[\],.?]+\s+(\w+)\s*"
|
|
1348
|
+
r"\([^;{)]*\)\s*(?:throws\s+[\w., ]+)?\{"),
|
|
1349
|
+
"objc": re.compile(r"^\s*[-+]\s*\([^)]*\)\s*(\w+)[^{;]*\{", re.M),
|
|
1350
|
+
"swift": re.compile(r"\bfunc\s+(\w+)\s*(?:<[^>]*>)?\s*\([^{]*\{"),
|
|
1351
|
+
"kotlin": re.compile(r"\bfun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?(\w+)\s*\([^{=]*\{"),
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def _enclosing_decl(nf: NativeFile, pos: int) -> tuple[str, int, int] | None:
|
|
1356
|
+
"""Enclosing Java / ObjC method (name, start, end) for receivers in files without a language plugin."""
|
|
1357
|
+
t = nf.t
|
|
1358
|
+
rx = _DECL_RX[nf.lang]
|
|
1359
|
+
best = None
|
|
1360
|
+
for m in rx.finditer(t.code, 0, pos + 1):
|
|
1361
|
+
end = t.match(m.end() - 1)
|
|
1362
|
+
if m.start() <= pos <= end or m.start(1) <= pos <= m.end(1):
|
|
1363
|
+
best = (m.group(1), m.start(), end)
|
|
1364
|
+
return best
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def _handler(builder, by_file, r: Receiver, nf: NativeFile, line: int) -> str | None:
|
|
1368
|
+
cands = by_file.get(nf.rel, [])
|
|
1369
|
+
if r.fn:
|
|
1370
|
+
hit = [n for n in cands if n.name == r.fn and n.line <= line <= (n.end_line or n.line)]
|
|
1371
|
+
hit = hit or [n for n in cands if n.name == r.fn and abs(n.line - line) <= 3]
|
|
1372
|
+
if hit:
|
|
1373
|
+
return min(hit, key=lambda n: (n.end_line or n.line) - n.line).id
|
|
1374
|
+
else:
|
|
1375
|
+
hit = [n for n in cands if n.line <= line <= (n.end_line or n.line)]
|
|
1376
|
+
if hit:
|
|
1377
|
+
return min(hit, key=lambda n: (n.end_line or n.line) - n.line).id
|
|
1378
|
+
# no language plugin node (Java, ObjC, or the Kotlin / Swift plugin did not run): a stub method node
|
|
1379
|
+
fn, lo, hi = r.fn, r.pos, r.pos
|
|
1380
|
+
d = _enclosing_decl(nf, r.pos)
|
|
1381
|
+
if d and (not r.fn or d[0] == r.fn or nf.lang == "java"):
|
|
1382
|
+
fn, lo, hi = r.fn or d[0], d[1], d[2]
|
|
1383
|
+
if not fn:
|
|
1384
|
+
return None
|
|
1385
|
+
c = nf.class_at(r.pos)
|
|
1386
|
+
cls = r.cls or (c[0] if c else None)
|
|
1387
|
+
if nf.lang == "objc":
|
|
1388
|
+
fqn = f"objc:{cls}.{fn}" if cls else f"objc:{nf.rel}#{fn}"
|
|
1389
|
+
elif nf.lang == "swift":
|
|
1390
|
+
fqn = f"{cls}.{fn}" if cls else f"{nf.rel}#{fn}"
|
|
1391
|
+
else:
|
|
1392
|
+
fqn = ".".join(x for x in (nf.package, cls, fn) if x)
|
|
1393
|
+
nid = builder.add_node("method", fqn, fn, fqn=fqn, file=nf.rel, line=nf.t.line(lo), end_line=nf.t.line(hi), lang=nf.lang,
|
|
1394
|
+
attrs={"bridge_stub": True})
|
|
1395
|
+
by_file[nf.rel].append(builder.nodes[nid])
|
|
1396
|
+
return nid
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def project_targets(project, builder) -> list[str]:
|
|
1400
|
+
"""Targets of a project without platform-specific code (platforms.apply returned nothing)."""
|
|
1401
|
+
from .platforms import declared_targets
|
|
1402
|
+
langs = {n.lang for n in builder.nodes.values() if n.lang}
|
|
1403
|
+
try:
|
|
1404
|
+
return declared_targets(Path(project.root), project.options.get("config") or {}, [], langs)[0]
|
|
1405
|
+
except Exception: # noqa: BLE001
|
|
1406
|
+
return []
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _process_roles(builder, eps, recv, send) -> None:
|
|
1410
|
+
"""Electron / Tauri: process role of each file taking part in IPC (attrs.process on its module node): main,
|
|
1411
|
+
preload (exposes a context bridge), renderer (Electron), webview / core (Tauri)."""
|
|
1412
|
+
roles = defaultdict(set)
|
|
1413
|
+
for nid, n in eps.items():
|
|
1414
|
+
if n.attrs.get("protocol") not in PROCESS_PROTOCOLS:
|
|
1415
|
+
continue
|
|
1416
|
+
for e in recv[nid]:
|
|
1417
|
+
dn = builder.nodes.get(e.dst)
|
|
1418
|
+
if dn and dn.file and e.attrs.get("process"):
|
|
1419
|
+
roles[dn.file].add(e.attrs["process"])
|
|
1420
|
+
for e in send[nid]:
|
|
1421
|
+
sn = builder.nodes.get(e.src)
|
|
1422
|
+
if sn and sn.file and e.attrs.get("process"):
|
|
1423
|
+
roles[sn.file].add(e.attrs["process"])
|
|
1424
|
+
if not roles:
|
|
1425
|
+
return
|
|
1426
|
+
for n in builder.nodes.values():
|
|
1427
|
+
if n.kind in ("module", "file", "mod", "crate") and n.file in roles:
|
|
1428
|
+
r = roles[n.file]
|
|
1429
|
+
n.attrs["process"] = "preload" if "preload" in r else next(iter(r)) if len(r) == 1 else "+".join(sorted(r))
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def finalize(project, builder, st: dict, targets: list[str] | None) -> dict:
|
|
1433
|
+
"""Per endpoint: platforms receiving it, sides seen, checks. Called after platforms.apply (targets known)."""
|
|
1434
|
+
eps = {nid: n for nid, n in builder.nodes.items() if n.kind == "endpoint" and n.attrs.get("protocol") in PROTOCOLS}
|
|
1435
|
+
if not eps:
|
|
1436
|
+
return st
|
|
1437
|
+
recv = defaultdict(list)
|
|
1438
|
+
send = defaultdict(list)
|
|
1439
|
+
tsend = defaultdict(list)
|
|
1440
|
+
for e in builder.edges.values():
|
|
1441
|
+
if e.kind == "RECEIVED_BY" and e.src in eps:
|
|
1442
|
+
recv[e.src].append(e)
|
|
1443
|
+
elif e.kind == "SENDS_TO" and e.dst in eps:
|
|
1444
|
+
send[e.dst].append(e)
|
|
1445
|
+
elif e.kind == "TEST_CALLS" and e.attrs.get("orig") == "SENDS_TO" and e.dst in eps:
|
|
1446
|
+
tsend[e.dst].append(e)
|
|
1447
|
+
ns_plat = defaultdict(set) # (protocol, namespace) -> platforms implementing any method of it
|
|
1448
|
+
for nid, es in recv.items():
|
|
1449
|
+
a = eps[nid].attrs
|
|
1450
|
+
for e in es:
|
|
1451
|
+
ns_plat[(a["protocol"], a["namespace"])].add(e.attrs.get("platform"))
|
|
1452
|
+
ev_sent = defaultdict(set) # event protocol -> namespaces some native code emits on (None: any)
|
|
1453
|
+
for nid, es in send.items():
|
|
1454
|
+
a = eps[nid].attrs
|
|
1455
|
+
if a["protocol"] in EVENT_PROTOCOLS and es:
|
|
1456
|
+
ev_sent[a["protocol"]].update((None, a["namespace"]))
|
|
1457
|
+
proto_recv = defaultdict(set)
|
|
1458
|
+
for nid in recv:
|
|
1459
|
+
proto_recv[eps[nid].attrs["protocol"]].add(None)
|
|
1460
|
+
_process_roles(builder, eps, recv, send)
|
|
1461
|
+
# macOS counts only where some bridge module is implemented for it (a Flutter macos/ runner)
|
|
1462
|
+
used = {e.attrs.get("platform") for es in recv.values() for e in es}
|
|
1463
|
+
mobile = [p for p in (targets or []) if p in ("android", "ios") or (p == "macos" and p in used)]
|
|
1464
|
+
checks = defaultdict(int)
|
|
1465
|
+
root = Path(project.root)
|
|
1466
|
+
pj_cache: dict = {}
|
|
1467
|
+
for nid, n in eps.items():
|
|
1468
|
+
a = n.attrs
|
|
1469
|
+
rp = sorted({e.attrs.get("platform") for e in recv[nid]} - {None})
|
|
1470
|
+
a["platforms_received"] = rp
|
|
1471
|
+
a["side"] = "both" if send[nid] and recv[nid] else "send" if send[nid] or tsend[nid] else "receive"
|
|
1472
|
+
# native -> app (Pigeon @FlutterApi, a native invokeMethod handled in Dart): the app side receives on every
|
|
1473
|
+
# platform; the platforms are the senders'
|
|
1474
|
+
to_app = any(e.attrs.get("direction") == "to_app" for e in send[nid]) or (
|
|
1475
|
+
recv[nid] and all((builder.nodes.get(e.dst) is not None and builder.nodes[e.dst].lang in APP_LANGS)
|
|
1476
|
+
for e in recv[nid]))
|
|
1477
|
+
if to_app:
|
|
1478
|
+
a["direction"] = "to_app"
|
|
1479
|
+
a["platforms_sending"] = sorted({e.attrs.get("platform") for e in send[nid]} - {None})
|
|
1480
|
+
elif a["protocol"] in ("pigeon", "flutter", "flutter-event", "capacitor", "react-native", "cordova"):
|
|
1481
|
+
a["direction"] = "to_native"
|
|
1482
|
+
a.pop("checks", None)
|
|
1483
|
+
ck = []
|
|
1484
|
+
proc = a["protocol"] in PROCESS_PROTOCOLS
|
|
1485
|
+
impl = ns_plat.get((a["protocol"], a["namespace"]))
|
|
1486
|
+
if proc:
|
|
1487
|
+
# one app, two processes: a receiver-less channel is a miss when the other side is in the repo at all
|
|
1488
|
+
ext_pkg = any(e.attrs.get("external") for e in send[nid] + tsend[nid])
|
|
1489
|
+
impl = None if ext_pkg else (proto_recv.get(a["protocol"]) or None)
|
|
1490
|
+
base = a.get("method") in BASE_METHODS.get(a["protocol"], ())
|
|
1491
|
+
if base:
|
|
1492
|
+
a["base_method"] = True # the framework's base class implements it on every platform; a module may override
|
|
1493
|
+
if a["protocol"] in EVENT_PROTOCOLS and (send[nid] or tsend[nid]) and not recv[nid]:
|
|
1494
|
+
a["no_listener"] = True # emitted natively, no JS listener found (one may sit in a library / dynamic name)
|
|
1495
|
+
elif (send[nid] or tsend[nid]) and not recv[nid]:
|
|
1496
|
+
if impl:
|
|
1497
|
+
if not base:
|
|
1498
|
+
ck.append("no_receiver")
|
|
1499
|
+
else:
|
|
1500
|
+
a["external"] = True
|
|
1501
|
+
ext = sorted({e.attrs.get("external") for e in send[nid] + tsend[nid]} - {None})
|
|
1502
|
+
if ext:
|
|
1503
|
+
a["package"] = ext[0]
|
|
1504
|
+
if a["protocol"] in EVENT_PROTOCOLS and recv[nid] and not send[nid]:
|
|
1505
|
+
# a listener for an event no native code in the repo emits: a miss only where the repo's native side
|
|
1506
|
+
# emits events at all (Capacitor: the same plugin's native code is here); else a library's event
|
|
1507
|
+
if a["protocol"] == "capacitor-event":
|
|
1508
|
+
here = a["namespace"] in ev_sent.get(a["protocol"], ()) or ns_plat.get(("capacitor", a["namespace"]))
|
|
1509
|
+
else:
|
|
1510
|
+
# NativeEventEmitter(Module) of a module implemented here; DeviceEventEmitter: where the repo's native
|
|
1511
|
+
# code emits events at all
|
|
1512
|
+
mods = {e.attrs.get("emitter_module") for e in recv[nid]}
|
|
1513
|
+
here = any(m and ns_plat.get(("react-native", m)) for m in mods)
|
|
1514
|
+
if not here and not any(mods):
|
|
1515
|
+
# DeviceEventEmitter / an emitter of an unknown module: often an in-app JS event bus whose emits
|
|
1516
|
+
# pass the name in a variable; not a bridge miss, not a library either
|
|
1517
|
+
base = True
|
|
1518
|
+
here = True
|
|
1519
|
+
if not here:
|
|
1520
|
+
a["external"] = True
|
|
1521
|
+
ext = sorted({e.attrs.get("external") for e in recv[nid]} - {None})
|
|
1522
|
+
if ext:
|
|
1523
|
+
a["package"] = ext[0]
|
|
1524
|
+
base = True
|
|
1525
|
+
if recv[nid] and not send[nid] and not base:
|
|
1526
|
+
ck.append("no_sender" if not tsend[nid] else "test_sender_only")
|
|
1527
|
+
if a.get("unregistered"):
|
|
1528
|
+
ck.append("unregistered")
|
|
1529
|
+
if recv[nid] and impl and not base and not proc and not to_app:
|
|
1530
|
+
expected = mobile or sorted(impl - {None})
|
|
1531
|
+
if a["protocol"] == "react-native":
|
|
1532
|
+
declared = set()
|
|
1533
|
+
for e in recv[nid]:
|
|
1534
|
+
dn = builder.nodes.get(e.dst)
|
|
1535
|
+
cp = dn and dn.file and _codegen_platforms(root, dn.file, pj_cache)
|
|
1536
|
+
if not cp:
|
|
1537
|
+
declared = None
|
|
1538
|
+
break
|
|
1539
|
+
declared |= set(cp)
|
|
1540
|
+
if declared:
|
|
1541
|
+
expected = [p for p in expected if p in declared]
|
|
1542
|
+
# Flutter: only the platform folders of the sending app's package (an iOS-only sample has no android/)
|
|
1543
|
+
if a["protocol"] in ("flutter", "flutter-event", "pigeon") and send[nid]:
|
|
1544
|
+
folders = [_flutter_folders(root, e.file, pj_cache) for e in send[nid]]
|
|
1545
|
+
if all(f is not None for f in folders):
|
|
1546
|
+
have = set().union(*folders)
|
|
1547
|
+
expected = [p for p in expected if p in have]
|
|
1548
|
+
# every sender is gated to some platforms (`Platform.OS === 'android'`, kIsWeb, ...): only those need it
|
|
1549
|
+
gated = [e.attrs.get("platforms") for e in send[nid]]
|
|
1550
|
+
if gated and all(g is not None for g in gated): # [] : on none of the targets (#74)
|
|
1551
|
+
only = sorted({p for g in gated for p in g})
|
|
1552
|
+
a["sender_platforms"] = only
|
|
1553
|
+
expected = [p for p in expected if p in only]
|
|
1554
|
+
miss = [p for p in expected if p not in rp]
|
|
1555
|
+
if miss:
|
|
1556
|
+
a["missing_on"] = miss
|
|
1557
|
+
ck.append("missing_on")
|
|
1558
|
+
else:
|
|
1559
|
+
a.pop("missing_on", None)
|
|
1560
|
+
if ck:
|
|
1561
|
+
a["checks"] = ck
|
|
1562
|
+
for c in ck:
|
|
1563
|
+
checks[c] += 1
|
|
1564
|
+
if a.get("external"):
|
|
1565
|
+
checks["external"] += 1
|
|
1566
|
+
st = dict(st or {})
|
|
1567
|
+
st["endpoints"] = len(eps)
|
|
1568
|
+
st["linked"] = sum(1 for nid in eps if send[nid] and recv[nid])
|
|
1569
|
+
st["checks"] = dict(sorted(checks.items()))
|
|
1570
|
+
st["targets"] = mobile
|
|
1571
|
+
return st
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
# ------------------------------------------------------------------ query: cg bridges / MCP bridges
|
|
1575
|
+
def _attrs(r) -> dict:
|
|
1576
|
+
return json.loads(r["attrs"] or "{}") if r["attrs"] else {}
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def bridges(st, pattern: str | None = None, protocol: str | None = None, unmatched: bool = False) -> dict:
|
|
1580
|
+
"""Bridge endpoints with their senders (JS / Dart call sites, entry points reaching them) and receivers per
|
|
1581
|
+
platform, plus the checks (missing_on, no_receiver, no_sender, external)."""
|
|
1582
|
+
rows = [dict(r) for r in st.q("SELECT * FROM nodes WHERE kind='endpoint' ORDER BY id")]
|
|
1583
|
+
out = []
|
|
1584
|
+
for n in rows:
|
|
1585
|
+
a = _attrs(n)
|
|
1586
|
+
if a.get("protocol") not in PROTOCOLS or (protocol and a["protocol"] != protocol):
|
|
1587
|
+
continue
|
|
1588
|
+
name = n["name"]
|
|
1589
|
+
if pattern:
|
|
1590
|
+
p = pattern[len("endpoint:"):] if pattern.startswith("endpoint:") else pattern
|
|
1591
|
+
if not (fnmatch.fnmatchcase(name, p) or fnmatch.fnmatchcase(n["id"].split(":", 1)[1], p)
|
|
1592
|
+
or p.lower() in name.lower()):
|
|
1593
|
+
continue
|
|
1594
|
+
if unmatched and not (a.get("checks") or a.get("external")):
|
|
1595
|
+
continue
|
|
1596
|
+
item = {"id": n["id"], "protocol": a["protocol"], "namespace": a.get("namespace"), "method": a.get("method"),
|
|
1597
|
+
"api": a.get("api"), "platforms_received": a.get("platforms_received") or [],
|
|
1598
|
+
"checks": a.get("checks") or [], "missing_on": a.get("missing_on") or [], "external": bool(a.get("external")),
|
|
1599
|
+
"package": a.get("package"), "test_only": bool(a.get("test_only")),
|
|
1600
|
+
"base_method": bool(a.get("base_method")), "sender_platforms": a.get("sender_platforms"),
|
|
1601
|
+
"direction": a.get("direction"), "platforms_sending": a.get("platforms_sending") or [],
|
|
1602
|
+
"no_listener": bool(a.get("no_listener")), "senders": [], "test_senders": [],
|
|
1603
|
+
"receivers": []}
|
|
1604
|
+
for r in st.q("SELECT src, kind, file, line, confidence, attrs FROM edges WHERE dst=? AND kind IN ('SENDS_TO','TEST_CALLS') "
|
|
1605
|
+
"ORDER BY file, line", (n["id"],)):
|
|
1606
|
+
ea = _attrs(r)
|
|
1607
|
+
ents = {e["entry_kind"]: e["entry_count"] for e in
|
|
1608
|
+
st.q("SELECT entry_kind, entry_count FROM node_entry WHERE node_id=?", (r["src"],))}
|
|
1609
|
+
s = {"fn": r["src"], "at": f"{r['file']}:{r['line']}", "confidence": r["confidence"], "via": ea.get("via"),
|
|
1610
|
+
"entry_kinds": ents}
|
|
1611
|
+
if ea.get("process"):
|
|
1612
|
+
s["process"] = ea["process"]
|
|
1613
|
+
(item["senders"] if r["kind"] == "SENDS_TO" else item["test_senders"]).append(s)
|
|
1614
|
+
for r in st.q("SELECT dst, file, line, confidence, attrs FROM edges WHERE src=? AND kind='RECEIVED_BY' ORDER BY file, line",
|
|
1615
|
+
(n["id"],)):
|
|
1616
|
+
ea = _attrs(r)
|
|
1617
|
+
item["receivers"].append({"handler": r["dst"], "at": f"{r['file']}:{r['line']}", "platform": ea.get("platform"),
|
|
1618
|
+
"via": ea.get("via"), "confidence": r["confidence"],
|
|
1619
|
+
"stub": bool(_attrs(st.node(r["dst"]) or {"attrs": None}).get("bridge_stub")),
|
|
1620
|
+
**({"process": ea["process"]} if ea.get("process") else {})})
|
|
1621
|
+
out.append(item)
|
|
1622
|
+
summ = defaultdict(lambda: defaultdict(int))
|
|
1623
|
+
for i in out:
|
|
1624
|
+
s = summ[i["protocol"]]
|
|
1625
|
+
s["endpoints"] += 1
|
|
1626
|
+
s["linked"] += bool(i["senders"] and i["receivers"])
|
|
1627
|
+
for c in i["checks"]:
|
|
1628
|
+
s[c] += 1
|
|
1629
|
+
s["external"] += i["external"]
|
|
1630
|
+
stats = (st.meta().get("stats") or {}).get("bridges") or {}
|
|
1631
|
+
# bridge calls cg could not name (NativeModules[x], addListener(evtVar), cordova.exec(.., svc, act)): listed, not linked
|
|
1632
|
+
dyn = [d for d in ((stats.get("dynamic") or {}).get("samples") or [])
|
|
1633
|
+
if (not protocol or d.get("protocol") == protocol or (d.get("protocol") or "").startswith(protocol + "-"))
|
|
1634
|
+
and (not pattern or pattern.lower() in (d.get("what") or "").lower() or pattern.lower() in (d.get("file") or "").lower())]
|
|
1635
|
+
return {"pattern": pattern, "protocol": protocol, "endpoints": out,
|
|
1636
|
+
"summary": {k: dict(v) for k, v in sorted(summ.items())},
|
|
1637
|
+
"unresolved": dyn, "unresolved_total": (stats.get("dynamic") or {}).get("count", 0) if not (protocol or pattern) else len(dyn),
|
|
1638
|
+
"stats": stats}
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def render_bridges(res: dict, max_items: int = 60) -> str:
|
|
1642
|
+
from .query import short_id
|
|
1643
|
+
eps = res["endpoints"]
|
|
1644
|
+
dyn = res.get("unresolved") or []
|
|
1645
|
+
if not eps and not dyn:
|
|
1646
|
+
return "no bridge endpoint matches " + repr(res["pattern"]) if res["pattern"] else "no web / native bridge calls in this graph"
|
|
1647
|
+
L = []
|
|
1648
|
+
for p, s in res["summary"].items():
|
|
1649
|
+
extra = ", ".join(f"{k} {v}" for k, v in s.items() if k not in ("endpoints", "linked") and v)
|
|
1650
|
+
L.append(f"{p}: {s['endpoints']} endpoint(s), {s.get('linked', 0)} linked" + (f" ({extra})" if extra else ""))
|
|
1651
|
+
tg = (res.get("stats") or {}).get("targets")
|
|
1652
|
+
if tg:
|
|
1653
|
+
L.append(f"mobile targets: {', '.join(tg)}")
|
|
1654
|
+
detail = len(eps) <= 6
|
|
1655
|
+
for i in eps[:max_items]:
|
|
1656
|
+
flags = []
|
|
1657
|
+
proc = i["protocol"] in PROCESS_PROTOCOLS
|
|
1658
|
+
if i["missing_on"]:
|
|
1659
|
+
flags.append("MISSING ON " + ", ".join(i["missing_on"]))
|
|
1660
|
+
for c in i["checks"]:
|
|
1661
|
+
if c == "no_receiver":
|
|
1662
|
+
flags.append("NO RECEIVER (this app handles other channels / commands, not this one)" if proc else
|
|
1663
|
+
"NO NATIVE RECEIVER (the module is implemented here, this method is not)")
|
|
1664
|
+
elif c == "no_sender":
|
|
1665
|
+
flags.append("no sender" if proc else "no native sender" if i.get("direction") == "to_app" and
|
|
1666
|
+
i["protocol"] in EVENT_PROTOCOLS else "no JS / Dart sender")
|
|
1667
|
+
elif c == "unregistered":
|
|
1668
|
+
flags.append("NOT REGISTERED (missing from generate_handler!)")
|
|
1669
|
+
elif c == "test_sender_only":
|
|
1670
|
+
flags.append("sent from tests only")
|
|
1671
|
+
if i.get("no_listener"):
|
|
1672
|
+
flags.append("no JS listener found")
|
|
1673
|
+
if i["external"]:
|
|
1674
|
+
flags.append("external" + (f" ({i['package']})" if i.get("package") else ": implemented outside this repo"))
|
|
1675
|
+
where = (f"received in: {', '.join(sorted({r.get('process') or '?' for r in i['receivers']})) or '-'}" if proc else
|
|
1676
|
+
f"sent from: {', '.join(i['platforms_sending']) or '-'}" if i["protocol"] in EVENT_PROTOCOLS else
|
|
1677
|
+
f"received on: {', '.join(i['platforms_received']) or '-'}")
|
|
1678
|
+
L.append(f"{i['id'].split(':', 1)[1]} {where}"
|
|
1679
|
+
+ (f" [{i['api']}]" if i.get("api") else "") + (f" ! {'; '.join(flags)}" if flags else ""))
|
|
1680
|
+
if detail:
|
|
1681
|
+
for s in i["senders"][:8]:
|
|
1682
|
+
ek = ", ".join(f"{k}({v})" for k, v in sorted(s["entry_kinds"].items()))
|
|
1683
|
+
L.append(f" sent by {short_id(s['fn'])} @ {s['at']} [{s['confidence']}"
|
|
1684
|
+
+ (f", {s['process']}" if s.get("process") else "") + "]" + (f" entries: {ek}" if ek else ""))
|
|
1685
|
+
for s in i["test_senders"][:4]:
|
|
1686
|
+
L.append(f" test {short_id(s['fn'])} @ {s['at']}")
|
|
1687
|
+
for r in i["receivers"]:
|
|
1688
|
+
L.append(f" received by {short_id(r['handler'])} @ {r['at']} [{r.get('process') or r['platform']}, {r['via']}]"
|
|
1689
|
+
+ (" (stub: no language plugin)" if r["stub"] else ""))
|
|
1690
|
+
else:
|
|
1691
|
+
L.append(f" senders {len(i['senders'])}" + (f" (+{len(i['test_senders'])} test)" if i["test_senders"] else "")
|
|
1692
|
+
+ f", receivers {len(i['receivers'])}")
|
|
1693
|
+
if len(eps) > max_items:
|
|
1694
|
+
L.append(f"... {len(eps) - max_items} more")
|
|
1695
|
+
if dyn:
|
|
1696
|
+
L.append(f"unresolved (name not a literal cg can evaluate): {res.get('unresolved_total') or len(dyn)}")
|
|
1697
|
+
for d in dyn[:12]:
|
|
1698
|
+
L.append(f" {d['protocol']}: {d['what']} @ {d['file']}:{d['line']}")
|
|
1699
|
+
if len(dyn) > 12:
|
|
1700
|
+
L.append(f" ... {len(dyn) - 12} more")
|
|
1701
|
+
return "\n".join(L)
|