@optave/codegraph 3.1.0 → 3.1.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 +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
package/src/embedder.js
CHANGED
|
@@ -2,7 +2,14 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
closeDb,
|
|
7
|
+
findCalleeNames,
|
|
8
|
+
findCallerNames,
|
|
9
|
+
findDbPath,
|
|
10
|
+
openDb,
|
|
11
|
+
openReadonlyOrFail,
|
|
12
|
+
} from './db.js';
|
|
6
13
|
import { info, warn } from './logger.js';
|
|
7
14
|
import { normalizeSymbol } from './queries.js';
|
|
8
15
|
|
|
@@ -166,7 +173,7 @@ function extractLeadingComment(lines, fnLineIndex) {
|
|
|
166
173
|
* Build graph-enriched text for a symbol using dependency context.
|
|
167
174
|
* Produces compact, semantic text (~100 tokens) instead of full source code.
|
|
168
175
|
*/
|
|
169
|
-
function buildStructuredText(node, file, lines,
|
|
176
|
+
function buildStructuredText(node, file, lines, db) {
|
|
170
177
|
const readable = splitIdentifier(node.name);
|
|
171
178
|
const parts = [`${node.kind} ${node.name} (${readable}) in ${file}`];
|
|
172
179
|
const startLine = Math.max(0, node.line - 1);
|
|
@@ -179,25 +186,15 @@ function buildStructuredText(node, file, lines, calleesStmt, callersStmt) {
|
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// Graph context: callees (capped at 10)
|
|
182
|
-
const callees =
|
|
189
|
+
const callees = findCalleeNames(db, node.id);
|
|
183
190
|
if (callees.length > 0) {
|
|
184
|
-
parts.push(
|
|
185
|
-
`Calls: ${callees
|
|
186
|
-
.slice(0, 10)
|
|
187
|
-
.map((c) => c.name)
|
|
188
|
-
.join(', ')}`,
|
|
189
|
-
);
|
|
191
|
+
parts.push(`Calls: ${callees.slice(0, 10).join(', ')}`);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// Graph context: callers (capped at 10)
|
|
193
|
-
const callers =
|
|
195
|
+
const callers = findCallerNames(db, node.id);
|
|
194
196
|
if (callers.length > 0) {
|
|
195
|
-
parts.push(
|
|
196
|
-
`Called by: ${callers
|
|
197
|
-
.slice(0, 10)
|
|
198
|
-
.map((c) => c.name)
|
|
199
|
-
.join(', ')}`,
|
|
200
|
-
);
|
|
197
|
+
parts.push(`Called by: ${callers.slice(0, 10).join(', ')}`);
|
|
201
198
|
}
|
|
202
199
|
|
|
203
200
|
// Leading comment (high semantic value) or first few lines of code
|
|
@@ -438,23 +435,6 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
|
|
|
438
435
|
|
|
439
436
|
console.log(`Building embeddings for ${nodes.length} symbols (strategy: ${strategy})...`);
|
|
440
437
|
|
|
441
|
-
// Prepare graph-context queries for structured strategy
|
|
442
|
-
let calleesStmt, callersStmt;
|
|
443
|
-
if (strategy === 'structured') {
|
|
444
|
-
calleesStmt = db.prepare(`
|
|
445
|
-
SELECT DISTINCT n.name FROM edges e
|
|
446
|
-
JOIN nodes n ON e.target_id = n.id
|
|
447
|
-
WHERE e.source_id = ? AND e.kind = 'calls'
|
|
448
|
-
ORDER BY n.name
|
|
449
|
-
`);
|
|
450
|
-
callersStmt = db.prepare(`
|
|
451
|
-
SELECT DISTINCT n.name FROM edges e
|
|
452
|
-
JOIN nodes n ON e.source_id = n.id
|
|
453
|
-
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
454
|
-
ORDER BY n.name
|
|
455
|
-
`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
438
|
const byFile = new Map();
|
|
459
439
|
for (const node of nodes) {
|
|
460
440
|
if (!byFile.has(node.file)) byFile.set(node.file, []);
|
|
@@ -482,7 +462,7 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
|
|
|
482
462
|
for (const node of fileNodes) {
|
|
483
463
|
let text =
|
|
484
464
|
strategy === 'structured'
|
|
485
|
-
? buildStructuredText(node, file, lines,
|
|
465
|
+
? buildStructuredText(node, file, lines, db)
|
|
486
466
|
: buildSourceText(node, file, lines);
|
|
487
467
|
|
|
488
468
|
// Detect and handle context window overflow
|
|
@@ -625,37 +605,39 @@ export async function searchData(query, customDbPath, opts = {}) {
|
|
|
625
605
|
if (!prepared) return null;
|
|
626
606
|
const { db, rows, modelKey, storedDim } = prepared;
|
|
627
607
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
608
|
+
try {
|
|
609
|
+
const {
|
|
610
|
+
vectors: [queryVec],
|
|
611
|
+
dim,
|
|
612
|
+
} = await embed([query], modelKey);
|
|
632
613
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
614
|
+
if (storedDim && dim !== storedDim) {
|
|
615
|
+
console.log(
|
|
616
|
+
`Warning: query model dimension (${dim}) doesn't match stored embeddings (${storedDim}).`,
|
|
617
|
+
);
|
|
618
|
+
console.log(` Re-run \`codegraph embed\` with the same model, or use --model to match.`);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
641
621
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
622
|
+
const hc = new Map();
|
|
623
|
+
const results = [];
|
|
624
|
+
for (const row of rows) {
|
|
625
|
+
const vec = new Float32Array(new Uint8Array(row.vector).buffer);
|
|
626
|
+
const sim = cosineSim(queryVec, vec);
|
|
647
627
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
628
|
+
if (sim >= minScore) {
|
|
629
|
+
results.push({
|
|
630
|
+
...normalizeSymbol(row, db, hc),
|
|
631
|
+
similarity: sim,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
653
634
|
}
|
|
654
|
-
}
|
|
655
635
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
636
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
637
|
+
return { results: results.slice(0, limit) };
|
|
638
|
+
} finally {
|
|
639
|
+
db.close();
|
|
640
|
+
}
|
|
659
641
|
}
|
|
660
642
|
|
|
661
643
|
/**
|
|
@@ -671,82 +653,84 @@ export async function multiSearchData(queries, customDbPath, opts = {}) {
|
|
|
671
653
|
if (!prepared) return null;
|
|
672
654
|
const { db, rows, modelKey, storedDim } = prepared;
|
|
673
655
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
for (let
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
`
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
656
|
+
try {
|
|
657
|
+
const { vectors: queryVecs, dim } = await embed(queries, modelKey);
|
|
658
|
+
|
|
659
|
+
// Warn about similar queries that may bias RRF results
|
|
660
|
+
const SIMILARITY_WARN_THRESHOLD = 0.85;
|
|
661
|
+
for (let i = 0; i < queryVecs.length; i++) {
|
|
662
|
+
for (let j = i + 1; j < queryVecs.length; j++) {
|
|
663
|
+
const sim = cosineSim(queryVecs[i], queryVecs[j]);
|
|
664
|
+
if (sim >= SIMILARITY_WARN_THRESHOLD) {
|
|
665
|
+
warn(
|
|
666
|
+
`Queries "${queries[i]}" and "${queries[j]}" are very similar ` +
|
|
667
|
+
`(${(sim * 100).toFixed(0)}% cosine similarity). ` +
|
|
668
|
+
`This may bias RRF results toward their shared matches. ` +
|
|
669
|
+
`Consider using more distinct queries.`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
688
672
|
}
|
|
689
673
|
}
|
|
690
|
-
}
|
|
691
674
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
675
|
+
if (storedDim && dim !== storedDim) {
|
|
676
|
+
console.log(
|
|
677
|
+
`Warning: query model dimension (${dim}) doesn't match stored embeddings (${storedDim}).`,
|
|
678
|
+
);
|
|
679
|
+
console.log(` Re-run \`codegraph embed\` with the same model, or use --model to match.`);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
700
682
|
|
|
701
|
-
|
|
702
|
-
|
|
683
|
+
// Parse row vectors once
|
|
684
|
+
const rowVecs = rows.map((row) => new Float32Array(new Uint8Array(row.vector).buffer));
|
|
703
685
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
686
|
+
// For each query: compute similarities, filter by minScore, rank
|
|
687
|
+
const perQueryRanked = queries.map((_query, qi) => {
|
|
688
|
+
const scored = [];
|
|
689
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
690
|
+
const sim = cosineSim(queryVecs[qi], rowVecs[ri]);
|
|
691
|
+
if (sim >= minScore) {
|
|
692
|
+
scored.push({ rowIndex: ri, similarity: sim });
|
|
693
|
+
}
|
|
711
694
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
695
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
696
|
+
// Assign 1-indexed ranks
|
|
697
|
+
return scored.map((item, rank) => ({ ...item, rank: rank + 1 }));
|
|
698
|
+
});
|
|
717
699
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
700
|
+
// Fuse results using RRF: for each unique row, sum 1/(k + rank_i) across queries
|
|
701
|
+
const fusionMap = new Map(); // rowIndex -> { rrfScore, queryScores[] }
|
|
702
|
+
for (let qi = 0; qi < queries.length; qi++) {
|
|
703
|
+
for (const item of perQueryRanked[qi]) {
|
|
704
|
+
if (!fusionMap.has(item.rowIndex)) {
|
|
705
|
+
fusionMap.set(item.rowIndex, { rrfScore: 0, queryScores: [] });
|
|
706
|
+
}
|
|
707
|
+
const entry = fusionMap.get(item.rowIndex);
|
|
708
|
+
entry.rrfScore += 1 / (k + item.rank);
|
|
709
|
+
entry.queryScores.push({
|
|
710
|
+
query: queries[qi],
|
|
711
|
+
similarity: item.similarity,
|
|
712
|
+
rank: item.rank,
|
|
713
|
+
});
|
|
724
714
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Build results sorted by RRF score
|
|
718
|
+
const hc = new Map();
|
|
719
|
+
const results = [];
|
|
720
|
+
for (const [rowIndex, entry] of fusionMap) {
|
|
721
|
+
const row = rows[rowIndex];
|
|
722
|
+
results.push({
|
|
723
|
+
...normalizeSymbol(row, db, hc),
|
|
724
|
+
rrf: entry.rrfScore,
|
|
725
|
+
queryScores: entry.queryScores,
|
|
731
726
|
});
|
|
732
727
|
}
|
|
733
|
-
}
|
|
734
728
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const row = rows[rowIndex];
|
|
740
|
-
results.push({
|
|
741
|
-
...normalizeSymbol(row, db, hc),
|
|
742
|
-
rrf: entry.rrfScore,
|
|
743
|
-
queryScores: entry.queryScores,
|
|
744
|
-
});
|
|
729
|
+
results.sort((a, b) => b.rrf - a.rrf);
|
|
730
|
+
return { results: results.slice(0, limit) };
|
|
731
|
+
} finally {
|
|
732
|
+
db.close();
|
|
745
733
|
}
|
|
746
|
-
|
|
747
|
-
results.sort((a, b) => b.rrf - a.rrf);
|
|
748
|
-
db.close();
|
|
749
|
-
return { results: results.slice(0, limit) };
|
|
750
734
|
}
|
|
751
735
|
|
|
752
736
|
/**
|
|
@@ -788,64 +772,64 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
|
|
|
788
772
|
|
|
789
773
|
const db = openReadonlyOrFail(customDbPath);
|
|
790
774
|
|
|
791
|
-
|
|
792
|
-
db
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const ftsQuery = sanitizeFtsQuery(query);
|
|
797
|
-
if (!ftsQuery) {
|
|
798
|
-
db.close();
|
|
799
|
-
return { results: [] };
|
|
800
|
-
}
|
|
775
|
+
try {
|
|
776
|
+
if (!hasFtsIndex(db)) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
801
779
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
JOIN nodes n ON f.rowid = n.id
|
|
807
|
-
WHERE fts_index MATCH ?
|
|
808
|
-
`;
|
|
809
|
-
const params = [ftsQuery];
|
|
780
|
+
const ftsQuery = sanitizeFtsQuery(query);
|
|
781
|
+
if (!ftsQuery) {
|
|
782
|
+
return { results: [] };
|
|
783
|
+
}
|
|
810
784
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
785
|
+
let sql = `
|
|
786
|
+
SELECT f.rowid AS node_id, rank AS bm25_score,
|
|
787
|
+
n.name, n.kind, n.file, n.line, n.end_line, n.role
|
|
788
|
+
FROM fts_index f
|
|
789
|
+
JOIN nodes n ON f.rowid = n.id
|
|
790
|
+
WHERE fts_index MATCH ?
|
|
791
|
+
`;
|
|
792
|
+
const params = [ftsQuery];
|
|
793
|
+
|
|
794
|
+
if (opts.kind) {
|
|
795
|
+
sql += ' AND n.kind = ?';
|
|
796
|
+
params.push(opts.kind);
|
|
797
|
+
}
|
|
815
798
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
799
|
+
const isGlob = opts.filePattern && /[*?[\]]/.test(opts.filePattern);
|
|
800
|
+
if (opts.filePattern && !isGlob) {
|
|
801
|
+
sql += ' AND n.file LIKE ?';
|
|
802
|
+
params.push(`%${opts.filePattern}%`);
|
|
803
|
+
}
|
|
821
804
|
|
|
822
|
-
|
|
823
|
-
|
|
805
|
+
sql += ' ORDER BY rank LIMIT ?';
|
|
806
|
+
params.push(limit * 5); // fetch generous set for post-filtering
|
|
824
807
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
808
|
+
let rows;
|
|
809
|
+
try {
|
|
810
|
+
rows = db.prepare(sql).all(...params);
|
|
811
|
+
} catch {
|
|
812
|
+
// Invalid FTS5 query syntax — return empty
|
|
813
|
+
return { results: [] };
|
|
814
|
+
}
|
|
833
815
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
816
|
+
if (isGlob) {
|
|
817
|
+
rows = rows.filter((row) => globMatch(row.file, opts.filePattern));
|
|
818
|
+
}
|
|
819
|
+
if (noTests) {
|
|
820
|
+
rows = rows.filter((row) => !TEST_PATTERN.test(row.file));
|
|
821
|
+
}
|
|
840
822
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
823
|
+
const hc = new Map();
|
|
824
|
+
const results = rows.slice(0, limit).map((row) => ({
|
|
825
|
+
...normalizeSymbol(row, db, hc),
|
|
826
|
+
bm25Score: -row.bm25_score, // FTS5 rank is negative; negate for display
|
|
827
|
+
}));
|
|
846
828
|
|
|
847
|
-
|
|
848
|
-
|
|
829
|
+
return { results };
|
|
830
|
+
} finally {
|
|
831
|
+
db.close();
|
|
832
|
+
}
|
|
849
833
|
}
|
|
850
834
|
|
|
851
835
|
/**
|
package/src/export.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debug } from '../logger.js';
|
|
1
2
|
import { findChild, nodeEndLine } from './helpers.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -173,6 +174,9 @@ function extractSymbolsQuery(tree, query) {
|
|
|
173
174
|
// Extract top-level constants via targeted walk (query patterns don't cover these)
|
|
174
175
|
extractConstantsWalk(tree.rootNode, definitions);
|
|
175
176
|
|
|
177
|
+
// Extract dynamic import() calls via targeted walk (query patterns don't match `import` function type)
|
|
178
|
+
extractDynamicImportsWalk(tree.rootNode, imports);
|
|
179
|
+
|
|
176
180
|
return { definitions, calls, imports, classes, exports: exps };
|
|
177
181
|
}
|
|
178
182
|
|
|
@@ -224,6 +228,41 @@ function extractConstantsWalk(rootNode, definitions) {
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Recursive walk to find dynamic import() calls.
|
|
233
|
+
* Query patterns match call_expression with identifier/member_expression/subscript_expression
|
|
234
|
+
* functions, but import() has function type `import` which none of those patterns cover.
|
|
235
|
+
*/
|
|
236
|
+
function extractDynamicImportsWalk(node, imports) {
|
|
237
|
+
if (node.type === 'call_expression') {
|
|
238
|
+
const fn = node.childForFieldName('function');
|
|
239
|
+
if (fn && fn.type === 'import') {
|
|
240
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
241
|
+
if (args) {
|
|
242
|
+
const strArg = findChild(args, 'string');
|
|
243
|
+
if (strArg) {
|
|
244
|
+
const modPath = strArg.text.replace(/['"]/g, '');
|
|
245
|
+
const names = extractDynamicImportNames(node);
|
|
246
|
+
imports.push({
|
|
247
|
+
source: modPath,
|
|
248
|
+
names,
|
|
249
|
+
line: node.startPosition.row + 1,
|
|
250
|
+
dynamicImport: true,
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
debug(
|
|
254
|
+
`Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return; // no need to recurse into import() children
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
262
|
+
extractDynamicImportsWalk(node.child(i), imports);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
function handleCommonJSAssignment(left, right, node, imports) {
|
|
228
267
|
if (!left || !right) return;
|
|
229
268
|
const leftText = left.text;
|
|
@@ -455,11 +494,36 @@ function extractSymbolsWalk(tree) {
|
|
|
455
494
|
case 'call_expression': {
|
|
456
495
|
const fn = node.childForFieldName('function');
|
|
457
496
|
if (fn) {
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
497
|
+
// Dynamic import(): import('./foo.js') → extract as an import entry
|
|
498
|
+
if (fn.type === 'import') {
|
|
499
|
+
const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
|
|
500
|
+
if (args) {
|
|
501
|
+
const strArg = findChild(args, 'string');
|
|
502
|
+
if (strArg) {
|
|
503
|
+
const modPath = strArg.text.replace(/['"]/g, '');
|
|
504
|
+
// Extract destructured names from parent context:
|
|
505
|
+
// const { a, b } = await import('./foo.js')
|
|
506
|
+
// (standalone import('./foo.js').then(...) calls produce an edge with empty names)
|
|
507
|
+
const names = extractDynamicImportNames(node);
|
|
508
|
+
imports.push({
|
|
509
|
+
source: modPath,
|
|
510
|
+
names,
|
|
511
|
+
line: node.startPosition.row + 1,
|
|
512
|
+
dynamicImport: true,
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
debug(
|
|
516
|
+
`Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
const callInfo = extractCallInfo(fn, node);
|
|
522
|
+
if (callInfo) calls.push(callInfo);
|
|
523
|
+
if (fn.type === 'member_expression') {
|
|
524
|
+
const cbDef = extractCallbackDefinition(node, fn);
|
|
525
|
+
if (cbDef) definitions.push(cbDef);
|
|
526
|
+
}
|
|
463
527
|
}
|
|
464
528
|
}
|
|
465
529
|
break;
|
|
@@ -941,3 +1005,64 @@ function extractImportNames(node) {
|
|
|
941
1005
|
scan(node);
|
|
942
1006
|
return names;
|
|
943
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Extract destructured names from a dynamic import() call expression.
|
|
1011
|
+
*
|
|
1012
|
+
* Handles:
|
|
1013
|
+
* const { a, b } = await import('./foo.js') → ['a', 'b']
|
|
1014
|
+
* const mod = await import('./foo.js') → ['mod']
|
|
1015
|
+
* import('./foo.js') → [] (no names extractable)
|
|
1016
|
+
*
|
|
1017
|
+
* Walks up the AST from the call_expression to find the enclosing
|
|
1018
|
+
* variable_declarator and reads the name/object_pattern.
|
|
1019
|
+
*/
|
|
1020
|
+
function extractDynamicImportNames(callNode) {
|
|
1021
|
+
// Walk up: call_expression → await_expression → variable_declarator
|
|
1022
|
+
let current = callNode.parent;
|
|
1023
|
+
// Skip await_expression wrapper if present
|
|
1024
|
+
if (current && current.type === 'await_expression') current = current.parent;
|
|
1025
|
+
// We should now be at a variable_declarator (or not, if standalone import())
|
|
1026
|
+
if (!current || current.type !== 'variable_declarator') return [];
|
|
1027
|
+
|
|
1028
|
+
const nameNode = current.childForFieldName('name');
|
|
1029
|
+
if (!nameNode) return [];
|
|
1030
|
+
|
|
1031
|
+
// const { a, b } = await import(...) → object_pattern
|
|
1032
|
+
if (nameNode.type === 'object_pattern') {
|
|
1033
|
+
const names = [];
|
|
1034
|
+
for (let i = 0; i < nameNode.childCount; i++) {
|
|
1035
|
+
const child = nameNode.child(i);
|
|
1036
|
+
if (child.type === 'shorthand_property_identifier_pattern') {
|
|
1037
|
+
names.push(child.text);
|
|
1038
|
+
} else if (child.type === 'pair_pattern') {
|
|
1039
|
+
// { a: localName } → use localName (the alias) for the local binding,
|
|
1040
|
+
// but use the key (original name) for import resolution
|
|
1041
|
+
const key = child.childForFieldName('key');
|
|
1042
|
+
if (key) names.push(key.text);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return names;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// const mod = await import(...) → identifier (namespace-like import)
|
|
1049
|
+
if (nameNode.type === 'identifier') {
|
|
1050
|
+
return [nameNode.text];
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// const [a, b] = await import(...) → array_pattern (rare but possible)
|
|
1054
|
+
if (nameNode.type === 'array_pattern') {
|
|
1055
|
+
const names = [];
|
|
1056
|
+
for (let i = 0; i < nameNode.childCount; i++) {
|
|
1057
|
+
const child = nameNode.child(i);
|
|
1058
|
+
if (child.type === 'identifier') names.push(child.text);
|
|
1059
|
+
else if (child.type === 'rest_pattern') {
|
|
1060
|
+
const inner = child.child(0) || child.childForFieldName('name');
|
|
1061
|
+
if (inner && inner.type === 'identifier') names.push(inner.text);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return names;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|