|
|
@@ -280,11 +280,13 @@ async function closureCollectionEdges(queries: QueryBuilder, ctx: ResolutionCont
|
|
|
|
|
|
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
|
async function eventEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const emitsByEvent = new Map<string, Set<string>>(); // event → dispatcher node ids
|
|
|
const handlersByEvent = new Map<string, Map<string, string>>(); // event → handler id → registration site (file:line)
|
|
|
|
|
|
let scanned = 0;
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content) continue;
|
|
|
@@ -348,10 +350,12 @@ async function eventEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): P
|
|
|
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
|
* accepted — it's reachability-correct, like the callback channels.
|
|
|
*/
|
|
|
-function reactRenderEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function reactRenderEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
- for (const cls of queries.getNodesByKind('class')) {
|
|
|
+ for (const cls of queries.iterateNodesByKind('class')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
|
.map((e) => queries.getNodeById(e.target))
|
|
|
.filter((n): n is Node => !!n && n.kind === 'method');
|
|
|
@@ -387,10 +391,12 @@ function reactRenderEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[]
|
|
|
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
|
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
|
*/
|
|
|
-function flutterBuildEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function flutterBuildEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
- for (const cls of queries.getNodesByKind('class')) {
|
|
|
+ for (const cls of queries.iterateNodesByKind('class')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
|
.map((e) => queries.getNodeById(e.target))
|
|
|
.filter((n): n is Node => !!n && n.kind === 'method');
|
|
|
@@ -447,10 +453,12 @@ const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
|
* with no reactive properties, gets nothing (this is the precision line the
|
|
|
* all-sibling-methods design would erase).
|
|
|
*/
|
|
|
-function arkuiStateBuildEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function arkuiStateBuildEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
- for (const struct of queries.getNodesByKind('struct')) {
|
|
|
+ for (const struct of queries.iterateNodesByKind('struct')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (struct.language !== 'arkts') continue;
|
|
|
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
|
.map((e) => queries.getNodeById(e.target))
|
|
|
@@ -515,7 +523,8 @@ const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
|
* handling — their bodies' calls already attribute to the registering method,
|
|
|
* so targeting that method keeps the chain connected.
|
|
|
*/
|
|
|
-function arkuiEmitterEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function arkuiEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
interface Site { nodeId: string; file: string; line: number }
|
|
|
// bucket key -> emit sites / handler sites
|
|
|
const emits = new Map<string, Site[]>();
|
|
|
@@ -533,6 +542,7 @@ function arkuiEmitterEdges(ctx: ResolutionContext): Edge[] {
|
|
|
};
|
|
|
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!file.endsWith('.ets')) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('emitter.')) continue;
|
|
|
@@ -619,7 +629,7 @@ const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,
|
|
|
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
|
* structs qualify as targets — the decorator is what makes a file a page.
|
|
|
*/
|
|
|
-function arkuiRouterEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function arkuiRouterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
@@ -635,7 +645,9 @@ function arkuiRouterEdges(ctx: ResolutionContext): Edge[] {
|
|
|
return '';
|
|
|
};
|
|
|
|
|
|
+ let scannedFiles = 0;
|
|
|
for (const file of allFiles) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!file.endsWith('.ets')) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('router.')) continue;
|
|
|
@@ -690,7 +702,8 @@ function arkuiRouterEdges(ctx: ResolutionContext): Edge[] {
|
|
|
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
|
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
|
*/
|
|
|
-function cppOverrideEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function cppOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const methodsOf = (classId: string): Node[] =>
|
|
|
@@ -698,7 +711,8 @@ function cppOverrideEdges(queries: QueryBuilder): Edge[] {
|
|
|
.getOutgoingEdges(classId, ['contains'])
|
|
|
.map((e) => queries.getNodeById(e.target))
|
|
|
.filter((n): n is Node => !!n && n.kind === 'method');
|
|
|
- for (const cls of queries.getNodesByKind('class')) {
|
|
|
+ for (const cls of queries.iterateNodesByKind('class')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
|
if (subMethods.length === 0) continue;
|
|
|
for (const ext of queries.getOutgoingEdges(cls.id, ['extends'])) {
|
|
|
@@ -762,7 +776,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
|
|
|
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
|
* (`any`) are skipped so they don't match every struct.
|
|
|
*/
|
|
|
-function goImplementsEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
@@ -775,11 +790,21 @@ function goImplementsEdges(queries: QueryBuilder): Edge[] {
|
|
|
.map((n) => n.name),
|
|
|
);
|
|
|
|
|
|
- const goStructs = queries.getNodesByKind('struct').filter((s) => s.language === 'go');
|
|
|
+ // Materializes GO structs only (the pass is language-gated by the caller),
|
|
|
+ // never the whole struct kind — that array is O(nodes) on struct-heavy
|
|
|
+ // repos like the Linux kernel (#1212).
|
|
|
+ const goStructs: Node[] = [];
|
|
|
+ for (const s of queries.iterateNodesByKind('struct')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+ if (s.language === 'go') goStructs.push(s);
|
|
|
+ }
|
|
|
const structMethods = new Map<string, Set<string>>();
|
|
|
for (const s of goStructs) structMethods.set(s.id, methodNameSet(s.id));
|
|
|
|
|
|
- for (const iface of queries.getNodesByKind('interface')) {
|
|
|
+ for (const iface of queries.iterateNodesByKind('interface')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (iface.language !== 'go') continue;
|
|
|
const want = methodNameSet(iface.id);
|
|
|
if (want.size === 0) continue; // empty interface (`any`) — would match everything
|
|
|
@@ -831,7 +856,8 @@ function goImplementsEdges(queries: QueryBuilder): Edge[] {
|
|
|
* matching the same-file edges extraction already emits). Skips methods that
|
|
|
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
|
*/
|
|
|
-function goCrossFileMethodContainsEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function goCrossFileMethodContainsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const TYPE_KINDS = new Set<NodeKind>(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
|
@@ -840,7 +866,10 @@ function goCrossFileMethodContainsEdges(queries: QueryBuilder): Edge[] {
|
|
|
return i >= 0 ? p.slice(0, i) : '';
|
|
|
};
|
|
|
|
|
|
- for (const method of queries.getNodesByKind('method')) {
|
|
|
+ for (const method of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (method.language !== 'go') continue;
|
|
|
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
|
// (extraction sets `${receiverType}::${name}` for receiver methods).
|
|
|
@@ -908,13 +937,18 @@ function kmpKindsCompatible(a: string, b: string): boolean {
|
|
|
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
|
}
|
|
|
|
|
|
-function kotlinExpectActualEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function kotlinExpectActualEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
- const actuals = queries
|
|
|
- .getAllNodes()
|
|
|
- .filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
|
|
|
- for (const act of actuals) {
|
|
|
+ // SQL-side language+decorator pre-filter, streamed. The old
|
|
|
+ // `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
|
|
|
+ // just to find kotlin `actual` declarations — on a 2M-node graph that alone
|
|
|
+ // exceeded Node's default heap and killed the index (#1212). The LIKE
|
|
|
+ // pre-filter can over-match (substring), so the exact decorator check stays.
|
|
|
+ for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+ if (!act.decorators?.includes('actual')) continue;
|
|
|
let added = 0;
|
|
|
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
|
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
|
|
|
@@ -944,7 +978,8 @@ function kotlinExpectActualEdges(queries: QueryBuilder): Edge[] {
|
|
|
return edges;
|
|
|
}
|
|
|
|
|
|
-function interfaceOverrideEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const methodsOf = (classId: string): Node[] =>
|
|
|
@@ -957,7 +992,8 @@ function interfaceOverrideEdges(queries: QueryBuilder): Edge[] {
|
|
|
// types that conform to protocols. Iterate both.
|
|
|
const concreteKinds = ['class', 'struct'] as const;
|
|
|
for (const kind of concreteKinds) {
|
|
|
- for (const cls of queries.getNodesByKind(kind)) {
|
|
|
+ for (const cls of queries.iterateNodesByKind(kind)) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
|
if (implMethods.length === 0) continue;
|
|
|
for (const sup of queries.getOutgoingEdges(cls.id, ['implements', 'extends'])) {
|
|
|
@@ -1023,7 +1059,8 @@ function interfaceOverrideEdges(queries: QueryBuilder): Edge[] {
|
|
|
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
|
* stub's source line is the wiring site shown in the trace trail.
|
|
|
*/
|
|
|
-function goGrpcStubImplEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function goGrpcStubImplEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
@@ -1037,7 +1074,8 @@ function goGrpcStubImplEdges(queries: QueryBuilder): Edge[] {
|
|
|
const methodNamesByStruct = new Map<string, Set<string>>();
|
|
|
const methodNodesByStruct = new Map<string, Node[]>();
|
|
|
const goStructs: Node[] = [];
|
|
|
- for (const s of queries.getNodesByKind('struct')) {
|
|
|
+ for (const s of queries.iterateNodesByKind('struct')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (s.language !== 'go') continue;
|
|
|
goStructs.push(s);
|
|
|
const ms = queries
|
|
|
@@ -1117,11 +1155,13 @@ function goGrpcStubImplEdges(queries: QueryBuilder): Edge[] {
|
|
|
* (or nothing) and are dropped.
|
|
|
*/
|
|
|
async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
|
let scanned = 0;
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('</') && !content.includes('/>'))) continue; // JSX-file gate
|
|
|
@@ -1167,7 +1207,9 @@ async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield):
|
|
|
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
|
* skipped.
|
|
|
*/
|
|
|
-function vueTemplateEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function vueTemplateEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
|
@@ -1181,11 +1223,13 @@ function vueTemplateEdges(ctx: ResolutionContext): Edge[] {
|
|
|
// misses it (flat components match by basename and don't need this). Map each
|
|
|
// nested component's Nuxt name → node so those template usages resolve.
|
|
|
const nuxtComponents = new Map<string, Node>();
|
|
|
- for (const c of ctx.getNodesByKind('component')) {
|
|
|
+ for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
const nn = nuxtComponentName(c.filePath);
|
|
|
if (nn && !nuxtComponents.has(nn)) nuxtComponents.set(nn, c);
|
|
|
}
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!file.endsWith('.vue')) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
const tpl = content && content.match(/<template[^>]*>([\s\S]*)<\/template>/i)?.[1];
|
|
|
@@ -1310,7 +1354,8 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
|
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
|
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
|
|
|
|
-function rnEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function rnEventEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// Native dispatchers (source = the native method whose body sends the
|
|
|
// event) and JS handlers (target = the function/method registered as
|
|
|
// the listener) keyed by event name.
|
|
|
@@ -1318,6 +1363,7 @@ function rnEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const jsHandlersByEvent = new Map<string, Map<string, string>>();
|
|
|
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content) continue;
|
|
|
|
|
|
@@ -1500,11 +1546,13 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
|
|
|
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
|
* `<file>::<module>.<method>` by the framework extractor.
|
|
|
*/
|
|
|
-function expoCrossPlatformEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function expoCrossPlatformEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const byKey = new Map<string, Node[]>();
|
|
|
- for (const m of queries.getNodesByKind('method')) {
|
|
|
+ for (const m of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (!m.id.startsWith('expo-module:')) continue;
|
|
|
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
|
if (!key) continue;
|
|
|
@@ -1547,7 +1595,8 @@ function expoCrossPlatformEdges(queries: QueryBuilder): Edge[] {
|
|
|
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
|
* lines up with the bare Android method name.
|
|
|
*/
|
|
|
-function rnCrossPlatformEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function rnCrossPlatformEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
|
@@ -1570,6 +1619,7 @@ function rnCrossPlatformEdges(queries: QueryBuilder): Edge[] {
|
|
|
// below only runs for genuine cross-platform candidates.
|
|
|
const byName = new Map<string, Node[]>();
|
|
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (!NATIVE.has(m.language)) continue;
|
|
|
const key = norm(m.name);
|
|
|
const arr = byName.get(key);
|
|
|
@@ -1613,18 +1663,25 @@ function rnCrossPlatformEdges(queries: QueryBuilder): Edge[] {
|
|
|
return edges;
|
|
|
}
|
|
|
|
|
|
-function fabricNativeImplEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function fabricNativeImplEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
|
- // filter to just those without iterating all `component` nodes.
|
|
|
- const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
|
|
|
+ // filter to just those while streaming — never materializing the whole
|
|
|
+ // `component` kind (#1212).
|
|
|
+ const components: Node[] = [];
|
|
|
+ for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+ if (n.id.startsWith('fabric-component:')) components.push(n);
|
|
|
+ }
|
|
|
if (components.length === 0) return edges;
|
|
|
|
|
|
// Pre-index native classes by name for O(1) lookup.
|
|
|
const nativeClassesByName = new Map<string, Node[]>();
|
|
|
- for (const n of ctx.getNodesByKind('class')) {
|
|
|
+ for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp') continue;
|
|
|
const arr = nativeClassesByName.get(n.name);
|
|
|
if (arr) arr.push(n);
|
|
|
@@ -1675,12 +1732,14 @@ function fabricNativeImplEdges(ctx: ResolutionContext): Edge[] {
|
|
|
* same simple name) are dropped. We need-not bridge by package because Java
|
|
|
* mapper interfaces are typically uniquely named within a project.
|
|
|
*/
|
|
|
-function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {
|
|
|
+async function mybatisJavaXmlEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
|
const javaIndex = new Map<string, Node[]>();
|
|
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (m.language !== 'java' && m.language !== 'kotlin') continue;
|
|
|
const parts = m.qualifiedName.split('::');
|
|
|
const last = parts[parts.length - 1];
|
|
|
@@ -1692,6 +1751,7 @@ function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {
|
|
|
}
|
|
|
|
|
|
for (const xml of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (xml.language !== 'xml') continue;
|
|
|
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
|
const colonIdx = xml.qualifiedName.lastIndexOf('::');
|
|
|
@@ -1785,10 +1845,13 @@ function goHandlerIdent(expr: string): string | null {
|
|
|
return m ? m[1]! : null;
|
|
|
}
|
|
|
|
|
|
-function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
+ let scannedFiles = 0;
|
|
|
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
|
const dispatchers: Node[] = [];
|
|
|
for (const n of queries.iterateNodesByKind('method')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if (n.language !== 'go') continue;
|
|
|
const content = ctx.readFile(n.filePath);
|
|
|
const src = content && sliceLines(content, n.startLine, n.endLine);
|
|
|
@@ -1801,6 +1864,7 @@ function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext):
|
|
|
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
|
const registered = new Map<string, string>(); // name → registeredAt (file:line)
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!file.endsWith('.go')) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('.Use(') && !/\.(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\(/.test(content))) continue;
|
|
|
@@ -1852,10 +1916,12 @@ function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext):
|
|
|
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
|
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
|
*/
|
|
|
-function pascalFormEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function pascalFormEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const allFiles = new Set(ctx.getAllFiles());
|
|
|
for (const file of allFiles) {
|
|
|
+ if ((++scannedFiles & 255) === 0) await onYield();
|
|
|
if (!/\.(dfm|fmx)$/i.test(file)) continue;
|
|
|
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
|
if (!allFiles.has(pasFile)) continue;
|
|
|
@@ -1889,12 +1955,14 @@ function pascalFormEdges(ctx: ResolutionContext): Edge[] {
|
|
|
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
|
* its loader.
|
|
|
*/
|
|
|
-function svelteKitLoadEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function svelteKitLoadEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const allFiles = new Set(ctx.getAllFiles());
|
|
|
const HOOKS = new Set(['load', 'actions']);
|
|
|
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
|
for (const file of allFiles) {
|
|
|
+ if ((++scannedFiles & 255) === 0) await onYield();
|
|
|
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
|
if (!m) continue;
|
|
|
const dir = m[1]!;
|
|
|
@@ -1941,10 +2009,12 @@ const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
|
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
|
const THUNK_FANOUT_CAP = 24;
|
|
|
|
|
|
-function reduxThunkEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function reduxThunkEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const node of queries.iterateNodesByKind('constant')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
|
// avoids reading every constant's body on a large repo.
|
|
|
if (!node.signature || !THUNK_DECL_RE.test(node.signature)) continue;
|
|
|
@@ -2069,10 +2139,12 @@ function resolveRegistryHandler(ctx: ResolutionContext, name: string, chained: s
|
|
|
}
|
|
|
|
|
|
async function objectRegistryEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
let scanned = 0;
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs
|
|
|
if (!REGISTRY_JS_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
@@ -2174,10 +2246,12 @@ function rtkEndpointNameFromHook(hook: string): string | null {
|
|
|
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
|
}
|
|
|
|
|
|
-function rtkQueryEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
+async function rtkQueryEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const hook of queries.iterateNodesByKind('function')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
|
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE) continue;
|
|
|
const endpointName = rtkEndpointNameFromHook(hook.name);
|
|
|
@@ -2219,10 +2293,12 @@ const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
|
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
|
const PINIA_FANOUT_CAP = 80;
|
|
|
|
|
|
-function piniaStoreEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function piniaStoreEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
|
const factoryFile = new Map<string, string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!PINIA_CONSUMER_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('defineStore')) continue;
|
|
|
@@ -2235,6 +2311,7 @@ function piniaStoreEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!PINIA_CONSUMER_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('Store')) continue;
|
|
|
@@ -2304,7 +2381,8 @@ function pathHasSegment(filePath: string, seg: string): boolean {
|
|
|
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
|
}
|
|
|
|
|
|
-function vuexDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function vuexDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
const storeFileCache = new Map<string, boolean>();
|
|
|
const isStoreFile = (file: string): boolean => {
|
|
|
let v = storeFileCache.get(file);
|
|
|
@@ -2339,6 +2417,7 @@ function vuexDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!PINIA_CONSUMER_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('dispatch(') && !content.includes('commit('))) continue;
|
|
|
@@ -2395,7 +2474,8 @@ const CELERY_PY_EXT = /\.py$/;
|
|
|
const CELERY_FANOUT_CAP = 80;
|
|
|
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
|
|
|
|
-function celeryDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function celeryDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
|
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
|
// receivers, so the candidate set stays small.
|
|
|
@@ -2434,6 +2514,7 @@ function celeryDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!CELERY_PY_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('.delay(') && !content.includes('.apply_async('))) continue;
|
|
|
@@ -2506,13 +2587,20 @@ function springFirstParamType(sig: string | undefined): string | null {
|
|
|
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
|
}
|
|
|
|
|
|
-function springEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function springEventEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
|
+ // This is the ONLY full read sweep: publisher files are recorded here so
|
|
|
+ // pass 2 re-reads just those instead of every .java file again (#1212 —
|
|
|
+ // the double full-repo read was one of the tail's longest unyielded spans).
|
|
|
const listeners = new Map<string, Node[]>();
|
|
|
+ const publisherFiles: string[] = [];
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!SPRING_JAVA_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content) continue;
|
|
|
+ if (content.includes('.publishEvent(')) publisherFiles.push(file);
|
|
|
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
|
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
|
if (!hasAnno && !hasAppListener) continue;
|
|
|
@@ -2543,11 +2631,12 @@ function springEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
}
|
|
|
if (!listeners.size) return [];
|
|
|
|
|
|
- // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
|
|
|
+ // Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
|
+ // XEvent. Only the publisher files recorded in pass 1 are (re-)read.
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
- for (const file of ctx.getAllFiles()) {
|
|
|
- if (!SPRING_JAVA_EXT.test(file)) continue;
|
|
|
+ for (const file of publisherFiles) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('.publishEvent(')) continue;
|
|
|
const safe = stripCommentsForRegex(content, 'java');
|
|
|
@@ -2626,10 +2715,12 @@ function resolveMediatrArgType(arg: string, lines: string[], methodStart: number
|
|
|
return declType;
|
|
|
}
|
|
|
|
|
|
-function mediatrDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function mediatrDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
|
const handlers = new Map<string, Node[]>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!MEDIATR_CS_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('IRequestHandler<') && !content.includes('INotificationHandler<'))) continue;
|
|
|
@@ -2657,6 +2748,7 @@ function mediatrDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!MEDIATR_CS_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || (!content.includes('.Send(') && !content.includes('.Publish('))) continue;
|
|
|
@@ -2714,7 +2806,8 @@ const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
|
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
|
const SIDEKIQ_FANOUT_CAP = 80;
|
|
|
|
|
|
-function sidekiqDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function sidekiqDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
|
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
|
const performCache = new Map<string, Node | null>();
|
|
|
@@ -2753,6 +2846,7 @@ function sidekiqDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!SIDEKIQ_RB_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !/\.perform_(?:async|in|at)\b/.test(content)) continue;
|
|
|
@@ -2917,6 +3011,7 @@ function nixLeadingPlainSegments(name: string): string[] {
|
|
|
}
|
|
|
|
|
|
async function nixOptionPathEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scanned255 = 0;
|
|
|
type Rec = { id: string; filePath: string; startLine: number; endLine: number; segs: string[] };
|
|
|
|
|
|
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
|
@@ -2925,6 +3020,7 @@ async function nixOptionPathEdges(queries: QueryBuilder, onYield: MaybeYield): P
|
|
|
let scanned = 0;
|
|
|
for (const kind of ['variable', 'function'] as NodeKind[]) {
|
|
|
for (const node of queries.iterateNodesByKind(kind)) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
if ((++scanned & 0x3fff) === 0 && onYield) await onYield();
|
|
|
if (node.language !== 'nix') continue;
|
|
|
const segs = nixLeadingPlainSegments(node.name);
|
|
|
@@ -3030,9 +3126,16 @@ async function nixOptionPathEdges(queries: QueryBuilder, onYield: MaybeYield): P
|
|
|
return edges;
|
|
|
}
|
|
|
|
|
|
-function erlangBehaviourDispatchEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
|
|
- // Cheap language gate: no Erlang modules → no cost beyond one kind query.
|
|
|
- const erlangModules = queries.getNodesByKind('namespace').filter((n) => n.language === 'erlang');
|
|
|
+async function erlangBehaviourDispatchEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
+ let scanned255 = 0;
|
|
|
+ // Cheap language gate: no Erlang modules → no cost beyond one streamed
|
|
|
+ // kind scan (never a materialized array of every namespace — #1212).
|
|
|
+ const erlangModules: Node[] = [];
|
|
|
+ for (const n of queries.iterateNodesByKind('namespace')) {
|
|
|
+ if ((++scanned255 & 63) === 0) await onYield();
|
|
|
+ if (n.language === 'erlang') erlangModules.push(n);
|
|
|
+ }
|
|
|
if (erlangModules.length === 0) return [];
|
|
|
|
|
|
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
|
@@ -3045,6 +3148,7 @@ function erlangBehaviourDispatchEdges(queries: QueryBuilder, ctx: ResolutionCont
|
|
|
const declaringBehaviours = new Map<string, Node[]>(); // `fn/arity` → behaviour namespaces
|
|
|
const callbackNames = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!ERLANG_EXT.test(file)) continue;
|
|
|
const behaviour = moduleByFile.get(file);
|
|
|
if (!behaviour) continue; // a .hrl or module-less file can't be a behaviour
|
|
|
@@ -3095,6 +3199,7 @@ function erlangBehaviourDispatchEdges(queries: QueryBuilder, ctx: ResolutionCont
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!ERLANG_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content)) continue;
|
|
|
@@ -3186,7 +3291,8 @@ function phpArrayBody(src: string, openIdx: number): string | null {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
-function laravelEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
+async function laravelEventEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
|
|
+ let scannedFiles = 0;
|
|
|
// event short name → its listener `handle` methods (deduped by node id).
|
|
|
const listeners = new Map<string, Map<string, Node>>();
|
|
|
const add = (event: string, handle: Node) => {
|
|
|
@@ -3204,6 +3310,7 @@ function laravelEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
|
|
|
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!LARAVEL_PHP_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content) continue;
|
|
|
@@ -3246,6 +3353,7 @@ function laravelEventEdges(ctx: ResolutionContext): Edge[] {
|
|
|
const edges: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
for (const file of ctx.getAllFiles()) {
|
|
|
+ if ((++scannedFiles & 15) === 0) await onYield();
|
|
|
if (!LARAVEL_PHP_EXT.test(file)) continue;
|
|
|
const content = ctx.readFile(file);
|
|
|
if (!content || !content.includes('event(')) continue;
|
|
|
@@ -3301,59 +3409,90 @@ export async function synthesizeCallbackEdges(queries: QueryBuilder, ctx: Resolu
|
|
|
// watchdog still catches that. See ./cooperative-yield.
|
|
|
const yieldToLoop = createYielder();
|
|
|
|
|
|
+ // Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
|
|
|
+ // (=1: passes over 250ms; =all: every pass). This is the diagnostic that
|
|
|
+ // located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
|
|
|
+ const markT = { t: Date.now() };
|
|
|
+ const __mark = (label: string): void => {
|
|
|
+ const now = Date.now();
|
|
|
+ const dt = now - markT.t;
|
|
|
+ markT.t = now;
|
|
|
+ if (process.env.CODEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.CODEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
|
+ console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Language gating: one indexed DISTINCT over the files table lets a pass
|
|
|
+ // whose own filters reference a specific language/extension be skipped
|
|
|
+ // outright when the project has no such files — its result is provably
|
|
|
+ // empty, so skipping is behavior-identical and the cost drops to zero
|
|
|
+ // (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
|
|
|
+ // Passes without an explicit language filter always run.
|
|
|
+ const langs = queries.getDistinctFileLanguages();
|
|
|
+ const has = (...ls: string[]): boolean => ls.some((l) => langs.has(l));
|
|
|
+ const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
|
|
|
+ const NONE: Edge[] = [];
|
|
|
+
|
|
|
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
|
// FIRST: a method declared in a different file from its receiver type is
|
|
|
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
|
// struct's method set from its `contains` edges — so without this it would
|
|
|
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
|
- const goMethodContains = goCrossFileMethodContainsEdges(queries);
|
|
|
- if (goMethodContains.length > 0) queries.insertEdges(goMethodContains);
|
|
|
- await yieldToLoop();
|
|
|
+ const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
|
|
|
+ for (let i = 0; i < goMethodContains.length; i += 2000) {
|
|
|
+ queries.insertEdges(goMethodContains.slice(i, i + 2000));
|
|
|
+ await yieldToLoop();
|
|
|
+ }
|
|
|
+ await yieldToLoop(); __mark('goMethodContains');
|
|
|
|
|
|
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
|
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
|
// Go has none statically. (Other languages already have static implements
|
|
|
// edges from extraction, so they don't need this pre-pass.)
|
|
|
- const goImpl = goImplementsEdges(queries);
|
|
|
- if (goImpl.length > 0) queries.insertEdges(goImpl);
|
|
|
- await yieldToLoop();
|
|
|
-
|
|
|
- const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop); await yieldToLoop();
|
|
|
- const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop); await yieldToLoop();
|
|
|
- const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop); await yieldToLoop();
|
|
|
- const renderEdges = reactRenderEdges(queries, ctx); await yieldToLoop();
|
|
|
- const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop); await yieldToLoop();
|
|
|
- const vueEdges = vueTemplateEdges(ctx); await yieldToLoop();
|
|
|
- const svelteKitEdges = svelteKitLoadEdges(ctx); await yieldToLoop();
|
|
|
- const pascalEdges = pascalFormEdges(ctx); await yieldToLoop();
|
|
|
- const flutterEdges = flutterBuildEdges(queries, ctx); await yieldToLoop();
|
|
|
- const arkuiStateEdges = arkuiStateBuildEdges(queries, ctx); await yieldToLoop();
|
|
|
- const arkuiEmitter = arkuiEmitterEdges(ctx); await yieldToLoop();
|
|
|
- const arkuiRoutes = arkuiRouterEdges(ctx); await yieldToLoop();
|
|
|
- const cppEdges = cppOverrideEdges(queries); await yieldToLoop();
|
|
|
- const ifaceEdges = interfaceOverrideEdges(queries); await yieldToLoop();
|
|
|
- const kotlinExpectActual = kotlinExpectActualEdges(queries); await yieldToLoop();
|
|
|
- const goGrpcEdges = goGrpcStubImplEdges(queries); await yieldToLoop();
|
|
|
- const rnEventEdgesList = rnEventEdges(ctx); await yieldToLoop();
|
|
|
- const fabricNativeEdges = fabricNativeImplEdges(ctx); await yieldToLoop();
|
|
|
- const expoXPlatEdges = expoCrossPlatformEdges(queries); await yieldToLoop();
|
|
|
- const rnXPlatEdges = rnCrossPlatformEdges(queries); await yieldToLoop();
|
|
|
- const mybatisEdges = mybatisJavaXmlEdges(queries); await yieldToLoop();
|
|
|
- const ginEdges = ginMiddlewareChainEdges(queries, ctx); await yieldToLoop();
|
|
|
- const thunkEdges = reduxThunkEdges(queries, ctx); await yieldToLoop();
|
|
|
- const registryEdges = await objectRegistryEdges(ctx, yieldToLoop); await yieldToLoop();
|
|
|
- const rtkEdges = rtkQueryEdges(queries, ctx); await yieldToLoop();
|
|
|
- const piniaEdges = piniaStoreEdges(ctx); await yieldToLoop();
|
|
|
- const vuexEdges = vuexDispatchEdges(ctx); await yieldToLoop();
|
|
|
- const celeryEdges = celeryDispatchEdges(ctx); await yieldToLoop();
|
|
|
- const springEdges = springEventEdges(ctx); await yieldToLoop();
|
|
|
- const mediatrEdges = mediatrDispatchEdges(ctx); await yieldToLoop();
|
|
|
- const sidekiqEdges = sidekiqDispatchEdges(ctx); await yieldToLoop();
|
|
|
- const erlangBehaviourEdges = erlangBehaviourDispatchEdges(queries, ctx); await yieldToLoop();
|
|
|
- const laravelEdges = laravelEventEdges(ctx); await yieldToLoop();
|
|
|
- const cFnPtrEdges = cFnPointerDispatchEdges(queries, ctx); await yieldToLoop();
|
|
|
- const goframeEdges = goframeRouteEdges(ctx); await yieldToLoop();
|
|
|
- const nixOptionEdges = await nixOptionPathEdges(queries, yieldToLoop); await yieldToLoop();
|
|
|
+ const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
|
|
|
+ for (let i = 0; i < goImpl.length; i += 2000) {
|
|
|
+ queries.insertEdges(goImpl.slice(i, i + 2000));
|
|
|
+ await yieldToLoop();
|
|
|
+ }
|
|
|
+ await yieldToLoop(); __mark('goImplements');
|
|
|
+
|
|
|
+ const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop); await yieldToLoop(); __mark('fieldEdges');
|
|
|
+ const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop); await yieldToLoop(); __mark('closureCollEdges');
|
|
|
+ const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop); await yieldToLoop(); __mark('emitterEdges');
|
|
|
+ const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop); await yieldToLoop(); __mark('renderEdges');
|
|
|
+ const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop); await yieldToLoop(); __mark('jsxEdges');
|
|
|
+ const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('vueEdges');
|
|
|
+ const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('svelteKitEdges');
|
|
|
+ const pascalEdges = await pascalFormEdges(ctx, yieldToLoop); await yieldToLoop(); __mark('pascalEdges');
|
|
|
+ const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('flutterEdges');
|
|
|
+ const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('arkuiStateEdges');
|
|
|
+ const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('arkuiEmitter');
|
|
|
+ const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('arkuiRoutes');
|
|
|
+ const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('cppEdges');
|
|
|
+ const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
|
|
|
+ ? await interfaceOverrideEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('ifaceEdges');
|
|
|
+ const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('kotlinExpectActual');
|
|
|
+ const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('goGrpcEdges');
|
|
|
+ const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('rnEventEdgesList');
|
|
|
+ const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop); await yieldToLoop(); __mark('fabricNativeEdges');
|
|
|
+ const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop); await yieldToLoop(); __mark('expoXPlatEdges');
|
|
|
+ const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop); await yieldToLoop(); __mark('rnXPlatEdges');
|
|
|
+ const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('mybatisEdges');
|
|
|
+ const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('ginEdges');
|
|
|
+ const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('thunkEdges');
|
|
|
+ const registryEdges = await objectRegistryEdges(ctx, yieldToLoop); await yieldToLoop(); __mark('registryEdges');
|
|
|
+ const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('rtkEdges');
|
|
|
+ const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('piniaEdges');
|
|
|
+ const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('vuexEdges');
|
|
|
+ const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('celeryEdges');
|
|
|
+ const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('springEdges');
|
|
|
+ const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('mediatrEdges');
|
|
|
+ const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('sidekiqEdges');
|
|
|
+ const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('erlangBehaviourEdges');
|
|
|
+ const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('laravelEdges');
|
|
|
+ const cFnPtrEdges = has('c', 'cpp') ? await cFnPointerDispatchEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('cFnPtrEdges');
|
|
|
+ const goframeEdges = has('go') ? await goframeRouteEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('goframeEdges');
|
|
|
+ const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('nixOptionEdges');
|
|
|
|
|
|
const merged: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
|
@@ -3400,6 +3539,14 @@ export async function synthesizeCallbackEdges(queries: QueryBuilder, ctx: Resolu
|
|
|
seen.add(key);
|
|
|
merged.push(e);
|
|
|
}
|
|
|
- if (merged.length > 0) queries.insertEdges(merged);
|
|
|
+ __mark('dedupe-merge');
|
|
|
+ // Chunked insert with yields: on the Linux kernel the merged synthesized
|
|
|
+ // edge set is ~275k rows, and one transaction for all of them was a 20s
|
|
|
+ // unyielded main-thread span (#1212 follow-up) — the last one in the tail.
|
|
|
+ for (let i = 0; i < merged.length; i += 2000) {
|
|
|
+ queries.insertEdges(merged.slice(i, i + 2000));
|
|
|
+ await yieldToLoop();
|
|
|
+ }
|
|
|
+ __mark('insertMergedEdges');
|
|
|
return merged.length + goImpl.length + goMethodContains.length;
|
|
|
}
|