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/platforms.py
ADDED
|
@@ -0,0 +1,1369 @@
|
|
|
1
|
+
"""Platform-specific code and multi-target builds: platform tags, the --platform filter and the divergence query.
|
|
2
|
+
|
|
3
|
+
One source tree often builds several targets (Windows / Linux / macOS from one Rust or C code base, iOS / Android /
|
|
4
|
+
web from one Flutter or React Native app). The index records which targets each piece of code exists on:
|
|
5
|
+
|
|
6
|
+
node.attrs / edge.attrs platforms targets the code (or the reference) exists on; absent = every target
|
|
7
|
+
platform_expr the source condition(s), e.g. `cfg(windows)`, `#if defined(__APPLE__)`
|
|
8
|
+
platform_at file:line of the condition
|
|
9
|
+
platform_unknown targets where part of the condition could not be evaluated (kept live)
|
|
10
|
+
|
|
11
|
+
Each language layer adds the conditions of its own idioms (see docs/platforms.md):
|
|
12
|
+
|
|
13
|
+
Rust #[cfg(...)] on items, `mod` declarations, statements and `#![cfg]`; `if cfg!(...)` branches
|
|
14
|
+
C / C++ #if / #ifdef / #elif regions over the platform macros (_WIN32, __APPLE__ + TARGET_OS_*, __linux__,
|
|
15
|
+
__ANDROID__, __EMSCRIPTEN__, BSDs); platform directories and file names (src/win/, unix/, foo_linux.c)
|
|
16
|
+
Dart conditional imports and exports (`import 'a.dart' if (dart.library.io) 'b.dart'`),
|
|
17
|
+
Platform.isX / kIsWeb / defaultTargetPlatform branches, ternaries and switches
|
|
18
|
+
TypeScript React Native / Expo platform files (.ios.ts, .android.ts, .native.ts, .web.ts, also tsx / js / jsx),
|
|
19
|
+
Platform.OS checks, Platform.select({...}), Node / Electron process.platform checks
|
|
20
|
+
any .cg.yaml platforms.paths globs
|
|
21
|
+
|
|
22
|
+
A condition is evaluated three-valued for every known target (True / False / unknown): a part cg cannot evaluate
|
|
23
|
+
(a feature flag, `HAVE_SOUND`, an arch check) keeps the code live on that target and is listed as unknown.
|
|
24
|
+
|
|
25
|
+
Variant implementations (one symbol defined per platform: `#[cfg(windows)] fn config_dir` + `#[cfg(unix)] fn
|
|
26
|
+
config_dir`, storage.ios.ts + storage.android.ts, a conditional-import stub and its io / web libraries) are grouped:
|
|
27
|
+
every reference to one variant also reaches the others, each tagged with its own platforms, so `impact` / `reaches`
|
|
28
|
+
on any target follow the implementation that target builds.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import fnmatch
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
from collections import defaultdict
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from . import presets
|
|
39
|
+
|
|
40
|
+
# dependency / VCS directories every walk skips (codegraph/presets/common.yaml skip_dirs)
|
|
41
|
+
COMMON_SKIP = presets.skip_dirs("common")
|
|
42
|
+
|
|
43
|
+
KNOWN = ("windows", "linux", "macos", "ios", "android", "web", "tvos", "watchos", "visionos")
|
|
44
|
+
DESKTOP = ("windows", "linux", "macos")
|
|
45
|
+
APPLE = ("macos", "ios", "tvos", "watchos", "visionos")
|
|
46
|
+
ALIASES = {"win": "windows", "win32": "windows", "win64": "windows", "darwin": "macos", "mac": "macos", "osx": "macos",
|
|
47
|
+
"macosx": "macos", "iphoneos": "ios", "iphone": "ios", "ipados": "ios", "browser": "web", "wasm": "web",
|
|
48
|
+
"wasm32": "web", "js": "web", "emscripten": "web", "appletvos": "tvos", "xros": "visionos",
|
|
49
|
+
"visionos": "visionos", "tvos": "tvos", "watchos": "watchos"}
|
|
50
|
+
APPLE_SRC = re.compile(r"\.(swift|c|h|cc|cpp|cxx|hh|hpp|hxx|m|mm)$") # what an Xcode project compiles
|
|
51
|
+
IPHONE_FAMILY = {"ios", "tvos", "watchos", "visionos"}
|
|
52
|
+
UNIX = ("linux", "macos", "ios", "android", "tvos", "watchos", "visionos")
|
|
53
|
+
BIG = 10 ** 9
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def norm(name: str | None) -> str | None:
|
|
57
|
+
"""'Darwin' -> 'macos', 'win32' -> 'windows', 'ios' -> 'ios'; None for a name that is not a known target."""
|
|
58
|
+
if not name:
|
|
59
|
+
return None
|
|
60
|
+
n = str(name).strip().lower()
|
|
61
|
+
n = ALIASES.get(n, n)
|
|
62
|
+
return n if n in KNOWN else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_platform(name: str) -> str:
|
|
66
|
+
"""A --platform value -> known target name; ValueError naming the known targets otherwise."""
|
|
67
|
+
p = norm(name)
|
|
68
|
+
if p is None:
|
|
69
|
+
raise ValueError(f"unknown platform {name!r}: known targets are {', '.join(KNOWN)} "
|
|
70
|
+
f"(aliases: {', '.join(sorted(ALIASES))})")
|
|
71
|
+
return p
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------ evaluation: cfg-style trees
|
|
75
|
+
DART_NATIVE_LIBS = {"io", "ffi", "isolate", "cli", "developer", "nativewrappers", "mirrors"}
|
|
76
|
+
DART_WEB_LIBS = {"html", "js", "js_util", "js_interop", "js_interop_unsafe", "web_audio", "web_gl", "indexed_db", "svg",
|
|
77
|
+
"web_sql"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def eval_atom(key: str, val: str | None, p: str):
|
|
81
|
+
"""One condition atom on target p: True / False / None (unknown)."""
|
|
82
|
+
if key == "platform": # Platform.OS === 'ios', Platform.isIOS, kIsWeb, file suffixes
|
|
83
|
+
v = ALIASES.get((val or "").lower(), (val or "").lower())
|
|
84
|
+
return p == v
|
|
85
|
+
if key == "never": # a condition no known target meets (Swift `os(macOS)` in a Catalyst-only app)
|
|
86
|
+
return False
|
|
87
|
+
if key == "unknown_on": # true or false on that target, false elsewhere (#74)
|
|
88
|
+
return None if p == val else False
|
|
89
|
+
if key == "native": # React Native `.native.ts` / Platform.select native: every non-web target
|
|
90
|
+
return p != "web"
|
|
91
|
+
if key == "target_platform": # Flutter TargetPlatform: on the web it is the browser's OS
|
|
92
|
+
return None if p == "web" else p == val
|
|
93
|
+
if key == "dart_library":
|
|
94
|
+
if val in DART_NATIVE_LIBS:
|
|
95
|
+
return p != "web"
|
|
96
|
+
if val in DART_WEB_LIBS:
|
|
97
|
+
return p == "web"
|
|
98
|
+
return None
|
|
99
|
+
if key == "unix":
|
|
100
|
+
return p in UNIX
|
|
101
|
+
if key == "windows":
|
|
102
|
+
return p == "windows"
|
|
103
|
+
if key == "target_os":
|
|
104
|
+
v = ALIASES.get(val or "", val or "")
|
|
105
|
+
if v in ("emscripten", "unknown", "wasi", "none"):
|
|
106
|
+
return None if p == "web" else False
|
|
107
|
+
return p == v
|
|
108
|
+
if key == "target_family":
|
|
109
|
+
if val == "unix":
|
|
110
|
+
return p in UNIX
|
|
111
|
+
if val == "windows":
|
|
112
|
+
return p == "windows"
|
|
113
|
+
if val == "wasm":
|
|
114
|
+
return p == "web"
|
|
115
|
+
return None
|
|
116
|
+
if key == "target_vendor":
|
|
117
|
+
if val == "apple":
|
|
118
|
+
return p in APPLE
|
|
119
|
+
return None
|
|
120
|
+
if key == "target_arch":
|
|
121
|
+
if val in ("wasm32", "wasm64"):
|
|
122
|
+
return p == "web"
|
|
123
|
+
return False if p == "web" else None
|
|
124
|
+
if key == "target_env" and val == "msvc":
|
|
125
|
+
return None if p == "windows" else False
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def eval_tree(t, p: str):
|
|
130
|
+
k = t[0]
|
|
131
|
+
if k == "atom":
|
|
132
|
+
return eval_atom(t[1], t[2], p)
|
|
133
|
+
if k == "not":
|
|
134
|
+
v = eval_tree(t[1], p)
|
|
135
|
+
return None if v is None else (not v)
|
|
136
|
+
vals = [eval_tree(x, p) for x in t[1]]
|
|
137
|
+
if k == "all":
|
|
138
|
+
if any(v is False for v in vals):
|
|
139
|
+
return False
|
|
140
|
+
return True if all(v is True for v in vals) else None
|
|
141
|
+
if any(v is True for v in vals):
|
|
142
|
+
return True
|
|
143
|
+
return False if vals and all(v is False for v in vals) else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------ evaluation: C preprocessor
|
|
147
|
+
# macro -> value per target ("1" / "0" defined with that value, None = may or may not be defined); every macro in
|
|
148
|
+
# C_MACROS that a target does not list is undefined there
|
|
149
|
+
C_FACTS: dict[str, dict[str, str | None]] = {
|
|
150
|
+
"windows": {"_WIN32": "1", "WIN32": "1", "__WIN32__": "1", "_WINDOWS": None, "_WIN64": None, "_MSC_VER": None,
|
|
151
|
+
"__MINGW32__": None, "__MINGW64__": None, "__CYGWIN__": None, "WINAPI_FAMILY": None},
|
|
152
|
+
"linux": {"__linux__": "1", "__linux": "1", "linux": None, "__gnu_linux__": "1", "__unix__": "1", "__unix": "1",
|
|
153
|
+
"unix": None, "__GLIBC__": None},
|
|
154
|
+
"android": {"__linux__": "1", "__linux": "1", "linux": None, "__ANDROID__": "1", "ANDROID": None, "__BIONIC__": "1",
|
|
155
|
+
"__unix__": "1", "__unix": "1", "unix": None, "__ANDROID_API__": None},
|
|
156
|
+
"macos": {"__APPLE__": "1", "__MACH__": "1", "TARGET_OS_MAC": "1", "TARGET_OS_OSX": "1", "TARGET_OS_IPHONE": "0",
|
|
157
|
+
"TARGET_OS_IOS": "0", "TARGET_OS_SIMULATOR": "0", "TARGET_IPHONE_SIMULATOR": "0", "TARGET_OS_TV": "0",
|
|
158
|
+
"TARGET_OS_WATCH": "0", "TARGET_OS_VISION": "0", "TARGET_OS_MACCATALYST": "0", "__APPLE_CC__": None},
|
|
159
|
+
"ios": {"__APPLE__": "1", "__MACH__": "1", "TARGET_OS_MAC": "1", "TARGET_OS_OSX": "0", "TARGET_OS_IPHONE": "1",
|
|
160
|
+
"TARGET_OS_IOS": "1", "TARGET_OS_SIMULATOR": None, "TARGET_IPHONE_SIMULATOR": None, "TARGET_OS_TV": "0",
|
|
161
|
+
"TARGET_OS_WATCH": "0", "TARGET_OS_VISION": "0", "TARGET_OS_MACCATALYST": None, "__APPLE_CC__": None},
|
|
162
|
+
"tvos": {"__APPLE__": "1", "__MACH__": "1", "TARGET_OS_MAC": "1", "TARGET_OS_OSX": "0", "TARGET_OS_IPHONE": "1",
|
|
163
|
+
"TARGET_OS_IOS": "0", "TARGET_OS_SIMULATOR": None, "TARGET_IPHONE_SIMULATOR": None, "TARGET_OS_TV": "1",
|
|
164
|
+
"TARGET_OS_WATCH": "0", "TARGET_OS_VISION": "0", "TARGET_OS_MACCATALYST": "0", "__APPLE_CC__": None},
|
|
165
|
+
"watchos": {"__APPLE__": "1", "__MACH__": "1", "TARGET_OS_MAC": "1", "TARGET_OS_OSX": "0", "TARGET_OS_IPHONE": "1",
|
|
166
|
+
"TARGET_OS_IOS": "0", "TARGET_OS_SIMULATOR": None, "TARGET_IPHONE_SIMULATOR": None, "TARGET_OS_TV": "0",
|
|
167
|
+
"TARGET_OS_WATCH": "1", "TARGET_OS_VISION": "0", "TARGET_OS_MACCATALYST": "0", "__APPLE_CC__": None},
|
|
168
|
+
"visionos": {"__APPLE__": "1", "__MACH__": "1", "TARGET_OS_MAC": "1", "TARGET_OS_OSX": "0", "TARGET_OS_IPHONE": "1",
|
|
169
|
+
"TARGET_OS_IOS": "0", "TARGET_OS_SIMULATOR": None, "TARGET_IPHONE_SIMULATOR": None, "TARGET_OS_TV": "0",
|
|
170
|
+
"TARGET_OS_WATCH": "0", "TARGET_OS_VISION": "1", "TARGET_OS_MACCATALYST": "0", "__APPLE_CC__": None},
|
|
171
|
+
"web": {"__EMSCRIPTEN__": "1", "EMSCRIPTEN": None, "__wasm__": "1", "__wasm32__": None, "__wasm64__": None,
|
|
172
|
+
"__unix__": "1", "__unix": "1", "unix": None},
|
|
173
|
+
}
|
|
174
|
+
C_OTHER_OS = ("__FreeBSD__", "__OpenBSD__", "__NetBSD__", "__DragonFly__", "__sun", "__sun__", "_AIX", "__HAIKU__",
|
|
175
|
+
"__Fuchsia__", "__QNX__", "__CYGWIN32__", "__MSYS__", "__hpux", "__GNU__", "__OS400__", "__VMS", "__MVS__",
|
|
176
|
+
"__ZEPHYR__", "__redox__")
|
|
177
|
+
C_MACROS = set(C_OTHER_OS) | {m for f in C_FACTS.values() for m in f}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def eval_c(expr: str, p: str):
|
|
181
|
+
from .plugins.native.gates import eval_pp
|
|
182
|
+
facts = C_FACTS[p]
|
|
183
|
+
defines = {m: v for m, v in facts.items() if v is not None}
|
|
184
|
+
undefined = {m for m in C_MACROS if m not in facts}
|
|
185
|
+
return eval_pp(expr, defines, undefined)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def c_relevant(expr: str) -> bool:
|
|
189
|
+
return any(m in C_MACROS for m in re.findall(r"[A-Za-z_]\w*", expr))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------ conditions and marks
|
|
193
|
+
class Cond:
|
|
194
|
+
"""A platform condition: kind 'tree' (cfg-style tree) or 'c' (preprocessor expression), with its source text."""
|
|
195
|
+
__slots__ = ("kind", "expr", "text", "_v")
|
|
196
|
+
|
|
197
|
+
def __init__(self, kind: str, expr, text: str):
|
|
198
|
+
self.kind, self.expr, self.text = kind, expr, text
|
|
199
|
+
self._v = None
|
|
200
|
+
|
|
201
|
+
def values(self) -> dict[str, bool | None]:
|
|
202
|
+
if self._v is None:
|
|
203
|
+
if self.kind == "c":
|
|
204
|
+
self._v = {p: eval_c(self.expr, p) for p in KNOWN}
|
|
205
|
+
else:
|
|
206
|
+
self._v = {p: eval_tree(self.expr, p) for p in KNOWN}
|
|
207
|
+
return self._v
|
|
208
|
+
|
|
209
|
+
def relevant(self) -> bool:
|
|
210
|
+
"""Does the condition tell targets apart (some target False, or True on some and unknown on others)?"""
|
|
211
|
+
v = self.values()
|
|
212
|
+
if any(x is False for x in v.values()):
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def cfg_cond(pred: str, text: str | None = None) -> Cond:
|
|
218
|
+
from .plugins.native.gates import parse_cfg
|
|
219
|
+
return Cond("tree", parse_cfg(pred), text or f"cfg({pred})")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def mark(builder, file: str, start: int, end: int, cond: Cond, line: int | None = None, scol: int | None = None,
|
|
223
|
+
ecol: int | None = None, nodes: bool = True) -> None:
|
|
224
|
+
"""Record that code in file[start:end] (1-based lines, inclusive; optional 0-based columns on the first / last
|
|
225
|
+
line) exists only where `cond` holds. Applied to nodes defined there (nodes=True) and references made there."""
|
|
226
|
+
if not cond.relevant():
|
|
227
|
+
return
|
|
228
|
+
builder.platform_marks.append({"file": file, "start": start, "end": end, "scol": scol, "ecol": ecol, "cond": cond,
|
|
229
|
+
"line": line or start, "nodes": nodes})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _combine(vals: list[dict]) -> dict:
|
|
233
|
+
out = {}
|
|
234
|
+
for p in KNOWN:
|
|
235
|
+
xs = [v[p] for v in vals]
|
|
236
|
+
out[p] = False if any(x is False for x in xs) else (True if all(x is True for x in xs) else None)
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------ file conventions
|
|
241
|
+
RN_TEST_SUFFIX_RE = re.compile(r"^(?P<stem>.+?)\.(?P<plat>ios|android|native|web|windows|macos)[.-](?:test|spec)\.[cm]?[jt]sx?$")
|
|
242
|
+
RN_EXPLICIT_IMPORT_RE = re.compile(r"""(?:from|import|require\s*\()\s*['"](\.{1,2}/[^'"]*\.(?:ios|android|native|web|windows|macos))['"]""")
|
|
243
|
+
RN_SUFFIX_RE = re.compile(r"^(?P<stem>.+?)\.(?P<plat>ios|android|native|web|windows|macos)\.(?P<ext>[cm]?[jt]sx?)$")
|
|
244
|
+
RN_DEPS = ("react-native", "expo", "react-native-web", "react-native-windows", "react-native-macos")
|
|
245
|
+
C_EXT = re.compile(r"\.(c|h|cc|cpp|cxx|hh|hpp|hxx|ipp|inl|m|mm)$")
|
|
246
|
+
PATH_DIRS = {"win": "windows", "win32": "windows", "windows": "windows", "unix": "unix", "posix": "unix",
|
|
247
|
+
"linux": "linux", "darwin": "macos", "macos": "macos", "osx": "macos", "mac": "macos", "ios": "ios",
|
|
248
|
+
"android": "android", "emscripten": "web", "wasm": "web"}
|
|
249
|
+
C_SOURCE_EXT = {"c", "cc", "cpp", "cxx", "m", "mm"}
|
|
250
|
+
PATH_STEM = re.compile(r"(?:^|[_-])(win32|win|windows|unix|posix|linux|darwin|macos|osx|ios|android|emscripten)$", re.I)
|
|
251
|
+
# files named after an OS outside the known targets (src/unix/aix.c, os390.c): built for none of them
|
|
252
|
+
OTHER_OS_STEM = re.compile(r"(?:^|[_-])(aix|sunos|solaris|os390|zos|ibmi|freebsd|openbsd|netbsd|dragonfly|haiku|qnx|"
|
|
253
|
+
r"cygwin|hurd|illumos|pase)$", re.I)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _plat_atom(name: str):
|
|
257
|
+
if name == "other": # an OS outside the known targets
|
|
258
|
+
return ("atom", "platform", "__other__")
|
|
259
|
+
if name == "unix":
|
|
260
|
+
return ("atom", "unix", None)
|
|
261
|
+
if name == "native":
|
|
262
|
+
return ("atom", "native", None)
|
|
263
|
+
return ("atom", "platform", name)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def path_convention(rel: str) -> tuple[str, str] | None:
|
|
267
|
+
"""C / C++ platform directories and file names: ('windows', 'directory win/') for src/win/fs.c."""
|
|
268
|
+
parts = rel.split("/")
|
|
269
|
+
stem, _, ext = parts[-1].rpartition(".")
|
|
270
|
+
if ext.lower() in C_SOURCE_EXT: # a header named after a platform (macos.h) is usually included everywhere
|
|
271
|
+
if OTHER_OS_STEM.search(stem):
|
|
272
|
+
return "other", f"file name {parts[-1]}"
|
|
273
|
+
m = PATH_STEM.search(stem)
|
|
274
|
+
if m and not (m.group(1).lower() in ("unix", "posix") and any(d.lower() in PATH_DIRS for d in parts[:-1])):
|
|
275
|
+
w = m.group(1).lower() # src/unix/linux.c: the file name is narrower than its directory
|
|
276
|
+
return PATH_DIRS.get(w, w), f"file name {parts[-1]}"
|
|
277
|
+
for d in parts[:-1]:
|
|
278
|
+
if d.lower() in PATH_DIRS:
|
|
279
|
+
return PATH_DIRS[d.lower()], f"directory {d}/"
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def uses_react_native(root: Path) -> bool:
|
|
284
|
+
for pj in [root / "package.json", *root.glob("*/package.json"), *root.glob("*/*/package.json")]:
|
|
285
|
+
if not COMMON_SKIP.isdisjoint(pj.parts) or not pj.is_file():
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
d = json.loads(pj.read_text(encoding="utf-8", errors="replace"))
|
|
289
|
+
except (ValueError, OSError):
|
|
290
|
+
continue
|
|
291
|
+
deps = {**(d.get("dependencies") or {}), **(d.get("devDependencies") or {}), **(d.get("peerDependencies") or {})}
|
|
292
|
+
if any(k in deps for k in RN_DEPS):
|
|
293
|
+
return True
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------ targets of the project
|
|
298
|
+
def declared_targets(root: Path, cfg: dict, marks: list, langs: set[str]) -> tuple[list[str], dict[str, str]]:
|
|
299
|
+
"""Targets the project builds: .cg.yaml platforms.targets, else Flutter platform folders, the Expo / React
|
|
300
|
+
Native config, Electron / Tauri (desktop), else the desktop targets plus every target a condition names on its
|
|
301
|
+
own (target_os = "android", __EMSCRIPTEN__). Returns (targets, target -> where it came from)."""
|
|
302
|
+
pc = (cfg or {}).get("platforms") or {}
|
|
303
|
+
if pc.get("targets"):
|
|
304
|
+
return list(pc["targets"]), {t: cfg.get("file", ".cg.yaml") for t in pc["targets"]}
|
|
305
|
+
src: dict[str, str] = {}
|
|
306
|
+
for pub in [root / "pubspec.yaml", *root.glob("*/pubspec.yaml"), *root.glob("*/*/pubspec.yaml")]:
|
|
307
|
+
if not pub.is_file() or "flutter" not in pub.read_text(encoding="utf-8", errors="replace"):
|
|
308
|
+
continue
|
|
309
|
+
for d in ("android", "ios", "web", "macos", "windows", "linux"):
|
|
310
|
+
if (pub.parent / d).is_dir():
|
|
311
|
+
src.setdefault(norm(d), f"Flutter folder {(pub.parent / d).relative_to(root).as_posix()}/")
|
|
312
|
+
for app in [root / "app.json", *root.glob("*/app.json"), *root.glob("*/*/app.json")]:
|
|
313
|
+
if not COMMON_SKIP.isdisjoint(app.parts) or not app.is_file():
|
|
314
|
+
continue
|
|
315
|
+
try:
|
|
316
|
+
ex = (json.loads(app.read_text(encoding="utf-8", errors="replace")) or {}).get("expo")
|
|
317
|
+
except (ValueError, OSError, AttributeError):
|
|
318
|
+
continue
|
|
319
|
+
if isinstance(ex, dict):
|
|
320
|
+
for t in ex.get("platforms") or ["ios", "android"]:
|
|
321
|
+
if norm(t):
|
|
322
|
+
src.setdefault(norm(t), f"{app.relative_to(root).as_posix()} expo.platforms")
|
|
323
|
+
for cap in [*root.glob("capacitor.config.*"), *root.glob("*/capacitor.config.*"), *root.glob("*/*/capacitor.config.*")]:
|
|
324
|
+
if cap.suffix not in (".ts", ".js", ".json") or not COMMON_SKIP.isdisjoint(cap.parts):
|
|
325
|
+
continue
|
|
326
|
+
where = cap.relative_to(root).as_posix()
|
|
327
|
+
for d in ("android", "ios"):
|
|
328
|
+
if (cap.parent / d).is_dir():
|
|
329
|
+
src.setdefault(d, f"Capacitor {d}/ project next to {where}")
|
|
330
|
+
if src:
|
|
331
|
+
src.setdefault("web", f"Capacitor web build ({where})")
|
|
332
|
+
if not src and langs & {"ts", "typescript"} and uses_react_native(root):
|
|
333
|
+
for d in ("ios", "android"):
|
|
334
|
+
src.setdefault(d, "React Native")
|
|
335
|
+
for dep, t in (("react-native-web", "web"), ("react-native-windows", "windows"), ("react-native-macos", "macos")):
|
|
336
|
+
if _has_dep(root, dep):
|
|
337
|
+
src.setdefault(t, f"{dep} dependency")
|
|
338
|
+
if not src: # Kotlin Multiplatform first: its iosApp/ Xcode project is one consumer of it
|
|
339
|
+
_kmp_targets(root, src)
|
|
340
|
+
if not src:
|
|
341
|
+
xcode = _swift_package_targets(root, src)
|
|
342
|
+
if src and not xcode: # SwiftPM platforms: only sets Apple minimums; it builds elsewhere too: the targets
|
|
343
|
+
for m in marks or []: # conditions name (an Xcode app builds only what its project lists)
|
|
344
|
+
if _positive(m["cond"]):
|
|
345
|
+
for p, x in m["cond"].values().items():
|
|
346
|
+
if x is True and p not in src and p in ("linux", "windows", "android", "web"):
|
|
347
|
+
src[p] = f"named by {m['cond'].text} @ {m['file']}:{m['line']}"
|
|
348
|
+
elif src: # an Xcode app next to code in other languages (an android/ module): what that code names
|
|
349
|
+
for m in marks or []:
|
|
350
|
+
only = [p for p, x in m["cond"].values().items() if x is True]
|
|
351
|
+
if (only in (["android"], ["web"]) and only[0] not in src and _positive(m["cond"])
|
|
352
|
+
and not APPLE_SRC.search(m["file"])): # (a build script's win32 check is no app target)
|
|
353
|
+
src[only[0]] = f"named by {m['cond'].text} @ {m['file']}:{m['line']}"
|
|
354
|
+
if not src: # Tauri 2 mobile: src-tauri/gen/android, src-tauri/gen/apple
|
|
355
|
+
for d, t in (("android", "android"), ("apple", "ios")):
|
|
356
|
+
if (root / "src-tauri" / "gen" / d).is_dir():
|
|
357
|
+
src.setdefault(t, f"Tauri mobile project src-tauri/gen/{d}/")
|
|
358
|
+
if src:
|
|
359
|
+
for d in DESKTOP:
|
|
360
|
+
src.setdefault(d, "Tauri desktop app")
|
|
361
|
+
if not src and (_has_dep(root, "electron") or (root / "src-tauri" / "tauri.conf.json").is_file()):
|
|
362
|
+
for d in DESKTOP:
|
|
363
|
+
src.setdefault(d, "Electron / Tauri desktop app")
|
|
364
|
+
if not src and marks and all(m.get("bridge") for m in marks):
|
|
365
|
+
for m in marks: # only native bridge modules are platform-specific: their platforms
|
|
366
|
+
for p, x in m["cond"].values().items():
|
|
367
|
+
if x is True:
|
|
368
|
+
src.setdefault(p, f"native bridge module {m['file']}")
|
|
369
|
+
if not src and marks:
|
|
370
|
+
for d in DESKTOP:
|
|
371
|
+
src[d] = "desktop default (native code conditions)" if langs & {"rust", "c_cpp"} else "desktop default"
|
|
372
|
+
for m in marks:
|
|
373
|
+
v = m["cond"].values()
|
|
374
|
+
only = [p for p, x in v.items() if x is True]
|
|
375
|
+
if "ios" in only and set(only) <= IPHONE_FAMILY:
|
|
376
|
+
only = ["ios"] # TARGET_OS_IPHONE, canImport(UIKit): names iOS (tvOS / watchOS only on their own)
|
|
377
|
+
if len(only) == 1 and only[0] not in src and _positive(m["cond"]):
|
|
378
|
+
src[only[0]] = f"named by {m['cond'].text} @ {m['file']}:{m['line']}"
|
|
379
|
+
order = [p for p in KNOWN if p in src]
|
|
380
|
+
return order, {p: src[p] for p in order}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
KMP_TARGET = [(re.compile(r"\b(?:androidTarget|android|androidLibrary|androidNative(?:Arm32|Arm64|X86|X64))\s*[({]"), "android"),
|
|
384
|
+
(re.compile(r"\bios(?:X64|Arm64|SimulatorArm64)?\s*[({]"), "ios"),
|
|
385
|
+
(re.compile(r"\btvos(?:X64|Arm64|SimulatorArm64)?\s*[({]"), "tvos"),
|
|
386
|
+
(re.compile(r"\bwatchos(?:X64|Arm64|SimulatorArm64|Arm32|DeviceArm64)?\s*[({]"), "watchos"),
|
|
387
|
+
(re.compile(r"\bmacos(?:X64|Arm64)\s*[({]"), "macos"),
|
|
388
|
+
(re.compile(r"\blinux(?:X64|Arm64)\s*[({]"), "linux"),
|
|
389
|
+
(re.compile(r"\bmingwX64\s*[({]"), "windows"),
|
|
390
|
+
(re.compile(r"\b(?:js|wasmJs|wasmWasi|wasm)\s*[({]"), "web")]
|
|
391
|
+
# the multiplatform plugin applied: kotlin("multiplatform"), id("org.jetbrains.kotlin.multiplatform"),
|
|
392
|
+
# alias(libs.plugins.kotlin.multiplatform) / alias(libs.plugins.kotlinMultiplatform), apply plugin: 'kotlin-multiplatform'
|
|
393
|
+
_KMP_PLUGIN = re.compile(r"kotlin\s*\(\s*\"multiplatform\"|kotlin\.multiplatform\b|['\"]kotlin-multiplatform['\"]|"
|
|
394
|
+
r"plugins\.(?:kotlin\.)?(?:kotlinM|m)ultiplatform\b|plugins\.kmp\b")
|
|
395
|
+
_KMP_DESKTOP = re.compile(r"\bjvm\s*\(\s*\"desktop\"|compose\.desktop\b|\bcompose\s*\.\s*desktop\s*\{")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _swift_package_targets(root: Path, src: dict) -> bool:
|
|
399
|
+
"""Apple targets from the build files (#74): the platforms of the Xcode projects' targets (SDKROOT,
|
|
400
|
+
SUPPORTED_PLATFORMS, SUPPORTS_MACCATALYST), else SwiftPM `platforms: [.iOS(.v15), .macOS(.v12)]` (the root
|
|
401
|
+
manifest, else the local packages'). True when an Xcode project named them."""
|
|
402
|
+
from .xcode import apple_build
|
|
403
|
+
ab = apple_build(root)
|
|
404
|
+
for p, where in ab["targets"].items():
|
|
405
|
+
src.setdefault(p, where)
|
|
406
|
+
for pk in [root / "Package.swift", *root.glob("*/Package.swift")]:
|
|
407
|
+
if not pk.is_file() or not src:
|
|
408
|
+
continue
|
|
409
|
+
txt = pk.read_text(encoding="utf-8", errors="replace")
|
|
410
|
+
srv = re.search(r"github\.com/(vapor/vapor|hummingbird-project/hummingbird|swift-server/[\w-]+)", txt)
|
|
411
|
+
if srv:
|
|
412
|
+
src.setdefault("linux", f"{pk.relative_to(root).as_posix()} server-side Swift dependency ({srv.group(1)})")
|
|
413
|
+
# an Xcode app without a root manifest builds only what its project lists (a package also builds elsewhere)
|
|
414
|
+
return any(" target " in w for w in ab["targets"].values()) and not (root / "Package.swift").is_file()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _kmp_targets(root: Path, src: dict) -> None:
|
|
418
|
+
"""Kotlin Multiplatform targets declared in the `kotlin { }` block of build.gradle(.kts) (root, one or two levels
|
|
419
|
+
down): androidTarget(), iosArm64(), jvm("desktop"), js(), wasmJs(), linuxX64(), mingwX64(), macosArm64()."""
|
|
420
|
+
files = [p for g in ("build.gradle.kts", "build.gradle", "*/build.gradle.kts", "*/build.gradle",
|
|
421
|
+
"*/*/build.gradle.kts", "*/*/build.gradle") for p in root.glob(g)]
|
|
422
|
+
for f in sorted(set(files)):
|
|
423
|
+
if not f.is_file() or not COMMON_SKIP.isdisjoint(f.relative_to(root).parts):
|
|
424
|
+
continue
|
|
425
|
+
txt = f.read_text(encoding="utf-8", errors="replace")
|
|
426
|
+
if not _KMP_PLUGIN.search(txt):
|
|
427
|
+
continue
|
|
428
|
+
txt = re.sub(r"//[^\n]*|/\*.*?\*/", " ", txt, flags=re.S)
|
|
429
|
+
where = f.relative_to(root).as_posix()
|
|
430
|
+
for rx, plat in KMP_TARGET:
|
|
431
|
+
m = rx.search(txt)
|
|
432
|
+
if m:
|
|
433
|
+
src.setdefault(plat, f"{where} kotlin {{ {m.group(0).rstrip('({ ')}() }}")
|
|
434
|
+
if _KMP_DESKTOP.search(txt):
|
|
435
|
+
for d in DESKTOP:
|
|
436
|
+
src.setdefault(d, f"{where} kotlin {{ jvm(\"desktop\") }}")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _positive(c: "Cond") -> bool:
|
|
440
|
+
"""Does the condition name its target positively (`target_os = "android"`, `defined(__ANDROID__)`)? A fallback
|
|
441
|
+
branch (`not(any(unix, windows))`, `#else`) is true on some exotic target without naming it."""
|
|
442
|
+
if c.kind == "c":
|
|
443
|
+
return "!" not in c.expr
|
|
444
|
+
|
|
445
|
+
def walk(t):
|
|
446
|
+
if not isinstance(t, tuple):
|
|
447
|
+
return True
|
|
448
|
+
if t[0] == "not":
|
|
449
|
+
return False
|
|
450
|
+
if t[0] in ("all", "any"):
|
|
451
|
+
return all(walk(x) for x in t[1])
|
|
452
|
+
return True
|
|
453
|
+
return walk(c.expr)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _has_dep(root: Path, name: str) -> bool:
|
|
457
|
+
pj = root / "package.json"
|
|
458
|
+
try:
|
|
459
|
+
d = json.loads(pj.read_text(encoding="utf-8"))
|
|
460
|
+
except (ValueError, OSError):
|
|
461
|
+
return False
|
|
462
|
+
return any(name in (d.get(k) or {}) for k in ("dependencies", "devDependencies"))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ------------------------------------------------------------------ the index pass
|
|
466
|
+
def _local_name(n) -> str:
|
|
467
|
+
nm = n.name or n.id.split(":", 1)[-1]
|
|
468
|
+
for sep in ("#", "::", "."):
|
|
469
|
+
nm = nm.rsplit(sep, 1)[-1]
|
|
470
|
+
return nm.split("@", 1)[0]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _inside(m: dict, line: int, name: str | None, lines_of) -> bool | None:
|
|
474
|
+
"""Is a reference / definition on `line` (to / named `name`) inside mark m? None when undecidable."""
|
|
475
|
+
if line is None:
|
|
476
|
+
return m["start"] <= 1 and m["end"] >= BIG
|
|
477
|
+
if line < m["start"] or line > m["end"]:
|
|
478
|
+
return False
|
|
479
|
+
if m["scol"] is None or (m["start"] < line < m["end"]):
|
|
480
|
+
return True
|
|
481
|
+
txt = lines_of(m["file"], line)
|
|
482
|
+
if txt is None or not name:
|
|
483
|
+
return None
|
|
484
|
+
occ = [x.start() for x in re.finditer(rf"(?<![\w$]){re.escape(name)}(?![\w$])", txt)]
|
|
485
|
+
if not occ:
|
|
486
|
+
return None
|
|
487
|
+
ins = [(line > m["start"] or c >= m["scol"]) and (line < m["end"] or c < m["ecol"]) for c in occ]
|
|
488
|
+
return True if all(ins) else (False if not any(ins) else None)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def apply(project, builder) -> dict:
|
|
492
|
+
"""Index pass: collect the platform conditions of every language, tag nodes and edges, link variant
|
|
493
|
+
implementations, and find divergence. Returns the `platforms` index stats (empty dict: no platform-specific code)."""
|
|
494
|
+
cfg = project.options.get("config") or {}
|
|
495
|
+
pc = cfg.get("platforms") or {}
|
|
496
|
+
root = Path(project.root)
|
|
497
|
+
marks = list(builder.platform_marks)
|
|
498
|
+
nodes_by_file: dict[str, list] = defaultdict(list)
|
|
499
|
+
for n in builder.nodes.values():
|
|
500
|
+
if n.file:
|
|
501
|
+
nodes_by_file[n.file].append(n)
|
|
502
|
+
langs = {lang for f, ns in nodes_by_file.items() for lang in (ns[0].lang,)}
|
|
503
|
+
texts: dict[str, list[str] | None] = {}
|
|
504
|
+
|
|
505
|
+
def lines_of(f: str, line: int):
|
|
506
|
+
if f not in texts:
|
|
507
|
+
try:
|
|
508
|
+
texts[f] = (root / f).read_text(encoding="utf-8", errors="replace").split("\n")
|
|
509
|
+
except OSError:
|
|
510
|
+
texts[f] = None
|
|
511
|
+
t = texts[f]
|
|
512
|
+
return t[line - 1] if t is not None and 0 < line <= len(t) else None
|
|
513
|
+
|
|
514
|
+
# source-text checks (Platform.OS, Platform.isX, kIsWeb, cfg!(...), process.platform)
|
|
515
|
+
from .platform_scan import TRIGGERS, scan
|
|
516
|
+
scanned = 0
|
|
517
|
+
for f, ns in nodes_by_file.items():
|
|
518
|
+
lang = {"ts": "ts", "dart": "dart", "rust": "rust"}.get(ns[0].lang or "")
|
|
519
|
+
if not lang or f.endswith(".vue"):
|
|
520
|
+
continue
|
|
521
|
+
try:
|
|
522
|
+
src = (root / f).read_text(encoding="utf-8", errors="replace")
|
|
523
|
+
except OSError:
|
|
524
|
+
continue
|
|
525
|
+
if not TRIGGERS[lang].search(src):
|
|
526
|
+
continue
|
|
527
|
+
texts[f] = src.split("\n")
|
|
528
|
+
scanned += 1
|
|
529
|
+
for sl, sc, el, ec, tree, text, at in scan(src, lang):
|
|
530
|
+
mark(builder, f, sl, el, Cond("tree", tree, text), line=at, scol=sc, ecol=ec)
|
|
531
|
+
marks = builder.platform_marks
|
|
532
|
+
# .cg.yaml platforms.paths and the C / C++ directory / file-name conventions
|
|
533
|
+
user_paths = pc.get("paths") or {}
|
|
534
|
+
for f, ns in nodes_by_file.items():
|
|
535
|
+
hit = next(((g, ts) for g, ts in user_paths.items() if fnmatch.fnmatch(f, g)), None)
|
|
536
|
+
if hit:
|
|
537
|
+
g, ts = hit
|
|
538
|
+
tree = ("any", [_plat_atom(t) for t in ts]) if len(ts) > 1 else _plat_atom(ts[0])
|
|
539
|
+
mark(builder, f, 1, BIG, Cond("tree", tree, f"{cfg.get('file', '.cg.yaml')} platforms.paths {g}"), line=1)
|
|
540
|
+
continue
|
|
541
|
+
if C_EXT.search(f) and pc.get("path_conventions", True) and ns[0].lang in ("c", "cpp"):
|
|
542
|
+
pcv = path_convention(f)
|
|
543
|
+
if pcv:
|
|
544
|
+
mark(builder, f, 1, BIG, Cond("tree", _plat_atom(pcv[0]), pcv[1]), line=1)
|
|
545
|
+
# React Native / Expo platform files
|
|
546
|
+
groups: list[dict] = []
|
|
547
|
+
if "ts" in langs and pc.get("file_suffixes", True) and uses_react_native(root):
|
|
548
|
+
mods = {n.file for n in builder.nodes.values() if n.lang == "ts" and n.kind in ("module", "component") and n.file}
|
|
549
|
+
fam: dict[tuple, dict[str, str]] = defaultdict(dict)
|
|
550
|
+
for f in mods:
|
|
551
|
+
d, _, base = f.rpartition("/")
|
|
552
|
+
mm = RN_SUFFIX_RE.match(base)
|
|
553
|
+
if mm:
|
|
554
|
+
fam[(d, mm.group("stem"))][mm.group("plat")] = f
|
|
555
|
+
for (d, stem), variants in fam.items():
|
|
556
|
+
members = []
|
|
557
|
+
conds = []
|
|
558
|
+
for plat, f in sorted(variants.items()):
|
|
559
|
+
c = Cond("tree", _plat_atom(plat), f".{plat} file")
|
|
560
|
+
members.append((f, c))
|
|
561
|
+
conds.append(_plat_atom(plat))
|
|
562
|
+
mark(builder, f, 1, BIG, c, line=1)
|
|
563
|
+
pre = f"{d}/{stem}" if d else stem
|
|
564
|
+
bases = sorted(x for x in mods if re.fullmatch(re.escape(pre) + r"\.[cm]?[jt]sx?", x))
|
|
565
|
+
for bf in bases[:1]:
|
|
566
|
+
c = Cond("tree", ("not", ("any", conds)) if len(conds) > 1 else ("not", conds[0]),
|
|
567
|
+
f"base file next to {', '.join('.' + p for p in sorted(variants))}")
|
|
568
|
+
members.append((bf, c))
|
|
569
|
+
mark(builder, bf, 1, BIG, c, line=1)
|
|
570
|
+
groups.append({"kind": "platform files", "name": pre, "members": members})
|
|
571
|
+
# test files naming a platform before the test part (`release.web.test.ts`, `x.ios-spec.tsx`): that target's
|
|
572
|
+
# tests, not a variant of anything (#63)
|
|
573
|
+
for f in mods:
|
|
574
|
+
mm = RN_TEST_SUFFIX_RE.match(f.rpartition("/")[2])
|
|
575
|
+
if mm:
|
|
576
|
+
plat = mm.group("plat")
|
|
577
|
+
mark(builder, f, 1, BIG, Cond("tree", _plat_atom(plat), f".{plat} test file"), line=1)
|
|
578
|
+
# `import x from './X.ios'` names one variant: Metro bundles that file wherever the importer is built (#63)
|
|
579
|
+
builder.explicit_variant_imports = _explicit_variant_imports(root, builder, groups)
|
|
580
|
+
# Dart conditional imports / exports
|
|
581
|
+
imp_sites = [s for s in builder.platform_imports if "plain" not in s]
|
|
582
|
+
if imp_sites:
|
|
583
|
+
plain = defaultdict(int)
|
|
584
|
+
for e in builder.edges.values():
|
|
585
|
+
if e.kind == "IMPORTS" and not e.attrs.get("conditional"):
|
|
586
|
+
plain[e.dst] += 1
|
|
587
|
+
for s in builder.platform_imports:
|
|
588
|
+
if "plain" in s:
|
|
589
|
+
plain[f"module:{s['plain']}"] += 1
|
|
590
|
+
per_file: dict[str, list] = defaultdict(list)
|
|
591
|
+
seen_groups = {}
|
|
592
|
+
for s in imp_sites:
|
|
593
|
+
conds, members = [], []
|
|
594
|
+
for name, val, f in s["configs"]:
|
|
595
|
+
atom = _dart_atom(name, val)
|
|
596
|
+
c = ("all", [("not", x) for x in conds] + [atom]) if conds else atom
|
|
597
|
+
conds.append(atom)
|
|
598
|
+
if f:
|
|
599
|
+
members.append((f, Cond("tree", c, f"if ({name}{'' if val in (None, 'true') else ' == ' + repr(val)})")))
|
|
600
|
+
if s.get("default"):
|
|
601
|
+
dc = ("all", [("not", x) for x in conds]) if len(conds) > 1 else ("not", conds[0])
|
|
602
|
+
members.insert(0, (s["default"], Cond("tree", dc, "default of " + " / ".join(n for n, _, _ in s["configs"]))))
|
|
603
|
+
for f, c in members:
|
|
604
|
+
per_file[f].append((c, s))
|
|
605
|
+
key = tuple(f for f, _ in members)
|
|
606
|
+
if key not in seen_groups and len(members) > 1:
|
|
607
|
+
seen_groups[key] = True
|
|
608
|
+
groups.append({"kind": "conditional import", "name": s["default"] or members[0][0], "members": members,
|
|
609
|
+
"at": f"{s['file']}:{s['line']}"})
|
|
610
|
+
for f, cs in per_file.items():
|
|
611
|
+
if plain.get(f"module:{f}"):
|
|
612
|
+
continue # also imported unconditionally: available everywhere
|
|
613
|
+
tree = cs[0][0].expr if len(cs) == 1 else ("any", [c.expr for c, _ in cs])
|
|
614
|
+
mark(builder, f, 1, BIG, Cond("tree", tree, cs[0][0].text + f" ({cs[0][1]['file']}:{cs[0][1]['line']})"), line=1)
|
|
615
|
+
# Xcode target membership: a source file only the targets of some platforms compile (a macOS-only target, a
|
|
616
|
+
# widget extension without Catalyst) exists only there (#74)
|
|
617
|
+
if langs & {"swift", "c", "cpp"} and pc.get("xcode_membership", True):
|
|
618
|
+
from .xcode import apple_build
|
|
619
|
+
ab = apple_build(root)
|
|
620
|
+
every = set(ab["targets"])
|
|
621
|
+
for f, m in sorted(ab["membership"].items()):
|
|
622
|
+
ps = [p for p in m["platforms"] if p in KNOWN]
|
|
623
|
+
if f in nodes_by_file and ps and set(ps) < every:
|
|
624
|
+
tree = ("any", [_plat_atom(p) for p in ps]) if len(ps) > 1 else _plat_atom(ps[0])
|
|
625
|
+
names = ", ".join(m["targets"][:3]) + (" …" if len(m["targets"]) > 3 else "")
|
|
626
|
+
mark(builder, f, 1, BIG, Cond("tree", tree, f"Xcode target membership ({names})"), line=1)
|
|
627
|
+
marks = builder.platform_marks
|
|
628
|
+
if not marks:
|
|
629
|
+
return {}
|
|
630
|
+
by_file = defaultdict(list)
|
|
631
|
+
for m in marks:
|
|
632
|
+
by_file[m["file"]].append(m)
|
|
633
|
+
# 1. nodes defined under a condition
|
|
634
|
+
ntag: dict[str, list] = defaultdict(list)
|
|
635
|
+
for f, ms in by_file.items():
|
|
636
|
+
for n in nodes_by_file.get(f, ()):
|
|
637
|
+
if (n.attrs or {}).get("swift_kind") == "extension":
|
|
638
|
+
continue # `extension URL { }`: the type exists everywhere; its members carry the condition (#74)
|
|
639
|
+
for m in ms:
|
|
640
|
+
if m["nodes"] and _inside(m, n.line, _local_name(n), lines_of):
|
|
641
|
+
ntag[n.id].append(m)
|
|
642
|
+
nvals = {nid: _combine([m["cond"].values() for m in ms]) for nid, ms in ntag.items()}
|
|
643
|
+
# 2. variant implementations: references to one variant reach every variant
|
|
644
|
+
mirrored = _mirror(builder, groups, nvals)
|
|
645
|
+
# 3. references made under a condition
|
|
646
|
+
etag: dict[tuple, list] = {}
|
|
647
|
+
efile = defaultdict(list)
|
|
648
|
+
for k, e in builder.edges.items():
|
|
649
|
+
if e.file in by_file:
|
|
650
|
+
efile[e.file].append((k, e))
|
|
651
|
+
for f, es in efile.items():
|
|
652
|
+
ms = by_file[f]
|
|
653
|
+
for k, e in es:
|
|
654
|
+
dn = builder.nodes.get(e.dst)
|
|
655
|
+
name = _local_name(dn) if dn is not None else e.dst.rsplit("#", 1)[-1].rsplit("::", 1)[-1]
|
|
656
|
+
hit = [m for m in ms if _inside(m, e.line, name, lines_of)]
|
|
657
|
+
if hit:
|
|
658
|
+
etag[k] = hit
|
|
659
|
+
targets, tsrc = declared_targets(root, cfg, marks, set(langs) | ({"c_cpp"} if langs & {"c", "cpp"} else set()))
|
|
660
|
+
_write_attrs(builder, ntag, nvals, etag, targets)
|
|
661
|
+
_variant_platforms(builder, nvals, targets)
|
|
662
|
+
unknown = [m for m in marks if any(m["cond"].values().get(p) is None for p in targets)]
|
|
663
|
+
tagged_files = sorted({m["file"] for m in marks})
|
|
664
|
+
st = {"targets": targets, "target_sources": tsrc, "conditions": len(marks), "files_with_conditions": len(tagged_files),
|
|
665
|
+
"tagged_nodes": len(ntag), "tagged_edges": len(etag), "variant_edges": mirrored, "text_scanned_files": scanned,
|
|
666
|
+
"unevaluated_conditions": len(unknown),
|
|
667
|
+
"unevaluated_per_target": {p: sum(1 for m in unknown if m["cond"].values().get(p) is None) for p in targets},
|
|
668
|
+
"unevaluated_samples": sorted({f"{m['cond'].text} @ {m['file']}:{m['line']}" for m in unknown})[:8]}
|
|
669
|
+
st["per_target"] = _per_target(builder, nodes_by_file, nvals, targets)
|
|
670
|
+
st["divergence"] = divergence_findings(builder, groups, nvals, targets)
|
|
671
|
+
return st
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _dart_atom(name: str, val):
|
|
675
|
+
mm = re.fullmatch(r"dart\.library\.(\w+)", name or "")
|
|
676
|
+
if mm and val in (None, "true"):
|
|
677
|
+
return ("atom", "dart_library", mm.group(1))
|
|
678
|
+
if mm and val == "false":
|
|
679
|
+
return ("not", ("atom", "dart_library", mm.group(1)))
|
|
680
|
+
return ("atom", "?", name)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _slot(n, f: str):
|
|
684
|
+
"""Position of a node inside its file, so the same symbol can be found in a sibling variant file."""
|
|
685
|
+
kind, key = n.id.split(":", 1)
|
|
686
|
+
if key == f:
|
|
687
|
+
return (kind, "")
|
|
688
|
+
if key.startswith(f + "#"):
|
|
689
|
+
return (kind, key[len(f) + 1:])
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _reexported(builder, f: str, depth: int = 0) -> dict[str, str]:
|
|
694
|
+
"""Names a file defines by re-exporting them: module attrs.reexports (TS `export {a as b} from './m'`,
|
|
695
|
+
`export {x}`; Dart `export 'src/x.dart' show f` and `const f = Impl.f` tear-offs in a variant library) and
|
|
696
|
+
attrs.reexports_all (TS `export * from './m'`: the exported symbols of that module)."""
|
|
697
|
+
n = builder.nodes.get(f"module:{f}")
|
|
698
|
+
a = (n.attrs or {}) if n is not None else {}
|
|
699
|
+
out = {k: v for k, v in (a.get("reexports") or {}).items() if v in builder.nodes}
|
|
700
|
+
if depth < 3:
|
|
701
|
+
for mid in a.get("reexports_all") or ():
|
|
702
|
+
mf = mid.split(":", 1)[1] if ":" in mid else mid
|
|
703
|
+
for x in builder.nodes.values():
|
|
704
|
+
if x.file == mf and (x.attrs or {}).get("exported"):
|
|
705
|
+
sl = _slot(x, mf)
|
|
706
|
+
if sl and sl[1] and "." not in sl[1] and "#" not in sl[1]:
|
|
707
|
+
out.setdefault(sl[1], x.id)
|
|
708
|
+
for k, v in _reexported(builder, mf, depth + 1).items():
|
|
709
|
+
out.setdefault(k, v)
|
|
710
|
+
return out
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _explicit_variant_imports(root: Path, builder, groups: list[dict]) -> set[tuple[str, str]]:
|
|
714
|
+
"""(importer file, member file) for imports that spell a platform file out (`from './X.ios'`) from outside its
|
|
715
|
+
variant group: the importer gets exactly that file on every target it is built for."""
|
|
716
|
+
members = {f for g in groups if g["kind"] == "platform files" for f, _ in g["members"]}
|
|
717
|
+
if not members:
|
|
718
|
+
return set()
|
|
719
|
+
by_noext = {re.sub(r"\.[cm]?[jt]sx?$", "", f): f for f in members}
|
|
720
|
+
importers = {e.file for e in builder.edges.values() if e.kind == "IMPORTS" and e.file
|
|
721
|
+
and e.dst.startswith("module:") and e.dst[7:] in members}
|
|
722
|
+
out = set()
|
|
723
|
+
for f in importers:
|
|
724
|
+
try:
|
|
725
|
+
src = (root / f).read_text(encoding="utf-8", errors="replace")
|
|
726
|
+
except OSError:
|
|
727
|
+
continue
|
|
728
|
+
d = os.path.dirname(f)
|
|
729
|
+
for m in RN_EXPLICIT_IMPORT_RE.finditer(src):
|
|
730
|
+
tgt = os.path.normpath(os.path.join(d, m.group(1))).replace(os.sep, "/")
|
|
731
|
+
if tgt in by_noext and by_noext[tgt] != f:
|
|
732
|
+
out.add((f, by_noext[tgt]))
|
|
733
|
+
return out
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _mirror(builder, groups: list[dict], nvals: dict) -> int:
|
|
737
|
+
"""Edges into one member of a variant group -> the same edge into the matching symbol of every other member."""
|
|
738
|
+
adds = []
|
|
739
|
+
slot_maps = []
|
|
740
|
+
explicit = getattr(builder, "explicit_variant_imports", None) or set()
|
|
741
|
+
for g in groups:
|
|
742
|
+
files = [f for f, _ in g["members"]]
|
|
743
|
+
fset = set(files)
|
|
744
|
+
by_slot: dict[tuple, dict[str, str]] = defaultdict(dict)
|
|
745
|
+
for n in builder.nodes.values():
|
|
746
|
+
if n.file in fset:
|
|
747
|
+
s = _slot(n, n.file)
|
|
748
|
+
if s is not None:
|
|
749
|
+
by_slot[s][n.file] = n.id
|
|
750
|
+
idx = {nid: (s, f) for s, fm in by_slot.items() for f, nid in fm.items()}
|
|
751
|
+
# a name a member re-exports from elsewhere: its variant is the re-exported symbol (not in idx: references
|
|
752
|
+
# to that symbol from outside the group do not go through the group)
|
|
753
|
+
kinds = defaultdict(set)
|
|
754
|
+
for kd, nm in by_slot:
|
|
755
|
+
kinds[nm].add(kd)
|
|
756
|
+
for f in files:
|
|
757
|
+
for nm, nid in _reexported(builder, f).items():
|
|
758
|
+
for kd in kinds.get(nm) or (builder.nodes[nid].kind,): # a tear-off of a method stands for a function
|
|
759
|
+
by_slot[(kd, nm)].setdefault(f, nid)
|
|
760
|
+
slot_maps.append((fset, by_slot, idx))
|
|
761
|
+
# Rust / C / C++ / Swift: alternative definitions of one symbol (`key` + `key@line` / `key@file:line`) on different targets
|
|
762
|
+
alts = _alt_groups(builder, nvals)
|
|
763
|
+
alt_of = {}
|
|
764
|
+
for base, ids in alts.items():
|
|
765
|
+
if len(ids) < 2:
|
|
766
|
+
continue
|
|
767
|
+
sets = {tuple(sorted(p for p, v in nvals.get(i, {p: True for p in KNOWN}).items() if v is not False)) for i in ids}
|
|
768
|
+
if len(sets) < 2:
|
|
769
|
+
continue # same targets: overloads / macro repeats, not platform variants
|
|
770
|
+
for i in ids:
|
|
771
|
+
alt_of[i] = ids
|
|
772
|
+
for k, e in list(builder.edges.items()):
|
|
773
|
+
if e.kind in ("CONTAINS", "DEFINES", "GATED_BY"):
|
|
774
|
+
continue
|
|
775
|
+
for fset, by_slot, idx in slot_maps:
|
|
776
|
+
hit = idx.get(e.dst)
|
|
777
|
+
if hit is None:
|
|
778
|
+
continue
|
|
779
|
+
sn = builder.nodes.get(e.src)
|
|
780
|
+
if sn is not None and sn.file in fset:
|
|
781
|
+
continue
|
|
782
|
+
s, f = hit
|
|
783
|
+
if (e.file, f) in explicit:
|
|
784
|
+
continue # the importer names this platform file: no sibling is reached
|
|
785
|
+
for f2, nid in by_slot[s].items():
|
|
786
|
+
if f2 != f:
|
|
787
|
+
adds.append((e, nid))
|
|
788
|
+
if e.dst in alt_of and e.src not in alt_of[e.dst]:
|
|
789
|
+
for nid in alt_of[e.dst]:
|
|
790
|
+
if nid != e.dst:
|
|
791
|
+
adds.append((e, nid))
|
|
792
|
+
n0 = len(builder.edges)
|
|
793
|
+
for e, nid in adds:
|
|
794
|
+
builder.add_edge(e.src, nid, e.kind, e.file, e.line, e.confidence, **{**e.attrs, "platform_variant_of": e.dst})
|
|
795
|
+
return len(builder.edges) - n0
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _write_attrs(builder, ntag, nvals, etag, targets=None) -> None:
|
|
799
|
+
"""platforms / platform_unknown attrs: the project's targets the code exists on (every known target when the
|
|
800
|
+
project declares none), so `#else` / `!os(iOS)` code lists the other targets, not every platform cg knows (#74)."""
|
|
801
|
+
shown = [p for p in KNOWN if p in (targets or KNOWN)]
|
|
802
|
+
|
|
803
|
+
def put(attrs: dict, ms: list, vals: dict):
|
|
804
|
+
attrs["platforms"] = [p for p in shown if vals[p] is not False]
|
|
805
|
+
other = [p for p in KNOWN if p not in shown and vals[p] is not False]
|
|
806
|
+
if other: # known platforms outside the targets the code would exist on: for `--platform <other>`
|
|
807
|
+
attrs["platforms_other"] = other
|
|
808
|
+
unk = [p for p in shown if vals[p] is None]
|
|
809
|
+
if unk:
|
|
810
|
+
attrs["platform_unknown"] = unk
|
|
811
|
+
texts = list(dict.fromkeys(m["cond"].text for m in ms))
|
|
812
|
+
attrs["platform_expr"] = " && ".join(texts)[:240]
|
|
813
|
+
attrs["platform_at"] = f"{ms[0]['file']}:{ms[0]['line']}"
|
|
814
|
+
for nid, ms in ntag.items():
|
|
815
|
+
n = builder.nodes.get(nid)
|
|
816
|
+
if n is not None:
|
|
817
|
+
if n.attrs is None:
|
|
818
|
+
n.attrs = {}
|
|
819
|
+
put(n.attrs, ms, nvals[nid])
|
|
820
|
+
for k, ms in etag.items():
|
|
821
|
+
e = builder.edges.get(k)
|
|
822
|
+
if e is not None:
|
|
823
|
+
e.attrs = dict(e.attrs or {})
|
|
824
|
+
put(e.attrs, ms, _combine([m["cond"].values() for m in ms]))
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _variant_platforms(builder, nvals: dict, targets) -> int:
|
|
828
|
+
"""A reference into one of a symbol's per-platform definitions (Rust / C / C++ / Swift `#if` branches) carries
|
|
829
|
+
`variant_platforms`: the targets that definition is built for. Every definition is linked (_mirror), so a caller
|
|
830
|
+
with no condition of its own reaches each variant, labelled with its platforms (#74)."""
|
|
831
|
+
shown = [p for p in KNOWN if p in (targets or KNOWN)]
|
|
832
|
+
alt_of = {}
|
|
833
|
+
for ids in _alt_groups(builder, nvals).values():
|
|
834
|
+
if len(ids) > 1 and any(i in nvals for i in ids):
|
|
835
|
+
for i in ids:
|
|
836
|
+
alt_of[i] = ids
|
|
837
|
+
n = 0
|
|
838
|
+
for e in builder.edges.values():
|
|
839
|
+
if e.dst in alt_of and e.kind not in ("CONTAINS", "DEFINES", "GATED_BY") and e.src not in alt_of[e.dst]:
|
|
840
|
+
e.attrs = {**(e.attrs or {}), "variant_platforms": [p for p in shown if _avail(nvals.get(e.dst), p)]}
|
|
841
|
+
n += 1
|
|
842
|
+
return n
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
CODE = ("function", "method", "class", "struct", "enum", "trait", "union", "typedef", "type_alias", "component",
|
|
846
|
+
"composable", "store", "macro")
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _per_target(builder, nodes_by_file, nvals, targets) -> dict:
|
|
850
|
+
out = {}
|
|
851
|
+
files = {f for f, ns in nodes_by_file.items() if any(n.kind in CODE for n in ns)}
|
|
852
|
+
for p in targets:
|
|
853
|
+
syms = sum(1 for n in builder.nodes.values() if n.kind in CODE and nvals.get(n.id, {}).get(p, True) is not False)
|
|
854
|
+
only = sum(1 for nid, v in nvals.items() if v.get(p) is not False and builder.nodes.get(nid) is not None
|
|
855
|
+
and builder.nodes[nid].kind in CODE)
|
|
856
|
+
fl = sum(1 for f in files if not all(nvals.get(n.id, {}).get(p, True) is False for n in nodes_by_file[f]
|
|
857
|
+
if n.kind in CODE))
|
|
858
|
+
out[p] = {"files": fl, "symbols": syms, "platform_specific_symbols": only}
|
|
859
|
+
return out
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _avail(v: dict | None, p: str) -> bool:
|
|
863
|
+
return v is None or v.get(p) is not False
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _vkey(nid: str, n, nvals: dict | None = None) -> str:
|
|
867
|
+
"""Variant key of a Rust / C / C++ definition: its id without the `@file:line` suffix. In C / C++ a function
|
|
868
|
+
and a function-like macro of the same name are variants too (`#ifdef _WIN32 #define f(h) ... #else` + `void f`).
|
|
869
|
+
In Rust, items of sibling modules gated per platform are variants (`#[cfg(unix)] mod unix; #[cfg(windows)] mod
|
|
870
|
+
windows;` each with `fn new`, re-exported by the parent): the gated module segment is replaced by `*`."""
|
|
871
|
+
b = nid.split("@", 1)[0]
|
|
872
|
+
if n is None:
|
|
873
|
+
return b
|
|
874
|
+
if n.lang in ("c", "cpp") and n.kind in ("function", "macro") and n.name:
|
|
875
|
+
# `file#name`: a static, file-local function (a non-static one defined in several files is one symbol)
|
|
876
|
+
return f"c-fn:{n.file}#{n.name}" if "#" in b and (n.attrs or {}).get("static") else f"c-fn:{n.name}"
|
|
877
|
+
if n.lang == "rust" and nvals and ":" in b:
|
|
878
|
+
kind, path = b.split(":", 1)
|
|
879
|
+
parts = path.split("::")
|
|
880
|
+
for i in range(len(parts) - 1, 1, -1):
|
|
881
|
+
if f"mod:{'::'.join(parts[:i])}" in nvals:
|
|
882
|
+
return f"{kind}:{'::'.join(parts[:i - 1])}::*::{'::'.join(parts[i:])}"
|
|
883
|
+
return b
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
ALT_KINDS = ("function", "method", "struct", "enum", "type_alias", "typedef", "class", "union", "macro", "const",
|
|
887
|
+
"static", "global", "enum_case", "constant")
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _swift_inherited(builder, nvals: dict, alts: dict, vkey_of: dict):
|
|
891
|
+
"""Swift: the targets where a member exists without a definition of its own in the graph (#86). A type defined
|
|
892
|
+
per `#if` branch (`class Popover: NSObject { var isShown }` / `class Popover: NSPopover { }`) has the member on
|
|
893
|
+
the other branch too when that variant conforms to a protocol requiring it, and an `init` when the variant's
|
|
894
|
+
SDK superclass gives it (no initializer of its own on that target)."""
|
|
895
|
+
parent, conforms, names, inits = {}, defaultdict(set), defaultdict(set), defaultdict(list)
|
|
896
|
+
for e in builder.edges.values():
|
|
897
|
+
if e.kind == "CONTAINS" and e.src.startswith("class:"):
|
|
898
|
+
sn = builder.nodes.get(e.src)
|
|
899
|
+
dn = builder.nodes.get(e.dst)
|
|
900
|
+
if sn is None or dn is None or sn.lang != "swift":
|
|
901
|
+
continue
|
|
902
|
+
parent[e.dst] = e.src
|
|
903
|
+
names[e.src].add(dn.name)
|
|
904
|
+
if dn.name == "init":
|
|
905
|
+
inits[e.src].append(e.dst)
|
|
906
|
+
elif e.kind == "IMPLEMENTS" and e.src.startswith("class:"):
|
|
907
|
+
conforms[e.src].add(e.dst)
|
|
908
|
+
for nid, n in builder.nodes.items():
|
|
909
|
+
if n.lang == "swift" and n.kind == "class":
|
|
910
|
+
names[nid] |= set((n.attrs or {}).get("property_requirements") or ())
|
|
911
|
+
|
|
912
|
+
def avail(i):
|
|
913
|
+
return {p for p in KNOWN if _avail(nvals.get(i), p)}
|
|
914
|
+
|
|
915
|
+
def of(dst: str) -> set:
|
|
916
|
+
dn = builder.nodes.get(dst)
|
|
917
|
+
c = parent.get(dst)
|
|
918
|
+
if dn is None or c is None:
|
|
919
|
+
return set()
|
|
920
|
+
have = set()
|
|
921
|
+
for v in alts.get(vkey_of.get(c, c)) or [c]:
|
|
922
|
+
vn = builder.nodes.get(v)
|
|
923
|
+
if vn is None:
|
|
924
|
+
continue
|
|
925
|
+
if any(dn.name in names.get(pr, ()) for pr in conforms.get(v, ())):
|
|
926
|
+
have |= avail(v)
|
|
927
|
+
elif dn.name == "init" and (vn.attrs or {}).get("external_supers"):
|
|
928
|
+
own = set().union(*[avail(i) for i in inits.get(v, ())]) if inits.get(v) else set()
|
|
929
|
+
have |= avail(v) - own
|
|
930
|
+
return have
|
|
931
|
+
return of
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _alt_groups(builder, nvals: dict) -> dict[str, list[str]]:
|
|
935
|
+
"""Rust / C / C++ / Swift alternative definitions of one symbol (one per `cfg` / `#if` branch), keyed by _vkey."""
|
|
936
|
+
alts: dict[str, list[str]] = defaultdict(list)
|
|
937
|
+
for nid, n in builder.nodes.items():
|
|
938
|
+
if n.lang in ("rust", "c", "cpp", "swift") and n.kind in ALT_KINDS:
|
|
939
|
+
alts[_vkey(nid, n, nvals)].append(nid)
|
|
940
|
+
# a static C function and a macro / function of the same name visible in its file are variants too
|
|
941
|
+
for k in [k for k in alts if k.startswith("c-fn:") and "#" in k]:
|
|
942
|
+
file, name = k[5:].rsplit("#", 1)
|
|
943
|
+
g = f"c-fn:{name}"
|
|
944
|
+
if g in alts and any(builder.nodes[i].file == file or builder.nodes[i].kind == "macro" for i in alts[g]):
|
|
945
|
+
alts[g] += alts.pop(k)
|
|
946
|
+
# same-named functions of separate programs (libuv docs/code/*/main.c `alloc_buffer`, each file with its own
|
|
947
|
+
# main()) are not one symbol: members in two or more files that define main() leave the group (#63)
|
|
948
|
+
mains = {n.file for n in builder.nodes.values() if n.lang in ("c", "cpp") and n.kind == "function" and n.name == "main"}
|
|
949
|
+
if mains:
|
|
950
|
+
for k, ids in list(alts.items()):
|
|
951
|
+
if not k.startswith("c-fn:"):
|
|
952
|
+
continue
|
|
953
|
+
prog = [i for i in ids if builder.nodes[i].file in mains]
|
|
954
|
+
if len({builder.nodes[i].file for i in prog}) >= 2:
|
|
955
|
+
rest = [i for i in ids if i not in prog]
|
|
956
|
+
if rest:
|
|
957
|
+
alts[k] = rest
|
|
958
|
+
else:
|
|
959
|
+
del alts[k]
|
|
960
|
+
return alts
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
API_KINDS = ("function", "method", "class", "component", "composable", "store")
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _public(n, group_files: set, ext_refs: dict, users: set) -> bool:
|
|
967
|
+
"""Part of the API the importers of a variant group rely on: used by a file that imports the group, or, when the
|
|
968
|
+
group has no importer in the graph, exported (TS) / public (Dart) / used from outside the group."""
|
|
969
|
+
refs = ext_refs.get(n.id, set())
|
|
970
|
+
if users:
|
|
971
|
+
return bool(refs & users)
|
|
972
|
+
if (n.attrs or {}).get("exported"):
|
|
973
|
+
return True
|
|
974
|
+
if n.lang == "dart" and not _local_name(n).startswith("_"):
|
|
975
|
+
return True
|
|
976
|
+
return bool(refs - group_files)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def divergence_findings(builder, groups: list[dict], nvals: dict, targets: list[str], limit: int = 400) -> dict:
|
|
980
|
+
"""Platform divergence for the declared targets:
|
|
981
|
+
variants a symbol / module implemented per platform (variant groups) and the targets none covers
|
|
982
|
+
api_surface symbols one variant file defines and a sibling variant does not
|
|
983
|
+
missing_callee references live on a target where the referenced code does not exist there (and no variant does)
|
|
984
|
+
"""
|
|
985
|
+
tset = [p for p in KNOWN if p in targets]
|
|
986
|
+
shown = tset or list(KNOWN) # platforms listed per member: the project's targets (#74)
|
|
987
|
+
out = {"variants": [], "api_surface": [], "missing_callee": [], "missing_callee_tests": []}
|
|
988
|
+
explicit = getattr(builder, "explicit_variant_imports", None) or set()
|
|
989
|
+
importers = defaultdict(set)
|
|
990
|
+
ext_refs = defaultdict(set) # node -> files referring to it
|
|
991
|
+
for e in builder.edges.values():
|
|
992
|
+
if e.kind in ("IMPORTS", "CALLS", "REFERENCES_FN") and e.file:
|
|
993
|
+
importers[e.dst].add(f"{e.file}:{e.line}")
|
|
994
|
+
if e.kind not in ("CONTAINS", "DEFINES") and e.file:
|
|
995
|
+
ext_refs[e.dst].add(e.file)
|
|
996
|
+
|
|
997
|
+
def importers_of(g) -> set:
|
|
998
|
+
return {u for f, _ in g["members"] for u in importers.get(f"module:{f}", ())}
|
|
999
|
+
for g in groups:
|
|
1000
|
+
covered = set()
|
|
1001
|
+
mem = []
|
|
1002
|
+
for f, c in g["members"]:
|
|
1003
|
+
ps = [p for p, v in c.values().items() if v is not False]
|
|
1004
|
+
covered |= set(ps)
|
|
1005
|
+
mem.append({"file": f, "platforms": [p for p in ps if p in shown], "condition": c.text})
|
|
1006
|
+
used = sorted({u for f, _ in g["members"] for u in importers.get(f"module:{f}", ())
|
|
1007
|
+
if not any(u.startswith(m + ":") for m, _ in g["members"])})
|
|
1008
|
+
missing = [p for p in tset if p not in covered]
|
|
1009
|
+
out["variants"].append({"kind": g["kind"], "name": g["name"], "members": mem, "covered": [p for p in KNOWN if p in covered and p in tset],
|
|
1010
|
+
"missing": missing, "used_at": used[:5], **({"at": g["at"]} if g.get("at") else {})})
|
|
1011
|
+
# API surface: public symbols a member defines that a sibling lacks (exported TS symbols, public Dart names,
|
|
1012
|
+
# anything referenced from outside the group)
|
|
1013
|
+
files = {f for f, _ in g["members"]}
|
|
1014
|
+
users = {u.rsplit(":", 1)[0] for u in importers_of(g)} - files # files that import the group
|
|
1015
|
+
defs, anyname = {}, {}
|
|
1016
|
+
for f, c in g["members"]:
|
|
1017
|
+
defs[f] = {(_slot(n, f) or ("?", n.id))[1]: n for n in builder.nodes.values() if n.file == f
|
|
1018
|
+
and n.kind in API_KINDS and _slot(n, f)
|
|
1019
|
+
and _public(n, files, ext_refs, users)}
|
|
1020
|
+
# every name the file defines in any form (also private / non-function symbols and re-exports): a
|
|
1021
|
+
# sibling lacks a symbol only when it defines nothing of that name
|
|
1022
|
+
anyname[f] = {sl[1] for n in builder.nodes.values() if n.file == f for sl in [_slot(n, f)] if sl and sl[1]}
|
|
1023
|
+
mn = builder.nodes.get(f"module:{f}")
|
|
1024
|
+
anyname[f] |= set(((mn.attrs or {}) if mn is not None else {}).get("reexports_external") or ())
|
|
1025
|
+
for nm, nid in _reexported(builder, f).items():
|
|
1026
|
+
anyname[f].add(nm)
|
|
1027
|
+
tn = builder.nodes[nid]
|
|
1028
|
+
if tn.kind in API_KINDS and (not users or ext_refs.get(nid, set()) & users):
|
|
1029
|
+
defs[f].setdefault(nm, tn)
|
|
1030
|
+
allnames = set().union(*[set(d) for d in defs.values()]) if defs else set()
|
|
1031
|
+
for nm in sorted(allnames):
|
|
1032
|
+
have = [f for f in defs if nm in defs[f]]
|
|
1033
|
+
lack = [f for f in defs if nm not in defs[f] and nm not in anyname.get(f, ())]
|
|
1034
|
+
if have and lack and "#" not in nm and "." not in nm:
|
|
1035
|
+
lack_pl = sorted({p for f, c in g["members"] if f in lack for p, v in c.values().items() if v is not False and p in tset})
|
|
1036
|
+
if lack_pl:
|
|
1037
|
+
out["api_surface"].append({"group": g["name"], "symbol": nm, "defined_in": have, "missing_in": lack,
|
|
1038
|
+
"missing_on": [p for p in KNOWN if p in lack_pl]})
|
|
1039
|
+
# id-variant groups (Rust / C): covered targets per symbol
|
|
1040
|
+
alts = {k: ids for k, ids in _alt_groups(builder, nvals).items() if any(i in nvals for i in ids)}
|
|
1041
|
+
vkey_of = {i: k for k, ids in alts.items() for i in ids}
|
|
1042
|
+
for base, ids in sorted(alts.items()):
|
|
1043
|
+
if len(ids) < 2 or not any(builder.nodes[i].kind in ("function", "method", "macro") for i in ids):
|
|
1044
|
+
continue
|
|
1045
|
+
sets = {tuple(p for p in KNOWN if _avail(nvals.get(i), p)) for i in ids}
|
|
1046
|
+
if len(sets) < 2:
|
|
1047
|
+
continue # same targets: overloads / repeated declarations, not platform variants
|
|
1048
|
+
covered = {p for i in ids for p in KNOWN if _avail(nvals.get(i), p)}
|
|
1049
|
+
n0 = builder.nodes[ids[0]]
|
|
1050
|
+
out["variants"].append({"kind": "per-platform definitions", "name": base.split(":", 1)[1] if "::*::" in base else (n0.fqn or n0.name),
|
|
1051
|
+
"members": [{"id": i, "file": builder.nodes[i].file, "line": builder.nodes[i].line,
|
|
1052
|
+
"platforms": [p for p in shown if _avail(nvals.get(i), p)],
|
|
1053
|
+
"condition": builder.nodes[i].attrs.get("platform_expr")} for i in ids],
|
|
1054
|
+
"covered": [p for p in KNOWN if p in covered and p in tset], "missing": [p for p in tset if p not in covered],
|
|
1055
|
+
"used_at": sorted(importers.get(ids[0], ()))[:5]})
|
|
1056
|
+
# references on a target where the callee does not exist (a reference to any variant counts for the group)
|
|
1057
|
+
gslot = {}
|
|
1058
|
+
gmembers = {g["name"]: g["members"] for g in groups}
|
|
1059
|
+
for g in groups:
|
|
1060
|
+
files = {f for f, _ in g["members"]}
|
|
1061
|
+
for n in builder.nodes.values():
|
|
1062
|
+
if n.file in files:
|
|
1063
|
+
sl = _slot(n, n.file)
|
|
1064
|
+
if sl is not None:
|
|
1065
|
+
gslot[n.id] = ("group", g["name"], sl)
|
|
1066
|
+
group_of = {f: g["name"] for g in groups for f, _ in g["members"]}
|
|
1067
|
+
inherited = _swift_inherited(builder, nvals, alts, vkey_of)
|
|
1068
|
+
sites = defaultdict(list)
|
|
1069
|
+
for e in builder.edges.values():
|
|
1070
|
+
if e.kind in ("CALLS", "REFERENCES_FN", "USES_TYPE", "USES_VALUE", "IMPORTS", "INSTANTIATES", "RENDERS",
|
|
1071
|
+
"USES_COMPOSABLE", "USES_STORE") and (e.dst in nvals or e.attrs.get("platform_variant_of") in nvals):
|
|
1072
|
+
if e.kind == "IMPORTS" and e.file in group_of:
|
|
1073
|
+
dn = builder.nodes.get(e.dst)
|
|
1074
|
+
if dn is not None and dn.lang != "dart" and group_of.get(dn.file) == group_of[e.file]:
|
|
1075
|
+
# a platform file naming its sibling explicitly (`export * from './X.ios'`): bundled as written
|
|
1076
|
+
continue
|
|
1077
|
+
if e.kind == "CALLS" and e.attrs.get("binding") in ("name", "candidate"):
|
|
1078
|
+
# a call bound by its selector alone (receiver type unknown, Swift / Kotlin #83): too weak as well
|
|
1079
|
+
continue
|
|
1080
|
+
if e.kind == "USES_VALUE" and (e.confidence == "heuristic" or e.dst.startswith("macro:")):
|
|
1081
|
+
# a global matched by name only, or a macro tested by #ifdef: too weak to call a target broken
|
|
1082
|
+
continue
|
|
1083
|
+
base = e.attrs.get("platform_variant_of") or e.dst
|
|
1084
|
+
dn = builder.nodes.get(base)
|
|
1085
|
+
if dn is not None and (e.file, dn.file) in explicit:
|
|
1086
|
+
continue # `from './X.ios'`: that file is bundled wherever the importer is
|
|
1087
|
+
if dn is not None and dn.lang == "swift" and dn.name == "init" and e.confidence == "heuristic":
|
|
1088
|
+
cn = builder.nodes.get("class:" + (dn.fqn or "").rpartition(".")[0])
|
|
1089
|
+
if cn is not None and (cn.attrs or {}).get("swift_kind") == "extension":
|
|
1090
|
+
# an initializer the project adds to an SDK type (`extension Image { init(systemName:) }` under
|
|
1091
|
+
# `#if os(macOS)`): elsewhere the call uses the SDK's own initializer (#63)
|
|
1092
|
+
continue
|
|
1093
|
+
if base in gslot:
|
|
1094
|
+
base = gslot[base]
|
|
1095
|
+
elif dn is not None and dn.lang in ("rust", "c", "cpp", "swift"):
|
|
1096
|
+
base = vkey_of.get(base, base) # Swift: `func close()` per `#if` branch (#74)
|
|
1097
|
+
sites[(e.src, e.file, e.line, e.kind, base)].append(e)
|
|
1098
|
+
# every module one import line can resolve to (a conditional import names several on one line)
|
|
1099
|
+
import_alts = defaultdict(set)
|
|
1100
|
+
outside_default = set() # conditional import whose default is outside the index (e.g. `dart:isolate`)
|
|
1101
|
+
for e in builder.edges.values():
|
|
1102
|
+
if e.kind == "IMPORTS":
|
|
1103
|
+
import_alts[(e.src, e.file, e.line)].add(e.dst)
|
|
1104
|
+
if e.attrs.get("conditional"):
|
|
1105
|
+
outside_default.add((e.src, e.file, e.line))
|
|
1106
|
+
for e in builder.edges.values():
|
|
1107
|
+
if e.kind == "IMPORTS" and e.attrs.get("conditional") and not e.attrs.get("condition"):
|
|
1108
|
+
outside_default.discard((e.src, e.file, e.line))
|
|
1109
|
+
skipped_no_target = 0
|
|
1110
|
+
for (src, f, line, kind, base), es in sites.items():
|
|
1111
|
+
sv = nvals.get(src)
|
|
1112
|
+
ev = es[0].attrs
|
|
1113
|
+
live = [p for p in tset if _avail(sv, p) and ("platforms" not in ev or p in ev["platforms"])]
|
|
1114
|
+
have = {p for e in es for p in KNOWN if _avail(nvals.get(e.dst), p)}
|
|
1115
|
+
if kind == "IMPORTS":
|
|
1116
|
+
have |= {p for d in import_alts.get((src, f, line), ()) for p in KNOWN if _avail(nvals.get(d), p)}
|
|
1117
|
+
if (src, f, line) in outside_default:
|
|
1118
|
+
have |= set(KNOWN)
|
|
1119
|
+
if isinstance(base, str) and base in alts: # any definition of the symbol on that target will do
|
|
1120
|
+
have |= {p for i in alts[base] for p in KNOWN if _avail(nvals.get(i), p)}
|
|
1121
|
+
if not set(live) <= have:
|
|
1122
|
+
have |= inherited(es[0].attrs.get("platform_variant_of") or es[0].dst)
|
|
1123
|
+
if isinstance(base, tuple) and base[0] == "group" and base[2][1]:
|
|
1124
|
+
# a member that re-exports the name from a package (`export {x} from 'react-dom'`, `export const X = Y`)
|
|
1125
|
+
for mf, c in gmembers.get(base[1], ()):
|
|
1126
|
+
mn = builder.nodes.get(f"module:{mf}")
|
|
1127
|
+
if mn is not None and base[2][1] in ((mn.attrs or {}).get("reexports_external") or ()):
|
|
1128
|
+
have |= {p for p, v in c.values().items() if v is not False}
|
|
1129
|
+
miss = [p for p in live if p not in have]
|
|
1130
|
+
if miss and not have:
|
|
1131
|
+
# the callee exists on no known target (sunos.c `strnlen`, aix.c helpers): the targets' builds use a system
|
|
1132
|
+
# or another definition the index does not see; that is no finding about them (#63)
|
|
1133
|
+
skipped_no_target += 1
|
|
1134
|
+
continue
|
|
1135
|
+
if miss:
|
|
1136
|
+
dst = es[0].attrs.get("platform_variant_of") or es[0].dst
|
|
1137
|
+
item = {"from": src, "to": dst, "kind": kind, "at": f"{f}:{line}", "missing_on": miss,
|
|
1138
|
+
"callee_platforms": [p for p in shown if p in have],
|
|
1139
|
+
"callee_condition": " | ".join(dict.fromkeys(
|
|
1140
|
+
(builder.nodes[e.dst].attrs or {}).get("platform_expr") or "" for e in es
|
|
1141
|
+
if e.dst in builder.nodes)) or None}
|
|
1142
|
+
# test code whose platforms are only the project-wide default (no #if, no target membership narrowing
|
|
1143
|
+
# them): an unguarded call into code excluded on a platform means that test target is not built there,
|
|
1144
|
+
# so it is listed apart (#91)
|
|
1145
|
+
if _test_code(builder.nodes.get(src), f) and not any(v is False for v in (sv or {}).values()):
|
|
1146
|
+
item["platform_source"] = "project default (test target)"
|
|
1147
|
+
out["missing_callee_tests"].append(item)
|
|
1148
|
+
else:
|
|
1149
|
+
out["missing_callee"].append(item)
|
|
1150
|
+
out["variants"] = [v for v in out["variants"]]
|
|
1151
|
+
out["counts"] = {k: len(v) for k, v in out.items()}
|
|
1152
|
+
if skipped_no_target:
|
|
1153
|
+
out["counts"]["missing_callee_skipped_no_target"] = skipped_no_target
|
|
1154
|
+
if not out["missing_callee_tests"]:
|
|
1155
|
+
del out["missing_callee_tests"], out["counts"]["missing_callee_tests"]
|
|
1156
|
+
for k in ("variants", "api_surface", "missing_callee", "missing_callee_tests"):
|
|
1157
|
+
if k in out:
|
|
1158
|
+
out[k] = out[k][:limit]
|
|
1159
|
+
return out
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
TEST_FILE_RE = re.compile(r"(?:^|/)(?:Tests?|tests?|__tests__|spec|androidTest|\w*Test|\w*Tests)/|"
|
|
1163
|
+
r"(?:Tests?|_test|\.test|\.spec|Spec)\.\w+$")
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _test_code(n, f: str) -> bool:
|
|
1167
|
+
if n is not None and (n.kind == "test" or (n.attrs or {}).get("test")):
|
|
1168
|
+
return True
|
|
1169
|
+
return bool(f and TEST_FILE_RE.search(f))
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
# ------------------------------------------------------------------ queries
|
|
1173
|
+
def has_platforms(st) -> bool:
|
|
1174
|
+
return bool((st.meta().get("stats") or {}).get("platforms"))
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def summary(st) -> dict:
|
|
1178
|
+
return (st.meta().get("stats") or {}).get("platforms") or {}
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def exclusions(st, platform: str) -> dict:
|
|
1182
|
+
"""Nodes and edges that do not exist on `platform` (their condition is false there), and how many conditions
|
|
1183
|
+
could not be evaluated for it (kept live). Cached per store; also fills temp.t_px with the excluded edge ids."""
|
|
1184
|
+
cache = getattr(st, "_platform_x", None)
|
|
1185
|
+
if cache is None:
|
|
1186
|
+
cache = st._platform_x = {}
|
|
1187
|
+
if platform in cache:
|
|
1188
|
+
return cache[platform]
|
|
1189
|
+
nodes, unknown_n = set(), 0
|
|
1190
|
+
for r in st.q("SELECT id, attrs FROM nodes WHERE attrs LIKE '%\"platforms\"%'"):
|
|
1191
|
+
a = json.loads(r["attrs"] or "{}")
|
|
1192
|
+
if "platforms" in a:
|
|
1193
|
+
if platform not in a["platforms"] and platform not in (a.get("platforms_other") or ()):
|
|
1194
|
+
nodes.add(r["id"])
|
|
1195
|
+
elif platform in (a.get("platform_unknown") or ()):
|
|
1196
|
+
unknown_n += 1
|
|
1197
|
+
edges, unknown_e = set(), 0
|
|
1198
|
+
for r in st.q("SELECT id, attrs FROM edges WHERE attrs LIKE '%\"platforms\"%'"):
|
|
1199
|
+
a = json.loads(r["attrs"] or "{}")
|
|
1200
|
+
if "platforms" in a:
|
|
1201
|
+
if platform not in a["platforms"] and platform not in (a.get("platforms_other") or ()):
|
|
1202
|
+
edges.add(r["id"])
|
|
1203
|
+
elif platform in (a.get("platform_unknown") or ()):
|
|
1204
|
+
unknown_e += 1
|
|
1205
|
+
if nodes:
|
|
1206
|
+
st.db.execute("DROP TABLE IF EXISTS temp.t_pxn")
|
|
1207
|
+
st.db.execute("CREATE TEMP TABLE t_pxn(id TEXT PRIMARY KEY)")
|
|
1208
|
+
st.db.executemany("INSERT OR IGNORE INTO t_pxn VALUES (?)", [(x,) for x in nodes])
|
|
1209
|
+
edges |= {r["id"] for r in st.q("SELECT e.id FROM edges e JOIN t_pxn n ON n.id = e.src")}
|
|
1210
|
+
edges |= {r["id"] for r in st.q("SELECT e.id FROM edges e JOIN t_pxn n ON n.id = e.dst")}
|
|
1211
|
+
res = {"platform": platform, "nodes": nodes, "edges": edges, "unknown": unknown_n + unknown_e}
|
|
1212
|
+
cache[platform] = res
|
|
1213
|
+
return res
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def temp_table(st, platform: str) -> str:
|
|
1217
|
+
"""Name of a temp table holding the edge ids excluded on `platform` (for SQL traversals)."""
|
|
1218
|
+
x = exclusions(st, platform)
|
|
1219
|
+
name = f"t_px_{platform}"
|
|
1220
|
+
if not getattr(st, "_platform_tbl", None):
|
|
1221
|
+
st._platform_tbl = set()
|
|
1222
|
+
if name not in st._platform_tbl:
|
|
1223
|
+
st.db.execute(f"DROP TABLE IF EXISTS temp.{name}")
|
|
1224
|
+
st.db.execute(f"CREATE TEMP TABLE {name}(id INTEGER PRIMARY KEY)")
|
|
1225
|
+
st.db.executemany(f"INSERT OR IGNORE INTO {name} VALUES (?)", [(i,) for i in x["edges"]])
|
|
1226
|
+
st._platform_tbl.add(name)
|
|
1227
|
+
return name
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def filter_info(st, platform: str | None) -> dict | None:
|
|
1231
|
+
"""What a --platform filter did, for the reply: the target, excluded nodes / edges, unevaluated conditions."""
|
|
1232
|
+
if not platform:
|
|
1233
|
+
return None
|
|
1234
|
+
x = exclusions(st, platform)
|
|
1235
|
+
s = summary(st)
|
|
1236
|
+
upt = s.get("unevaluated_per_target") or {}
|
|
1237
|
+
info = {"platform": platform, "excluded_nodes": len(x["nodes"]), "excluded_edges": len(x["edges"]),
|
|
1238
|
+
# conditions per declared target; for another target, the tagged code whose condition is unknown there
|
|
1239
|
+
"unevaluated_conditions": upt[platform] if platform in upt else x["unknown"],
|
|
1240
|
+
"kept_unevaluated": x["unknown"]}
|
|
1241
|
+
if not s:
|
|
1242
|
+
info["note"] = "this graph has no platform-specific code: the filter changes nothing"
|
|
1243
|
+
elif s.get("targets") and platform not in s["targets"]:
|
|
1244
|
+
info["note"] = f"{platform} is not a declared target of this project ({', '.join(s['targets'])})"
|
|
1245
|
+
return info
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def render_filter(info: dict | None) -> str:
|
|
1249
|
+
if not info:
|
|
1250
|
+
return ""
|
|
1251
|
+
s = (f"platform: {info['platform']} ({info['excluded_nodes']} nodes and {info['excluded_edges']} references not built "
|
|
1252
|
+
f"for it left out; {info['unevaluated_conditions']} conditions could not be evaluated for it, the code under "
|
|
1253
|
+
f"them stays in)")
|
|
1254
|
+
return s + (f"; {info['note']}" if info.get("note") else "")
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def node_platforms(attrs_json: str | None) -> dict:
|
|
1258
|
+
if not attrs_json or ('"platforms"' not in attrs_json and '"available"' not in attrs_json):
|
|
1259
|
+
return {}
|
|
1260
|
+
try:
|
|
1261
|
+
a = json.loads(attrs_json) or {}
|
|
1262
|
+
except ValueError:
|
|
1263
|
+
return {}
|
|
1264
|
+
out = {}
|
|
1265
|
+
if isinstance(a.get("available"), dict):
|
|
1266
|
+
out["available"] = a["available"]
|
|
1267
|
+
if "platforms" in a:
|
|
1268
|
+
out["platforms"] = a["platforms"]
|
|
1269
|
+
if a.get("platform_unknown"):
|
|
1270
|
+
out["platform_unknown"] = a["platform_unknown"]
|
|
1271
|
+
if a.get("platform_expr"):
|
|
1272
|
+
out["platform_expr"] = a["platform_expr"]
|
|
1273
|
+
return out
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def label(n: dict) -> str:
|
|
1277
|
+
"""' [ios, android]' for platform-specific code, '' for code on every target."""
|
|
1278
|
+
ps = n.get("platforms")
|
|
1279
|
+
av = n.get("available") if isinstance(n.get("available"), dict) else None
|
|
1280
|
+
out = ""
|
|
1281
|
+
if ps is not None:
|
|
1282
|
+
unk = n.get("platform_unknown") or []
|
|
1283
|
+
out = " [" + (", ".join(p + ("?" if p in unk else "") for p in ps) or "no known target") + "]"
|
|
1284
|
+
if av: # Swift @available minimum OS versions
|
|
1285
|
+
out += " [" + ", ".join(f"{k} {v}+" for k, v in av.items()) + "]"
|
|
1286
|
+
return out
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def divergence(st, kind: str | None = None, target: str | None = None) -> dict:
|
|
1290
|
+
s = summary(st)
|
|
1291
|
+
d = dict(s.get("divergence") or {})
|
|
1292
|
+
if target:
|
|
1293
|
+
d["variants"] = [v for v in d.get("variants", []) if target in v.get("missing", [])]
|
|
1294
|
+
d["api_surface"] = [v for v in d.get("api_surface", []) if target in v.get("missing_on", [])]
|
|
1295
|
+
d["missing_callee"] = [v for v in d.get("missing_callee", []) if target in v.get("missing_on", [])]
|
|
1296
|
+
if "missing_callee_tests" in d:
|
|
1297
|
+
d["missing_callee_tests"] = [v for v in d["missing_callee_tests"] if target in v.get("missing_on", [])]
|
|
1298
|
+
if kind:
|
|
1299
|
+
d = {k: (v if k == kind else []) for k, v in d.items() if k != "counts"} | {"counts": d.get("counts")}
|
|
1300
|
+
return {"targets": s.get("targets") or [], "target_sources": s.get("target_sources") or {}, **d}
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def render_divergence(res: dict, limit: int = 40) -> str:
|
|
1304
|
+
if not res.get("targets") and not res.get("variants"):
|
|
1305
|
+
return "no platform-specific code in this graph (no platform conditions, platform files or conditional imports)"
|
|
1306
|
+
out = [f"targets: {', '.join(res['targets']) or '(none declared)'}"
|
|
1307
|
+
+ (f" ({'; '.join(f'{k}: {v}' for k, v in list(res.get('target_sources', {}).items())[:4])})" if res.get("target_sources") else "")]
|
|
1308
|
+
var = res.get("variants") or []
|
|
1309
|
+
gaps = [v for v in var if v.get("missing")]
|
|
1310
|
+
out.append(f"\n== VARIANTS (implemented per platform): {len(var)}, {len(gaps)} with a declared target no variant covers")
|
|
1311
|
+
for v in sorted(var, key=lambda v: (not v.get("missing"), v["name"]))[:limit]:
|
|
1312
|
+
miss = f" missing: {', '.join(v['missing'])}" if v.get("missing") else ""
|
|
1313
|
+
out.append(f" {v['name']} [{v['kind']}] covered: {', '.join(v['covered']) or '-'}{miss}")
|
|
1314
|
+
for m in v["members"][:6]:
|
|
1315
|
+
loc = m.get("file", "") + (f":{m['line']}" if m.get("line") else "")
|
|
1316
|
+
out.append(f" {loc} {', '.join(m['platforms']) or 'no known target'} ({m.get('condition') or '-'})")
|
|
1317
|
+
if v.get("used_at"):
|
|
1318
|
+
out.append(f" used at: {', '.join(v['used_at'][:3])}")
|
|
1319
|
+
api = res.get("api_surface") or []
|
|
1320
|
+
if api:
|
|
1321
|
+
out.append(f"\n== API SURFACE DIFFERS (a variant lacks a symbol its siblings define): {len(api)}")
|
|
1322
|
+
for a in api[:limit]:
|
|
1323
|
+
out.append(f" {a['symbol']} in {', '.join(a['defined_in'])}; missing in {', '.join(a['missing_in'])} "
|
|
1324
|
+
f"(targets {', '.join(a['missing_on'])})")
|
|
1325
|
+
mc = res.get("missing_callee") or []
|
|
1326
|
+
if mc:
|
|
1327
|
+
out.append(f"\n== REFERENCED WHERE THE CALLEE IS NOT BUILT: {len(mc)}")
|
|
1328
|
+
for m in mc[:limit]:
|
|
1329
|
+
out.append(f" {m['from']} -{m['kind']}-> {m['to']} @ {m['at']} missing on: {', '.join(m['missing_on'])}"
|
|
1330
|
+
f" (callee: {', '.join(m['callee_platforms']) or 'no known target'}"
|
|
1331
|
+
+ (f", {m['callee_condition']}" if m.get("callee_condition") else "") + ")")
|
|
1332
|
+
mt = res.get("missing_callee_tests") or []
|
|
1333
|
+
if mt:
|
|
1334
|
+
out.append(f"\n== FROM TEST CODE WHOSE PLATFORMS ARE THE PROJECT DEFAULT: {len(mt)} (the test target is "
|
|
1335
|
+
f"probably not built there; `--kind missing_callee_tests`)")
|
|
1336
|
+
for m in mt[:limit]:
|
|
1337
|
+
out.append(f" {m['from']} -{m['kind']}-> {m['to']} @ {m['at']} missing on: {', '.join(m['missing_on'])}"
|
|
1338
|
+
f" (callee: {', '.join(m['callee_platforms']) or 'no known target'}"
|
|
1339
|
+
+ (f", {m['callee_condition']}" if m.get("callee_condition") else "") + ")")
|
|
1340
|
+
c = res.get("counts") or {}
|
|
1341
|
+
if c.get("missing_callee_skipped_no_target"):
|
|
1342
|
+
out.append(f" ({c['missing_callee_skipped_no_target']} reference(s) to code built for no declared target not listed:"
|
|
1343
|
+
f" the targets use a system or unindexed definition)")
|
|
1344
|
+
if any(c.get(k, 0) > len(res.get(k) or []) for k in ("variants", "api_surface", "missing_callee")):
|
|
1345
|
+
out.append(f"\n(lists capped; totals: {', '.join(f'{k} {v}' for k, v in c.items())})")
|
|
1346
|
+
return "\n".join(out)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def render_summary(s: dict) -> str:
|
|
1350
|
+
if not s:
|
|
1351
|
+
return "no platform-specific code in this graph"
|
|
1352
|
+
out = [f"targets: {', '.join(s.get('targets') or []) or '(none)'}"]
|
|
1353
|
+
for p, src in (s.get("target_sources") or {}).items():
|
|
1354
|
+
out.append(f" {p}: {src}")
|
|
1355
|
+
out.append(f"conditions: {s['conditions']} in {s['files_with_conditions']} files; tagged {s['tagged_nodes']} symbols and "
|
|
1356
|
+
f"{s['tagged_edges']} references; {s.get('variant_edges', 0)} variant links")
|
|
1357
|
+
pt = s.get("per_target") or {}
|
|
1358
|
+
for p in s.get("targets") or []:
|
|
1359
|
+
if p in pt:
|
|
1360
|
+
x = pt[p]
|
|
1361
|
+
out.append(f" {p:8} {x['files']} files, {x['symbols']} symbols ({x['platform_specific_symbols']} platform-specific)")
|
|
1362
|
+
if s.get("unevaluated_conditions"):
|
|
1363
|
+
out.append(f"not evaluated: {s['unevaluated_conditions']} conditions (kept live), e.g. "
|
|
1364
|
+
+ "; ".join(s.get("unevaluated_samples") or [])[:300])
|
|
1365
|
+
d = (s.get("divergence") or {}).get("counts") or {}
|
|
1366
|
+
if d:
|
|
1367
|
+
out.append(f"divergence: {d.get('variants', 0)} variant groups, {d.get('api_surface', 0)} API differences, "
|
|
1368
|
+
f"{d.get('missing_callee', 0)} references to code missing on a target (cg platforms divergence)")
|
|
1369
|
+
return "\n".join(out)
|