genesis-compiler 1.10.6 → 1.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,17 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
2
4
  import path from 'node:path';
3
5
 
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import { asDiagnostic } from '../errors.js';
8
+ import { extractionCachePath, loadExtractions, storeExtractions } from '../extraction-cache.js';
6
9
  import { sha256 } from '../utils.js';
7
10
  import { runProcess } from '../process.js';
8
11
  import { parserAbortSignal, prepareCodeParsers } from './parsers.js';
9
12
 
10
13
  const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
14
+ const COLLECT_AFTER_CHARACTERS = 512 * 1024;
11
15
  const OPENING_PARAMETER_DELIMITERS = '([{<';
12
16
  const CLOSING_PARAMETER_DELIMITERS = ')]}>';
13
17
  const EXCLUDED_DIRECTORIES = new Set([
@@ -61,6 +65,16 @@ const DECLARATION_KINDS = {
61
65
  go: ['type_spec', 'const_spec', 'var_spec'], rust: ['struct_item', 'enum_item', 'trait_item', 'type_item', 'const_item', 'static_item'],
62
66
  ruby: ['class', 'module', 'assignment'], kotlin: ['class_declaration', 'object_declaration', 'property_declaration'], shell: [],
63
67
  };
68
+ const DEPENDENCY_KINDS = {
69
+ javascript: ['import_statement', 'export_statement', 'call_expression'],
70
+ python: ['import_statement', 'import_from_statement'], cpp: ['preproc_include'],
71
+ java: ['import_declaration'], csharp: ['using_directive'], go: ['import_spec'],
72
+ rust: ['use_declaration', 'mod_item'], php: ['namespace_use_declaration', 'include_expression', 'require_expression'],
73
+ ruby: ['call'], kotlin: ['import_header'], shell: ['command'],
74
+ };
75
+ // A JavaScript call is a dependency only when its text starts with require or
76
+ // import, so the traversal skips calls whose callee cannot start that way.
77
+ const DEPENDENCY_CALLS = { call_expression: { has: { field: 'function', regex: '^(?:require|import)' } } };
64
78
 
65
79
  function nodeRange(node, lineOffset = 0, columnOffset = 0) {
66
80
  const range = node.range();
@@ -91,20 +105,18 @@ function declarationNode(node) {
91
105
  return node.parent()?.kind() === 'decorated_definition' ? node.parent() : node;
92
106
  }
93
107
 
94
- function declarationsInTree(root, language, source, lineOffset, columnOffset, pythonExports = null) {
108
+ function declarationsInTree(nodes, language, source, lineOffset, columnOffset, pythonExports = null) {
95
109
  const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : pythonExports?.names;
96
110
  const result = [];
97
111
  for (const kind of DECLARATION_KINDS[language] || []) {
98
- let nodes;
99
- try { nodes = root.findAll({ rule: { kind } }); } catch { continue; }
100
- for (const node of nodes) {
112
+ for (const node of nodes.get(kind)) {
101
113
  let declaredNodes = [node];
102
114
  if (language === 'java' && ['field_declaration', 'constant_declaration'].includes(kind)) declaredNodes = node.children().filter(child => child.kind() === 'variable_declarator');
103
115
  if (language === 'csharp' && kind === 'field_declaration') declaredNodes = node.children().find(child => child.kind() === 'variable_declaration')?.children().filter(child => child.kind() === 'variable_declarator') || [];
104
116
  if (language === 'php' && ['const_declaration', 'property_declaration'].includes(kind)) declaredNodes = node.children().filter(child => ['const_element', 'property_element'].includes(child.kind()));
105
117
  if (language === 'go' && ['const_spec', 'var_spec'].includes(kind)) declaredNodes = node.children().filter(child => child.kind() === 'identifier');
106
118
  if (language === 'ruby' && kind === 'assignment' && safeField(node, 'left')?.kind() !== 'constant') continue;
107
- if (language === 'python' && kind === 'assignment' && (safeField(node, 'left')?.text() === '__all__' || node.ancestors().some(parent => parent.kind() === 'function_definition'))) continue;
119
+ if (language === 'python' && kind === 'assignment' && (safeField(node, 'left')?.text() === '__all__' || lineage(node).kinds.includes('function_definition'))) continue;
108
120
  if (['variable_declarator', 'field_definition', 'public_field_definition'].includes(kind) && ['arrow_function', 'function_expression'].includes(safeField(node, 'value')?.kind())) continue;
109
121
  const declaration = kind === 'variable_declarator' ? node.parent() : declarationNode(node);
110
122
  const range = nodeRange(declaration, lineOffset, columnOffset);
@@ -123,20 +135,14 @@ function declarationsInTree(root, language, source, lineOffset, columnOffset, py
123
135
 
124
136
  // These are syntax observations. Resolution belongs to the native index, and
125
137
  // unsupported/dynamic targets remain explicit rather than guessed.
126
- function dependencyReferences(root, language, source, lineOffset, columnOffset) {
127
- const kinds = {
128
- javascript: ['import_statement', 'export_statement', 'call_expression'],
129
- python: ['import_statement', 'import_from_statement'], cpp: ['preproc_include'],
130
- java: ['import_declaration'], csharp: ['using_directive'], go: ['import_spec'],
131
- rust: ['use_declaration', 'mod_item'], php: ['namespace_use_declaration', 'include_expression', 'require_expression'],
132
- ruby: ['call'], kotlin: ['import_header'], shell: ['command'],
133
- }[language] || [];
138
+ function dependencyReferences(nodes, language, source, lineOffset, columnOffset) {
134
139
  const references = [];
135
- for (const kind of kinds) {
136
- let nodes;
137
- try { nodes = root.findAll({ rule: { kind } }); } catch { continue; }
138
- for (const node of nodes) {
139
- const text = node.text();
140
+ for (const kind of DEPENDENCY_KINDS[language] || []) {
141
+ for (const node of nodes.get(kind)) {
142
+ // napi indexes are UTF-16 offsets into this source; a slice avoids
143
+ // copying the text of every call that is not an import.
144
+ const { start, end } = node.range();
145
+ const text = source.slice(start.index, end.index);
140
146
  let target;
141
147
  let relationKind = 'import';
142
148
  let typeOnly = false;
@@ -156,7 +162,7 @@ function dependencyReferences(root, language, source, lineOffset, columnOffset)
156
162
  }
157
163
  typeOnly = node.children().some(child => child.kind() === 'type') ||
158
164
  (specifiers.length > 0 && specifiers.every(specifier => specifier.children().some(child => child.kind() === 'type')) && !clause.children().some(child => ['identifier', 'namespace_import'].includes(child.kind()))) ||
159
- (kind === 'call_expression' && /^import\s*\(/u.test(text) && node.ancestors().some(parent => ['type_alias_declaration', 'type_annotation', 'type_query'].includes(parent.kind())));
165
+ (kind === 'call_expression' && /^import\s*\(/u.test(text) && lineage(node).kinds.some(parent => ['type_alias_declaration', 'type_annotation', 'type_query'].includes(parent)));
160
166
  if (typeOnly && kind !== 'export_statement') relationKind = 'type-import';
161
167
  } else if (language === 'python') {
162
168
  const from = text.match(/^from\s+([.\w]+)\s+import/u);
@@ -262,9 +268,23 @@ function splitParameters(value) {
262
268
  return result.filter(Boolean);
263
269
  }
264
270
 
271
+ // napi builds new objects on every call, and several checks read one node's
272
+ // ancestry, so each node's ancestors and their kinds are read once per script
273
+ // unit. The unit clears them so its syntax tree can be released.
274
+ const lineages = new Map();
275
+ function lineage(node) {
276
+ let value = lineages.get(node);
277
+ if (!value) {
278
+ const nodes = node.ancestors();
279
+ value = { nodes, kinds: nodes.map(entry => entry.kind()) };
280
+ lineages.set(node, value);
281
+ }
282
+ return value;
283
+ }
284
+
265
285
  function ancestor(node, kinds) {
266
- const wanted = new Set(kinds);
267
- return node.ancestors().find((candidate) => wanted.has(candidate.kind())) || null;
286
+ const { nodes, kinds: names } = lineage(node);
287
+ return nodes[names.findIndex(kind => kinds.includes(kind))] || null;
268
288
  }
269
289
 
270
290
  const CONTAINER_KINDS = new Set([
@@ -277,10 +297,10 @@ function containerName(node, language) {
277
297
  const receiver = safeField(node, 'receiver')?.text().trim();
278
298
  if (receiver) return receiver.replace(/^\(|\)$/gu, '').replace(/^\w+\s+\*?/u, '').trim();
279
299
  }
280
- const container = node.ancestors().find((candidate) => CONTAINER_KINDS.has(candidate.kind()) && !(language === 'python' && candidate.kind() === 'module'));
300
+ const { nodes, kinds } = lineage(node);
301
+ const container = nodes[kinds.findIndex(kind => CONTAINER_KINDS.has(kind) && !(language === 'python' && kind === 'module'))];
281
302
  if (!container) {
282
- const parentFunction = node.ancestors().find((candidate) => DEFINITIONS.javascript.kinds.includes(candidate.kind())
283
- || candidate.kind() === 'function_definition');
303
+ const parentFunction = nodes[kinds.findIndex(kind => DEFINITIONS.javascript.kinds.includes(kind) || kind === 'function_definition')];
284
304
  return parentFunction ? declaredName(parentFunction) : null;
285
305
  }
286
306
  if (container.kind() === 'impl_item') {
@@ -310,7 +330,8 @@ function directText(node) {
310
330
  }
311
331
 
312
332
  function cPlusPlusVisibility(node) {
313
- if (node.ancestors().some(parent => parent.kind() === 'namespace_definition' && !safeField(parent, 'name'))) return 'internal';
333
+ const { nodes, kinds } = lineage(node);
334
+ if (nodes.some((parent, index) => kinds[index] === 'namespace_definition' && !safeField(parent, 'name'))) return 'internal';
314
335
  const classBody = ancestor(node, ['field_declaration_list']);
315
336
  if (!classBody) return /\bstatic\b/u.test(directText(node)) ? 'internal' : 'public';
316
337
  const classNode = ancestor(node, ['class_specifier', 'struct_specifier', 'union_specifier']);
@@ -354,17 +375,19 @@ function javascriptExportedNames(source) {
354
375
 
355
376
  function pythonExportedNames(root) {
356
377
  let names = null; let partial = false;
357
- const nodes = root.findAll({ rule: { any: [{ kind: 'assignment' }, { kind: 'augmented_assignment' }, { kind: 'call' }] } });
358
- for (const node of nodes.sort((a, b) => a.range().start.index - b.range().start.index)) {
359
- const parents = node.ancestors();
360
- if (parents.some(parent => ['function_definition', 'class_definition', 'lambda'].includes(parent.kind()))) continue;
378
+ // A statement whose text never mentions __all__ cannot define it.
379
+ const nodes = root.findAll({ rule: { any: [{ kind: 'assignment' }, { kind: 'augmented_assignment' }, { kind: 'call' }], regex: '__all__' } })
380
+ .map(node => ({ node, start: node.range().start.index }));
381
+ for (const { node } of nodes.sort((a, b) => a.start - b.start)) {
382
+ const parents = lineage(node).kinds;
383
+ if (parents.some(kind => ['function_definition', 'class_definition', 'lambda'].includes(kind))) continue;
361
384
  const call = node.kind() === 'call';
362
385
  if (call ? !node.findAll({ rule: { kind: 'identifier' } }).some(identifier => identifier.text() === '__all__') : safeField(node, 'left')?.text() !== '__all__') continue;
363
386
  const right = safeField(node, 'right');
364
387
  const literals = right && ['list', 'tuple'].includes(right.kind()) ? right.children().filter(child => !['[', ']', '(', ')', ',', 'comment'].includes(child.kind())) : null;
365
388
  const values = literals?.map(child => child.kind() === 'string' ? child.text().match(/^(?:r|u)?(['"])([^'"\\\r\n]*)\1$/iu)?.[2] : undefined);
366
389
  const addition = node.kind() === 'augmented_assignment';
367
- if (call || parents.some(parent => !['module', 'expression_statement'].includes(parent.kind())) || !values || values.some(value => value === undefined) || (addition && (!names || safeField(node, 'operator')?.text() !== '+='))) {
390
+ if (call || parents.some(kind => !['module', 'expression_statement'].includes(kind)) || !values || values.some(value => value === undefined) || (addition && (!names || safeField(node, 'operator')?.text() !== '+='))) {
368
391
  names = null; partial = true;
369
392
  } else {
370
393
  names = new Set([...(addition ? names : []), ...values]); partial = false;
@@ -378,7 +401,7 @@ function javascriptVisibility(node, name, exportedNames) {
378
401
  if (['field_definition', 'public_field_definition'].includes(node.parent()?.kind()) && node.parent().children().some(child => child.kind() === 'accessibility_modifier' && ['private', 'protected'].includes(child.text()))) return 'internal';
379
402
  // An exported class exposes its members, not locals inside those members.
380
403
  // An export-name match also cannot expose a shadowing nested declaration.
381
- if (node.ancestors().some(parent => DEFINITIONS.javascript.kinds.includes(parent.kind()))) return 'internal';
404
+ if (lineage(node).kinds.some(kind => DEFINITIONS.javascript.kinds.includes(kind))) return 'internal';
382
405
  const classNode = ancestor(node, ['class_declaration', 'class_expression', 'class']);
383
406
  if (classNode) {
384
407
  const className = declaredName(classNode);
@@ -394,15 +417,16 @@ function javascriptVisibility(node, name, exportedNames) {
394
417
  }
395
418
 
396
419
  function visibility(node, name, language, exportedNames) {
397
- const head = directText(node);
398
- if (node.ancestors().some(parent => CALLABLE_KINDS.has(parent.kind()) || ['lambda_expression', 'closure_expression', 'lambda_literal', 'func_literal', 'lambda', 'anonymous_method_expression'].includes(parent.kind()))) return 'internal';
420
+ const { kinds } = lineage(node);
421
+ if (kinds.some(kind => CALLABLE_KINDS.has(kind) || ['lambda_expression', 'closure_expression', 'lambda_literal', 'func_literal', 'lambda', 'anonymous_method_expression'].includes(kind))) return 'internal';
399
422
  if (language === 'javascript') return javascriptVisibility(node, name, exportedNames);
400
423
  if (language === 'python') {
401
- const nestedFunction = node.ancestors().some((candidate) => candidate.kind() === 'function_definition');
424
+ const nestedFunction = kinds.includes('function_definition');
402
425
  if (nestedFunction) return 'internal';
403
- const classMember = node.ancestors().some(parent => parent.kind() === 'class_definition');
426
+ const classMember = kinds.includes('class_definition');
404
427
  return (exportedNames && !classMember ? exportedNames.has(name) : !name.startsWith('_')) ? 'public' : 'internal';
405
428
  }
429
+ const head = directText(node);
406
430
  if (['java', 'csharp'].includes(language)) return /\bpublic\b/u.test(head) || (!/\b(?:private|protected|internal)\b/u.test(head) && ancestor(node, ['interface_declaration'])) ? 'public' : 'internal';
407
431
  if (language === 'cpp') return cPlusPlusVisibility(node);
408
432
  if (language === 'php') return /\b(?:private|protected)\b/u.test(head) ? 'internal' : 'public';
@@ -417,10 +441,10 @@ function visibility(node, name, language, exportedNames) {
417
441
  function callableKind(node, name, language) {
418
442
  if (node.kind().includes('constructor') || ['__construct', '__init__', 'constructor'].includes(name)) return 'constructor';
419
443
  if (node.kind().includes('method')) return 'method';
420
- const owner = node.ancestors().find((candidate) => (
421
- !(language === 'python' && candidate.kind() === 'module') && (CONTAINER_KINDS.has(candidate.kind()) || CALLABLE_KINDS.has(candidate.kind()))
444
+ const owner = lineage(node).kinds.find(kind => (
445
+ !(language === 'python' && kind === 'module') && (CONTAINER_KINDS.has(kind) || CALLABLE_KINDS.has(kind))
422
446
  ));
423
- return owner && CONTAINER_KINDS.has(owner.kind()) ? 'method' : 'function';
447
+ return owner && CONTAINER_KINDS.has(owner) ? 'method' : 'function';
424
448
  }
425
449
 
426
450
  function scriptUnits(source, filePath, definition) {
@@ -444,28 +468,27 @@ function scriptUnits(source, filePath, definition) {
444
468
  return units;
445
469
  }
446
470
 
447
- function functionsInTree(root, definition, { language, lineOffset, columnOffset, source, pythonExports = null }) {
471
+ function functionsInTree(nodes, definition, { language, lineOffset, columnOffset, source, pythonExports = null }) {
448
472
  const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : pythonExports?.names;
449
- const nodes = new Map();
450
- for (const kind of definition.kinds) {
451
- for (const node of root.findAll({ rule: { kind } })) nodes.set(node.id(), node);
452
- }
453
- return [...nodes.values()].sort((left, right) => left.range().start.index - right.range().start.index)
454
- .map((node) => {
473
+ return definition.kinds.flatMap(kind => nodes.get(kind)).map(node => ({ node, own: node.range() }))
474
+ .sort((left, right) => left.own.start.index - right.own.start.index)
475
+ .map(({ node, own }) => {
455
476
  const name = declaredName(node);
456
477
  if (!name) return null;
457
478
  const container = containerName(node, language);
458
- const location = node.range().start;
459
- const range = nodeRange(declarationNode(node), lineOffset, columnOffset);
479
+ const location = own.start;
480
+ const declaration = declarationNode(node);
481
+ const range = nodeRange(declaration, lineOffset, columnOffset);
460
482
  const parameters = splitParameters(parameterText(node));
461
483
  const resolvedVisibility = visibility(node, name, language, exportedNames);
462
484
  const scope = language === 'rust' ? node.children().find(child => child.kind() === 'visibility_modifier')?.text() || 'private' : null;
485
+ const text = source.slice(own.start.index, own.end.index);
463
486
  return {
464
487
  name,
465
488
  qualifiedName: [container, name].filter(Boolean).join(language === 'php' ? '::' : '.'),
466
489
  kind: callableKind(node, name, language),
467
490
  visibility: resolvedVisibility,
468
- ...(language === 'javascript' && declarationNode(node).parent()?.kind() === 'export_statement' && declarationNode(node).parent().children().some(child => child.kind() === 'default') ? { exportName: 'default' } : {}),
491
+ ...(language === 'javascript' && declaration.parent()?.kind() === 'export_statement' && declaration.parent().children().some(child => child.kind() === 'default') ? { exportName: 'default' } : {}),
469
492
  ...(scope ? { visibilityScope: scope === 'pub' ? 'public' : scope.replace(/^pub\((.*)\)$/u, '$1') } : {}),
470
493
  container,
471
494
  line: location.line + lineOffset + 1,
@@ -473,13 +496,279 @@ function functionsInTree(root, definition, { language, lineOffset, columnOffset,
473
496
  start: range.start,
474
497
  end: range.end,
475
498
  parameters,
476
- async: /^\s*async\b/u.test(node.text()),
477
- generator: /\bfunction\s*\*|\byield\b/u.test(node.text()),
499
+ async: /^\s*async\b/u.test(text),
500
+ generator: /\bfunction\s*\*|\byield\b/u.test(text),
478
501
  static: /\bstatic\b/u.test(directText(node)),
479
502
  };
480
503
  }).filter(Boolean);
481
504
  }
482
505
 
506
+ // Call-web syntax: every callable site (including anonymous ones), its call
507
+ // sites and its import bindings. These are cached per file content; the
508
+ // call-web owner resolves them against the whole snapshot.
509
+ const CALL_SITES = Object.freeze({
510
+ javascript: {
511
+ functions: { function_declaration: 'function', generator_function_declaration: 'function', function_expression: 'closure', function: 'closure', generator_function: 'closure', arrow_function: 'arrow', method_definition: 'method' },
512
+ calls: ['call_expression'], containers: ['class_declaration', 'class', 'abstract_class_declaration'],
513
+ },
514
+ python: { functions: { function_definition: 'function', lambda: 'closure' }, calls: ['call'], containers: ['class_definition'] },
515
+ go: { functions: { function_declaration: 'function', method_declaration: 'method', func_literal: 'closure' }, calls: ['call_expression'], containers: [] },
516
+ rust: { functions: { function_item: 'function', closure_expression: 'closure' }, calls: ['call_expression'], containers: ['impl_item', 'trait_item'] },
517
+ java: { functions: { method_declaration: 'method', constructor_declaration: 'constructor', compact_constructor_declaration: 'constructor', lambda_expression: 'closure' }, calls: ['method_invocation'], containers: ['class_declaration', 'interface_declaration', 'enum_declaration', 'record_declaration'] },
518
+ kotlin: { functions: { function_declaration: 'function', secondary_constructor: 'constructor', anonymous_function: 'closure', lambda_literal: 'closure' }, calls: ['call_expression'], containers: ['class_declaration', 'object_declaration'] },
519
+ csharp: { functions: { method_declaration: 'method', constructor_declaration: 'constructor', local_function_statement: 'function', lambda_expression: 'closure', anonymous_method_expression: 'closure' }, calls: ['invocation_expression'], containers: ['class_declaration', 'struct_declaration', 'interface_declaration', 'record_declaration'] },
520
+ php: { functions: { function_definition: 'function', method_declaration: 'method', anonymous_function: 'closure', anonymous_function_creation_expression: 'closure', arrow_function: 'arrow' }, calls: ['function_call_expression', 'member_call_expression', 'nullsafe_member_call_expression', 'scoped_call_expression'], containers: ['class_declaration', 'interface_declaration', 'trait_declaration', 'enum_declaration'] },
521
+ ruby: { functions: { method: 'function', singleton_method: 'method', lambda: 'closure', block: 'closure', do_block: 'closure' }, calls: ['call'], containers: ['class', 'module'] },
522
+ cpp: { functions: { function_definition: 'function', lambda_expression: 'closure' }, calls: ['call_expression'], containers: ['class_specifier', 'struct_specifier'] },
523
+ shell: { functions: { function_definition: 'function' }, calls: ['command'], containers: [] },
524
+ });
525
+ const CALL_IMPORT_KINDS = { javascript: ['import_statement'], python: ['import_statement', 'import_from_statement'], go: ['import_spec'], rust: ['use_declaration'], java: ['import_declaration'], kotlin: ['import_header'], php: ['namespace_use_declaration'] };
526
+ const CONSTRUCTOR_NAMES = new Set(['constructor', '__init__', '__construct', 'initialize']);
527
+ // Leading decorators, attributes and comments are not the canonical start.
528
+ const LEADING_SYNTAX = new Set(['decorator', 'comment', 'line_comment', 'block_comment', 'attribute_list', 'attribute_item', 'attribute', 'annotation', 'marker_annotation']);
529
+ const LITERAL_RECEIVER = /string|array|list|dictionary|tuple|regex|number|integer|float|literal|comprehension/u;
530
+ const PROJECT_LITERAL = new Set(['composite_literal', 'func_literal', 'lambda_literal']);
531
+ const validKinds = new Map();
532
+
533
+ // ast-grep rejects unknown kinds; grammars differ (JavaScript/TypeScript/Tsx).
534
+ function knownKinds(root, parser, kinds) {
535
+ const key = `${parser}\0${kinds.join(',')}`;
536
+ if (!validKinds.has(key)) validKinds.set(key, kinds.filter(kind => { try { root.find({ rule: { kind } }); return true; } catch { return false; } }));
537
+ return validKinds.get(key);
538
+ }
539
+
540
+ function findKinds(root, parser, kinds, refinements = {}) {
541
+ const known = knownKinds(root, parser, kinds);
542
+ return known.length ? root.findAll({ rule: { any: known.map(kind => ({ kind, ...refinements[kind] })) } }) : [];
543
+ }
544
+
545
+ function canonicalStart(node) {
546
+ const first = node.child(0);
547
+ if (!first || (!LEADING_SYNTAX.has(first.kind()) && first.kind() !== 'modifiers')) return node.range().start;
548
+ for (const child of node.children()) {
549
+ if (LEADING_SYNTAX.has(child.kind())) continue;
550
+ if (child.kind() === 'modifiers') {
551
+ const inner = child.children().find(entry => !LEADING_SYNTAX.has(entry.kind()));
552
+ if (inner) return inner.range().start;
553
+ continue;
554
+ }
555
+ return child.range().start;
556
+ }
557
+ return node.range().start;
558
+ }
559
+
560
+ function objectOwnerName(node) {
561
+ // `const api = { list() {} }` and `{ list: () => {} }` belong to `api`.
562
+ const object = node.parent()?.kind() === 'object' ? node.parent() : node.parent()?.kind() === 'pair' && node.parent().parent()?.kind() === 'object' ? node.parent().parent() : null;
563
+ const owner = object?.parent();
564
+ if (!owner) return null;
565
+ if (owner.kind() === 'variable_declarator') return safeField(owner, 'name')?.text() || null;
566
+ if (owner.kind() === 'pair') return safeField(owner, 'key')?.text() || null;
567
+ if (owner.kind() === 'assignment_expression') return safeField(owner, 'left')?.text().match(/[A-Za-z_$][\w$]*$/u)?.[0] || null;
568
+ return null;
569
+ }
570
+
571
+ function callContainerName(node, language) {
572
+ if (node.kind() === 'impl_item') return safeField(node, 'type')?.text().trim() || null;
573
+ return safeField(node, 'name')?.text().trim()
574
+ || node.children().find(child => ['constant', 'identifier', 'name', 'simple_identifier', 'type_identifier'].includes(child.kind()))?.text().trim()
575
+ || (language === 'javascript' && node.parent()?.kind() === 'variable_declarator' ? safeField(node.parent(), 'name')?.text() : null)
576
+ || null;
577
+ }
578
+
579
+ function calleeParts(node, language) {
580
+ const field = name => safeField(node, name);
581
+ let callee = null; let nameNode = null; let receiver = null;
582
+ if (['javascript', 'python', 'go', 'rust', 'csharp', 'cpp'].includes(language)) {
583
+ callee = field('function');
584
+ const kind = callee?.kind();
585
+ if (['identifier', 'field_identifier'].includes(kind)) nameNode = callee;
586
+ else if (['member_expression', 'attribute', 'selector_expression', 'field_expression', 'member_access_expression'].includes(kind)) {
587
+ nameNode = safeField(callee, { javascript: 'property', python: 'attribute', go: 'field', rust: 'field', csharp: 'name', cpp: 'field' }[language]);
588
+ receiver = safeField(callee, { javascript: 'object', python: 'object', go: 'operand', rust: 'value', csharp: 'expression', cpp: 'argument' }[language]);
589
+ } else if (['scoped_identifier', 'qualified_identifier'].includes(kind)) {
590
+ nameNode = safeField(callee, 'name'); receiver = safeField(callee, kind === 'scoped_identifier' ? 'path' : 'scope');
591
+ } else if (kind === 'generic_name' || kind === 'template_function') nameNode = safeField(callee, 'name') || callee.child(0);
592
+ } else if (language === 'java') {
593
+ nameNode = field('name'); receiver = field('object');
594
+ } else if (language === 'kotlin') {
595
+ callee = node.child(0);
596
+ if (callee?.kind() === 'simple_identifier') nameNode = callee;
597
+ else if (callee?.kind() === 'navigation_expression') {
598
+ const suffix = callee.children().at(-1);
599
+ nameNode = suffix?.kind() === 'navigation_suffix' ? suffix.children().find(child => child.kind() === 'simple_identifier') : null;
600
+ receiver = callee.child(0);
601
+ }
602
+ } else if (language === 'php') {
603
+ if (node.kind() === 'function_call_expression') {
604
+ callee = field('function');
605
+ nameNode = callee?.kind() === 'qualified_name' ? callee.children().at(-1) : callee?.kind() === 'name' ? callee : null;
606
+ } else {
607
+ nameNode = field('name'); receiver = field(node.kind() === 'scoped_call_expression' ? 'scope' : 'object');
608
+ }
609
+ } else if (language === 'ruby') {
610
+ nameNode = field('method'); receiver = field('receiver');
611
+ } else if (language === 'shell') {
612
+ nameNode = field('name');
613
+ }
614
+ return { callee, nameNode, receiver };
615
+ }
616
+
617
+ function bindingsInTree(imports, requires, language, point) {
618
+ const bindings = [];
619
+ for (const node of imports) {
620
+ const at = point(node.range().start);
621
+ const add = (local, imported = local, extra = {}) => { if (local) bindings.push({ at, local, imported, ...extra }); };
622
+ if (language === 'javascript') {
623
+ if (node.children().some(child => child.kind() === 'type')) continue;
624
+ const clause = node.children().find(child => child.kind() === 'import_clause');
625
+ for (const child of clause?.children() || []) {
626
+ if (child.kind() === 'identifier') add(child.text(), 'default');
627
+ else if (child.kind() === 'namespace_import') add(child.children().find(entry => entry.kind() === 'identifier')?.text(), '*', { namespace: true });
628
+ else if (child.kind() === 'named_imports') {
629
+ for (const specifier of child.children().filter(entry => entry.kind() === 'import_specifier')) {
630
+ if (specifier.children().some(entry => entry.kind() === 'type')) continue;
631
+ const name = safeField(specifier, 'name')?.text();
632
+ add(safeField(specifier, 'alias')?.text() || name, name);
633
+ }
634
+ }
635
+ }
636
+ } else if (language === 'python') {
637
+ if (node.kind() === 'import_statement') {
638
+ for (const child of node.children()) {
639
+ if (child.kind() === 'dotted_name') add(child.text(), '*', { namespace: true });
640
+ else if (child.kind() === 'aliased_import') add(safeField(child, 'alias')?.text(), '*', { namespace: true });
641
+ }
642
+ } else {
643
+ const module = safeField(node, 'module_name');
644
+ for (const child of node.children()) {
645
+ if (child.id() === module?.id()) continue;
646
+ if (child.kind() === 'wildcard_import') bindings.push({ at, local: '*', imported: '*', wildcard: true });
647
+ else if (child.kind() === 'dotted_name') add(child.text());
648
+ else if (child.kind() === 'aliased_import') add(safeField(child, 'alias')?.text(), safeField(child, 'name')?.text());
649
+ }
650
+ }
651
+ } else if (language === 'go') {
652
+ const target = node.text().match(/["`]([^"`]+)["`]/u)?.[1];
653
+ const alias = safeField(node, 'name')?.text();
654
+ if (target && alias !== '_' && alias !== '.') add(alias || target.split('/').at(-1), '*', { namespace: true });
655
+ } else {
656
+ // Java/Kotlin/Rust/PHP name the imported type or function last.
657
+ const text = node.text().replace(/\s+/gu, ' ');
658
+ const alias = text.match(/\bas\s+([A-Za-z_][\w]*)/u)?.[1];
659
+ for (const group of text.match(/\{([^}]*)\}/u)?.[1].split(',') || [text.replace(/\bas\s+[A-Za-z_]\w*/u, '')]) {
660
+ const name = group.trim().replace(/[;{}]/gu, '').split(/::|\\|\./u).at(-1)?.trim().split(/\s+/u).at(-1);
661
+ if (name && name !== '*' && /^[A-Za-z_]\w*$/u.test(name)) add(alias && !group.includes('{') ? alias : name, name);
662
+ }
663
+ }
664
+ }
665
+ // JavaScript `const name = require(...)` and `const { a, b: c } = require(...)`.
666
+ for (const node of requires) {
667
+ if (node.parent()?.kind() !== 'variable_declarator') continue;
668
+ const at = point(node.range().start);
669
+ const name = safeField(node.parent(), 'name');
670
+ if (name?.kind() === 'identifier') bindings.push({ at, local: name.text(), imported: '*', namespace: true });
671
+ else if (name?.kind() === 'object_pattern') {
672
+ for (const entry of name.children()) {
673
+ if (entry.kind() === 'shorthand_property_identifier_pattern') bindings.push({ at, local: entry.text(), imported: entry.text() });
674
+ else if (entry.kind() === 'pair_pattern') bindings.push({ at, local: safeField(entry, 'value')?.text(), imported: safeField(entry, 'key')?.text() });
675
+ }
676
+ }
677
+ }
678
+ return bindings.filter(entry => entry.local && entry.imported);
679
+ }
680
+
681
+ function callSitesInTree(root, { language, parser, lineOffset = 0, columnOffset = 0, source }) {
682
+ const definition = CALL_SITES[language];
683
+ const point = value => ({ line: value.line + lineOffset + 1, column: value.column + 1 + (value.line === 0 ? columnOffset : 0) });
684
+ // One traversal finds every node of interest; the four roles use disjoint kinds.
685
+ const roles = new Map([
686
+ ...Object.keys(definition.functions).map(kind => [kind, 'functions']), ...definition.calls.map(kind => [kind, 'calls']),
687
+ ...definition.containers.map(kind => [kind, 'containers']), ...(CALL_IMPORT_KINDS[language] || []).map(kind => [kind, 'imports']),
688
+ ]);
689
+ const found = { functions: [], calls: [], containers: [], imports: [] };
690
+ for (const node of findKinds(root, parser, [...roles.keys()])) {
691
+ const kind = node.kind();
692
+ found[roles.get(kind)].push({ node, kind });
693
+ }
694
+ const functions = found.functions.map(({ node, kind }) => {
695
+ const range = node.range();
696
+ return { node, kind, startIndex: range.start.index, endIndex: range.end.index, start: point(canonicalStart(node)), end: point(range.end) };
697
+ }).sort((left, right) => left.startIndex - right.startIndex || right.endIndex - left.endIndex);
698
+ const containers = found.containers.map(({ node }) => {
699
+ const range = node.range();
700
+ return { name: callContainerName(node, language), startIndex: range.start.index, endIndex: range.end.index };
701
+ }).sort((left, right) => left.startIndex - right.startIndex);
702
+ // Syntax nests, so one sweep in start order finds each innermost owner.
703
+ const innermost = (items, owners) => {
704
+ const result = new Map(); const open = []; let next = 0;
705
+ for (const item of [...items].sort((left, right) => left.startIndex - right.startIndex)) {
706
+ while (next < owners.length && owners[next].startIndex <= item.startIndex) {
707
+ while (open.length && open.at(-1).endIndex <= owners[next].startIndex) open.pop();
708
+ open.push(owners[next++]);
709
+ }
710
+ while (open.length && open.at(-1).endIndex <= item.startIndex) open.pop();
711
+ let owner = open.at(-1);
712
+ if (owner === item) owner = open.at(-2);
713
+ result.set(item, owner || null);
714
+ }
715
+ return result;
716
+ };
717
+ const parents = innermost(functions, functions);
718
+ const owners = innermost(functions, containers);
719
+ const records = functions.map((entry, index) => {
720
+ entry.index = index;
721
+ const base = definition.functions[entry.kind];
722
+ const parent = parents.get(entry);
723
+ const owner = owners.get(entry);
724
+ // A class member is directly inside its class, not inside a local function.
725
+ const member = owner && (!parent || owner.startIndex > parent.startIndex) ? owner : null;
726
+ const objectOwner = language === 'javascript' ? objectOwnerName(entry.node) : null;
727
+ // Only JavaScript names an anonymous callable after the binding that holds it.
728
+ let name = '';
729
+ if (language === 'javascript' || (base !== 'arrow' && base !== 'closure')) { try { name = declaredName(entry.node); } catch { name = ''; } }
730
+ const container = objectOwner || member?.name || owner?.name || (entry.kind === 'method_declaration' && language === 'go' ? containerName(entry.node, language) : null);
731
+ let kind = base;
732
+ if (base === 'function' || base === 'method') kind = (member || objectOwner) && CONSTRUCTOR_NAMES.has(name) ? 'constructor' : base === 'method' || member ? 'method' : 'function';
733
+ const record = { name, kind, start: entry.start, end: entry.end, parent: parent ? parent.index : -1 };
734
+ if (container) record.container = container;
735
+ if (language === 'javascript' && entry.node.parent()?.kind() === 'export_statement' && entry.node.parent().children().some(child => child.kind() === 'default')) record.default = true;
736
+ return record;
737
+ });
738
+ const requires = [];
739
+ const calls = found.calls.map(({ node }) => {
740
+ const range = node.range();
741
+ const { callee, nameNode, receiver } = calleeParts(node, language);
742
+ const nameRange = (nameNode || callee || node).range();
743
+ const receiverRange = receiver?.range();
744
+ const calleeStart = (receiverRange || (callee || nameNode || node).range()).start.index;
745
+ const name = nameNode ? nameNode.text() : null;
746
+ const record = { name, callee: source.slice(calleeStart, Math.max(calleeStart, nameRange.end.index)).replace(/\s+/gu, ' ').slice(0, 120), at: point((nameNode ? nameRange : range).start), start: point(range.start), end: point(range.end), startIndex: range.start.index };
747
+ if (receiver) {
748
+ record.receiver = source.slice(receiverRange.start.index, receiverRange.end.index).replace(/\s+/gu, ' ').slice(0, 120);
749
+ const kind = receiver.kind();
750
+ if (LITERAL_RECEIVER.test(kind) && !PROJECT_LITERAL.has(kind)) record.literal = true;
751
+ } else if (language === 'javascript' && name === 'require' && nameNode === callee) requires.push(node);
752
+ return record;
753
+ });
754
+ const callers = innermost(calls, functions);
755
+ for (const call of calls) { call.from = callers.get(call)?.index ?? -1; delete call.startIndex; }
756
+ return { functions: records, calls, bindings: bindingsInTree(found.imports.map(({ node }) => node), requires, language, point) };
757
+ }
758
+
759
+ // Identical content parsed by a different grammar, or admitted only by its
760
+ // shebang, is a different extraction.
761
+ function extractionVariant(filePath, indexer) {
762
+ if (indexer === 'javascript') return filePath.endsWith('.vue') ? 'vue' : javascriptParser(filePath);
763
+ if (indexer === 'cpp') return cFamilyParser(filePath);
764
+ return hasSupportedExtension(filePath, DEFINITIONS[indexer]) ? indexer : `${indexer}+shebang`;
765
+ }
766
+
767
+ function moduleEnd(source) {
768
+ const lines = source.split('\n');
769
+ return { line: lines.length, column: lines.at(-1).length + 1 };
770
+ }
771
+
483
772
  function hasSupportedExtension(filePath, definition) {
484
773
  const normalized = filePath.toLowerCase();
485
774
  return definition.extensions.some((extension) => normalized.endsWith(extension));
@@ -499,8 +788,8 @@ function fileRole(filePath) {
499
788
  : 'source';
500
789
  }
501
790
 
502
- function syntaxDiagnostics(root, filePath, lineOffset) {
503
- return root.findAll({ rule: { kind: 'ERROR' } }).slice(0, 20).map((node) => ({
791
+ function syntaxDiagnostics(nodes, filePath, lineOffset) {
792
+ return nodes.get('ERROR').slice(0, 20).map((node) => ({
504
793
  code: 'CODE_INDEX_PARSE_RECOVERED',
505
794
  message: 'The parser recovered from invalid or unsupported syntax.',
506
795
  path: filePath,
@@ -512,10 +801,15 @@ function createIndexer(id) {
512
801
  const definition = DEFINITIONS[id];
513
802
  return {
514
803
  id,
515
- async extract({ files, projectRoot, parseAsync, parserErrors = {} }) {
804
+ async extract({ files, projectRoot, parse, parserErrors = {}, callSites = new Set() }) {
516
805
  const indexedFiles = [];
517
806
  const diagnostics = [];
807
+ let parsed = 0;
518
808
  for (const file of files.filter((candidate) => isCandidate(candidate, definition))) {
809
+ // A syntax tree lives in native memory, which V8 cannot see, until every
810
+ // node object that refers to it is collected. The worker exposes gc so
811
+ // that memory stays bounded by recently parsed source.
812
+ if (globalThis.gc && parsed > COLLECT_AFTER_CHARACTERS) { globalThis.gc(); parsed = 0; }
519
813
  let source;
520
814
  try {
521
815
  source = await readFile(path.join(projectRoot, file.path), 'utf8');
@@ -543,26 +837,38 @@ function createIndexer(id) {
543
837
  const functions = [];
544
838
  const symbols = [];
545
839
  const dependencies = [];
840
+ const sites = callSites.has(file.path) ? { functions: [], calls: [], bindings: [], moduleEnd: moduleEnd(source) } : null;
546
841
  for (const unit of scriptUnits(source, file.path, definition)) {
547
842
  try {
548
843
  if (parserErrors[unit.parser]) {
549
844
  diagnostics.push({ ...parserErrors[unit.parser], path: file.path });
550
845
  continue;
551
846
  }
552
- const parsed = await parseAsync(unit.parser, unit.source);
847
+ const parsed = await parse(unit.parser, unit.source);
553
848
  const root = parsed.root();
554
849
  const pythonExports = id === 'python' ? pythonExportedNames(root) : null;
555
850
  if (pythonExports?.partial) diagnostics.push({ code: 'CODE_INDEX_PYTHON_EXPORTS_PARTIAL', path: file.path, message: 'Python __all__ is dynamic or conditional. Public naming is shown; runtime exports remain unresolved.' });
556
- functions.push(...functionsInTree(root, definition, {
851
+ // One traversal finds every node the readers below use, by kind in document order.
852
+ const nodes = new Map([...definition.kinds, ...DECLARATION_KINDS[id], ...DEPENDENCY_KINDS[id], 'ERROR'].map(kind => [kind, []]));
853
+ for (const node of findKinds(root, unit.parser, [...nodes.keys()], id === 'javascript' ? DEPENDENCY_CALLS : {})) nodes.get(node.kind()).push(node);
854
+ functions.push(...functionsInTree(nodes, definition, {
557
855
  language: id,
558
856
  lineOffset: unit.lineOffset,
559
857
  columnOffset: unit.columnOffset,
560
858
  source: unit.source,
561
859
  pythonExports,
562
860
  }));
563
- symbols.push(...declarationsInTree(root, id, unit.source, unit.lineOffset, unit.columnOffset, pythonExports));
564
- dependencies.push(...dependencyReferences(root, id, unit.source, unit.lineOffset, unit.columnOffset));
565
- diagnostics.push(...syntaxDiagnostics(root, file.path, unit.lineOffset));
861
+ symbols.push(...declarationsInTree(nodes, id, unit.source, unit.lineOffset, unit.columnOffset, pythonExports));
862
+ dependencies.push(...dependencyReferences(nodes, id, unit.source, unit.lineOffset, unit.columnOffset));
863
+ diagnostics.push(...syntaxDiagnostics(nodes, file.path, unit.lineOffset));
864
+ if (sites) {
865
+ // Several Vue script blocks share one file-level function list.
866
+ const unitSites = callSitesInTree(root, { language: id, parser: unit.parser, lineOffset: unit.lineOffset, columnOffset: unit.columnOffset, source: unit.source });
867
+ const offset = sites.functions.length;
868
+ sites.functions.push(...unitSites.functions.map(entry => ({ ...entry, parent: entry.parent < 0 ? -1 : entry.parent + offset })));
869
+ sites.calls.push(...unitSites.calls.map(entry => ({ ...entry, from: entry.from < 0 ? -1 : entry.from + offset })));
870
+ sites.bindings.push(...unitSites.bindings);
871
+ }
566
872
  } catch (error) {
567
873
  if (error.code === 'PROCESS_ABORTED') throw error;
568
874
  diagnostics.push({
@@ -570,6 +876,8 @@ function createIndexer(id) {
570
876
  message: error.message,
571
877
  path: file.path,
572
878
  });
879
+ } finally {
880
+ lineages.clear();
573
881
  }
574
882
  }
575
883
  // Vue SFCs expose a component as their default public interface even
@@ -578,6 +886,7 @@ function createIndexer(id) {
578
886
  const lines = source.split('\n');
579
887
  symbols.push({ name: 'Component', qualifiedName: 'default component', exportName: 'default', kind: 'component', visibility: 'public', container: null, line: 1, column: 1, start: { line: 1, column: 1 }, end: { line: lines.length, column: lines.at(-1).length + 1 } });
580
888
  }
889
+ parsed += source.length;
581
890
  indexedFiles.push({
582
891
  path: file.path,
583
892
  language: id,
@@ -589,6 +898,7 @@ function createIndexer(id) {
589
898
  functions,
590
899
  symbols,
591
900
  dependencies,
901
+ ...(sites ? { callSites: sites } : {}),
592
902
  });
593
903
  }
594
904
  return { files: indexedFiles, diagnostics };
@@ -596,35 +906,101 @@ function createIndexer(id) {
596
906
  };
597
907
  }
598
908
 
909
+ /** The files each selected indexer parses, with the grammar variant that keys their cached extractions. */
910
+ export function extractionCandidates(indexers, files) {
911
+ return indexers.filter(id => Object.hasOwn(DEFINITIONS, id)).flatMap(id => files.filter(file => isCandidate(file, DEFINITIONS[id]))
912
+ .map(file => ({ path: file.path, language: id, variant: extractionVariant(file.path, id), hash: file.hash, mode: file.mode })));
913
+ }
914
+
915
+ // A cached extraction is reused only by the same extractor code, grammars and
916
+ // runtime, so changing any of them extracts every file again.
917
+ let identity = null;
918
+ export function extractorIdentity() {
919
+ identity ??= Promise.all(['ast-grep.js', 'parsers.js', 'worker.js'].map(name => readFile(new URL(`./${name}`, import.meta.url))))
920
+ .then(sources => {
921
+ const hash = createHash('sha256').update(`${createRequire(import.meta.url)('@ast-grep/napi/package.json').version}\n${process.version}\n`);
922
+ for (const source of sources) hash.update(source);
923
+ return hash.digest('hex').slice(0, 16);
924
+ });
925
+ return identity;
926
+ }
927
+
599
928
  export const astGrepCodeIndexers = new Map(
600
929
  Object.keys(DEFINITIONS).map((id) => [id, createIndexer(id)]),
601
930
  );
602
931
 
932
+ // Only outcomes that follow from the content alone are cached. A failed read,
933
+ // an unavailable parser or a failed parse is tried again by the next run.
934
+ const CONTENT_DIAGNOSTICS = new Set(['CODE_INDEX_PARSE_RECOVERED', 'CODE_INDEX_PYTHON_EXPORTS_PARTIAL']);
935
+
603
936
  // ast-grep permits one registration per process. A finite indexing child loads
604
937
  // this request's languages once and releases every native parser when it exits.
605
- export async function extractCodeIndexes({ indexers, files, projectRoot, signal }) {
938
+ // With a cache root, it parses only files whose extraction is not cached; the
939
+ // result is the same as extracting every file.
940
+ export async function extractCodeIndexes({ indexers, files, projectRoot, signal, callSites = [], cacheRoot = null }) {
606
941
  const selected = indexers.filter(id => astGrepCodeIndexers.has(id));
607
942
  const diagnostics = indexers.filter(id => !astGrepCodeIndexers.has(id)).map(id => ({
608
943
  code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.`,
609
944
  }));
610
- const languages = selected.flatMap(id => files.filter(file => isCandidate(file, DEFINITIONS[id]))
611
- .map(file => typeof DEFINITIONS[id].parser === 'function'
612
- ? DEFINITIONS[id].parser(file.path) : DEFINITIONS[id].parser));
613
- if (languages.length === 0) return { contributions: [], diagnostics };
614
- const { registrations, errors } = await prepareCodeParsers(languages);
615
- let contributions;
616
- try {
617
- const result = await runProcess(process.execPath, [fileURLToPath(new URL('./worker.js', import.meta.url))], {
618
- input: JSON.stringify({ indexers: selected, files, projectRoot, registrations, errors }),
619
- signal: signal || parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
620
- });
621
- contributions = JSON.parse(result.stdout.toString('utf8'));
622
- } catch (error) {
623
- if (error.code === 'PROCESS_ABORTED') throw error;
624
- return { contributions: [], diagnostics: [...diagnostics, {
625
- ...asDiagnostic(error), code: 'CODE_INDEXER_FAILED',
626
- }] };
945
+ const version = cacheRoot ? await extractorIdentity() : null;
946
+ const candidates = new Map(selected.map(id => [id, extractionCandidates([id], files)
947
+ .map(entry => ({ ...entry, location: cacheRoot ? extractionCachePath(cacheRoot, { version, ...entry }) : null }))]));
948
+ const cacheLocations = [...candidates.values()].flat().map(entry => entry.location).filter(Boolean);
949
+ if ([...candidates.values()].every(entries => entries.length === 0)) return { contributions: [], diagnostics, cacheLocations };
950
+ const cached = new Map();
951
+ for (const [id, entries] of candidates) cached.set(id, cacheRoot ? await loadExtractions(cacheRoot, entries) : { values: new Map(), misses: entries });
952
+ const wanted = new Set([...[...cached.values()].flatMap(value => value.misses.map(entry => entry.path)), ...callSites]);
953
+ let extracted = [];
954
+ if (wanted.size) {
955
+ const sent = files.filter(file => wanted.has(file.path));
956
+ const languages = selected.flatMap(id => sent.filter(file => isCandidate(file, DEFINITIONS[id]))
957
+ .map(file => typeof DEFINITIONS[id].parser === 'function'
958
+ ? DEFINITIONS[id].parser(file.path) : DEFINITIONS[id].parser));
959
+ const { registrations, errors } = await prepareCodeParsers(languages);
960
+ try {
961
+ const result = await runProcess(process.execPath, ['--expose-gc', fileURLToPath(new URL('./worker.js', import.meta.url))], {
962
+ input: JSON.stringify({ indexers: selected, files: sent, projectRoot, registrations, errors, callSites }),
963
+ signal: signal || parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
964
+ });
965
+ extracted = JSON.parse(result.stdout.toString('utf8'));
966
+ } catch (error) {
967
+ if (error.code === 'PROCESS_ABORTED') throw error;
968
+ return { contributions: [], diagnostics: [...diagnostics, {
969
+ ...asDiagnostic(error), code: 'CODE_INDEXER_FAILED',
970
+ }], cacheLocations };
971
+ }
627
972
  }
973
+ // Reassemble each indexer's files and per-file diagnostics in candidate order.
974
+ const stored = [];
975
+ const contributions = selected.map((id, index) => {
976
+ const { values, misses } = cached.get(id);
977
+ const missed = new Set(misses.map(entry => entry.path));
978
+ const records = new Map((extracted[index]?.files || []).map(file => [file.path, file]));
979
+ const found = new Map();
980
+ for (const diagnostic of extracted[index]?.diagnostics || []) {
981
+ if (!found.has(diagnostic.path)) found.set(diagnostic.path, []);
982
+ found.get(diagnostic.path).push(diagnostic);
983
+ }
984
+ const contribution = { files: [], diagnostics: [], extractor: id };
985
+ for (const entry of candidates.get(id)) {
986
+ let record = records.get(entry.path);
987
+ let fileDiagnostics = found.get(entry.path) || [];
988
+ if (!wanted.has(entry.path)) {
989
+ const value = values.get(entry.path);
990
+ const { bytes, lines, functions, symbols, dependencies } = value.record || {};
991
+ record = value.record && { path: entry.path, language: id, role: fileRole(entry.path), mode: entry.mode, bytes, lines, hash: entry.hash, functions, symbols, dependencies };
992
+ fileDiagnostics = value.diagnostics.map(diagnostic => ({ ...diagnostic, path: entry.path }));
993
+ } else if (cacheRoot && missed.has(entry.path) && fileDiagnostics.every(diagnostic => CONTENT_DIAGNOSTICS.has(diagnostic.code))) {
994
+ const value = record ? { bytes: record.bytes, lines: record.lines, functions: record.functions, symbols: record.symbols, dependencies: record.dependencies } : null;
995
+ // Diagnostics keep their member order; the path is the reader's own.
996
+ stored.push({ location: entry.location, value: { record: value, diagnostics: fileDiagnostics.map(diagnostic => ({ ...diagnostic, path: '' })) } });
997
+ }
998
+ if (record) contribution.files.push(record);
999
+ contribution.diagnostics.push(...fileDiagnostics);
1000
+ }
1001
+ return contribution;
1002
+ });
1003
+ await storeExtractions(stored);
628
1004
  return { contributions, diagnostics: [...diagnostics, ...contributions.flatMap(value =>
629
- value.diagnostics.map(diagnostic => ({ ...diagnostic, extractor: value.extractor }))) ] };
1005
+ value.diagnostics.map(diagnostic => ({ ...diagnostic, extractor: value.extractor }))) ], cacheLocations };
630
1006
  }