@optave/codegraph 3.11.0 → 3.11.2
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 +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
- package/dist/domain/graph/builder/call-resolver.js +130 -0
- package/dist/domain/graph/builder/call-resolver.js.map +1 -0
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +214 -127
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- 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 +151 -192
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- 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 +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +10 -4
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +149 -52
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/call-resolver.ts +181 -0
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +321 -152
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +229 -275
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +12 -4
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +199 -79
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +1 -1
|
@@ -573,6 +573,90 @@ interface SetupResult {
|
|
|
573
573
|
dataflowVisitor: Visitor | null;
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Build the AST-store visitor for `langId`. Returns `null` when AST is
|
|
578
|
+
* disabled or the language has no AST type map. db-free — passes an empty
|
|
579
|
+
* nodeIdMap. The main thread re-resolves parent node IDs in
|
|
580
|
+
* `features/ast.ts::collectFileAstRows`.
|
|
581
|
+
*/
|
|
582
|
+
function buildAstVisitor(
|
|
583
|
+
langId: string,
|
|
584
|
+
defs: ExtractorOutput['definitions'],
|
|
585
|
+
relPath: string,
|
|
586
|
+
enabled: boolean,
|
|
587
|
+
): Visitor | null {
|
|
588
|
+
if (!enabled) return null;
|
|
589
|
+
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
590
|
+
if (!astTypeMap) return null;
|
|
591
|
+
const stringConfig = AST_STRING_CONFIGS.get(langId);
|
|
592
|
+
return createAstStoreVisitor(
|
|
593
|
+
astTypeMap,
|
|
594
|
+
defs,
|
|
595
|
+
relPath,
|
|
596
|
+
new Map<string, number>(),
|
|
597
|
+
stringConfig,
|
|
598
|
+
astStopRecurseKinds(langId),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Build the complexity visitor when enabled, the language has complexity
|
|
604
|
+
* rules, and at least one definition still lacks a `complexity` payload.
|
|
605
|
+
* Side-effect: extends `walkerOpts` with nesting-node types and a
|
|
606
|
+
* `getFunctionName` resolver suitable for this language.
|
|
607
|
+
*/
|
|
608
|
+
function buildComplexityVisitor(
|
|
609
|
+
langId: string,
|
|
610
|
+
defs: ExtractorOutput['definitions'],
|
|
611
|
+
enabled: boolean,
|
|
612
|
+
walkerOpts: WalkOptions,
|
|
613
|
+
): Visitor | null {
|
|
614
|
+
if (!enabled) return null;
|
|
615
|
+
const cRules = COMPLEXITY_RULES.get(langId);
|
|
616
|
+
if (!cRules || !defs.some((d) => hasFuncBody(d) && !d.complexity)) return null;
|
|
617
|
+
|
|
618
|
+
const hRules = HALSTEAD_RULES.get(langId);
|
|
619
|
+
const visitor = createComplexityVisitor(cRules, hRules, { fileLevelWalk: true, langId });
|
|
620
|
+
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
|
|
621
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
622
|
+
walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
|
|
623
|
+
const nameNode = node.childForFieldName('name');
|
|
624
|
+
if (nameNode) return nameNode.text;
|
|
625
|
+
// dfRules shape varies per language; visitor-utils accepts any shape
|
|
626
|
+
if (dfRules) return getFuncName(node, dfRules as any);
|
|
627
|
+
return null;
|
|
628
|
+
};
|
|
629
|
+
return visitor;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Build the CFG visitor when enabled and at least one definition still lacks blocks. */
|
|
633
|
+
function buildCfgVisitor(
|
|
634
|
+
langId: string,
|
|
635
|
+
defs: ExtractorOutput['definitions'],
|
|
636
|
+
enabled: boolean,
|
|
637
|
+
): Visitor | null {
|
|
638
|
+
if (!enabled) return null;
|
|
639
|
+
const cfgRulesForLang = CFG_RULES.get(langId);
|
|
640
|
+
if (!cfgRulesForLang) return null;
|
|
641
|
+
const needsCfg = defs.some(
|
|
642
|
+
(d) => hasFuncBody(d) && d.cfg !== null && !Array.isArray(d.cfg?.blocks),
|
|
643
|
+
);
|
|
644
|
+
if (!needsCfg) return null;
|
|
645
|
+
return createCfgVisitor(cfgRulesForLang);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Build the dataflow visitor when enabled and `symbols.dataflow` is not yet populated. */
|
|
649
|
+
function buildDataflowVisitor(
|
|
650
|
+
langId: string,
|
|
651
|
+
symbols: ExtractorOutput,
|
|
652
|
+
enabled: boolean,
|
|
653
|
+
): Visitor | null {
|
|
654
|
+
if (!enabled) return null;
|
|
655
|
+
const dfRules = DATAFLOW_RULES.get(langId);
|
|
656
|
+
if (!dfRules || symbols.dataflow) return null;
|
|
657
|
+
return createDataflowVisitor(dfRules);
|
|
658
|
+
}
|
|
659
|
+
|
|
576
660
|
function setupVisitorsLocal(
|
|
577
661
|
symbols: ExtractorOutput,
|
|
578
662
|
relPath: string,
|
|
@@ -580,82 +664,161 @@ function setupVisitorsLocal(
|
|
|
580
664
|
opts: WorkerParseRequest['opts'],
|
|
581
665
|
): SetupResult {
|
|
582
666
|
const defs = symbols.definitions || [];
|
|
583
|
-
const visitors: Visitor[] = [];
|
|
584
667
|
const walkerOpts: WalkOptions = {
|
|
585
668
|
functionNodeTypes: new Set<string>(),
|
|
586
669
|
nestingNodeTypes: new Set<string>(),
|
|
587
670
|
getFunctionName: (_node: TreeSitterNode) => null,
|
|
588
671
|
};
|
|
589
672
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const astTypeMap = AST_TYPE_MAPS.get(langId);
|
|
595
|
-
if (astTypeMap) {
|
|
596
|
-
const stringConfig = AST_STRING_CONFIGS.get(langId);
|
|
597
|
-
astVisitor = createAstStoreVisitor(
|
|
598
|
-
astTypeMap,
|
|
599
|
-
defs,
|
|
600
|
-
relPath,
|
|
601
|
-
new Map<string, number>(),
|
|
602
|
-
stringConfig,
|
|
603
|
-
astStopRecurseKinds(langId),
|
|
604
|
-
);
|
|
605
|
-
visitors.push(astVisitor);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
673
|
+
const astVisitor = buildAstVisitor(langId, defs, relPath, !!opts.ast);
|
|
674
|
+
const complexityVisitor = buildComplexityVisitor(langId, defs, !!opts.complexity, walkerOpts);
|
|
675
|
+
const cfgVisitor = buildCfgVisitor(langId, defs, !!opts.cfg);
|
|
676
|
+
const dataflowVisitor = buildDataflowVisitor(langId, symbols, !!opts.dataflow);
|
|
608
677
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const hRules = HALSTEAD_RULES.get(langId);
|
|
615
|
-
complexityVisitor = createComplexityVisitor(cRules, hRules, {
|
|
616
|
-
fileLevelWalk: true,
|
|
617
|
-
langId,
|
|
618
|
-
});
|
|
619
|
-
for (const t of cRules.nestingNodes) walkerOpts.nestingNodeTypes?.add(t);
|
|
620
|
-
const dfRules = DATAFLOW_RULES.get(langId);
|
|
621
|
-
walkerOpts.getFunctionName = (node: TreeSitterNode): string | null => {
|
|
622
|
-
const nameNode = node.childForFieldName('name');
|
|
623
|
-
if (nameNode) return nameNode.text;
|
|
624
|
-
// dfRules shape varies per language; visitor-utils accepts any shape
|
|
625
|
-
if (dfRules) return getFuncName(node, dfRules as any);
|
|
626
|
-
return null;
|
|
627
|
-
};
|
|
628
|
-
visitors.push(complexityVisitor);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
678
|
+
const visitors: Visitor[] = [];
|
|
679
|
+
if (astVisitor) visitors.push(astVisitor);
|
|
680
|
+
if (complexityVisitor) visitors.push(complexityVisitor);
|
|
681
|
+
if (cfgVisitor) visitors.push(cfgVisitor);
|
|
682
|
+
if (dataflowVisitor) visitors.push(dataflowVisitor);
|
|
631
683
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
684
|
+
return { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ── Main parse handler ──────────────────────────────────────────────────────
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Run tree-sitter parse + extractor on `code`. Returns `null` when either
|
|
691
|
+
* step yields no usable output. Throws (for the caller to report back to the
|
|
692
|
+
* pool) only on a hard tree-sitter parse error.
|
|
693
|
+
*/
|
|
694
|
+
function parseAndExtract(
|
|
695
|
+
parser: Parser,
|
|
696
|
+
entry: LanguageRegistryEntry,
|
|
697
|
+
filePath: string,
|
|
698
|
+
code: string,
|
|
699
|
+
): { tree: Tree; symbols: ExtractorOutput } | null {
|
|
700
|
+
let tree: Tree | null;
|
|
701
|
+
try {
|
|
702
|
+
tree = parser.parse(code);
|
|
703
|
+
} catch (e: unknown) {
|
|
704
|
+
// Parse error — report back but keep worker alive.
|
|
705
|
+
throw new Error(`parse failed: ${(e as Error).message}`);
|
|
643
706
|
}
|
|
707
|
+
if (!tree) return null;
|
|
644
708
|
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
709
|
+
// Extractor — on failure, skip file (ok:true, null) to match parser.ts
|
|
710
|
+
// behavior where extractor issues don't crash the build. Dispose the tree
|
|
711
|
+
// before returning null so WASM linear memory doesn't accumulate in the worker.
|
|
712
|
+
let symbols: ExtractorOutput | null;
|
|
713
|
+
try {
|
|
714
|
+
const query = _queries.get(entry.id);
|
|
715
|
+
// tree-sitter's Tree/Query are structurally compatible with
|
|
716
|
+
// TreeSitterTree/TreeSitterQuery at runtime — same cast style as
|
|
717
|
+
// parser.ts::wasmExtractSymbols (parser.ts:789).
|
|
718
|
+
symbols = entry.extractor(tree as any, filePath, query as any) ?? null;
|
|
719
|
+
} catch {
|
|
720
|
+
disposeTree(tree);
|
|
721
|
+
return null;
|
|
653
722
|
}
|
|
723
|
+
if (!symbols) {
|
|
724
|
+
disposeTree(tree);
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
return { tree, symbols };
|
|
728
|
+
}
|
|
654
729
|
|
|
655
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Project the visitor `ast-store` rows into the wire-safe shape returned to
|
|
732
|
+
* the main thread. Strips `file` and `parentNodeId` — both are re-resolved in
|
|
733
|
+
* `features/ast.ts::collectFileAstRows`. Always returns an array (even empty)
|
|
734
|
+
* so `engine.ts::fileNeedsWasmTree` doesn't treat the file as un-walked and
|
|
735
|
+
* trigger a full ensureWasmTrees re-parse (#1036).
|
|
736
|
+
*/
|
|
737
|
+
function projectAstNodes(results: WalkResults): SerializedExtractorOutput['astNodes'] {
|
|
738
|
+
const astRows = (results['ast-store'] || []) as Array<{
|
|
739
|
+
line: number;
|
|
740
|
+
kind: string;
|
|
741
|
+
name: string | null | undefined;
|
|
742
|
+
text: string | null;
|
|
743
|
+
receiver: string | null;
|
|
744
|
+
file?: string;
|
|
745
|
+
parentNodeId?: number | null;
|
|
746
|
+
}>;
|
|
747
|
+
return astRows.map((n) => ({
|
|
748
|
+
line: n.line,
|
|
749
|
+
kind: n.kind,
|
|
750
|
+
name: n.name ?? '',
|
|
751
|
+
text: n.text ?? undefined,
|
|
752
|
+
receiver: n.receiver ?? undefined,
|
|
753
|
+
}));
|
|
656
754
|
}
|
|
657
755
|
|
|
658
|
-
|
|
756
|
+
/**
|
|
757
|
+
* Run the configured visitor walk over `tree.rootNode` and apply each
|
|
758
|
+
* visitor's results back onto `symbols`. Returns the serialized astNodes
|
|
759
|
+
* (or `undefined` when AST is disabled / no rows produced).
|
|
760
|
+
*
|
|
761
|
+
* Mirrors engine.ts:791-829. Runs BEFORE `tree.delete()` because
|
|
762
|
+
* storeComplexityResults / storeCfgResults read `funcNode` off live nodes.
|
|
763
|
+
*/
|
|
764
|
+
function runVisitorWalk(
|
|
765
|
+
tree: Tree,
|
|
766
|
+
symbols: ExtractorOutput,
|
|
767
|
+
langId: string,
|
|
768
|
+
setup: SetupResult,
|
|
769
|
+
): SerializedExtractorOutput['astNodes'] {
|
|
770
|
+
if (setup.visitors.length === 0) return undefined;
|
|
771
|
+
// rootNode shape matches TreeSitterNode at runtime — same cast as parser.ts:789.
|
|
772
|
+
const results = walkWithVisitors(tree.rootNode as any, setup.visitors, langId, setup.walkerOpts);
|
|
773
|
+
const defs = symbols.definitions || [];
|
|
774
|
+
let serializedAstNodes: SerializedExtractorOutput['astNodes'];
|
|
775
|
+
if (setup.astVisitor) serializedAstNodes = projectAstNodes(results);
|
|
776
|
+
if (setup.complexityVisitor) storeComplexityResults(results, defs, langId);
|
|
777
|
+
if (setup.cfgVisitor) storeCfgResults(results, defs);
|
|
778
|
+
if (setup.dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
|
|
779
|
+
return serializedAstNodes;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Pack the in-memory ExtractorOutput into the structured-clone-safe shape
|
|
784
|
+
* sent back across the worker boundary. Converts the typeMap into a tuple
|
|
785
|
+
* array and intentionally omits `_tree` (cannot cross the boundary).
|
|
786
|
+
*/
|
|
787
|
+
function serializeExtractorOutput(
|
|
788
|
+
symbols: ExtractorOutput,
|
|
789
|
+
langId: LanguageId,
|
|
790
|
+
code: string,
|
|
791
|
+
astNodes: SerializedExtractorOutput['astNodes'],
|
|
792
|
+
): SerializedExtractorOutput {
|
|
793
|
+
return {
|
|
794
|
+
definitions: symbols.definitions,
|
|
795
|
+
calls: symbols.calls,
|
|
796
|
+
imports: symbols.imports,
|
|
797
|
+
classes: symbols.classes,
|
|
798
|
+
exports: symbols.exports,
|
|
799
|
+
typeMap: Array.from(symbols.typeMap.entries()),
|
|
800
|
+
_langId: langId,
|
|
801
|
+
_lineCount: code.split('\n').length,
|
|
802
|
+
dataflow: symbols.dataflow,
|
|
803
|
+
astNodes,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Release WASM linear memory backing a tree. Best-effort — swallows errors so
|
|
809
|
+
* the worker keeps serving requests. Deferring this would let trees accumulate
|
|
810
|
+
* in the worker's WASM heap and defeat the point of isolating parse calls.
|
|
811
|
+
*/
|
|
812
|
+
function disposeTree(tree: Tree | null): void {
|
|
813
|
+
if (!tree) return;
|
|
814
|
+
const deletable = tree as unknown as { delete?: () => void };
|
|
815
|
+
if (typeof deletable.delete !== 'function') return;
|
|
816
|
+
try {
|
|
817
|
+
deletable.delete();
|
|
818
|
+
} catch {
|
|
819
|
+
// best-effort cleanup — swallow; worker continues.
|
|
820
|
+
}
|
|
821
|
+
}
|
|
659
822
|
|
|
660
823
|
async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractorOutput | null> {
|
|
661
824
|
const ext = path.extname(msg.filePath).toLowerCase();
|
|
@@ -666,100 +829,20 @@ async function handleParse(msg: WorkerParseRequest): Promise<SerializedExtractor
|
|
|
666
829
|
const parser = await loadLanguageLazy(entry);
|
|
667
830
|
if (!parser) return null;
|
|
668
831
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
tree = parser.parse(msg.code);
|
|
673
|
-
} catch (e: unknown) {
|
|
674
|
-
// Parse error — report back but keep worker alive.
|
|
675
|
-
throw new Error(`parse failed: ${(e as Error).message}`);
|
|
676
|
-
}
|
|
677
|
-
if (!tree) return null;
|
|
678
|
-
|
|
679
|
-
// Extractor — on failure, skip file (ok:true, null) to match parser.ts
|
|
680
|
-
// behavior where extractor issues don't crash the build.
|
|
681
|
-
let symbols: ExtractorOutput | null;
|
|
682
|
-
try {
|
|
683
|
-
const query = _queries.get(entry.id);
|
|
684
|
-
// tree-sitter's Tree/Query are structurally compatible with
|
|
685
|
-
// TreeSitterTree/TreeSitterQuery at runtime — same cast style as
|
|
686
|
-
// parser.ts::wasmExtractSymbols (parser.ts:789).
|
|
687
|
-
symbols = entry.extractor(tree as any, msg.filePath, query as any) ?? null;
|
|
688
|
-
} catch {
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
if (!symbols) return null;
|
|
692
|
-
|
|
693
|
-
// Unified visitor walk — mirrors engine.ts:791-829. Runs BEFORE tree.delete()
|
|
694
|
-
// because storeComplexityResults/storeCfgResults read funcNode off live nodes.
|
|
695
|
-
const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
|
|
696
|
-
setupVisitorsLocal(symbols, msg.filePath, entry.id, msg.opts);
|
|
832
|
+
const parsed = parseAndExtract(parser, entry, msg.filePath, msg.code);
|
|
833
|
+
if (!parsed) return null;
|
|
834
|
+
const { tree, symbols } = parsed;
|
|
697
835
|
|
|
698
|
-
|
|
836
|
+
try {
|
|
837
|
+
const setup = setupVisitorsLocal(symbols, msg.filePath, entry.id, msg.opts);
|
|
838
|
+
// astNodes kept in the serialized shape (without `file`/`parentNodeId`),
|
|
699
839
|
// not assigned back to symbols.astNodes — ExtractorOutput.astNodes is
|
|
700
840
|
// ASTNodeRow[] (DB row shape with node_id), which is a different type.
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (visitors.length > 0) {
|
|
704
|
-
// rootNode shape matches TreeSitterNode at runtime — same cast as parser.ts:789.
|
|
705
|
-
const results = walkWithVisitors(tree.rootNode as any, visitors, entry.id, walkerOpts);
|
|
706
|
-
|
|
707
|
-
const defs = symbols.definitions || [];
|
|
708
|
-
if (astVisitor) {
|
|
709
|
-
const astRows = (results['ast-store'] || []) as Array<{
|
|
710
|
-
line: number;
|
|
711
|
-
kind: string;
|
|
712
|
-
name: string | null | undefined;
|
|
713
|
-
text: string | null;
|
|
714
|
-
receiver: string | null;
|
|
715
|
-
file?: string;
|
|
716
|
-
parentNodeId?: number | null;
|
|
717
|
-
}>;
|
|
718
|
-
// Always set an array (even empty) — leaving astNodes undefined makes
|
|
719
|
-
// engine.ts::fileNeedsWasmTree treat the file as un-walked and trigger
|
|
720
|
-
// a full ensureWasmTrees re-parse of every WASM-parseable file (#1036).
|
|
721
|
-
// Strip `file` and `parentNodeId` — main thread re-resolves both in
|
|
722
|
-
// features/ast.ts::collectFileAstRows.
|
|
723
|
-
serializedAstNodes = astRows.map((n) => ({
|
|
724
|
-
line: n.line,
|
|
725
|
-
kind: n.kind,
|
|
726
|
-
name: n.name ?? '',
|
|
727
|
-
text: n.text ?? undefined,
|
|
728
|
-
receiver: n.receiver ?? undefined,
|
|
729
|
-
}));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (complexityVisitor) storeComplexityResults(results, defs, entry.id);
|
|
733
|
-
if (cfgVisitor) storeCfgResults(results, defs);
|
|
734
|
-
if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Serialize — convert Map<string, TypeMapEntry> to tuple array for the wire.
|
|
738
|
-
const serialized: SerializedExtractorOutput = {
|
|
739
|
-
definitions: symbols.definitions,
|
|
740
|
-
calls: symbols.calls,
|
|
741
|
-
imports: symbols.imports,
|
|
742
|
-
classes: symbols.classes,
|
|
743
|
-
exports: symbols.exports,
|
|
744
|
-
typeMap: Array.from(symbols.typeMap.entries()),
|
|
745
|
-
_langId: entry.id as LanguageId,
|
|
746
|
-
_lineCount: msg.code.split('\n').length,
|
|
747
|
-
dataflow: symbols.dataflow,
|
|
748
|
-
astNodes: serializedAstNodes,
|
|
749
|
-
};
|
|
750
|
-
// _tree is deliberately not serialized — it cannot cross the worker boundary.
|
|
751
|
-
return serialized;
|
|
841
|
+
const serializedAstNodes = runVisitorWalk(tree, symbols, entry.id, setup);
|
|
842
|
+
return serializeExtractorOutput(symbols, entry.id as LanguageId, msg.code, serializedAstNodes);
|
|
752
843
|
} finally {
|
|
753
|
-
// ALWAYS release WASM memory before responding
|
|
754
|
-
|
|
755
|
-
// the point of isolating parse calls.
|
|
756
|
-
if (tree && typeof (tree as unknown as { delete?: () => void }).delete === 'function') {
|
|
757
|
-
try {
|
|
758
|
-
(tree as unknown as { delete: () => void }).delete();
|
|
759
|
-
} catch {
|
|
760
|
-
// best-effort cleanup — swallow; worker continues.
|
|
761
|
-
}
|
|
762
|
-
}
|
|
844
|
+
// ALWAYS release WASM memory before responding (see disposeTree note).
|
|
845
|
+
disposeTree(tree);
|
|
763
846
|
}
|
|
764
847
|
}
|
|
765
848
|
|
package/src/extractors/elixir.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TreeSitterNode,
|
|
6
6
|
TreeSitterTree,
|
|
7
7
|
} from '../types.js';
|
|
8
|
-
import { findChild, nodeEndLine } from './helpers.js';
|
|
8
|
+
import { findChild, iterChildren, nodeEndLine, PUNCTUATION_TOKENS } from './helpers.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Extract symbols from Elixir files.
|
|
@@ -197,77 +197,104 @@ function extractElixirParams(defCallNode: TreeSitterNode): SubDeclaration[] {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
200
|
+
* Walk a parameter pattern and emit each bound identifier as a `parameter`
|
|
201
|
+
* child. Handles bare identifiers, default-value `a \\ default`, list-cons
|
|
202
|
+
* `[head | tail]`, list `[a, b, c]`, tuple `{x, y}`, and map / struct
|
|
203
|
+
* destructuring (`%{k: v}`, `%Foo{k: v}`).
|
|
204
|
+
*
|
|
205
|
+
* Implemented as an iterative worklist (rather than recursion + helpers) so
|
|
206
|
+
* the call graph has no function-level cycle: only one function performs the
|
|
207
|
+
* traversal and it invokes only leaf helpers (`pushSubNodes`, `pushMapValues`).
|
|
204
208
|
*/
|
|
205
|
-
function collectElixirParamIdentifiers(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (right) collectElixirParamIdentifiers(right, out);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
209
|
+
function collectElixirParamIdentifiers(root: TreeSitterNode, out: SubDeclaration[]): void {
|
|
210
|
+
const stack: TreeSitterNode[] = [root];
|
|
211
|
+
while (stack.length > 0) {
|
|
212
|
+
const node = stack.pop();
|
|
213
|
+
if (!node) continue;
|
|
214
|
+
switch (node.type) {
|
|
215
|
+
case 'identifier':
|
|
216
|
+
out.push({ name: node.text, kind: 'parameter', line: node.startPosition.row + 1 });
|
|
217
|
+
break;
|
|
218
|
+
case 'binary_operator':
|
|
219
|
+
pushElixirBinaryOperatorOperands(node, stack);
|
|
220
|
+
break;
|
|
221
|
+
case 'list':
|
|
222
|
+
case 'tuple':
|
|
223
|
+
pushElixirSequenceItems(node, stack);
|
|
224
|
+
break;
|
|
225
|
+
case 'map':
|
|
226
|
+
pushElixirMapValues(node, stack);
|
|
227
|
+
break;
|
|
228
228
|
}
|
|
229
|
-
case 'list':
|
|
230
|
-
// `[a, b, c]` or `[head | tail]` — walk children, skipping punctuation. The
|
|
231
|
-
// `|` cons case is handled by the `binary_operator` arm when we recurse.
|
|
232
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
233
|
-
const c = node.child(i);
|
|
234
|
-
if (!c || c.type === '[' || c.type === ']' || c.type === ',') continue;
|
|
235
|
-
collectElixirParamIdentifiers(c, out);
|
|
236
|
-
}
|
|
237
|
-
return;
|
|
238
|
-
case 'tuple':
|
|
239
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
240
|
-
const c = node.child(i);
|
|
241
|
-
if (!c || c.type === '{' || c.type === '}' || c.type === ',') continue;
|
|
242
|
-
collectElixirParamIdentifiers(c, out);
|
|
243
|
-
}
|
|
244
|
-
return;
|
|
245
|
-
case 'map':
|
|
246
|
-
// `%{k: v}` or `%Foo{k: v}` — walk map_content > keywords > pair and emit each
|
|
247
|
-
// pair's value side (the bound name). The struct alias (`Foo`) is a type, not a
|
|
248
|
-
// bound identifier, so the leading `struct` child is intentionally skipped.
|
|
249
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
250
|
-
const c = node.child(i);
|
|
251
|
-
if (c && c.type === 'map_content') collectElixirMapBindings(c, out);
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
254
229
|
}
|
|
255
230
|
}
|
|
256
231
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Push the binding-relevant operands of a `binary_operator` parameter onto the
|
|
234
|
+
* worklist:
|
|
235
|
+
* - `name \\ default` (default-value) binds the left operand only.
|
|
236
|
+
* - `head | tail` (list-cons, appears inside a `list` pattern) binds both.
|
|
237
|
+
*/
|
|
238
|
+
function pushElixirBinaryOperatorOperands(node: TreeSitterNode, stack: TreeSitterNode[]): void {
|
|
239
|
+
const op = node.child(1);
|
|
240
|
+
if (!op) return;
|
|
241
|
+
if (op.type === '\\\\') {
|
|
242
|
+
const left = node.child(0);
|
|
243
|
+
if (left) stack.push(left);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (op.type === '|') {
|
|
247
|
+
const right = node.child(2);
|
|
248
|
+
const left = node.child(0);
|
|
249
|
+
if (right) stack.push(right);
|
|
250
|
+
if (left) stack.push(left);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Push the binding-relevant elements of a `list` or `tuple` parameter onto
|
|
256
|
+
* the worklist, skipping punctuation tokens.
|
|
257
|
+
*
|
|
258
|
+
* Items are pushed in reverse document order so that, with a LIFO stack, they
|
|
259
|
+
* are processed left-to-right — preserving the source ordering of bound names.
|
|
260
|
+
*/
|
|
261
|
+
function pushElixirSequenceItems(node: TreeSitterNode, stack: TreeSitterNode[]): void {
|
|
262
|
+
const items: TreeSitterNode[] = [...iterChildren(node, PUNCTUATION_TOKENS)];
|
|
263
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
264
|
+
stack.push(items[i] as TreeSitterNode);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Push the value side of every pair in a `map` or `%Foo{...}` parameter onto
|
|
270
|
+
* the worklist. The struct alias (`Foo`) is a type, not a bound identifier, so
|
|
271
|
+
* the leading `struct` child is intentionally skipped.
|
|
272
|
+
*
|
|
273
|
+
* Items are collected in document order and pushed in reverse so that, with a
|
|
274
|
+
* LIFO stack, they are processed left-to-right — preserving source ordering.
|
|
275
|
+
*/
|
|
276
|
+
function pushElixirMapValues(node: TreeSitterNode, stack: TreeSitterNode[]): void {
|
|
277
|
+
const parts: TreeSitterNode[] = [];
|
|
278
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
279
|
+
const content = node.child(i);
|
|
280
|
+
if (!content || content.type !== 'map_content') continue;
|
|
281
|
+
for (let j = 0; j < content.childCount; j++) {
|
|
282
|
+
const kws = content.child(j);
|
|
283
|
+
if (!kws || kws.type !== 'keywords') continue;
|
|
284
|
+
for (let k = 0; k < kws.childCount; k++) {
|
|
285
|
+
const pair = kws.child(k);
|
|
286
|
+
if (!pair || pair.type !== 'pair') continue;
|
|
287
|
+
for (let p = 0; p < pair.childCount; p++) {
|
|
288
|
+
const part = pair.child(p);
|
|
289
|
+
if (!part || part.type === 'keyword') continue;
|
|
290
|
+
parts.push(part);
|
|
291
|
+
}
|
|
268
292
|
}
|
|
269
293
|
}
|
|
270
294
|
}
|
|
295
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
296
|
+
stack.push(parts[i] as TreeSitterNode);
|
|
297
|
+
}
|
|
271
298
|
}
|
|
272
299
|
|
|
273
300
|
function handleDefprotocol(node: TreeSitterNode, ctx: ExtractorOutput): void {
|