From eca4d81565e1a4a09bffc96feeeb3dd5ef53d0e3 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 1 Aug 2026 00:49:28 -0400 Subject: [PATCH 1/3] feat(metadata): add bundle-based metadata filtering --- .gitignore | 3 + metadata-generator/CMakeLists.txt | 9 + metadata-generator/README.md | 29 + metadata-generator/benchmarks/RESULTS.md | 64 ++ metadata-generator/benchmarks/run-macos.sh | 48 ++ .../build-step-metadata-generator.py | 26 +- metadata-generator/include/IR.h | 2 + metadata-generator/include/MetadataFilter.h | 139 ++++ metadata-generator/src/IR/Class.cpp | 19 +- metadata-generator/src/IR/Factory.cpp | 31 +- metadata-generator/src/main.cpp | 10 +- metadata-generator/symbol-analyzer/Cargo.lock | 761 ++++++++++++++++++ metadata-generator/symbol-analyzer/Cargo.toml | 18 + metadata-generator/symbol-analyzer/src/lib.rs | 173 ++++ .../symbol-analyzer/src/main.rs | 198 +++++ .../symbol-analyzer/tests/cli.rs | 58 ++ .../tests/MetadataFilterTests.cpp | 42 + scripts/build_metadata_generator.sh | 12 +- scripts/build_nativescript.sh | 7 +- scripts/build_react_native_turbomodule.sh | 7 +- 20 files changed, 1635 insertions(+), 21 deletions(-) create mode 100644 metadata-generator/benchmarks/RESULTS.md create mode 100755 metadata-generator/benchmarks/run-macos.sh create mode 100644 metadata-generator/include/MetadataFilter.h create mode 100644 metadata-generator/symbol-analyzer/Cargo.lock create mode 100644 metadata-generator/symbol-analyzer/Cargo.toml create mode 100644 metadata-generator/symbol-analyzer/src/lib.rs create mode 100644 metadata-generator/symbol-analyzer/src/main.rs create mode 100644 metadata-generator/symbol-analyzer/tests/cli.rs create mode 100644 metadata-generator/tests/MetadataFilterTests.cpp diff --git a/.gitignore b/.gitignore index 95ef13f4b..f8435771d 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ packages/react-native/dist/ packages/react-native/ios/vendor/ packages/react-native/metadata/ packages/react-native/native-api/ + +# Rust metadata bundle analyzer build output +metadata-generator/symbol-analyzer/target/ diff --git a/metadata-generator/CMakeLists.txt b/metadata-generator/CMakeLists.txt index 97a4c8119..4f3afc702 100644 --- a/metadata-generator/CMakeLists.txt +++ b/metadata-generator/CMakeLists.txt @@ -9,6 +9,8 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -target ${METADATA_BINARY_ set(CMAKE_CXX_STANDARD 20) +option(METADATA_GENERATOR_BUILD_TESTS "Build metadata generator unit tests" OFF) + if(NOT DEFINED METADATA_BINARY_ARCH) set(METADATA_BINARY_ARCH "${CMAKE_HOST_SYSTEM_PROCESSOR}") endif(NOT DEFINED METADATA_BINARY_ARCH) @@ -109,3 +111,10 @@ install(TARGETS ${NAME} RUNTIME DESTINATION bin) configure_file(build-step-metadata-generator.py ${CMAKE_CURRENT_BINARY_DIR}/bin/build-step-metadata-generator.py COPYONLY) + +if(METADATA_GENERATOR_BUILD_TESTS) + enable_testing() + add_executable(metadata-filter-tests tests/MetadataFilterTests.cpp) + target_include_directories(metadata-filter-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + add_test(NAME metadata-filter-tests COMMAND metadata-filter-tests) +endif() diff --git a/metadata-generator/README.md b/metadata-generator/README.md index 388ac6ebb..df1e22859 100644 --- a/metadata-generator/README.md +++ b/metadata-generator/README.md @@ -46,3 +46,32 @@ Example command line arguments: ``` For a better way of generating these arguments, just run the TestRunner scheme on the v8ios-runtime project and get the arguments from the log. + +## Opt-in bundle-based metadata filtering + +`symbol-analyzer/` contains a Rust/Oxc analyzer that scans the emitted JavaScript +or TypeScript bundle for unresolved global symbols and writes them as a normal +NativeScript metadata whitelist. It processes multiple bundle/chunk files in +parallel and emits deterministically sorted rules. Parse or semantic-analysis +errors, dynamic global property access, and dynamic code execution fail open by +writing `*:*`, so an unsupported bundle cannot accidentally remove metadata. +Foundation and Runtime are retained by default as a conservative safety margin. + +The existing `whitelist.mdg` and `blacklist.mdg` behavior remains available. +When automatic filtering is enabled, `whitelist.mdg` is merged into the generated +whitelist and `blacklist.mdg` is still applied afterwards. + +Set these environment variables on the metadata-generator build phase: + +```bash +NS_METADATA_AUTO_FILTER=1 +# A shell-quoted list of emitted bundle files or directories. +NS_METADATA_BUNDLE_PATHS="$CONFIGURATION_BUILD_DIR/app/bundle.js" +``` + +The packaged analyzer next to `objc-metadata-generator` is used by default. +`NS_METADATA_SYMBOL_ANALYZER` can override its path for local development. + +For repeatable full-SDK performance runs on macOS, use +`benchmarks/run-macos.sh` with a fresh output directory. Each iteration records +wall/CPU time, peak memory, and SHA-256 hashes for every generated artifact. diff --git a/metadata-generator/benchmarks/RESULTS.md b/metadata-generator/benchmarks/RESULTS.md new file mode 100644 index 000000000..2646c9211 --- /dev/null +++ b/metadata-generator/benchmarks/RESULTS.md @@ -0,0 +1,64 @@ +# Metadata generator benchmark — 2026-08-01 + +Baseline commit: `15e5418cb07c01e3a567b4791325b87809668bd9` +(`origin/refactor`). Workload: the full macOS 26.5 SDK, binary metadata, +Foundation/AppKit TypeScript declarations, umbrella header, and signature +dispatch bindings. Each result below is the median of five warm-cache runs from +`run-macos.sh`. + +Host: Apple M5 Pro (18 cores, 64 GiB), macOS 27.0 (26A5378n), Xcode 26.6, +Apple Clang 21.0.0, CMake 4.3.2. + +| Measurement | Baseline | Optimized | Change | +| --- | ---: | ---: | ---: | +| Wall time | 6.98 s | 4.94 s | -29.2% | +| User CPU | 5.62 s | 4.35 s | -22.6% | +| System CPU | 0.80 s | 0.55 s | -31.2% | +| Retired instructions | 54.86 B | 52.66 B | -4.0% | +| Maximum RSS | 1,212,743,680 B | 1,181,007,872 B | -2.6% | +| Peak footprint | 1,125,811,544 B | 1,095,992,640 B | -2.6% | + +All 207 generated artifacts were byte-for-byte identical in every baseline and +optimized run. The primary changes were parsing declarations with +`CXTranslationUnit_SkipFunctionBodies` and reserving Objective-C class member +storage before constructing members. + +## Profile summary + +The baseline Time Profiler trace contained 4,735 ms of sampled CPU time: + +| Stage | Sampled CPU | Share | +| --- | ---: | ---: | +| Clang parse | 3,093 ms | 65.3% | +| IR construction/post-processing | 1,311 ms | 27.7% | +| Metadata serialization | 164 ms | 3.5% | +| TypeScript emission | 64 ms | 1.4% | +| Umbrella discovery | 36 ms | 0.8% | + +The largest generator-level CPU sites were `MetadataFactory::process` (889 ms), +retained-return attribute detection (614 ms), class processing (421 ms), and +`MetadataFactory::postProcess` (417 ms). Token/pretty-print scans accounted for +562 ms. `open` and `stat` accounted for about 360 ms; output writing was not a +dominant cost. + +A malloc-stack snapshot at an 808.7 MiB process footprint was dominated by +libclang diagnostics, `SmallVector`, `StringMap`, and source buffers. The largest +generator-owned site was Objective-C class member-vector growth (26.9 MiB), +which motivated the exact-capacity reservation. + +## Opt-in filtering example + +A small AppKit bundle using `NSView` produced a conservative whitelist retaining +Foundation, Runtime, the referenced symbols, and dependency closure: + +| Output | Unfiltered | Filtered | Change | +| --- | ---: | ---: | ---: | +| Binary metadata | 6,426,238 B | 485,325 B | -92.4% | +| Signature bindings | 8,447,422 B | 1,322,558 B | -84.3% | +| TypeScript output | 14,864 KiB | 1,024 KiB | -93.1% | +| Wall time | 6.98 s median | 3.80 s | -45.6% | +| Maximum RSS | 1,212,743,680 B | 982,958,080 B | -18.9% | + +This is an illustrative workload, not a size guarantee; the dependency closure +and default Foundation/Runtime retention intentionally prefer false positives +over missing native metadata. diff --git a/metadata-generator/benchmarks/run-macos.sh b/metadata-generator/benchmarks/run-macos.sh new file mode 100755 index 000000000..ead252efc --- /dev/null +++ b/metadata-generator/benchmarks/run-macos.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -euo pipefail + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage: $0 [iterations]" >&2 + exit 2 +fi + +generator=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +output=$2 +iterations=${3:-5} + +if [ ! -x "$generator" ]; then + echo "Generator is not executable: $generator" >&2 + exit 2 +fi +if [ -e "$output" ]; then + echo "Output path already exists (use a new path): $output" >&2 + exit 2 +fi + +sdk_root=$(xcrun --sdk macosx --show-sdk-path) +sdk_version=$(xcrun --sdk macosx --show-sdk-version) +mkdir -p "$output" + +for iteration in $(jot "$iterations"); do + run="$output/run-$iteration" + mkdir -p "$run/types" + /usr/bin/time -l -o "$run/timing.txt" "$generator" \ + "types=$run/types" \ + "ts-index-mode=frameworks-list" \ + "ts-index-frameworks=Foundation,AppKit" \ + -output-bin "$run/metadata.bin" \ + -output-umbrella "$run/umbrella.h" \ + -output-signature-bindings-cpp "$run/signatures.inc" \ + Xclang -isysroot "$sdk_root" -std=gnu99 \ + -target "arm64-apple-macosx$sdk_version" \ + >"$run/stdout.log" 2>"$run/stderr.log" + + ( + cd "$run" + find . -type f ! -name '*.log' ! -name timing.txt ! -name hashes.txt -print0 | \ + LC_ALL=C sort -z | xargs -0 shasum -a 256 + ) >"$run/hashes.txt" +done + +echo "Completed $iterations run(s) in $output" +echo "Compare timing.txt files and verify hashes.txt lists are identical." diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 444b37d9d..8af36f279 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -175,6 +175,11 @@ def is_nativescript_source_root(search_path): default_signature_bindings_path = os.path.join(src_root, "NativeScript", "ffi", "objc", "napi", "GeneratedSignatureDispatch.inc") if os.path.isdir(os.path.dirname(default_signature_bindings_path)): signature_bindings_cpp_path = default_signature_bindings_path +auto_filter_enabled = env_bool("NS_METADATA_AUTO_FILTER") or env_bool("TNS_METADATA_AUTO_FILTER") +auto_filter_bundle_paths = env_or_none("NS_METADATA_BUNDLE_PATHS") or env_or_none("TNS_METADATA_BUNDLE_PATHS") +symbol_analyzer_path = env_or_none("NS_METADATA_SYMBOL_ANALYZER") or env_or_none("TNS_METADATA_SYMBOL_ANALYZER") +if symbol_analyzer_path is None: + symbol_analyzer_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ns-metadata-symbols") def save_stream_to_file(filename, stream): @@ -212,8 +217,25 @@ def generate_metadata(arch): print("Generating signature dispatch bindings in: \"{}\"".format(signature_bindings_cpp_path)) whitelist_file_name = os.path.join(src_root, "whitelist.mdg") - if os.path.exists(whitelist_file_name): - generator_call.extend(["--whitelist-modules-file", whitelist_file_name]) + effective_whitelist_file_name = whitelist_file_name if os.path.exists(whitelist_file_name) else None + if auto_filter_enabled: + if not auto_filter_bundle_paths: + raise RuntimeError("NS_METADATA_AUTO_FILTER requires NS_METADATA_BUNDLE_PATHS") + if not os.path.isfile(symbol_analyzer_path) or not os.access(symbol_analyzer_path, os.X_OK): + raise RuntimeError("Metadata symbol analyzer is not executable: {}".format(symbol_analyzer_path)) + + effective_whitelist_file_name = os.path.join( + conf_build_dir, "metadata-auto-whitelist-{}.mdg".format(arch)) + analyzer_call = [symbol_analyzer_path, "--output", effective_whitelist_file_name] + if os.path.exists(whitelist_file_name): + analyzer_call.extend(["--include-whitelist", whitelist_file_name]) + analyzer_call.extend(shlex.split(auto_filter_bundle_paths)) + print("Generating metadata whitelist from app bundle(s):") + print(" ".join(analyzer_call)) + subprocess.check_call(analyzer_call) + + if effective_whitelist_file_name is not None: + generator_call.extend(["--whitelist-modules-file", effective_whitelist_file_name]) blacklist_file_name = os.path.join(src_root, "blacklist.mdg") if os.path.exists(blacklist_file_name): diff --git a/metadata-generator/include/IR.h b/metadata-generator/include/IR.h index e494f5d1e..663f48ab7 100644 --- a/metadata-generator/include/IR.h +++ b/metadata-generator/include/IR.h @@ -7,6 +7,7 @@ #include #include "Metadata.h" +#include "MetadataFilter.h" #include "Util.h" #include "clang-c/Index.h" @@ -364,6 +365,7 @@ class MetadataFactory { void processProtocolRefs(); std::unordered_set includePaths; + MetadataFilter metadataFilter; std::unordered_map variables; std::unordered_map enums; diff --git a/metadata-generator/include/MetadataFilter.h b/metadata-generator/include/MetadataFilter.h new file mode 100644 index 000000000..e43606b4c --- /dev/null +++ b/metadata-generator/include/MetadataFilter.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace metagen { + +class MetadataFilter { + public: + void configure(const std::string& whitelistFile, + const std::string& blacklistFile) { + whitelistDefined = !whitelistFile.empty(); + whitelist = readPatterns(whitelistFile); + blacklist = readPatterns(blacklistFile); + } + + bool active() const { + return whitelistDefined || !blacklist.globalSymbols.empty() || + !blacklist.patterns.empty(); + } + + bool isAllowed(std::string_view module, std::string_view symbol) const { + // Preserve the original NativeScript filter invariant: NSObject is + // required by both the runtime and the generator and is never removed. + if (symbol == "NSObject") { + return true; + } + bool enabled = !whitelistDefined || matchesAny(whitelist, module, symbol); + return enabled && !matchesAny(blacklist, module, symbol); + } + + static bool wildcardMatch(std::string_view pattern, + std::string_view value) { + size_t patternIndex = 0; + size_t valueIndex = 0; + size_t starIndex = std::string_view::npos; + size_t starValueIndex = 0; + + while (valueIndex < value.size()) { + if (patternIndex < pattern.size() && + (pattern[patternIndex] == '?' || + pattern[patternIndex] == value[valueIndex])) { + patternIndex++; + valueIndex++; + } else if (patternIndex < pattern.size() && + pattern[patternIndex] == '*') { + starIndex = patternIndex++; + starValueIndex = valueIndex; + } else if (starIndex != std::string_view::npos) { + patternIndex = starIndex + 1; + valueIndex = ++starValueIndex; + } else { + return false; + } + } + + while (patternIndex < pattern.size() && pattern[patternIndex] == '*') { + patternIndex++; + } + return patternIndex == pattern.size(); + } + + private: + struct Pattern { + std::string module; + std::string symbol; + }; + + struct TransparentStringHash { + using is_transparent = void; + + size_t operator()(std::string_view value) const { + return std::hash{}(value); + } + }; + + struct PatternSet { + // The bundle analyzer emits one `*:symbol` rule per unresolved native + // symbol. Index that common case so filtering stays O(1) per declaration. + std::unordered_set> + globalSymbols; + std::vector patterns; + }; + + static PatternSet readPatterns(const std::string& path) { + PatternSet result; + if (path.empty()) { + return result; + } + + std::ifstream input(path); + if (!input) { + throw std::invalid_argument("Metadata filter file not found: " + path); + } + + std::string line; + while (std::getline(input, line)) { + if (line.empty() || line.starts_with('#') || line.starts_with("//")) { + continue; + } + + size_t colon = line.find(':'); + Pattern pattern = {line.substr(0, colon), + colon == std::string::npos ? std::string() + : line.substr(colon + 1)}; + if (pattern.module == "*" && + pattern.symbol.find_first_of("*?") == std::string::npos) { + result.globalSymbols.insert(std::move(pattern.symbol)); + } else { + result.patterns.push_back(std::move(pattern)); + } + } + return result; + } + + static bool matchesAny(const PatternSet& patternSet, + std::string_view module, std::string_view symbol) { + if (patternSet.globalSymbols.contains(symbol)) { + return true; + } + for (const Pattern& pattern : patternSet.patterns) { + if ((pattern.module.empty() || wildcardMatch(pattern.module, module)) && + (pattern.symbol.empty() || wildcardMatch(pattern.symbol, symbol))) { + return true; + } + } + return false; + } + + bool whitelistDefined = false; + PatternSet whitelist; + PatternSet blacklist; +}; + +} // namespace metagen diff --git a/metadata-generator/src/IR/Class.cpp b/metadata-generator/src/IR/Class.cpp index 7393f348a..d8a694d3c 100644 --- a/metadata-generator/src/IR/Class.cpp +++ b/metadata-generator/src/IR/Class.cpp @@ -30,10 +30,16 @@ ClassDecl::ClassDecl(CXCursor cursor) { } } + struct ClassVisitorState { + ClassDecl* cls; + size_t memberCount = 0; + } state = {this}; + clang_visitChildren( cursor, [](CXCursor cursor, CXCursor, CXClientData clientData) { - auto cls = (ClassDecl*)clientData; + auto* state = static_cast(clientData); + auto* cls = state->cls; CXCursorKind kind = clang_getCursorKind(cursor); @@ -69,13 +75,22 @@ ClassDecl::ClassDecl(CXCursor cursor) { break; } + case CXCursor_ObjCPropertyDecl: + case CXCursor_ObjCClassMethodDecl: + case CXCursor_ObjCInstanceMethodDecl: { + state->memberCount++; + break; + } + default: break; } return CXChildVisit_Continue; }, - this); + &state); + + members.reserve(state.memberCount); clang_visitChildren( cursor, diff --git a/metadata-generator/src/IR/Factory.cpp b/metadata-generator/src/IR/Factory.cpp index 932bf5e44..7ddc67996 100644 --- a/metadata-generator/src/IR/Factory.cpp +++ b/metadata-generator/src/IR/Factory.cpp @@ -96,20 +96,33 @@ bool MetadataFactory::shouldProcess(CXCursor cursor, bool required) { clang_disposeString(fileName); auto cached = shouldProcessCache.find(fileNameStr); + bool shouldInclude = false; if (cached != shouldProcessCache.end()) { - return cached->second; + shouldInclude = cached->second; + } else { + for (const std::string& path : includePaths) { + if (fileNameStr.find(path) != std::string::npos) { + shouldInclude = true; + break; + } + } + shouldProcessCache.emplace(std::move(fileNameStr), shouldInclude); } - bool shouldInclude = false; - for (const std::string& path : includePaths) { - if (fileNameStr.find(path) != std::string::npos) { - shouldInclude = true; - break; - } + if (!shouldInclude || !metadataFilter.active()) { + return shouldInclude; } - shouldProcessCache.emplace(std::move(fileNameStr), shouldInclude); - return shouldInclude; + CXString cxSymbol = clang_getCursorSpelling(cursor); + const char* symbolCString = clang_getCString(cxSymbol); + std::string symbol = symbolCString ? symbolCString : ""; + clang_disposeString(cxSymbol); + + // Anonymous records and enums can contain multiple globally-visible child + // symbols. Keep them conservatively; referenced named declarations are + // also retained through the existing `required` dependency path. + return symbol.empty() || + metadataFilter.isAllowed(getFrameworkName(cursor), symbol); } void MetadataFactory::implementClassProtocols( diff --git a/metadata-generator/src/main.cpp b/metadata-generator/src/main.cpp index 800a626e6..985b1d018 100644 --- a/metadata-generator/src/main.cpp +++ b/metadata-generator/src/main.cpp @@ -118,9 +118,11 @@ int main(int argc, char** argv) { outputDtsFolder = argv[++i]; } else if (arg == "-docset-path") { docSetFile = argv[++i]; - } else if (arg == "-blacklist-modules") { + } else if (arg == "-blacklist-modules" || + arg == "--blacklist-modules-file") { blacklistModulesFile = argv[++i]; - } else if (arg == "-whitelist-modules") { + } else if (arg == "-whitelist-modules" || + arg == "--whitelist-modules-file") { whitelistModulesFile = argv[++i]; } else if (arg.find("framework=") == 0) { addFramework(arg.substr(10)); @@ -228,7 +230,8 @@ int main(int argc, char** argv) { CXIndex index = clang_createIndex(0, 0); CXTranslationUnit unit = clang_parseTranslationUnit( index, umbrellaHeaderName.c_str(), argsC.data(), - (MDSectionOffset)argsC.size(), nullptr, 0, CXTranslationUnit_None); + (MDSectionOffset)argsC.size(), nullptr, 0, + CXTranslationUnit_SkipFunctionBodies); // std::remove(umbrellaHeaderName.c_str()); @@ -259,6 +262,7 @@ int main(int argc, char** argv) { MetadataFactory factory; factory.includePaths = includePaths; + factory.metadataFilter.configure(whitelistModulesFile, blacklistModulesFile); factory.process(cursor); factory.postProcess(); diff --git a/metadata-generator/symbol-analyzer/Cargo.lock b/metadata-generator/symbol-analyzer/Cargo.lock new file mode 100644 index 000000000..e181149f2 --- /dev/null +++ b/metadata-generator/symbol-analyzer/Cargo.lock @@ -0,0 +1,761 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "static_assertions", + "zmij", +] + +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "dragonbox_ecma" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "ns-metadata-symbols" +version = "0.1.0" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_parser", + "oxc_semantic", + "oxc_span", + "rayon", + "tempfile", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "oxc-miette" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0df30faa68797917ca4263e7a2f889ec829e4da2dcb3d6dc752f7a494180f3" +dependencies = [ + "cfg-if", + "memchr", + "owo-colors", + "oxc-miette-derive", + "textwrap", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "oxc-miette-derive" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc072d11d45ebe7801459b4e829184ba0934d68027fdc51d327335b53a95a49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "oxc_allocator" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ffd0bdf76f7272e208a4f55740d309f303a104f198bf4902a7377ae33e0a34" +dependencies = [ + "allocator-api2", + "hashbrown", + "oxc_data_structures", + "rustc-hash", +] + +[[package]] +name = "oxc_ast" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e4a9979cd3cebde3db75fafa713d72e6e891827c29e3ed11d005791052456d9" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_estree", + "oxc_regular_expression", + "oxc_span", + "oxc_str", + "oxc_syntax", +] + +[[package]] +name = "oxc_ast_macros" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0adbaf016626ad630020c2d5f9078809aeb115966a9268b313e920c98e58040" +dependencies = [ + "phf", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "oxc_ast_visit" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95874e8d4c97ee55054ae50746347fc6d018d0bed9e4f40fef483eb4cf22825a" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_data_structures" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c41eb52557e3255ebacd0d0b5406d1c8703a59cb5300938940b91de41a46f" + +[[package]] +name = "oxc_diagnostics" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d355bdf332e8412c7e7e061bf53ac3bf71956225722e8ce1b9428402d97e9db" +dependencies = [ + "cow-utils", + "oxc-miette", + "percent-encoding", +] + +[[package]] +name = "oxc_ecmascript" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3729826d5e8e50b6788c79ae3c5f18b6d4b372fb1d5a926813c0edfa69ee5831" +dependencies = [ + "num-bigint", + "num-traits", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_estree" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aec60e099d4c904848f51e2737f290c665be0ed66fb295def44f2e91891b604" + +[[package]] +name = "oxc_index" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e" +dependencies = [ + "nonmax", + "serde", +] + +[[package]] +name = "oxc_parser" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8f483bbfae70f4f7c9a90c4a4a3a165685c2d929e0765e35db6db1ac825c32" +dependencies = [ + "bitflags", + "cow-utils", + "memchr", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_regular_expression", + "oxc_span", + "oxc_str", + "oxc_syntax", + "rustc-hash", + "seq-macro", +] + +[[package]] +name = "oxc_regular_expression" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a41b057cf6d615e5736e80eddf9f611827fc0927bc1a7f33b4b10e171cfae4d" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_diagnostics", + "oxc_span", + "oxc_str", + "phf", + "rustc-hash", + "unicode-id-start", +] + +[[package]] +name = "oxc_semantic" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfab481b9838e23958cfd03638fcb14672be7ca1a3a23d9a87c1a47a00c7cd8" +dependencies = [ + "itertools", + "memchr", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_index", + "oxc_span", + "oxc_str", + "oxc_syntax", + "rustc-hash", + "self_cell", + "smallvec", +] + +[[package]] +name = "oxc_span" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adc1cfe13a710b2c948ffd26470351a8f0e44b6d5f72e9f8d62dcd2dbab500a" +dependencies = [ + "compact_str", + "oxc-miette", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_str", +] + +[[package]] +name = "oxc_str" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7423b8162376b5dd53e6431d6b25a4917048cbd10429778e9d4ae7701062e239" +dependencies = [ + "compact_str", + "hashbrown", + "oxc_allocator", + "oxc_estree", +] + +[[package]] +name = "oxc_syntax" +version = "0.141.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6a9cea50ff9fa2c17bdb1dec8ab69d0e934015b2ba1414040da7f8db822154" +dependencies = [ + "bitflags", + "cow-utils", + "dragonbox_ecma", + "nonmax", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_index", + "oxc_span", + "oxc_str", + "phf", + "unicode-id-start", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "unicode-id-start" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/metadata-generator/symbol-analyzer/Cargo.toml b/metadata-generator/symbol-analyzer/Cargo.toml new file mode 100644 index 000000000..898ecc87b --- /dev/null +++ b/metadata-generator/symbol-analyzer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ns-metadata-symbols" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +description = "Conservative NativeScript bundle symbol analyzer for metadata filtering" + +[dependencies] +oxc_allocator = "0.141.0" +oxc_ast = "0.141.0" +oxc_ast_visit = "0.141.0" +oxc_parser = "0.141.0" +oxc_semantic = "0.141.0" +oxc_span = "0.141.0" +rayon = "1.12.0" + +[dev-dependencies] +tempfile = "3.23.0" diff --git a/metadata-generator/symbol-analyzer/src/lib.rs b/metadata-generator/symbol-analyzer/src/lib.rs new file mode 100644 index 000000000..b4a25a867 --- /dev/null +++ b/metadata-generator/symbol-analyzer/src/lib.rs @@ -0,0 +1,173 @@ +use std::collections::BTreeSet; +use std::path::Path; + +use oxc_allocator::Allocator; +use oxc_ast::ast::{ + Argument, CallExpression, ComputedMemberExpression, Expression, StaticMemberExpression, +}; +use oxc_ast_visit::{Visit, walk}; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::SourceType; + +#[derive(Debug, Default)] +pub struct Analysis { + pub symbols: BTreeSet, + pub diagnostics: usize, + pub bailed_out: bool, +} + +#[derive(Default)] +struct GlobalPropertyCollector { + symbols: BTreeSet, + dynamic_access: bool, +} + +fn is_global_object(expression: &Expression<'_>) -> bool { + matches!( + expression, + Expression::Identifier(identifier) + if matches!(identifier.name.as_str(), "globalThis" | "global") + ) +} + +impl<'a> Visit<'a> for GlobalPropertyCollector { + fn visit_static_member_expression(&mut self, expression: &StaticMemberExpression<'a>) { + if is_global_object(&expression.object) { + self.symbols.insert(expression.property.name.to_string()); + } + walk::walk_static_member_expression(self, expression); + } + + fn visit_computed_member_expression(&mut self, expression: &ComputedMemberExpression<'a>) { + if is_global_object(&expression.object) { + if let Expression::StringLiteral(property) = &expression.expression { + self.symbols.insert(property.value.to_string()); + } else { + // A computed global property cannot be resolved statically. Keep all + // metadata rather than risk a false negative. + self.dynamic_access = true; + } + } + walk::walk_computed_member_expression(self, expression); + } + + fn visit_call_expression(&mut self, expression: &CallExpression<'a>) { + let is_dynamic_native_lookup = matches!( + &expression.callee, + Expression::Identifier(identifier) + if matches!( + identifier.name.as_str(), + "NSClassFromString" + | "NSProtocolFromString" + | "objc_getClass" + | "objc_lookUpClass" + | "objc_getProtocol" + ) + ); + if is_dynamic_native_lookup { + match expression.arguments.first() { + Some(Argument::StringLiteral(name)) => { + self.symbols.insert(name.value.to_string()); + } + _ => self.dynamic_access = true, + } + } + walk::walk_call_expression(self, expression); + } +} + +pub fn analyze_source(path: &Path, source: &str) -> Analysis { + let source_type = SourceType::from_path(path).unwrap_or_else(|_| SourceType::unambiguous()); + let allocator = Allocator::default(); + let parsed = Parser::new(&allocator, source, source_type).parse(); + + if parsed.panicked || !parsed.diagnostics.is_empty() { + return Analysis { + diagnostics: parsed.diagnostics.len().max(1), + bailed_out: true, + ..Analysis::default() + }; + } + + let mut global_properties = GlobalPropertyCollector::default(); + global_properties.visit_program(&parsed.program); + + let semantic = SemanticBuilder::new().build(&parsed.program); + if !semantic.diagnostics.is_empty() { + return Analysis { + diagnostics: semantic.diagnostics.len(), + bailed_out: true, + ..Analysis::default() + }; + } + + let mut symbols: BTreeSet = semantic + .semantic + .scoping() + .root_unresolved_references() + .keys() + .map(ToString::to_string) + .collect(); + let uses_dynamic_code = symbols.contains("eval") || symbols.contains("Function"); + symbols.extend(global_properties.symbols); + + Analysis { + symbols, + diagnostics: 0, + bailed_out: global_properties.dynamic_access || uses_dynamic_code, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collects_only_unbound_runtime_symbols() { + let result = analyze_source( + Path::new("bundle.js"), + "const local = 1; new UIView(); CGRectMake(0, 0, local, 1);", + ); + + assert!(!result.bailed_out); + assert!(result.symbols.contains("UIView")); + assert!(result.symbols.contains("CGRectMake")); + assert!(!result.symbols.contains("local")); + } + + #[test] + fn bails_out_when_the_bundle_cannot_be_parsed() { + let result = analyze_source(Path::new("bundle.js"), "function {"); + assert!(result.bailed_out); + assert!(result.diagnostics > 0); + } + + #[test] + fn collects_static_global_properties_and_fails_open_for_dynamic_access() { + let static_result = analyze_source( + Path::new("bundle.js"), + "new globalThis.UIView(); global['CGRectMake'](0, 0, 1, 1);", + ); + assert!(!static_result.bailed_out); + assert!(static_result.symbols.contains("UIView")); + assert!(static_result.symbols.contains("CGRectMake")); + + let dynamic_result = analyze_source(Path::new("bundle.js"), "globalThis[className]"); + assert!(dynamic_result.bailed_out); + } + + #[test] + fn extracts_literal_native_lookups_and_fails_open_for_dynamic_names() { + let literal = analyze_source( + Path::new("bundle.js"), + "NSClassFromString('NSView'); objc_getProtocol('NSDraggingDestination');", + ); + assert!(!literal.bailed_out); + assert!(literal.symbols.contains("NSView")); + assert!(literal.symbols.contains("NSDraggingDestination")); + + let dynamic = analyze_source(Path::new("bundle.js"), "NSClassFromString(className)"); + assert!(dynamic.bailed_out); + } +} diff --git a/metadata-generator/symbol-analyzer/src/main.rs b/metadata-generator/symbol-analyzer/src/main.rs new file mode 100644 index 000000000..ae12c0e80 --- /dev/null +++ b/metadata-generator/symbol-analyzer/src/main.rs @@ -0,0 +1,198 @@ +use std::collections::BTreeSet; +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +use ns_metadata_symbols::analyze_source; +use rayon::prelude::*; + +#[derive(Default)] +struct Options { + output: Option, + include_whitelists: Vec, + keep_modules: Vec, + inputs: Vec, + no_default_modules: bool, +} + +fn usage() -> &'static str { + "Usage: ns-metadata-symbols [options] ...\n\ + Options:\n\ + --output Write an mdg whitelist instead of stdout\n\ + --include-whitelist Merge an existing NativeScript whitelist\n\ + --keep-module Keep an entire native module/framework\n\ + --no-default-modules Do not automatically keep Foundation/Runtime\n\ + --help Show this help" +} + +fn parse_options() -> Result { + let mut options = Options::default(); + let mut args = env::args_os().skip(1); + while let Some(arg) = args.next() { + match arg.to_string_lossy().as_ref() { + "--output" => { + options.output = Some(args.next().ok_or("--output requires a path")?.into()); + } + "--include-whitelist" => options.include_whitelists.push( + args.next() + .ok_or("--include-whitelist requires a path")? + .into(), + ), + "--keep-module" => options.keep_modules.push( + args.next() + .ok_or("--keep-module requires a name")? + .to_string_lossy() + .into_owned(), + ), + "--no-default-modules" => options.no_default_modules = true, + "--help" | "-h" => return Err(usage().to_owned()), + value if value.starts_with('-') => return Err(format!("Unknown option: {value}")), + _ => options.inputs.push(arg.into()), + } + } + + if options.inputs.is_empty() { + return Err("At least one bundle or directory is required".to_owned()); + } + Ok(options) +} + +fn is_source_file(path: &Path) -> bool { + matches!( + path.extension().and_then(|value| value.to_str()), + Some("js" | "mjs" | "cjs" | "jsx" | "ts" | "mts" | "cts" | "tsx") + ) +} + +fn collect_files(path: &Path, output: &mut Vec) -> io::Result<()> { + if path.is_file() { + if is_source_file(path) { + output.push(path.to_owned()); + } + return Ok(()); + } + + for entry in fs::read_dir(path)? { + let entry = entry?; + let file_type = entry.file_type()?; + if file_type.is_symlink() { + continue; + } + if file_type.is_dir() { + collect_files(&entry.path(), output)?; + } else if file_type.is_file() && is_source_file(&entry.path()) { + output.push(entry.path()); + } + } + Ok(()) +} + +fn run(options: Options) -> Result<(), String> { + let mut files = Vec::new(); + for input in &options.inputs { + collect_files(input, &mut files) + .map_err(|error| format!("Unable to read {}: {error}", input.display()))?; + } + files.sort(); + files.dedup(); + if files.is_empty() { + return Err("No JavaScript or TypeScript bundle files were found".to_owned()); + } + + let analyses: Vec<_> = files + .par_iter() + .map(|path| { + let bytes = fs::read(path) + .map_err(|error| format!("Unable to read {}: {error}", path.display()))?; + let analysis = match std::str::from_utf8(&bytes) { + Ok(source) => analyze_source(path, source), + Err(_) => ns_metadata_symbols::Analysis { + diagnostics: 1, + bailed_out: true, + ..ns_metadata_symbols::Analysis::default() + }, + }; + Ok::<_, String>((path, analysis)) + }) + .collect::>()?; + + let mut lines = BTreeSet::new(); + for whitelist in &options.include_whitelists { + let contents = fs::read_to_string(whitelist) + .map_err(|error| format!("Unable to read {}: {error}", whitelist.display()))?; + lines.extend( + contents + .lines() + .map(str::trim) + .filter(|line| { + !line.is_empty() && !line.starts_with('#') && !line.starts_with("//") + }) + .map(str::to_owned), + ); + } + + if !options.no_default_modules { + lines.insert("Foundation:*".to_owned()); + lines.insert("Runtime:*".to_owned()); + } + lines.extend( + options + .keep_modules + .iter() + .map(|module| format!("{module}:*")), + ); + + let mut diagnostics = 0; + let mut bailed_out = false; + for (_, analysis) in &analyses { + diagnostics += analysis.diagnostics; + bailed_out |= analysis.bailed_out; + lines.extend(analysis.symbols.iter().map(|symbol| format!("*:{symbol}"))); + } + + if bailed_out { + // Filtering must fail open. A malformed or unsupported bundle should + // never cause required native metadata to be removed. + lines.insert("*:*".to_owned()); + } + + let mut rendered = String::from("# Generated by ns-metadata-symbols; do not edit.\n"); + for line in &lines { + rendered.push_str(line); + rendered.push('\n'); + } + + match options.output { + Some(path) => fs::write(&path, rendered) + .map_err(|error| format!("Unable to write {}: {error}", path.display()))?, + None => io::stdout() + .write_all(rendered.as_bytes()) + .map_err(|error| format!("Unable to write stdout: {error}"))?, + } + + eprintln!( + "Analyzed {} file(s) on {} worker(s): {} symbol pattern(s), {} diagnostic(s){}", + files.len(), + rayon::current_num_threads(), + lines.len(), + diagnostics, + if bailed_out { + "; filtering disabled (fail-open)" + } else { + "" + } + ); + Ok(()) +} + +fn main() -> ExitCode { + match parse_options().and_then(run) { + Ok(()) => ExitCode::SUCCESS, + Err(message) => { + eprintln!("{message}"); + ExitCode::FAILURE + } + } +} diff --git a/metadata-generator/symbol-analyzer/tests/cli.rs b/metadata-generator/symbol-analyzer/tests/cli.rs new file mode 100644 index 000000000..efae8759f --- /dev/null +++ b/metadata-generator/symbol-analyzer/tests/cli.rs @@ -0,0 +1,58 @@ +use std::fs; +use std::process::Command; + +#[test] +fn writes_deterministic_mdg_and_merges_manual_whitelist() { + let root = tempfile::tempdir().unwrap(); + let bundle = root.path().join("bundle.js"); + let manual = root.path().join("whitelist.mdg"); + let output = root.path().join("generated.mdg"); + let second_output = root.path().join("generated-again.mdg"); + fs::write(&bundle, "new UIView(); CGRectMake(0, 0, 1, 1);").unwrap(); + fs::write(&manual, "# existing rules\nAppKit:NSView\n").unwrap(); + + let status = Command::new(env!("CARGO_BIN_EXE_ns-metadata-symbols")) + .args(["--output"]) + .arg(&output) + .args(["--include-whitelist"]) + .arg(&manual) + .arg(&bundle) + .status() + .unwrap(); + assert!(status.success()); + + let result = fs::read_to_string(output).unwrap(); + assert!(result.contains("*:CGRectMake\n")); + assert!(result.contains("*:UIView\n")); + assert!(result.contains("AppKit:NSView\n")); + assert!(result.contains("Foundation:*\n")); + assert!(result.contains("Runtime:*\n")); + + let second_status = Command::new(env!("CARGO_BIN_EXE_ns-metadata-symbols")) + .args(["--output"]) + .arg(&second_output) + .args(["--include-whitelist"]) + .arg(&manual) + .arg(&bundle) + .status() + .unwrap(); + assert!(second_status.success()); + assert_eq!(result, fs::read_to_string(second_output).unwrap()); +} + +#[test] +fn non_utf8_input_disables_filtering_instead_of_failing_the_build() { + let root = tempfile::tempdir().unwrap(); + let bundle = root.path().join("bundle.js"); + let output = root.path().join("generated.mdg"); + fs::write(&bundle, [0xff, 0xfe]).unwrap(); + + let status = Command::new(env!("CARGO_BIN_EXE_ns-metadata-symbols")) + .args(["--output"]) + .arg(&output) + .arg(&bundle) + .status() + .unwrap(); + assert!(status.success()); + assert!(fs::read_to_string(output).unwrap().contains("*:*\n")); +} diff --git a/metadata-generator/tests/MetadataFilterTests.cpp b/metadata-generator/tests/MetadataFilterTests.cpp new file mode 100644 index 000000000..cba189d18 --- /dev/null +++ b/metadata-generator/tests/MetadataFilterTests.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "MetadataFilter.h" + +using metagen::MetadataFilter; + +int main() { + assert(MetadataFilter::wildcardMatch("*", "")); + assert(MetadataFilter::wildcardMatch("UI*", "UIView")); + assert(MetadataFilter::wildcardMatch("NS?rray", "NSArray")); + assert(!MetadataFilter::wildcardMatch("UI*", "NSObject")); + + std::filesystem::path root = + std::filesystem::temp_directory_path() / "metadata-filter-tests"; + std::filesystem::create_directories(root); + std::filesystem::path whitelist = root / "whitelist.mdg"; + std::filesystem::path blacklist = root / "blacklist.mdg"; + + { + std::ofstream output(whitelist); + output << "Foundation:NSObject\n"; + output << "UIKit:UI*\n"; + } + { + std::ofstream output(blacklist); + output << "UIKit:UIWebView\n"; + output << "Foundation:NSObject\n"; + } + + MetadataFilter filter; + filter.configure(whitelist, blacklist); + assert(filter.isAllowed("Foundation", "NSObject")); + assert(filter.isAllowed("UIKit", "UIView")); + assert(!filter.isAllowed("UIKit", "UIWebView")); + assert(!filter.isAllowed("AppKit", "NSView")); + assert(filter.isAllowed("Foundation", "NSObject")); + + std::filesystem::remove_all(root); + return 0; +} diff --git a/scripts/build_metadata_generator.sh b/scripts/build_metadata_generator.sh index 1d4b3c0e2..1c1cb35c1 100755 --- a/scripts/build_metadata_generator.sh +++ b/scripts/build_metadata_generator.sh @@ -3,7 +3,9 @@ set -e source "$(dirname "$0")/build_utils.sh" function metadata_generator_source_hash { - find src include CMakeLists.txt -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' + find src include tests symbol-analyzer CMakeLists.txt build-step-metadata-generator.py \ + \( -name target -type d -prune \) -o -type f -print | \ + LC_ALL=C sort | xargs shasum | awk '{print $1}' | shasum | awk '{print $1}' } function build { @@ -14,6 +16,14 @@ function build { cmake --build build -j$NUMJOBS mkdir "dist/$1" cp -r "build/bin" "dist/$1" + + local rust_arch="$1" + if [ "$1" = "arm64" ]; then + rust_arch="aarch64" + fi + local rust_target="${rust_arch}-apple-darwin" + cargo build --locked --release --manifest-path symbol-analyzer/Cargo.toml --target "$rust_target" + cp "symbol-analyzer/target/$rust_target/release/ns-metadata-symbols" "dist/$1/bin/" } pushd "metadata-generator" diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index f3c70d67c..42d070983 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -171,8 +171,11 @@ function signature_dispatch_path () { } function metadata_generator_source_hash () { - find ./metadata-generator/src ./metadata-generator/include ./metadata-generator/CMakeLists.txt \ - -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' + find ./metadata-generator/src ./metadata-generator/include ./metadata-generator/tests \ + ./metadata-generator/symbol-analyzer ./metadata-generator/CMakeLists.txt \ + ./metadata-generator/build-step-metadata-generator.py \ + \( -name target -type d -prune \) -o -type f -print | \ + LC_ALL=C sort | xargs shasum | awk '{print $1}' | shasum | awk '{print $1}' } function signature_dispatch_stamp () { diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index e094063f7..08839a788 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -12,8 +12,11 @@ DEVICE_SIGNATURE_DISPATCH="$REPO_ROOT/dist/intermediates/react-native/GeneratedS SKIP_PACK=false function metadata_generator_source_hash { - find "$REPO_ROOT/metadata-generator/src" "$REPO_ROOT/metadata-generator/include" "$REPO_ROOT/metadata-generator/CMakeLists.txt" \ - -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' + find "$REPO_ROOT/metadata-generator/src" "$REPO_ROOT/metadata-generator/include" \ + "$REPO_ROOT/metadata-generator/tests" "$REPO_ROOT/metadata-generator/symbol-analyzer" \ + "$REPO_ROOT/metadata-generator/CMakeLists.txt" "$REPO_ROOT/metadata-generator/build-step-metadata-generator.py" \ + \( -name target -type d -prune \) -o -type f -print | \ + LC_ALL=C sort | xargs shasum | awk '{print $1}' | shasum | awk '{print $1}' } function ensure_metadata_generator { From 5c2904281c55b7e9f23c12cd963e2c054826e8bd Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 1 Aug 2026 02:00:33 -0400 Subject: [PATCH 2/3] fix(metadata): harden bundle filtering integration --- .github/workflows/ci.yml | 2 +- metadata-generator/README.md | 20 ++++++ metadata-generator/benchmarks/RESULTS.md | 26 ++++++++ .../benchmarks/fixtures/split-entry.js | 4 ++ .../benchmarks/validate-bundles.sh | 66 +++++++++++++++++++ .../build-step-metadata-generator.py | 1 + metadata-generator/symbol-analyzer/src/lib.rs | 53 ++++++++++++++- .../symbol-analyzer/tests/cli.rs | 45 +++++++++++++ scripts/build_metadata_generator.sh | 23 +++---- scripts/build_nativescript.sh | 8 ++- scripts/run-tests-ios.js | 26 +++++++- scripts/run-tests-macos.js | 28 +++++++- 12 files changed, 280 insertions(+), 22 deletions(-) create mode 100644 metadata-generator/benchmarks/fixtures/split-entry.js create mode 100755 metadata-generator/benchmarks/validate-bundles.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0a5aa548..cec2081c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: run: npm run build-libffi - name: Build metadata generator - run: npm run build-metagen + run: METADATA_GENERATOR_ARCHS="$(uname -m)" npm run build-metagen - name: Generate macOS metadata run: npm run metagen macos diff --git a/metadata-generator/README.md b/metadata-generator/README.md index df1e22859..81c52d1c1 100644 --- a/metadata-generator/README.md +++ b/metadata-generator/README.md @@ -26,6 +26,14 @@ cmake -B build -DCMAKE_BUILD_TYPE=Release -DMETADATA_BINARY_ARCH=$METADATA_ARCH cmake --build build ``` +The repository packaging script builds both Intel and Apple Silicon host tools +by default. For local development on Xcode installations whose `libclang` only +contains the current host architecture, build just that architecture: + +```bash +METADATA_GENERATOR_ARCHS="$(uname -m)" npm run build-metagen +``` + ## Debugging the metadata generator To debug the metadata generator you first need to generate the xcode project for it: @@ -75,3 +83,15 @@ The packaged analyzer next to `objc-metadata-generator` is used by default. For repeatable full-SDK performance runs on macOS, use `benchmarks/run-macos.sh` with a fresh output directory. Each iteration records wall/CPU time, peak memory, and SHA-256 hashes for every generated artifact. + +To validate the analyzer against the repository's real NativeScript macOS +examples as source, minified bundles, and split chunks, run: + +```bash +metadata-generator/benchmarks/validate-bundles.sh \ + metadata-generator/symbol-analyzer/target/release/ns-metadata-symbols +``` + +The script pins esbuild 0.25.8, requires identical analyzer output for all three +forms, and verifies that the intentionally dynamic/malformed TestRunner corpus +fails open. diff --git a/metadata-generator/benchmarks/RESULTS.md b/metadata-generator/benchmarks/RESULTS.md index 2646c9211..63bdaa62d 100644 --- a/metadata-generator/benchmarks/RESULTS.md +++ b/metadata-generator/benchmarks/RESULTS.md @@ -62,3 +62,29 @@ Foundation, Runtime, the referenced symbols, and dependency closure: This is an illustrative workload, not a size guarantee; the dependency closure and default Foundation/Runtime retention intentionally prefer false positives over missing native metadata. + +## Production bundle and build integration + +`validate-bundles.sh` bundles the three repository macOS NativeScript examples +with pinned esbuild 0.25.8. The original source files, separately minified ESM +bundles, and minified dynamic-import code-split output each produced the same 48 +rules, with no diagnostics or fail-open marker. All three whitelist files were +byte-identical with SHA-256 +`ecf7218d52356b797bb723d0594ce8b0832308137551630977ff189ff1e8af4d`. + +The full TestRunner app corpus exercised 184 JavaScript files, including worker +files, intentional invalid-syntax fixtures, `eval`, and dynamic global access. +The analyzer used 18 workers, reported three diagnostics, and emitted `*:*` as +designed, preserving all metadata. + +Across 100 warm-cache process invocations of that 184-file corpus, analysis +averaged 9.9 ms with one Rayon worker and 6.5 ms with 18 workers (-34.3%). The +whitelist output was identical at every worker count. + +The real Xcode TestRunner build completed with automatic filtering enabled and +ran its focused Metadata test suite (2 specs, 0 failures). The generated +fail-open metadata was 6,457,719 bytes and signature dispatch output was +8,477,285 bytes. A separate end-to-end build-step run using the statically +analyzable split example bundles generated 533,247 bytes of metadata, 1,403,490 +bytes of signature bindings, and 1,148 KiB of TypeScript declarations; every +referenced native symbol was present in the declarations. diff --git a/metadata-generator/benchmarks/fixtures/split-entry.js b/metadata-generator/benchmarks/fixtures/split-entry.js new file mode 100644 index 000000000..5ec9d5a5c --- /dev/null +++ b/metadata-generator/benchmarks/fixtures/split-entry.js @@ -0,0 +1,4 @@ +import "../../../platforms/apple/examples/appkit.js"; + +import("../../../platforms/apple/examples/view_controller.js"); +import("../../../platforms/apple/examples/webview.js"); diff --git a/metadata-generator/benchmarks/validate-bundles.sh b/metadata-generator/benchmarks/validate-bundles.sh new file mode 100755 index 000000000..57713017b --- /dev/null +++ b/metadata-generator/benchmarks/validate-bundles.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then + echo "Usage: $0 [work-directory]" >&2 + exit 2 +fi + +analyzer=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +script_dir=$(cd "$(dirname "$0")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +work_dir=${2:-$(mktemp -d "${TMPDIR:-/tmp}/ns-metadata-bundles.XXXXXX")} + +if [ ! -x "$analyzer" ]; then + echo "Analyzer is not executable: $analyzer" >&2 + exit 2 +fi +if [ -e "$work_dir" ] && [ -n "$(find "$work_dir" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]; then + echo "Work directory is not empty: $work_dir" >&2 + exit 2 +fi + +mkdir -p "$work_dir/minified" "$work_dir/split" +sources=( + "$repo_root/platforms/apple/examples/appkit.js" + "$repo_root/platforms/apple/examples/view_controller.js" + "$repo_root/platforms/apple/examples/webview.js" +) + +# Pin the bundler so the emitted-bundle regression is repeatable. Dynamic +# imports exercise multiple emitted chunks while the imported code remains the +# repository's real app source. +npx --yes esbuild@0.25.8 "${sources[@]}" \ + --bundle --minify --format=esm --external:@nativescript/* \ + --outdir="$work_dir/minified" +npx --yes esbuild@0.25.8 "$script_dir/fixtures/split-entry.js" \ + --bundle --minify --splitting --format=esm --external:@nativescript/* \ + --chunk-names=chunks/[name]-[hash] --outdir="$work_dir/split" + +if [ "$(find "$work_dir/split/chunks" -type f -name '*.js' | wc -l | tr -d ' ')" -lt 2 ]; then + echo "Expected esbuild to emit at least two split chunks" >&2 + exit 1 +fi + +"$analyzer" --output "$work_dir/source.mdg" "${sources[@]}" +"$analyzer" --output "$work_dir/minified.mdg" "$work_dir/minified" +"$analyzer" --output "$work_dir/split.mdg" "$work_dir/split" + +cmp "$work_dir/source.mdg" "$work_dir/minified.mdg" +cmp "$work_dir/source.mdg" "$work_dir/split.mdg" +if grep -q '^\*:\*$' "$work_dir/source.mdg"; then + echo "Representative bundles unexpectedly disabled filtering" >&2 + exit 1 +fi + +# The runtime test corpus contains malformed fixtures, eval, and dynamic global +# access. It must conservatively disable filtering rather than under-strip. +"$analyzer" --output "$work_dir/fail-open.mdg" \ + "$repo_root/platforms/apple/test/runtime/runner/app" +grep -q '^\*:\*$' "$work_dir/fail-open.mdg" + +echo "Bundle validation passed. Outputs: $work_dir" +shasum -a 256 \ + "$work_dir/source.mdg" \ + "$work_dir/minified.mdg" \ + "$work_dir/split.mdg" diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 8af36f279..49dcb6d7a 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -202,6 +202,7 @@ def generate_metadata(arch): # optionally add typescript output folder if typescript_output_folder is not None: current_typescript_output_folder = os.path.join(typescript_output_folder, arch) + os.makedirs(current_typescript_output_folder, exist_ok=True) generator_call.extend(["-output-typescript", current_typescript_output_folder]) print("Generating TypeScript declarations in: \"{}\"".format(current_typescript_output_folder)) diff --git a/metadata-generator/symbol-analyzer/src/lib.rs b/metadata-generator/symbol-analyzer/src/lib.rs index b4a25a867..f9b13dabf 100644 --- a/metadata-generator/symbol-analyzer/src/lib.rs +++ b/metadata-generator/symbol-analyzer/src/lib.rs @@ -3,7 +3,8 @@ use std::path::Path; use oxc_allocator::Allocator; use oxc_ast::ast::{ - Argument, CallExpression, ComputedMemberExpression, Expression, StaticMemberExpression, + Argument, CallExpression, ComputedMemberExpression, Expression, IdentifierReference, + StaticMemberExpression, }; use oxc_ast_visit::{Visit, walk}; use oxc_parser::Parser; @@ -21,6 +22,8 @@ pub struct Analysis { struct GlobalPropertyCollector { symbols: BTreeSet, dynamic_access: bool, + global_object_references: usize, + handled_global_object_references: usize, } fn is_global_object(expression: &Expression<'_>) -> bool { @@ -32,15 +35,27 @@ fn is_global_object(expression: &Expression<'_>) -> bool { } impl<'a> Visit<'a> for GlobalPropertyCollector { + fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'a>) { + if matches!(identifier.name.as_str(), "globalThis" | "global") { + self.global_object_references += 1; + } + walk::walk_identifier_reference(self, identifier); + } + fn visit_static_member_expression(&mut self, expression: &StaticMemberExpression<'a>) { if is_global_object(&expression.object) { + self.handled_global_object_references += 1; self.symbols.insert(expression.property.name.to_string()); + if matches!(expression.property.name.as_str(), "globalThis" | "global") { + self.dynamic_access = true; + } } walk::walk_static_member_expression(self, expression); } fn visit_computed_member_expression(&mut self, expression: &ComputedMemberExpression<'a>) { if is_global_object(&expression.object) { + self.handled_global_object_references += 1; if let Expression::StringLiteral(property) = &expression.expression { self.symbols.insert(property.value.to_string()); } else { @@ -64,6 +79,18 @@ impl<'a> Visit<'a> for GlobalPropertyCollector { | "objc_lookUpClass" | "objc_getProtocol" ) + ) || matches!( + &expression.callee, + Expression::StaticMemberExpression(member) + if is_global_object(&member.object) + && matches!( + member.property.name.as_str(), + "NSClassFromString" + | "NSProtocolFromString" + | "objc_getClass" + | "objc_lookUpClass" + | "objc_getProtocol" + ) ); if is_dynamic_native_lookup { match expression.arguments.first() { @@ -92,6 +119,13 @@ pub fn analyze_source(path: &Path, source: &str) -> Analysis { let mut global_properties = GlobalPropertyCollector::default(); global_properties.visit_program(&parsed.program); + if global_properties.global_object_references + != global_properties.handled_global_object_references + { + // Aliasing, destructuring, or passing the global object to another + // function can hide native symbol access from this local AST pass. + global_properties.dynamic_access = true; + } let semantic = SemanticBuilder::new().build(&parsed.program); if !semantic.diagnostics.is_empty() { @@ -155,6 +189,16 @@ mod tests { let dynamic_result = analyze_source(Path::new("bundle.js"), "globalThis[className]"); assert!(dynamic_result.bailed_out); + + let aliased_result = analyze_source( + Path::new("bundle.js"), + "const nativeGlobal = globalThis; new nativeGlobal.UIView();", + ); + assert!(aliased_result.bailed_out); + + let destructured_result = + analyze_source(Path::new("bundle.js"), "const { UIView } = globalThis;"); + assert!(destructured_result.bailed_out); } #[test] @@ -167,6 +211,13 @@ mod tests { assert!(literal.symbols.contains("NSView")); assert!(literal.symbols.contains("NSDraggingDestination")); + let global_literal = analyze_source( + Path::new("bundle.js"), + "globalThis.NSClassFromString('NSWindow')", + ); + assert!(!global_literal.bailed_out); + assert!(global_literal.symbols.contains("NSWindow")); + let dynamic = analyze_source(Path::new("bundle.js"), "NSClassFromString(className)"); assert!(dynamic.bailed_out); } diff --git a/metadata-generator/symbol-analyzer/tests/cli.rs b/metadata-generator/symbol-analyzer/tests/cli.rs index efae8759f..a4c464bb8 100644 --- a/metadata-generator/symbol-analyzer/tests/cli.rs +++ b/metadata-generator/symbol-analyzer/tests/cli.rs @@ -56,3 +56,48 @@ fn non_utf8_input_disables_filtering_instead_of_failing_the_build() { assert!(status.success()); assert!(fs::read_to_string(output).unwrap().contains("*:*\n")); } + +#[test] +fn merges_main_bundle_and_worker_chunk_symbols() { + let root = tempfile::tempdir().unwrap(); + let chunks = root.path().join("chunks"); + let output = root.path().join("generated.mdg"); + fs::create_dir(&chunks).unwrap(); + fs::write(chunks.join("main.min.js"), "new UIView;new NSWindow;").unwrap(); + fs::write( + chunks.join("worker.min.js"), + "onmessage=()=>NSLocale.currentLocale;", + ) + .unwrap(); + + let status = Command::new(env!("CARGO_BIN_EXE_ns-metadata-symbols")) + .args(["--output"]) + .arg(&output) + .arg(&chunks) + .status() + .unwrap(); + assert!(status.success()); + + let result = fs::read_to_string(output).unwrap(); + assert!(result.contains("*:UIView\n")); + assert!(result.contains("*:NSWindow\n")); + assert!(result.contains("*:NSLocale\n")); + assert!(!result.contains("*:*\n")); +} + +#[test] +fn dynamic_global_alias_disables_cli_filtering() { + let root = tempfile::tempdir().unwrap(); + let bundle = root.path().join("bundle.js"); + let output = root.path().join("generated.mdg"); + fs::write(&bundle, "const nativeGlobal=globalThis;nativeGlobal[name]").unwrap(); + + let status = Command::new(env!("CARGO_BIN_EXE_ns-metadata-symbols")) + .args(["--output"]) + .arg(&output) + .arg(&bundle) + .status() + .unwrap(); + assert!(status.success()); + assert!(fs::read_to_string(output).unwrap().contains("*:*\n")); +} diff --git a/scripts/build_metadata_generator.sh b/scripts/build_metadata_generator.sh index 1c1cb35c1..2ded7b84c 100755 --- a/scripts/build_metadata_generator.sh +++ b/scripts/build_metadata_generator.sh @@ -27,18 +27,19 @@ function build { } pushd "metadata-generator" -rm -rf dist -mkdir dist -checkpoint "Building metadata generator for x86_64 ..." -build "x86_64" -# make sure the binary is linked against the system libc++ instead of an @rpath one (which happens when compiling on arm64) -# todo: perhaps there is a better way to do this with cmake? -#install_name_tool -change @rpath/libc++.1.dylib /usr/lib/libc++.1.dylib dist/x86_64/bin/objc-metadata-generator -otool -L dist/x86_64/bin/objc-metadata-generator +mkdir -p dist +architectures=${METADATA_GENERATOR_ARCHS:-"x86_64 arm64"} +for architecture in $architectures; do + case "$architecture" in + x86_64|arm64) ;; + *) echo "Unsupported metadata generator architecture: $architecture" >&2; exit 2 ;; + esac -checkpoint "Building metadata generator for arm64 ..." -build "arm64" -otool -L dist/arm64/bin/objc-metadata-generator + checkpoint "Building metadata generator for $architecture ..." + rm -rf "dist/$architecture" + build "$architecture" + otool -L "dist/$architecture/bin/objc-metadata-generator" +done metadata_generator_source_hash > dist/.source_hash rm -rf build popd diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index 42d070983..a86e90697 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -195,11 +195,13 @@ function ensure_metadata_generator () { local expected_hash expected_hash=$(metadata_generator_source_hash) local hash_file="./metadata-generator/dist/.source_hash" - if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ] || \ - [ ! -x "./metadata-generator/dist/x86_64/bin/objc-metadata-generator" ] || \ + local host_arch + host_arch=$(uname -m) + if [ ! -x "./metadata-generator/dist/$host_arch/bin/objc-metadata-generator" ] || \ + [ ! -x "./metadata-generator/dist/$host_arch/bin/ns-metadata-symbols" ] || \ [ ! -f "$hash_file" ] || \ [ "$(cat "$hash_file")" != "$expected_hash" ]; then - "$SCRIPT_DIR/build_metadata_generator.sh" + METADATA_GENERATOR_ARCHS="$host_arch" "$SCRIPT_DIR/build_metadata_generator.sh" fi } diff --git a/scripts/run-tests-ios.js b/scripts/run-tests-ios.js index b39352eaa..b3a761bb2 100644 --- a/scripts/run-tests-ios.js +++ b/scripts/run-tests-ios.js @@ -55,6 +55,13 @@ const metadataGeneratorBuildStepScript = path.join( "bin", "build-step-metadata-generator.py" ); +const metadataGeneratorSymbolAnalyzer = path.join( + metadataGeneratorRoot, + "dist", + "arm64", + "bin", + "ns-metadata-symbols" +); const nativeScriptSourceRoot = path.join(__dirname, "../NativeScript"); const nativeScriptXCFramework = path.join(__dirname, "../dist", "NativeScript.xcframework"); @@ -67,10 +74,12 @@ const iosBuildInputs = [ path.join(__dirname, "../platforms/apple/TKLiveSync"), path.join(metadataGeneratorRoot, "src"), path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "symbol-analyzer"), path.join(metadataGeneratorRoot, "CMakeLists.txt"), path.join(__dirname, "build_metadata_generator.sh"), metadataGeneratorBinary, metadataGeneratorBuildStepScript, + metadataGeneratorSymbolAnalyzer, nativeScriptXCFramework, tkLiveSyncXCFramework ]; @@ -173,6 +182,8 @@ function ensureMetadataGeneratorBuilt() { const sourceInputs = [ path.join(metadataGeneratorRoot, "src"), path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "symbol-analyzer"), + path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), path.join(metadataGeneratorRoot, "CMakeLists.txt") ]; @@ -180,14 +191,23 @@ function ensureMetadataGeneratorBuilt() { (latest, inputPath) => Math.max(latest, getPathStats(inputPath).maxMtimeMs), 0 ); - const binaryMtime = getPathStats(metadataGeneratorBinary).maxMtimeMs; + const artifactMtime = Math.min( + getPathStats(metadataGeneratorBinary).maxMtimeMs, + getPathStats(metadataGeneratorSymbolAnalyzer).maxMtimeMs, + getPathStats(metadataGeneratorBuildStepScript).maxMtimeMs + ); - if (binaryMtime > 0 && binaryMtime >= sourceMtime) { + if (artifactMtime > 0 && artifactMtime >= sourceMtime) { return; } console.log("Metadata generator is missing or stale; running build-metagen..."); - runAndRequireSuccess("npm", ["run", "build-metagen"], buildTimeoutMs); + const hostArch = process.arch === "x64" ? "x86_64" : process.arch; + runAndRequireSuccess( + "env", + [`METADATA_GENERATOR_ARCHS=${hostArch}`, "npm", "run", "build-metagen"], + buildTimeoutMs + ); } function getPathStats(targetPath) { diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 91ae1c9cb..2cb241248 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -50,6 +50,13 @@ const metadataGeneratorBuildStepScript = path.join( "bin", "build-step-metadata-generator.py" ); +const metadataGeneratorSymbolAnalyzer = path.join( + metadataGeneratorRoot, + "dist", + "arm64", + "bin", + "ns-metadata-symbols" +); const buildStatePath = path.join(derivedDataPath, ".macos-test-build-state.json"); const macosBuildInputs = [ path.join(__dirname, "../platforms/apple/NativeScriptRuntime.xcodeproj", "project.pbxproj"), @@ -59,10 +66,12 @@ const macosBuildInputs = [ path.join(__dirname, "../platforms/apple/TKLiveSync"), path.join(metadataGeneratorRoot, "src"), path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "symbol-analyzer"), path.join(metadataGeneratorRoot, "CMakeLists.txt"), path.join(__dirname, "build_metadata_generator.sh"), metadataGeneratorBinary, metadataGeneratorBuildStepScript, + metadataGeneratorSymbolAnalyzer, nativeScriptXCFramework, tkLiveSyncXCFramework ]; @@ -474,6 +483,8 @@ function ensureMetadataGeneratorBuilt() { const sourceInputs = [ path.join(metadataGeneratorRoot, "src"), path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "symbol-analyzer"), + path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), path.join(metadataGeneratorRoot, "CMakeLists.txt") ]; @@ -481,14 +492,23 @@ function ensureMetadataGeneratorBuilt() { (latest, inputPath) => Math.max(latest, getPathStats(inputPath).maxMtimeMs), 0 ); - const binaryMtime = getPathStats(metadataGeneratorBinary).maxMtimeMs; + const artifactMtime = Math.min( + getPathStats(metadataGeneratorBinary).maxMtimeMs, + getPathStats(metadataGeneratorSymbolAnalyzer).maxMtimeMs, + getPathStats(metadataGeneratorBuildStepScript).maxMtimeMs + ); - if (binaryMtime > 0 && binaryMtime >= sourceMtime) { + if (artifactMtime > 0 && artifactMtime >= sourceMtime) { return; } console.log("Metadata generator is missing or stale; running build-metagen..."); - runBuildAndRequireSuccess("npm", ["run", "build-metagen"], commandTimeoutMs); + const hostArch = os.arch() === "x64" ? "x86_64" : os.arch(); + runBuildAndRequireSuccess( + "env", + [`METADATA_GENERATOR_ARCHS=${hostArch}`, "npm", "run", "build-metagen"], + commandTimeoutMs + ); } function ensureMacOSRuntimeArtifactsBuilt() { @@ -497,9 +517,11 @@ function ensureMacOSRuntimeArtifactsBuilt() { nativeScriptSourceRoot, path.join(metadataGeneratorRoot, "src"), path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "symbol-analyzer"), path.join(metadataGeneratorRoot, "CMakeLists.txt"), metadataGeneratorBinary, metadataGeneratorBuildStepScript, + metadataGeneratorSymbolAnalyzer, path.join(__dirname, "build_metadata_generator.sh"), path.join(__dirname, "build_nativescript.sh") ]; From 3d8b6c268ed83bf2e2cd0af2fa4eca674fa9c891 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 11 Aug 2026 20:17:50 -0400 Subject: [PATCH 3/3] fix(ci): avoid redundant metadata generator rebuilds --- scripts/run-tests-ios.js | 28 ++++++++++++++-------------- scripts/run-tests-macos.js | 34 +++++++++++++++------------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/scripts/run-tests-ios.js b/scripts/run-tests-ios.js index b3a761bb2..0078b3536 100644 --- a/scripts/run-tests-ios.js +++ b/scripts/run-tests-ios.js @@ -62,6 +62,18 @@ const metadataGeneratorSymbolAnalyzer = path.join( "bin", "ns-metadata-symbols" ); +const metadataGeneratorSourceInputs = [ + path.join(metadataGeneratorRoot, "src"), + path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "tests"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "Cargo.toml"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "Cargo.lock"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "src"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "tests"), + path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), + path.join(metadataGeneratorRoot, "CMakeLists.txt"), + path.join(__dirname, "build_metadata_generator.sh") +]; const nativeScriptSourceRoot = path.join(__dirname, "../NativeScript"); const nativeScriptXCFramework = path.join(__dirname, "../dist", "NativeScript.xcframework"); @@ -72,11 +84,7 @@ const iosBuildInputs = [ path.join(__dirname, "../platforms/apple/test/runtime/runner", "Info.plist"), path.join(__dirname, "../platforms/apple/test/runtime/fixtures"), path.join(__dirname, "../platforms/apple/TKLiveSync"), - path.join(metadataGeneratorRoot, "src"), - path.join(metadataGeneratorRoot, "include"), - path.join(metadataGeneratorRoot, "symbol-analyzer"), - path.join(metadataGeneratorRoot, "CMakeLists.txt"), - path.join(__dirname, "build_metadata_generator.sh"), + ...metadataGeneratorSourceInputs, metadataGeneratorBinary, metadataGeneratorBuildStepScript, metadataGeneratorSymbolAnalyzer, @@ -179,15 +187,7 @@ function runAndRequireSuccess(command, args, timeoutMs = commandTimeoutMs) { } function ensureMetadataGeneratorBuilt() { - const sourceInputs = [ - path.join(metadataGeneratorRoot, "src"), - path.join(metadataGeneratorRoot, "include"), - path.join(metadataGeneratorRoot, "symbol-analyzer"), - path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), - path.join(metadataGeneratorRoot, "CMakeLists.txt") - ]; - - const sourceMtime = sourceInputs.reduce( + const sourceMtime = metadataGeneratorSourceInputs.reduce( (latest, inputPath) => Math.max(latest, getPathStats(inputPath).maxMtimeMs), 0 ); diff --git a/scripts/run-tests-macos.js b/scripts/run-tests-macos.js index 2cb241248..096e9d8f5 100644 --- a/scripts/run-tests-macos.js +++ b/scripts/run-tests-macos.js @@ -57,6 +57,18 @@ const metadataGeneratorSymbolAnalyzer = path.join( "bin", "ns-metadata-symbols" ); +const metadataGeneratorSourceInputs = [ + path.join(metadataGeneratorRoot, "src"), + path.join(metadataGeneratorRoot, "include"), + path.join(metadataGeneratorRoot, "tests"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "Cargo.toml"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "Cargo.lock"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "src"), + path.join(metadataGeneratorRoot, "symbol-analyzer", "tests"), + path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), + path.join(metadataGeneratorRoot, "CMakeLists.txt"), + path.join(__dirname, "build_metadata_generator.sh") +]; const buildStatePath = path.join(derivedDataPath, ".macos-test-build-state.json"); const macosBuildInputs = [ path.join(__dirname, "../platforms/apple/NativeScriptRuntime.xcodeproj", "project.pbxproj"), @@ -64,11 +76,7 @@ const macosBuildInputs = [ path.join(__dirname, "../platforms/apple/test/runtime/runner", "Info.plist"), path.join(__dirname, "../platforms/apple/test/runtime/fixtures"), path.join(__dirname, "../platforms/apple/TKLiveSync"), - path.join(metadataGeneratorRoot, "src"), - path.join(metadataGeneratorRoot, "include"), - path.join(metadataGeneratorRoot, "symbol-analyzer"), - path.join(metadataGeneratorRoot, "CMakeLists.txt"), - path.join(__dirname, "build_metadata_generator.sh"), + ...metadataGeneratorSourceInputs, metadataGeneratorBinary, metadataGeneratorBuildStepScript, metadataGeneratorSymbolAnalyzer, @@ -480,15 +488,7 @@ function runBuildAndRequireSuccess(command, args, timeoutMs = commandTimeoutMs) } function ensureMetadataGeneratorBuilt() { - const sourceInputs = [ - path.join(metadataGeneratorRoot, "src"), - path.join(metadataGeneratorRoot, "include"), - path.join(metadataGeneratorRoot, "symbol-analyzer"), - path.join(metadataGeneratorRoot, "build-step-metadata-generator.py"), - path.join(metadataGeneratorRoot, "CMakeLists.txt") - ]; - - const sourceMtime = sourceInputs.reduce( + const sourceMtime = metadataGeneratorSourceInputs.reduce( (latest, inputPath) => Math.max(latest, getPathStats(inputPath).maxMtimeMs), 0 ); @@ -515,14 +515,10 @@ function ensureMacOSRuntimeArtifactsBuilt() { const cachePath = path.join(__dirname, "../dist", "intermediates", "macos", "CMakeCache.txt"); const sourceInputs = [ nativeScriptSourceRoot, - path.join(metadataGeneratorRoot, "src"), - path.join(metadataGeneratorRoot, "include"), - path.join(metadataGeneratorRoot, "symbol-analyzer"), - path.join(metadataGeneratorRoot, "CMakeLists.txt"), + ...metadataGeneratorSourceInputs, metadataGeneratorBinary, metadataGeneratorBuildStepScript, metadataGeneratorSymbolAnalyzer, - path.join(__dirname, "build_metadata_generator.sh"), path.join(__dirname, "build_nativescript.sh") ];