genesis-compiler 1.10.5 → 1.10.7
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.
- package/README.md +21 -3
- package/docs/templates.md +13 -4
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/src/cli.js +22 -7
- package/src/index/architecture-observations.js +94 -7
- package/src/index/architecture.js +21 -2
- package/src/index/call-web.js +419 -0
- package/src/index/code-index.js +154 -16
- package/src/index/code-indexers/ast-grep.js +453 -77
- package/src/index/code-indexers/npm-packages.js +4 -3
- package/src/index/code-indexers/worker.js +5 -2
- package/src/index/entry-points.js +269 -0
- package/src/index/extraction-cache.js +81 -0
- package/src/index/project-files.js +7 -3
- package/src/index/project-inspection.js +5 -2
- package/src/index/template-catalog.js +2 -2
- package/src/index/template-project.js +7 -8
- package/src/index/template-source.js +6 -3
- package/src/index/utils.js +34 -0
- package/src/index.js +3 -1
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
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
|
|
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
|
|
267
|
-
return
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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(
|
|
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
|
|
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
|
|
398
|
-
if (
|
|
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 =
|
|
424
|
+
const nestedFunction = kinds.includes('function_definition');
|
|
402
425
|
if (nestedFunction) return 'internal';
|
|
403
|
-
const classMember =
|
|
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
|
|
421
|
-
!(language === 'python' &&
|
|
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
|
|
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(
|
|
471
|
+
function functionsInTree(nodes, definition, { language, lineOffset, columnOffset, source, pythonExports = null }) {
|
|
448
472
|
const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : pythonExports?.names;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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 =
|
|
459
|
-
const
|
|
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' &&
|
|
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(
|
|
477
|
-
generator: /\bfunction\s*\*|\byield\b/u.test(
|
|
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(
|
|
503
|
-
return
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(
|
|
564
|
-
dependencies.push(...dependencyReferences(
|
|
565
|
-
diagnostics.push(...syntaxDiagnostics(
|
|
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
|
-
|
|
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
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
}
|