feat(impact): cross-language blast-radius coverage (22 languages + 14 frameworks) (#708)
Completes the cross-file dependency graph behind impact / affected / explore across all 22 supported languages and 14 web frameworks, validated on real-world repos (measured fair-coverage table added to the README). Per-language resolution + framework resolvers/synthesizers (Lua/Luau require, Shopify OS 2.0 Liquid sections, Delphi forms, Rust cross-module + Rocket macros, Swift Fluent, SvelteKit/Nuxt loader/component conventions, RN/Expo bridges). 0 cross-family false edges, full suite green (1187 passed). See #708. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bfa84d32b8
commit
07af3db6c7
@@ -54,9 +54,14 @@ import {
|
||||
* line as the keyword, which matches every real Expo Module declaration
|
||||
* style. Multi-line `AsyncFunction(\n"x"\n)` forms aren't a real shape in
|
||||
* the SDK; if any appear we'd extend the regex.
|
||||
*
|
||||
* The optional `<…>` covers Kotlin's GENERIC-typed declarations
|
||||
* (`AsyncFunction<Float>("getBatteryLevelAsync")`, `AsyncFunction<Int, String>(…)`)
|
||||
* — without it, every Android Expo Module method was silently dropped, so a JS
|
||||
* callsite resolved only to the iOS Swift impl and never the Android one.
|
||||
*/
|
||||
const EXPO_DECL_RE =
|
||||
/\b(Function|AsyncFunction|Property|Constants)\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g;
|
||||
/\b(Function|AsyncFunction|Property|Constants)\s*(?:<[^(]*>)?\s*\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g;
|
||||
|
||||
/**
|
||||
* Match the module name literal `Name("ExpoX")`. Used to enrich each emitted
|
||||
|
||||
@@ -48,10 +48,14 @@ export const djangoResolver: FrameworkResolver = {
|
||||
return null;
|
||||
},
|
||||
|
||||
// Let the ORM dynamic-dispatch ref reach resolve() despite no symbol being
|
||||
// named `_iterable_class` (it's a QuerySet attribute, not a declared method).
|
||||
// Let two ref shapes past resolveOne's "no possible match" pre-filter so they
|
||||
// reach resolution: the ORM dynamic-dispatch `_iterable_class` (a QuerySet
|
||||
// attribute, not a declared symbol), and a Django `include('app.urls')` module
|
||||
// path — a dotted module name with no symbol/import to match, which resolution
|
||||
// (resolvePythonAbsoluteModule) then maps to its `urls.py` file so the included
|
||||
// URLconf records a dependency on the root urlconf.
|
||||
claimsReference(name) {
|
||||
return name === '_iterable_class';
|
||||
return name === '_iterable_class' || name.endsWith('.urls');
|
||||
},
|
||||
|
||||
extract(filePath, content) {
|
||||
|
||||
@@ -99,8 +99,8 @@ function defaultObjcModuleName(className: string): string {
|
||||
function parseObjcRNExports(
|
||||
source: string,
|
||||
className: string | null
|
||||
): Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> {
|
||||
const results: Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> = [];
|
||||
): Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string; line: number }> {
|
||||
const results: Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string; line: number }> = [];
|
||||
|
||||
// RCT_EXPORT_MODULE — one per file by convention. Capture the optional arg.
|
||||
const moduleMatch = source.match(/RCT_EXPORT_MODULE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)?\s*\)/);
|
||||
@@ -111,6 +111,12 @@ function parseObjcRNExports(
|
||||
(className ? defaultObjcModuleName(className) : null);
|
||||
if (!moduleName) return results;
|
||||
|
||||
const lineOf = (idx: number): number => {
|
||||
let line = 1;
|
||||
for (let i = 0; i < idx && i < source.length; i++) if (source.charCodeAt(i) === 10) line++;
|
||||
return line;
|
||||
};
|
||||
|
||||
// RCT_EXPORT_METHOD(selectorFirstKw:(args)…)
|
||||
// The first keyword (everything up to the first `:` or open paren) is the
|
||||
// JS-visible name. We don't try to parse full multi-keyword selectors —
|
||||
@@ -119,7 +125,7 @@ function parseObjcRNExports(
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = exportRegex.exec(source)) !== null) {
|
||||
const kw = m[1];
|
||||
if (kw) results.push({ moduleName, jsName: kw, nativeSelectorFirstKw: kw });
|
||||
if (kw) results.push({ moduleName, jsName: kw, nativeSelectorFirstKw: kw, line: lineOf(m.index) });
|
||||
}
|
||||
|
||||
// RCT_REMAP_METHOD(jsName, nativeSelectorFirstKw:(args)…)
|
||||
@@ -129,7 +135,7 @@ function parseObjcRNExports(
|
||||
const jsName = m[1];
|
||||
const nativeKw = m[2];
|
||||
if (jsName && nativeKw) {
|
||||
results.push({ moduleName, jsName, nativeSelectorFirstKw: nativeKw });
|
||||
results.push({ moduleName, jsName, nativeSelectorFirstKw: nativeKw, line: lineOf(m.index) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +361,48 @@ function buildRNMaps(context: ResolutionContext): { byJsName: Map<string, Native
|
||||
|
||||
export const reactNativeBridgeResolver: FrameworkResolver = {
|
||||
name: 'react-native-bridge',
|
||||
languages: ['javascript', 'typescript', 'tsx', 'jsx'],
|
||||
// objc/mm included so `extract()` sees the native files — `resolve()` still
|
||||
// only redirects JS callers (it returns null for native languages).
|
||||
languages: ['javascript', 'typescript', 'tsx', 'jsx', 'objc'],
|
||||
|
||||
/**
|
||||
* Extract `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` declarations as method
|
||||
* nodes. These macros parse as a macro-expression (an ERROR node), NOT a
|
||||
* `method_definition`, so the ObjC extractor never made a node for them — the
|
||||
* iOS half of a native module was invisible, so a JS call couldn't resolve to
|
||||
* it and the cross-platform pairing had nothing to pair. The node is named by
|
||||
* the JS-visible name (the selector's first keyword, or the explicit
|
||||
* `RCT_REMAP_METHOD` JS name) so it matches the Android `@ReactMethod` method.
|
||||
*/
|
||||
extract(filePath, source) {
|
||||
if (!filePath.endsWith('.m') && !filePath.endsWith('.mm')) return { nodes: [], references: [] };
|
||||
if (!/RCT_EXPORT_MODULE\b/.test(source)) return { nodes: [], references: [] };
|
||||
const exports = parseObjcRNExports(source, findObjcClassName(source));
|
||||
const now = Date.now();
|
||||
const nodes: Node[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const e of exports) {
|
||||
if (seen.has(e.jsName)) continue;
|
||||
seen.add(e.jsName);
|
||||
nodes.push({
|
||||
id: `rn-export:${filePath}:${e.moduleName}.${e.jsName}`,
|
||||
kind: 'method',
|
||||
name: e.jsName,
|
||||
qualifiedName: `${filePath}::${e.moduleName}.${e.jsName}`,
|
||||
filePath,
|
||||
language: 'objc',
|
||||
startLine: e.line,
|
||||
endLine: e.line,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
isExported: true,
|
||||
docstring: `RCT_EXPORT_METHOD ${e.nativeSelectorFirstKw} (module ${e.moduleName})`,
|
||||
signature: `RCT_EXPORT_METHOD(${e.nativeSelectorFirstKw}:…)`,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return { nodes, references: [] };
|
||||
},
|
||||
|
||||
/**
|
||||
* Detect: package.json depends on `react-native`, OR any source file
|
||||
|
||||
Reference in New Issue
Block a user