@optave/codegraph 3.9.3 → 3.9.4
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 +8 -8
- package/dist/ast-analysis/visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitor.js +14 -0
- package/dist/ast-analysis/visitor.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +15 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +7 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +92 -48
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +67 -6
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +2 -2
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +51 -10
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +10 -4
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/run-analyses.js +5 -20
- package/dist/domain/graph/builder/stages/run-analyses.js.map +1 -1
- package/dist/extractors/javascript.js +120 -0
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +14 -1
- package/dist/features/structure.js.map +1 -1
- package/package.json +7 -7
- package/src/ast-analysis/visitor.ts +15 -0
- package/src/domain/graph/builder/context.ts +17 -0
- package/src/domain/graph/builder/pipeline.ts +93 -46
- package/src/domain/graph/builder/stages/build-edges.ts +80 -6
- package/src/domain/graph/builder/stages/build-structure.ts +2 -2
- package/src/domain/graph/builder/stages/detect-changes.ts +61 -12
- package/src/domain/graph/builder/stages/finalize.ts +11 -4
- package/src/domain/graph/builder/stages/run-analyses.ts +5 -26
- package/src/extractors/javascript.ts +142 -0
- package/src/features/structure.ts +17 -1
|
@@ -274,14 +274,17 @@ function dispatchQueryMatch(
|
|
|
274
274
|
name: c.callfn_name!.text,
|
|
275
275
|
line: c.callfn_node.startPosition.row + 1,
|
|
276
276
|
});
|
|
277
|
+
calls.push(...extractCallbackReferenceCalls(c.callfn_node));
|
|
277
278
|
} else if (c.callmem_node) {
|
|
278
279
|
const callInfo = extractCallInfo(c.callmem_fn!, c.callmem_node);
|
|
279
280
|
if (callInfo) calls.push(callInfo);
|
|
280
281
|
const cbDef = extractCallbackDefinition(c.callmem_node, c.callmem_fn);
|
|
281
282
|
if (cbDef) definitions.push(cbDef);
|
|
283
|
+
calls.push(...extractCallbackReferenceCalls(c.callmem_node));
|
|
282
284
|
} else if (c.callsub_node) {
|
|
283
285
|
const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
|
|
284
286
|
if (callInfo) calls.push(callInfo);
|
|
287
|
+
calls.push(...extractCallbackReferenceCalls(c.callsub_node));
|
|
285
288
|
} else if (c.newfn_node) {
|
|
286
289
|
calls.push({
|
|
287
290
|
name: c.newfn_name!.text,
|
|
@@ -321,6 +324,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
|
|
|
321
324
|
// Extract typeMap from type annotations and new expressions
|
|
322
325
|
extractTypeMapWalk(tree.rootNode, typeMap);
|
|
323
326
|
|
|
327
|
+
// Extract definitions from destructured bindings (query patterns don't match object_pattern)
|
|
328
|
+
extractDestructuredBindingsWalk(tree.rootNode, definitions);
|
|
329
|
+
|
|
324
330
|
return { definitions, calls, imports, classes, exports: exps, typeMap };
|
|
325
331
|
}
|
|
326
332
|
|
|
@@ -334,6 +340,20 @@ const FUNCTION_SCOPE_TYPES = new Set([
|
|
|
334
340
|
'generator_function',
|
|
335
341
|
]);
|
|
336
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Return true when `node` has an ancestor whose type is in FUNCTION_SCOPE_TYPES.
|
|
345
|
+
* Used by the walk path to skip declarations inside function bodies, matching
|
|
346
|
+
* the query path's top-down FUNCTION_SCOPE_TYPES filter.
|
|
347
|
+
*/
|
|
348
|
+
function hasFunctionScopeAncestor(node: TreeSitterNode): boolean {
|
|
349
|
+
let p: TreeSitterNode | null = node.parent ?? null;
|
|
350
|
+
while (p) {
|
|
351
|
+
if (FUNCTION_SCOPE_TYPES.has(p.type)) return true;
|
|
352
|
+
p = p.parent ?? null;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
337
357
|
/**
|
|
338
358
|
* Recursively walk the AST to extract `const x = <literal>` as constants.
|
|
339
359
|
* Skips nodes inside function scopes so only file-level / block-level constants
|
|
@@ -363,6 +383,48 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
|
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Walk the AST to find destructured const bindings (query patterns don't match object_pattern).
|
|
388
|
+
* e.g. `const { handleToken, checkPermissions } = initAuth(config)`
|
|
389
|
+
*/
|
|
390
|
+
function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Definition[]): void {
|
|
391
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
392
|
+
const child = node.child(i);
|
|
393
|
+
if (!child) continue;
|
|
394
|
+
if (FUNCTION_SCOPE_TYPES.has(child.type)) continue;
|
|
395
|
+
|
|
396
|
+
let declNode = child;
|
|
397
|
+
if (child.type === 'export_statement') {
|
|
398
|
+
const inner = child.childForFieldName('declaration');
|
|
399
|
+
if (inner) declNode = inner;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const t = declNode.type;
|
|
403
|
+
if (
|
|
404
|
+
(t === 'lexical_declaration' || t === 'variable_declaration') &&
|
|
405
|
+
declNode.text.startsWith('const ')
|
|
406
|
+
) {
|
|
407
|
+
for (let j = 0; j < declNode.childCount; j++) {
|
|
408
|
+
const declarator = declNode.child(j);
|
|
409
|
+
if (!declarator || declarator.type !== 'variable_declarator') continue;
|
|
410
|
+
const nameN = declarator.childForFieldName('name');
|
|
411
|
+
if (nameN && nameN.type === 'object_pattern') {
|
|
412
|
+
extractDestructuredBindings(
|
|
413
|
+
nameN,
|
|
414
|
+
declNode.startPosition.row + 1,
|
|
415
|
+
nodeEndLine(declNode),
|
|
416
|
+
definitions,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (child.type !== 'export_statement') {
|
|
423
|
+
extractDestructuredBindingsWalk(child, definitions);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
366
428
|
/** Extract constant definitions from a `const` declaration node. */
|
|
367
429
|
function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definition[]): void {
|
|
368
430
|
const t = declNode.type;
|
|
@@ -637,6 +699,39 @@ function handleTypeAliasDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
637
699
|
}
|
|
638
700
|
}
|
|
639
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Extract definitions from destructured object bindings.
|
|
704
|
+
* `const { handleToken, checkPermissions } = initAuth(...)` creates definitions
|
|
705
|
+
* for handleToken and checkPermissions so they can be resolved as call targets.
|
|
706
|
+
*/
|
|
707
|
+
function extractDestructuredBindings(
|
|
708
|
+
pattern: TreeSitterNode,
|
|
709
|
+
line: number,
|
|
710
|
+
endLine: number,
|
|
711
|
+
definitions: Definition[],
|
|
712
|
+
): void {
|
|
713
|
+
for (let i = 0; i < pattern.childCount; i++) {
|
|
714
|
+
const child = pattern.child(i);
|
|
715
|
+
if (!child) continue;
|
|
716
|
+
if (
|
|
717
|
+
child.type === 'shorthand_property_identifier_pattern' ||
|
|
718
|
+
child.type === 'shorthand_property_identifier'
|
|
719
|
+
) {
|
|
720
|
+
// { handleToken } — shorthand binding
|
|
721
|
+
definitions.push({ name: child.text, kind: 'function', line, endLine });
|
|
722
|
+
} else if (child.type === 'pair_pattern' || child.type === 'pair') {
|
|
723
|
+
// { original: renamed } — renamed binding, use the local alias
|
|
724
|
+
const value = child.childForFieldName('value');
|
|
725
|
+
if (
|
|
726
|
+
value &&
|
|
727
|
+
(value.type === 'identifier' || value.type === 'shorthand_property_identifier_pattern')
|
|
728
|
+
) {
|
|
729
|
+
definitions.push({ name: value.text, kind: 'function', line, endLine });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
640
735
|
function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
641
736
|
const isConst = node.text.startsWith('const ');
|
|
642
737
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -667,6 +762,20 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
667
762
|
line: node.startPosition.row + 1,
|
|
668
763
|
endLine: nodeEndLine(node),
|
|
669
764
|
});
|
|
765
|
+
} else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
|
|
766
|
+
// Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
|
|
767
|
+
// Each destructured property becomes a function definition so it can be
|
|
768
|
+
// resolved when passed as a callback (e.g. router.use(handleToken)).
|
|
769
|
+
// Restricted to const to avoid creating spurious definitions for
|
|
770
|
+
// transient let/var destructuring (e.g. let { userId } = parseRequest(req)).
|
|
771
|
+
// Scope guard mirrors extractDestructuredBindingsWalk (query path) and
|
|
772
|
+
// handle_var_decl (Rust path) — skips bindings inside function bodies.
|
|
773
|
+
extractDestructuredBindings(
|
|
774
|
+
nameN,
|
|
775
|
+
node.startPosition.row + 1,
|
|
776
|
+
nodeEndLine(node),
|
|
777
|
+
ctx.definitions,
|
|
778
|
+
);
|
|
670
779
|
}
|
|
671
780
|
}
|
|
672
781
|
}
|
|
@@ -715,6 +824,7 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
|
|
|
715
824
|
const cbDef = extractCallbackDefinition(node, fn);
|
|
716
825
|
if (cbDef) ctx.definitions.push(cbDef);
|
|
717
826
|
}
|
|
827
|
+
ctx.calls.push(...extractCallbackReferenceCalls(node));
|
|
718
828
|
}
|
|
719
829
|
}
|
|
720
830
|
|
|
@@ -1167,6 +1277,38 @@ function extractSubscriptCallInfo(fn: TreeSitterNode, callNode: TreeSitterNode):
|
|
|
1167
1277
|
return null;
|
|
1168
1278
|
}
|
|
1169
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* Extract Call entries for named function references passed as arguments.
|
|
1282
|
+
* e.g. `router.use(handleToken, checkAuth)` yields calls to handleToken and checkAuth.
|
|
1283
|
+
* `app.use(auth.validate)` yields a call to validate with receiver auth.
|
|
1284
|
+
* Skips literals, objects, arrays, anonymous functions, and call expressions (already handled).
|
|
1285
|
+
*/
|
|
1286
|
+
function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
|
|
1287
|
+
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
|
|
1288
|
+
if (!args) return [];
|
|
1289
|
+
|
|
1290
|
+
const result: Call[] = [];
|
|
1291
|
+
const callLine = callNode.startPosition.row + 1;
|
|
1292
|
+
|
|
1293
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
1294
|
+
const child = args.child(i);
|
|
1295
|
+
if (!child) continue;
|
|
1296
|
+
|
|
1297
|
+
if (child.type === 'identifier') {
|
|
1298
|
+
result.push({ name: child.text, line: callLine, dynamic: true });
|
|
1299
|
+
} else if (child.type === 'member_expression') {
|
|
1300
|
+
const prop = child.childForFieldName('property');
|
|
1301
|
+
const obj = child.childForFieldName('object');
|
|
1302
|
+
if (prop) {
|
|
1303
|
+
const receiver = extractReceiverName(obj);
|
|
1304
|
+
result.push({ name: prop.text, line: callLine, dynamic: true, receiver });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1170
1312
|
function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null {
|
|
1171
1313
|
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1172
1314
|
const child = argsNode.child(i);
|
|
@@ -166,6 +166,22 @@ function computeFileMetrics(
|
|
|
166
166
|
fanOutMap: Map<string, number>,
|
|
167
167
|
): void {
|
|
168
168
|
db.transaction(() => {
|
|
169
|
+
// Batch-load import counts per file (distinct imported files,
|
|
170
|
+
// matching the fast-path semantics in updateChangedFileMetrics).
|
|
171
|
+
// Runs inside the transaction for parity with the Rust path.
|
|
172
|
+
const importCountMap = new Map<string, number>();
|
|
173
|
+
for (const row of db
|
|
174
|
+
.prepare(
|
|
175
|
+
`SELECT n1.file AS src, COUNT(DISTINCT n2.file) AS cnt FROM edges e
|
|
176
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
177
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
178
|
+
WHERE e.kind = 'imports'
|
|
179
|
+
GROUP BY n1.file`,
|
|
180
|
+
)
|
|
181
|
+
.all() as { src: string; cnt: number }[]) {
|
|
182
|
+
importCountMap.set(row.src, row.cnt);
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
for (const [relPath, symbols] of fileSymbols) {
|
|
170
186
|
const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
171
187
|
if (!fileRow) continue;
|
|
@@ -180,7 +196,7 @@ function computeFileMetrics(
|
|
|
180
196
|
symbolCount++;
|
|
181
197
|
}
|
|
182
198
|
}
|
|
183
|
-
const importCount =
|
|
199
|
+
const importCount = importCountMap.get(relPath) || 0;
|
|
184
200
|
const exportCount = symbols.exports.length;
|
|
185
201
|
const fanIn = fanInMap.get(relPath) || 0;
|
|
186
202
|
const fanOut = fanOutMap.get(relPath) || 0;
|