scip-query 0.1.0
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/IMPROVEMENTS.md +143 -0
- package/PLAN.md +320 -0
- package/README.md +1213 -0
- package/dist/chunk-2QZ23IBN.js +55 -0
- package/dist/chunk-2QZ23IBN.js.map +1 -0
- package/dist/chunk-36OMT7ZJ.js +144 -0
- package/dist/chunk-36OMT7ZJ.js.map +1 -0
- package/dist/chunk-3E2X7RIE.js +101 -0
- package/dist/chunk-3E2X7RIE.js.map +1 -0
- package/dist/chunk-3UOUTZQT.js +45 -0
- package/dist/chunk-3UOUTZQT.js.map +1 -0
- package/dist/chunk-3ZZJVBIO.js +88 -0
- package/dist/chunk-3ZZJVBIO.js.map +1 -0
- package/dist/chunk-4TYLS5XX.js +10 -0
- package/dist/chunk-4TYLS5XX.js.map +1 -0
- package/dist/chunk-5FGUEU7N.js +101 -0
- package/dist/chunk-5FGUEU7N.js.map +1 -0
- package/dist/chunk-5WTJAXY2.js +61 -0
- package/dist/chunk-5WTJAXY2.js.map +1 -0
- package/dist/chunk-6NBLIDF4.js +24 -0
- package/dist/chunk-6NBLIDF4.js.map +1 -0
- package/dist/chunk-6SXADWLW.js +43 -0
- package/dist/chunk-6SXADWLW.js.map +1 -0
- package/dist/chunk-6VJ6Q7IE.js +65 -0
- package/dist/chunk-6VJ6Q7IE.js.map +1 -0
- package/dist/chunk-7OZPA5OO.js +258 -0
- package/dist/chunk-7OZPA5OO.js.map +1 -0
- package/dist/chunk-BEPIEVLR.js +76 -0
- package/dist/chunk-BEPIEVLR.js.map +1 -0
- package/dist/chunk-BFSCMC22.js +42 -0
- package/dist/chunk-BFSCMC22.js.map +1 -0
- package/dist/chunk-BP2ATLK2.js +110 -0
- package/dist/chunk-BP2ATLK2.js.map +1 -0
- package/dist/chunk-CM454WL3.js +114 -0
- package/dist/chunk-CM454WL3.js.map +1 -0
- package/dist/chunk-DCKMSTJ4.js +74 -0
- package/dist/chunk-DCKMSTJ4.js.map +1 -0
- package/dist/chunk-DEZKCZXD.js +40 -0
- package/dist/chunk-DEZKCZXD.js.map +1 -0
- package/dist/chunk-DVWGWHFW.js +99 -0
- package/dist/chunk-DVWGWHFW.js.map +1 -0
- package/dist/chunk-EMDQWNYR.js +102 -0
- package/dist/chunk-EMDQWNYR.js.map +1 -0
- package/dist/chunk-FFSWWE5O.js +33 -0
- package/dist/chunk-FFSWWE5O.js.map +1 -0
- package/dist/chunk-FGXRVW7G.js +73 -0
- package/dist/chunk-FGXRVW7G.js.map +1 -0
- package/dist/chunk-FUHJCHS4.js +158 -0
- package/dist/chunk-FUHJCHS4.js.map +1 -0
- package/dist/chunk-GJFURBEW.js +64 -0
- package/dist/chunk-GJFURBEW.js.map +1 -0
- package/dist/chunk-GTILYBH6.js +102 -0
- package/dist/chunk-GTILYBH6.js.map +1 -0
- package/dist/chunk-JJP7KQND.js +1 -0
- package/dist/chunk-JJP7KQND.js.map +1 -0
- package/dist/chunk-JKP5GH6T.js +213 -0
- package/dist/chunk-JKP5GH6T.js.map +1 -0
- package/dist/chunk-KCBMVQL5.js +38 -0
- package/dist/chunk-KCBMVQL5.js.map +1 -0
- package/dist/chunk-KVSW5KYP.js +78 -0
- package/dist/chunk-KVSW5KYP.js.map +1 -0
- package/dist/chunk-LAWMH22O.js +172 -0
- package/dist/chunk-LAWMH22O.js.map +1 -0
- package/dist/chunk-LB7OS35Q.js +72 -0
- package/dist/chunk-LB7OS35Q.js.map +1 -0
- package/dist/chunk-LUSIFBXO.js +57 -0
- package/dist/chunk-LUSIFBXO.js.map +1 -0
- package/dist/chunk-MBVNHJVN.js +44 -0
- package/dist/chunk-MBVNHJVN.js.map +1 -0
- package/dist/chunk-MGNMHKX3.js +15 -0
- package/dist/chunk-MGNMHKX3.js.map +1 -0
- package/dist/chunk-N5KEREIA.js +41 -0
- package/dist/chunk-N5KEREIA.js.map +1 -0
- package/dist/chunk-NDSQYIWT.js +71 -0
- package/dist/chunk-NDSQYIWT.js.map +1 -0
- package/dist/chunk-NUZ4OMU3.js +28 -0
- package/dist/chunk-NUZ4OMU3.js.map +1 -0
- package/dist/chunk-QOV2R2WT.js +170 -0
- package/dist/chunk-QOV2R2WT.js.map +1 -0
- package/dist/chunk-SEFSL2GF.js +78 -0
- package/dist/chunk-SEFSL2GF.js.map +1 -0
- package/dist/chunk-T6ARFSBZ.js +103 -0
- package/dist/chunk-T6ARFSBZ.js.map +1 -0
- package/dist/chunk-TBP6BICL.js +46 -0
- package/dist/chunk-TBP6BICL.js.map +1 -0
- package/dist/chunk-TDNNOR6D.js +97 -0
- package/dist/chunk-TDNNOR6D.js.map +1 -0
- package/dist/chunk-TSPZOMHC.js +195 -0
- package/dist/chunk-TSPZOMHC.js.map +1 -0
- package/dist/chunk-UNTPVD36.js +55 -0
- package/dist/chunk-UNTPVD36.js.map +1 -0
- package/dist/chunk-VRUJH4BO.js +88 -0
- package/dist/chunk-VRUJH4BO.js.map +1 -0
- package/dist/chunk-VZ7AMAFL.js +76 -0
- package/dist/chunk-VZ7AMAFL.js.map +1 -0
- package/dist/chunk-XFXDXEUN.js +24 -0
- package/dist/chunk-XFXDXEUN.js.map +1 -0
- package/dist/chunk-YZAA4LYG.js +169 -0
- package/dist/chunk-YZAA4LYG.js.map +1 -0
- package/dist/chunk-Z73NYSBZ.js +92 -0
- package/dist/chunk-Z73NYSBZ.js.map +1 -0
- package/dist/chunk-ZJRYBOEE.js +125 -0
- package/dist/chunk-ZJRYBOEE.js.map +1 -0
- package/dist/cli.js +5798 -0
- package/dist/cli.js.map +1 -0
- package/dist/db-BxaevAyc.d.ts +683 -0
- package/dist/index.d.ts +254 -0
- package/dist/index.js +1271 -0
- package/dist/index.js.map +1 -0
- package/dist/postinstall.js +167 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/queries/affected.d.ts +14 -0
- package/dist/queries/affected.js +9 -0
- package/dist/queries/affected.js.map +1 -0
- package/dist/queries/bottlenecks.d.ts +18 -0
- package/dist/queries/bottlenecks.js +8 -0
- package/dist/queries/bottlenecks.js.map +1 -0
- package/dist/queries/by-kind.d.ts +20 -0
- package/dist/queries/by-kind.js +10 -0
- package/dist/queries/by-kind.js.map +1 -0
- package/dist/queries/call-graph.d.ts +13 -0
- package/dist/queries/call-graph.js +9 -0
- package/dist/queries/call-graph.js.map +1 -0
- package/dist/queries/change-surface.d.ts +10 -0
- package/dist/queries/change-surface.js +9 -0
- package/dist/queries/change-surface.js.map +1 -0
- package/dist/queries/clean-signature.d.ts +9 -0
- package/dist/queries/clean-signature.js +7 -0
- package/dist/queries/clean-signature.js.map +1 -0
- package/dist/queries/code.d.ts +17 -0
- package/dist/queries/code.js +9 -0
- package/dist/queries/code.js.map +1 -0
- package/dist/queries/complexity-hotspots.d.ts +19 -0
- package/dist/queries/complexity-hotspots.js +9 -0
- package/dist/queries/complexity-hotspots.js.map +1 -0
- package/dist/queries/complexity.d.ts +13 -0
- package/dist/queries/complexity.js +9 -0
- package/dist/queries/complexity.js.map +1 -0
- package/dist/queries/convergence.d.ts +11 -0
- package/dist/queries/convergence.js +9 -0
- package/dist/queries/convergence.js.map +1 -0
- package/dist/queries/coupling.d.ts +17 -0
- package/dist/queries/coupling.js +9 -0
- package/dist/queries/coupling.js.map +1 -0
- package/dist/queries/cycles.d.ts +16 -0
- package/dist/queries/cycles.js +8 -0
- package/dist/queries/cycles.js.map +1 -0
- package/dist/queries/dataflow.d.ts +19 -0
- package/dist/queries/dataflow.js +9 -0
- package/dist/queries/dataflow.js.map +1 -0
- package/dist/queries/dead.d.ts +10 -0
- package/dist/queries/dead.js +9 -0
- package/dist/queries/dead.js.map +1 -0
- package/dist/queries/deep-chains.d.ts +16 -0
- package/dist/queries/deep-chains.js +8 -0
- package/dist/queries/deep-chains.js.map +1 -0
- package/dist/queries/deps.d.ts +9 -0
- package/dist/queries/deps.js +9 -0
- package/dist/queries/deps.js.map +1 -0
- package/dist/queries/diff-impact.d.ts +13 -0
- package/dist/queries/diff-impact.js +9 -0
- package/dist/queries/diff-impact.js.map +1 -0
- package/dist/queries/doc-coverage.d.ts +14 -0
- package/dist/queries/doc-coverage.js +8 -0
- package/dist/queries/doc-coverage.js.map +1 -0
- package/dist/queries/drift.d.ts +25 -0
- package/dist/queries/drift.js +8 -0
- package/dist/queries/drift.js.map +1 -0
- package/dist/queries/extract-candidates.d.ts +25 -0
- package/dist/queries/extract-candidates.js +9 -0
- package/dist/queries/extract-candidates.js.map +1 -0
- package/dist/queries/fan.d.ts +29 -0
- package/dist/queries/fan.js +14 -0
- package/dist/queries/fan.js.map +1 -0
- package/dist/queries/files.d.ts +6 -0
- package/dist/queries/files.js +7 -0
- package/dist/queries/files.js.map +1 -0
- package/dist/queries/health.d.ts +18 -0
- package/dist/queries/health.js +21 -0
- package/dist/queries/health.js.map +1 -0
- package/dist/queries/hierarchy.d.ts +13 -0
- package/dist/queries/hierarchy.js +8 -0
- package/dist/queries/hierarchy.js.map +1 -0
- package/dist/queries/hotspots.d.ts +13 -0
- package/dist/queries/hotspots.js +8 -0
- package/dist/queries/hotspots.js.map +1 -0
- package/dist/queries/imports.d.ts +19 -0
- package/dist/queries/imports.js +12 -0
- package/dist/queries/imports.js.map +1 -0
- package/dist/queries/index.d.ts +47 -0
- package/dist/queries/index.js +207 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/isolated.d.ts +14 -0
- package/dist/queries/isolated.js +9 -0
- package/dist/queries/isolated.js.map +1 -0
- package/dist/queries/members.d.ts +10 -0
- package/dist/queries/members.js +8 -0
- package/dist/queries/members.js.map +1 -0
- package/dist/queries/methods.d.ts +6 -0
- package/dist/queries/methods.js +8 -0
- package/dist/queries/methods.js.map +1 -0
- package/dist/queries/outline.d.ts +10 -0
- package/dist/queries/outline.js +8 -0
- package/dist/queries/outline.js.map +1 -0
- package/dist/queries/passthrough-candidates.d.ts +18 -0
- package/dist/queries/passthrough-candidates.js +9 -0
- package/dist/queries/passthrough-candidates.js.map +1 -0
- package/dist/queries/redundant-reexports.d.ts +22 -0
- package/dist/queries/redundant-reexports.js +8 -0
- package/dist/queries/redundant-reexports.js.map +1 -0
- package/dist/queries/refs.d.ts +6 -0
- package/dist/queries/refs.js +7 -0
- package/dist/queries/refs.js.map +1 -0
- package/dist/queries/similar-chains.d.ts +29 -0
- package/dist/queries/similar-chains.js +8 -0
- package/dist/queries/similar-chains.js.map +1 -0
- package/dist/queries/similar-files.d.ts +19 -0
- package/dist/queries/similar-files.js +8 -0
- package/dist/queries/similar-files.js.map +1 -0
- package/dist/queries/similar-signatures.d.ts +21 -0
- package/dist/queries/similar-signatures.js +8 -0
- package/dist/queries/similar-signatures.js.map +1 -0
- package/dist/queries/similar.d.ts +34 -0
- package/dist/queries/similar.js +11 -0
- package/dist/queries/similar.js.map +1 -0
- package/dist/queries/slice.d.ts +21 -0
- package/dist/queries/slice.js +9 -0
- package/dist/queries/slice.js.map +1 -0
- package/dist/queries/stale-abstractions.d.ts +18 -0
- package/dist/queries/stale-abstractions.js +9 -0
- package/dist/queries/stale-abstractions.js.map +1 -0
- package/dist/queries/stats.d.ts +6 -0
- package/dist/queries/stats.js +7 -0
- package/dist/queries/stats.js.map +1 -0
- package/dist/queries/surface.d.ts +7 -0
- package/dist/queries/surface.js +8 -0
- package/dist/queries/surface.js.map +1 -0
- package/dist/queries/symbols.d.ts +6 -0
- package/dist/queries/symbols.js +9 -0
- package/dist/queries/symbols.js.map +1 -0
- package/dist/queries/system.d.ts +7 -0
- package/dist/queries/system.js +9 -0
- package/dist/queries/system.js.map +1 -0
- package/dist/queries/test-coverage.d.ts +22 -0
- package/dist/queries/test-coverage.js +11 -0
- package/dist/queries/test-coverage.js.map +1 -0
- package/dist/queries/trace.d.ts +6 -0
- package/dist/queries/trace.js +8 -0
- package/dist/queries/trace.js.map +1 -0
- package/dist/queries/wrapper-candidates.d.ts +17 -0
- package/dist/queries/wrapper-candidates.js +9 -0
- package/dist/queries/wrapper-candidates.js.map +1 -0
- package/dist/reindex-worker.js +368 -0
- package/dist/reindex-worker.js.map +1 -0
- package/docs/AGENT_GUIDE.md +359 -0
- package/package.json +70 -0
- package/reports/debloat/2026-04-10-scip-query-self-audit.md +161 -0
- package/skills/concrete-plan/SKILL.md +318 -0
- package/skills/scip-debloat/SKILL.md +413 -0
- package/skills/scip-explore/SKILL.md +235 -0
- package/skills/scip-verify/SKILL.md +323 -0
- package/src/cli.ts +1480 -0
- package/src/config.ts +117 -0
- package/src/db.ts +127 -0
- package/src/gitignore-filter.ts +143 -0
- package/src/index.ts +11 -0
- package/src/postinstall.ts +8 -0
- package/src/queries/affected.ts +86 -0
- package/src/queries/bottlenecks.ts +67 -0
- package/src/queries/by-kind.ts +204 -0
- package/src/queries/call-graph.ts +66 -0
- package/src/queries/change-surface.ts +110 -0
- package/src/queries/clean-signature.ts +22 -0
- package/src/queries/code.ts +101 -0
- package/src/queries/complexity-hotspots.ts +119 -0
- package/src/queries/complexity.ts +152 -0
- package/src/queries/convergence.ts +82 -0
- package/src/queries/coupling.ts +99 -0
- package/src/queries/cycles.ts +78 -0
- package/src/queries/dataflow.ts +128 -0
- package/src/queries/dead.ts +122 -0
- package/src/queries/deep-chains.ts +59 -0
- package/src/queries/deps.ts +46 -0
- package/src/queries/diff-impact.ts +204 -0
- package/src/queries/doc-coverage.ts +86 -0
- package/src/queries/drift.ts +224 -0
- package/src/queries/extract-candidates.ts +167 -0
- package/src/queries/fan.ts +148 -0
- package/src/queries/files.ts +16 -0
- package/src/queries/health.ts +324 -0
- package/src/queries/hierarchy.ts +49 -0
- package/src/queries/hotspots.ts +53 -0
- package/src/queries/imports.ts +95 -0
- package/src/queries/index.ts +45 -0
- package/src/queries/isolated.ts +67 -0
- package/src/queries/members.ts +54 -0
- package/src/queries/methods.ts +27 -0
- package/src/queries/outline.ts +52 -0
- package/src/queries/passthrough-candidates.ts +94 -0
- package/src/queries/redundant-reexports.ts +170 -0
- package/src/queries/refs.ts +27 -0
- package/src/queries/similar-chains.ts +314 -0
- package/src/queries/similar-files.ts +140 -0
- package/src/queries/similar-signatures.ts +151 -0
- package/src/queries/similar.ts +305 -0
- package/src/queries/slice.ts +154 -0
- package/src/queries/stale-abstractions.ts +82 -0
- package/src/queries/stats.ts +22 -0
- package/src/queries/surface.ts +34 -0
- package/src/queries/symbols.ts +39 -0
- package/src/queries/system.ts +86 -0
- package/src/queries/test-coverage.ts +106 -0
- package/src/queries/trace.ts +55 -0
- package/src/queries/wrapper-candidates.ts +112 -0
- package/src/query-support.ts +226 -0
- package/src/reindex/detect.ts +58 -0
- package/src/reindex/index.ts +153 -0
- package/src/reindex/indexers.ts +220 -0
- package/src/reindex/install.ts +125 -0
- package/src/reindex-worker.ts +35 -0
- package/src/setup.ts +202 -0
- package/src/symbol-parser.ts +278 -0
- package/src/types.ts +654 -0
- package/src/watch.ts +274 -0
- package/tests/gitignore-filter.test.ts +48 -0
- package/tests/queries.test.ts +300 -0
- package/tests/symbol-parser.test.ts +157 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +40 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ScipDatabase } from '../db.js';
|
|
4
|
+
import { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';
|
|
5
|
+
import type { ComplexityResult } from '../types.js';
|
|
6
|
+
import { shortenSymbol } from '../symbol-parser.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-symbol complexity analysis combining source-level branch counting
|
|
10
|
+
* with index-level metrics (fan-in, fan-out, callee count).
|
|
11
|
+
*
|
|
12
|
+
* Branch counting uses language-aware regex. The language is read from
|
|
13
|
+
* the SCIP documents table, so it works for any indexed language.
|
|
14
|
+
*/
|
|
15
|
+
export function complexity(
|
|
16
|
+
db: ScipDatabase,
|
|
17
|
+
symbolPattern: string,
|
|
18
|
+
): ComplexityResult | null {
|
|
19
|
+
const match = findFirstSymbolMatch(db, symbolPattern);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
|
|
22
|
+
// Get language
|
|
23
|
+
const doc = db.get<{ language: string | null }>(
|
|
24
|
+
`SELECT language FROM documents WHERE relative_path = ?`,
|
|
25
|
+
match.relativePath,
|
|
26
|
+
);
|
|
27
|
+
const language = doc?.language ?? 'unknown';
|
|
28
|
+
|
|
29
|
+
// Read source for branch counting
|
|
30
|
+
const filePath = join(db.config.projectRoot, match.relativePath);
|
|
31
|
+
let source = '';
|
|
32
|
+
try {
|
|
33
|
+
const lines = readFileSync(filePath, 'utf-8').split('\n');
|
|
34
|
+
source = lines.slice(match.startLine, match.endLine + 1).join('\n');
|
|
35
|
+
} catch {
|
|
36
|
+
// If we can't read the file, just skip branch counting
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const branches = countBranches(source, language);
|
|
40
|
+
const loc = match.endLine - match.startLine + 1;
|
|
41
|
+
|
|
42
|
+
// Callee count
|
|
43
|
+
const callees = getCalleeRowsForSymbol(db, match);
|
|
44
|
+
const uniqueCallees = new Set(callees.map((c) => c.symbol));
|
|
45
|
+
|
|
46
|
+
// Fan-in
|
|
47
|
+
const fanInRow = db.get<{ c: number }>(
|
|
48
|
+
`SELECT COUNT(DISTINCT c.document_id) AS c
|
|
49
|
+
FROM mentions m
|
|
50
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
51
|
+
WHERE m.symbol_id = ? AND m.role = 0`,
|
|
52
|
+
match.symbolId,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Fan-out (callees in other files)
|
|
56
|
+
const fanOut = new Set(
|
|
57
|
+
callees.filter((c) => c.file !== match.relativePath).map((c) => c.symbol),
|
|
58
|
+
).size;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
symbol: match.symbol,
|
|
62
|
+
shortName: shortenSymbol(match.symbol),
|
|
63
|
+
relativePath: match.relativePath,
|
|
64
|
+
startLine: match.startLine,
|
|
65
|
+
endLine: match.endLine,
|
|
66
|
+
loc,
|
|
67
|
+
branches,
|
|
68
|
+
cyclomaticEstimate: branches + 1,
|
|
69
|
+
calleeCount: uniqueCallees.size,
|
|
70
|
+
fanIn: fanInRow?.c ?? 0,
|
|
71
|
+
fanOut,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Count branch points in source code using language-aware regex.
|
|
77
|
+
* Works across all SCIP-supported languages.
|
|
78
|
+
*/
|
|
79
|
+
function countBranches(source: string, language: string): number {
|
|
80
|
+
// Strip comments and strings to avoid false positives
|
|
81
|
+
const stripped = stripCommentsAndStrings(source);
|
|
82
|
+
let count = 0;
|
|
83
|
+
|
|
84
|
+
// Universal branch keywords (work across most C-family languages)
|
|
85
|
+
const universalPatterns = [
|
|
86
|
+
/\bif\b/g,
|
|
87
|
+
/\belse\s+if\b/g,
|
|
88
|
+
/\belse\b/g,
|
|
89
|
+
/\bfor\b/g,
|
|
90
|
+
/\bwhile\b/g,
|
|
91
|
+
/\bswitch\b/g,
|
|
92
|
+
/\bcase\b/g,
|
|
93
|
+
/\bcatch\b/g,
|
|
94
|
+
/\?\s*[^?]/g, // ternary (but not ??)
|
|
95
|
+
/&&/g,
|
|
96
|
+
/\|\|/g,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const pattern of universalPatterns) {
|
|
100
|
+
const matches = stripped.match(pattern);
|
|
101
|
+
if (matches) count += matches.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Language-specific patterns
|
|
105
|
+
if (language === 'python') {
|
|
106
|
+
const pyPatterns = [/\belif\b/g, /\bexcept\b/g, /\bfinally\b/g];
|
|
107
|
+
for (const p of pyPatterns) {
|
|
108
|
+
const m = stripped.match(p);
|
|
109
|
+
if (m) count += m.length;
|
|
110
|
+
}
|
|
111
|
+
} else if (language === 'rust') {
|
|
112
|
+
const rustPatterns = [/\bmatch\b/g, /=>/g, /\bloop\b/g];
|
|
113
|
+
for (const p of rustPatterns) {
|
|
114
|
+
const m = stripped.match(p);
|
|
115
|
+
if (m) count += m.length;
|
|
116
|
+
}
|
|
117
|
+
} else if (language === 'ruby') {
|
|
118
|
+
const rubyPatterns = [/\belsif\b/g, /\bunless\b/g, /\brescue\b/g, /\bwhen\b/g];
|
|
119
|
+
for (const p of rubyPatterns) {
|
|
120
|
+
const m = stripped.match(p);
|
|
121
|
+
if (m) count += m.length;
|
|
122
|
+
}
|
|
123
|
+
} else if (language === 'go') {
|
|
124
|
+
const goPatterns = [/\bselect\b/g, /\bdefer\b/g];
|
|
125
|
+
for (const p of goPatterns) {
|
|
126
|
+
const m = stripped.match(p);
|
|
127
|
+
if (m) count += m.length;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return count;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Rough strip of comments and string literals to reduce false positives
|
|
136
|
+
* in branch counting. Not perfect but good enough for estimation.
|
|
137
|
+
*/
|
|
138
|
+
function stripCommentsAndStrings(source: string): string {
|
|
139
|
+
return source
|
|
140
|
+
// Block comments
|
|
141
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
142
|
+
// Line comments
|
|
143
|
+
.replace(/\/\/.*/g, '')
|
|
144
|
+
// Python/Ruby line comments
|
|
145
|
+
.replace(/#.*/g, '')
|
|
146
|
+
// Double-quoted strings
|
|
147
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
148
|
+
// Single-quoted strings
|
|
149
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
|
|
150
|
+
// Template literals
|
|
151
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, '``');
|
|
152
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ScipDatabase } from '../db.js';
|
|
2
|
+
import { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';
|
|
3
|
+
import type { ConvergenceResult } from '../types.js';
|
|
4
|
+
import { shortenSymbol } from '../symbol-parser.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Given two similar symbols, show what a consolidated version would look like.
|
|
8
|
+
* The shared callee set becomes the common body. The unique callees become
|
|
9
|
+
* the parameterization points.
|
|
10
|
+
*/
|
|
11
|
+
export function convergence(
|
|
12
|
+
db: ScipDatabase,
|
|
13
|
+
symbolPatternA: string,
|
|
14
|
+
symbolPatternB: string,
|
|
15
|
+
): ConvergenceResult | null {
|
|
16
|
+
const matchA = findFirstSymbolMatch(db, symbolPatternA);
|
|
17
|
+
const matchB = findFirstSymbolMatch(db, symbolPatternB);
|
|
18
|
+
|
|
19
|
+
if (!matchA || !matchB) return null;
|
|
20
|
+
|
|
21
|
+
const calleesA = new Set(
|
|
22
|
+
getCalleeRowsForSymbol(db, matchA).map((r) => r.symbol),
|
|
23
|
+
);
|
|
24
|
+
const calleesB = new Set(
|
|
25
|
+
getCalleeRowsForSymbol(db, matchB).map((r) => r.symbol),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const shared: string[] = [];
|
|
29
|
+
for (const c of calleesA) {
|
|
30
|
+
if (calleesB.has(c)) shared.push(c);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const uniqueA: string[] = [];
|
|
34
|
+
for (const c of calleesA) {
|
|
35
|
+
if (!calleesB.has(c)) uniqueA.push(c);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const uniqueB: string[] = [];
|
|
39
|
+
for (const c of calleesB) {
|
|
40
|
+
if (!calleesA.has(c)) uniqueB.push(c);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const union = new Set([...calleesA, ...calleesB]);
|
|
44
|
+
const similarity = union.size > 0 ? shared.length / union.size : 0;
|
|
45
|
+
|
|
46
|
+
// Generate a consolidation strategy description
|
|
47
|
+
let strategy: string;
|
|
48
|
+
if (uniqueA.length === 0 && uniqueB.length === 0) {
|
|
49
|
+
strategy = 'These functions have identical callee sets. One can replace the other directly.';
|
|
50
|
+
} else if (uniqueA.length === 0) {
|
|
51
|
+
strategy = `A is a subset of B. A can be replaced by calling B (B does everything A does plus more).`;
|
|
52
|
+
} else if (uniqueB.length === 0) {
|
|
53
|
+
strategy = `B is a subset of A. B can be replaced by calling A (A does everything B does plus more).`;
|
|
54
|
+
} else if (uniqueA.length <= 2 && uniqueB.length <= 2) {
|
|
55
|
+
strategy = `Create a shared function with the ${shared.length} common callees. Pass the ${uniqueA.length + uniqueB.length} divergent callees as parameters or strategy callbacks.`;
|
|
56
|
+
} else {
|
|
57
|
+
strategy = `Extract the ${shared.length} shared callees into a common helper. Each function calls the helper plus its own unique logic (${uniqueA.length} callees in A, ${uniqueB.length} in B).`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const locA = matchA.endLine - matchA.startLine + 1;
|
|
61
|
+
const locB = matchB.endLine - matchB.startLine + 1;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
symbolA: {
|
|
65
|
+
symbol: matchA.symbol,
|
|
66
|
+
shortName: shortenSymbol(matchA.symbol),
|
|
67
|
+
file: matchA.relativePath,
|
|
68
|
+
loc: locA,
|
|
69
|
+
},
|
|
70
|
+
symbolB: {
|
|
71
|
+
symbol: matchB.symbol,
|
|
72
|
+
shortName: shortenSymbol(matchB.symbol),
|
|
73
|
+
file: matchB.relativePath,
|
|
74
|
+
loc: locB,
|
|
75
|
+
},
|
|
76
|
+
similarity,
|
|
77
|
+
sharedCallees: shared.map(shortenSymbol),
|
|
78
|
+
uniqueToA: uniqueA.map(shortenSymbol),
|
|
79
|
+
uniqueToB: uniqueB.map(shortenSymbol),
|
|
80
|
+
consolidationStrategy: strategy,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ScipDatabase } from '../db.js';
|
|
2
|
+
import type { CouplingResult } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Measure coupling between two files: how many symbols do they share
|
|
6
|
+
* (symbols defined in one and referenced in the other, or vice versa).
|
|
7
|
+
*/
|
|
8
|
+
export function coupling(
|
|
9
|
+
db: ScipDatabase,
|
|
10
|
+
file1: string,
|
|
11
|
+
file2: string,
|
|
12
|
+
): CouplingResult {
|
|
13
|
+
const row = db.get<{ shared: number }>(
|
|
14
|
+
`SELECT COUNT(DISTINCT gs.id) AS shared
|
|
15
|
+
FROM global_symbols gs
|
|
16
|
+
WHERE (
|
|
17
|
+
-- Defined in file1, referenced in file2
|
|
18
|
+
EXISTS (
|
|
19
|
+
SELECT 1 FROM defn_enclosing_ranges der
|
|
20
|
+
JOIN documents d ON der.document_id = d.id
|
|
21
|
+
WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?
|
|
22
|
+
)
|
|
23
|
+
AND EXISTS (
|
|
24
|
+
SELECT 1 FROM mentions m
|
|
25
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
26
|
+
JOIN documents d ON c.document_id = d.id
|
|
27
|
+
WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?
|
|
28
|
+
)
|
|
29
|
+
) OR (
|
|
30
|
+
-- Defined in file2, referenced in file1
|
|
31
|
+
EXISTS (
|
|
32
|
+
SELECT 1 FROM defn_enclosing_ranges der
|
|
33
|
+
JOIN documents d ON der.document_id = d.id
|
|
34
|
+
WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?
|
|
35
|
+
)
|
|
36
|
+
AND EXISTS (
|
|
37
|
+
SELECT 1 FROM mentions m
|
|
38
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
39
|
+
JOIN documents d ON c.document_id = d.id
|
|
40
|
+
WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?
|
|
41
|
+
)
|
|
42
|
+
)`,
|
|
43
|
+
`%${file1}%`, `%${file2}%`,
|
|
44
|
+
`%${file2}%`, `%${file1}%`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
file1,
|
|
49
|
+
file2,
|
|
50
|
+
sharedSymbols: row?.shared ?? 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find the most coupled file pairs in the codebase.
|
|
56
|
+
*/
|
|
57
|
+
export function topCoupling(
|
|
58
|
+
db: ScipDatabase,
|
|
59
|
+
opts: { limit?: number; scope?: string } = {},
|
|
60
|
+
): CouplingResult[] {
|
|
61
|
+
const { limit = 20, scope } = opts;
|
|
62
|
+
const scopeFilter = scope
|
|
63
|
+
? `AND d1.relative_path LIKE '%${scope}%' AND d2.relative_path LIKE '%${scope}%'`
|
|
64
|
+
: '';
|
|
65
|
+
|
|
66
|
+
// Find file pairs that share the most symbols (one defines, other references)
|
|
67
|
+
const rows = db.all<{
|
|
68
|
+
file1: string;
|
|
69
|
+
file2: string;
|
|
70
|
+
shared: number;
|
|
71
|
+
}>(
|
|
72
|
+
`SELECT
|
|
73
|
+
def_d.relative_path AS file1,
|
|
74
|
+
ref_d.relative_path AS file2,
|
|
75
|
+
COUNT(DISTINCT gs.id) AS shared
|
|
76
|
+
FROM mentions m
|
|
77
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
78
|
+
JOIN documents ref_d ON c.document_id = ref_d.id
|
|
79
|
+
JOIN global_symbols gs ON m.symbol_id = gs.id
|
|
80
|
+
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
81
|
+
JOIN documents def_d ON der.document_id = def_d.id
|
|
82
|
+
WHERE m.role = 0
|
|
83
|
+
AND def_d.id != ref_d.id
|
|
84
|
+
${db.pathExclusionsFor('def_d', 'ref_d')}
|
|
85
|
+
${scopeFilter}
|
|
86
|
+
GROUP BY def_d.id, ref_d.id
|
|
87
|
+
ORDER BY shared DESC
|
|
88
|
+
LIMIT ?`,
|
|
89
|
+
limit,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return rows
|
|
93
|
+
.filter((r) => !db.isIgnored(r.file1) && !db.isIgnored(r.file2))
|
|
94
|
+
.map((r) => ({
|
|
95
|
+
file1: r.file1,
|
|
96
|
+
file2: r.file2,
|
|
97
|
+
sharedSymbols: r.shared,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ScipDatabase } from '../db.js';
|
|
2
|
+
import { buildFileDepGraph } from '../query-support.js';
|
|
3
|
+
import type { CycleResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect circular dependency chains between files.
|
|
7
|
+
* A cycle exists when file A depends on B, B depends on C, and C depends on A.
|
|
8
|
+
*
|
|
9
|
+
* Uses the same dependency edges as the `deps` command (symbol definitions
|
|
10
|
+
* referenced across files), then runs DFS cycle detection.
|
|
11
|
+
*/
|
|
12
|
+
export function cycles(
|
|
13
|
+
db: ScipDatabase,
|
|
14
|
+
opts: { scope?: string; maxDepth?: number } = {},
|
|
15
|
+
): CycleResult[] {
|
|
16
|
+
const { scope, maxDepth = 10 } = opts;
|
|
17
|
+
const graph = buildFileDepGraph(db, scope);
|
|
18
|
+
|
|
19
|
+
// DFS cycle detection
|
|
20
|
+
const allCycles: CycleResult[] = [];
|
|
21
|
+
const visited = new Set<string>();
|
|
22
|
+
const inStack = new Set<string>();
|
|
23
|
+
const stack: string[] = [];
|
|
24
|
+
|
|
25
|
+
function dfs(node: string, depth: number): void {
|
|
26
|
+
if (depth > maxDepth) return;
|
|
27
|
+
if (inStack.has(node)) {
|
|
28
|
+
// Found a cycle — extract it from the stack
|
|
29
|
+
const cycleStart = stack.indexOf(node);
|
|
30
|
+
if (cycleStart !== -1) {
|
|
31
|
+
const cyclePath = stack.slice(cycleStart).concat(node);
|
|
32
|
+
// Normalize: start from the lexicographically smallest file
|
|
33
|
+
const minIdx = cyclePath.indexOf(
|
|
34
|
+
cyclePath.reduce((a, b) => (a < b ? a : b)),
|
|
35
|
+
);
|
|
36
|
+
const normalized = [
|
|
37
|
+
...cyclePath.slice(minIdx, -1),
|
|
38
|
+
...cyclePath.slice(0, minIdx),
|
|
39
|
+
cyclePath[minIdx]!,
|
|
40
|
+
];
|
|
41
|
+
// Deduplicate
|
|
42
|
+
const key = normalized.join(' -> ');
|
|
43
|
+
if (!seenCycles.has(key)) {
|
|
44
|
+
seenCycles.add(key);
|
|
45
|
+
allCycles.push({ path: normalized });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (visited.has(node)) return;
|
|
51
|
+
|
|
52
|
+
visited.add(node);
|
|
53
|
+
inStack.add(node);
|
|
54
|
+
stack.push(node);
|
|
55
|
+
|
|
56
|
+
const neighbors = graph.get(node);
|
|
57
|
+
if (neighbors) {
|
|
58
|
+
for (const neighbor of neighbors) {
|
|
59
|
+
dfs(neighbor, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stack.pop();
|
|
64
|
+
inStack.delete(node);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const seenCycles = new Set<string>();
|
|
68
|
+
for (const node of graph.keys()) {
|
|
69
|
+
if (!visited.has(node)) {
|
|
70
|
+
dfs(node, 0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sort by cycle length (shorter cycles are more actionable)
|
|
75
|
+
allCycles.sort((a, b) => a.path.length - b.path.length);
|
|
76
|
+
|
|
77
|
+
return allCycles;
|
|
78
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ScipDatabase } from '../db.js';
|
|
2
|
+
import { findFirstSymbolMatch } from '../query-support.js';
|
|
3
|
+
import type { DataflowResult } from '../types.js';
|
|
4
|
+
import { shortenSymbol } from '../symbol-parser.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reference-level dataflow analysis: where does data around this symbol
|
|
8
|
+
* come from and where does it go?
|
|
9
|
+
*
|
|
10
|
+
* This is not value-level dataflow (we can't trace x = foo(); bar(x);
|
|
11
|
+
* as a chain). Instead it shows:
|
|
12
|
+
* - Where the symbol is defined and used
|
|
13
|
+
* - What other symbols appear in the same enclosing scope (co-occurring data)
|
|
14
|
+
* - What feeds into the function that defines it (producers)
|
|
15
|
+
* - What consumes the function that uses it (consumers)
|
|
16
|
+
*
|
|
17
|
+
* Language-agnostic: works with any SCIP index.
|
|
18
|
+
*/
|
|
19
|
+
export function dataflow(
|
|
20
|
+
db: ScipDatabase,
|
|
21
|
+
symbolPattern: string,
|
|
22
|
+
): DataflowResult | null {
|
|
23
|
+
const match = findFirstSymbolMatch(db, symbolPattern);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
|
|
26
|
+
// Definition sites (role=1)
|
|
27
|
+
const defSites = db.all<{ file: string; line: number }>(
|
|
28
|
+
`SELECT d.relative_path AS file, c.start_line AS line
|
|
29
|
+
FROM mentions m
|
|
30
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
31
|
+
JOIN documents d ON c.document_id = d.id
|
|
32
|
+
WHERE m.symbol_id = ? AND m.role = 1
|
|
33
|
+
ORDER BY d.relative_path, c.start_line`,
|
|
34
|
+
match.symbolId,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Usage sites (role=0) with enclosing symbol
|
|
38
|
+
const usageSites = db.all<{
|
|
39
|
+
file: string;
|
|
40
|
+
line: number;
|
|
41
|
+
enclosing_symbol: string | null;
|
|
42
|
+
}>(
|
|
43
|
+
`SELECT d.relative_path AS file, c.start_line AS line,
|
|
44
|
+
(SELECT enc_gs.symbol
|
|
45
|
+
FROM defn_enclosing_ranges enc_der
|
|
46
|
+
JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
|
|
47
|
+
WHERE enc_der.document_id = d.id
|
|
48
|
+
AND enc_der.start_line <= c.start_line
|
|
49
|
+
AND enc_der.end_line >= c.end_line
|
|
50
|
+
ORDER BY (enc_der.end_line - enc_der.start_line) ASC
|
|
51
|
+
LIMIT 1
|
|
52
|
+
) AS enclosing_symbol
|
|
53
|
+
FROM mentions m
|
|
54
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
55
|
+
JOIN documents d ON c.document_id = d.id
|
|
56
|
+
WHERE m.symbol_id = ? AND m.role = 0
|
|
57
|
+
${db.pathExclusionsFor('d')}
|
|
58
|
+
ORDER BY d.relative_path, c.start_line`,
|
|
59
|
+
match.symbolId,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Producers: other symbols referenced within the same function that defines our target
|
|
63
|
+
const producers = db.all<{ symbol: string; file: string }>(
|
|
64
|
+
`SELECT DISTINCT other_gs.symbol, other_d.relative_path AS file
|
|
65
|
+
FROM mentions other_m
|
|
66
|
+
JOIN chunks other_c ON other_m.chunk_id = other_c.id
|
|
67
|
+
JOIN global_symbols other_gs ON other_m.symbol_id = other_gs.id
|
|
68
|
+
JOIN defn_enclosing_ranges other_der ON other_gs.id = other_der.symbol_id
|
|
69
|
+
JOIN documents other_d ON other_der.document_id = other_d.id
|
|
70
|
+
WHERE other_c.document_id = ?
|
|
71
|
+
AND other_c.start_line >= ? AND other_c.end_line <= ?
|
|
72
|
+
AND other_m.role = 0
|
|
73
|
+
AND other_gs.id != ?
|
|
74
|
+
${db.symbolNoiseFor('other_gs')}
|
|
75
|
+
${db.pathExclusionsFor('other_d')}
|
|
76
|
+
ORDER BY other_d.relative_path
|
|
77
|
+
LIMIT 30`,
|
|
78
|
+
match.documentId, match.startLine, match.endLine, match.symbolId,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Consumers: symbols exported/defined by functions that reference our target
|
|
82
|
+
// (what does the data flow into after being used)
|
|
83
|
+
const consumers = db.all<{ symbol: string; file: string }>(
|
|
84
|
+
`SELECT DISTINCT consumer_gs.symbol, consumer_d.relative_path AS file
|
|
85
|
+
FROM mentions ref_m
|
|
86
|
+
JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
|
|
87
|
+
JOIN documents ref_d ON ref_c.document_id = ref_d.id
|
|
88
|
+
-- Find the enclosing function at each usage site
|
|
89
|
+
JOIN defn_enclosing_ranges enc_der
|
|
90
|
+
ON enc_der.document_id = ref_d.id
|
|
91
|
+
AND enc_der.start_line <= ref_c.start_line
|
|
92
|
+
AND enc_der.end_line >= ref_c.end_line
|
|
93
|
+
JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
|
|
94
|
+
-- Find other symbols defined by that enclosing function's file
|
|
95
|
+
JOIN mentions consumer_m ON consumer_m.symbol_id = enc_gs.id AND consumer_m.role = 0
|
|
96
|
+
JOIN chunks consumer_c ON consumer_m.chunk_id = consumer_c.id
|
|
97
|
+
JOIN documents consumer_d ON consumer_c.document_id = consumer_d.id
|
|
98
|
+
JOIN global_symbols consumer_gs ON consumer_m.symbol_id = consumer_gs.id
|
|
99
|
+
WHERE ref_m.symbol_id = ? AND ref_m.role = 0
|
|
100
|
+
AND consumer_d.id != ref_d.id
|
|
101
|
+
${db.symbolNoiseFor('consumer_gs')}
|
|
102
|
+
${db.pathExclusionsFor('consumer_d')}
|
|
103
|
+
ORDER BY consumer_d.relative_path
|
|
104
|
+
LIMIT 30`,
|
|
105
|
+
match.symbolId,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
symbol: match.symbol,
|
|
110
|
+
shortName: shortenSymbol(match.symbol),
|
|
111
|
+
relativePath: match.relativePath,
|
|
112
|
+
definitionSites: defSites.filter((s) => !db.isIgnored(s.file)),
|
|
113
|
+
usageSites: usageSites
|
|
114
|
+
.filter((s) => !db.isIgnored(s.file))
|
|
115
|
+
.map((s) => ({
|
|
116
|
+
file: s.file,
|
|
117
|
+
line: s.line,
|
|
118
|
+
enclosingSymbol: s.enclosing_symbol ?? '(top-level)',
|
|
119
|
+
enclosingShort: s.enclosing_symbol ? shortenSymbol(s.enclosing_symbol) : '(top-level)',
|
|
120
|
+
})),
|
|
121
|
+
producers: producers
|
|
122
|
+
.filter((p) => !db.isIgnored(p.file))
|
|
123
|
+
.map((p) => ({ symbol: p.symbol, shortName: shortenSymbol(p.symbol), file: p.file })),
|
|
124
|
+
consumers: consumers
|
|
125
|
+
.filter((c) => !db.isIgnored(c.file))
|
|
126
|
+
.map((c) => ({ symbol: c.symbol, shortName: shortenSymbol(c.symbol), file: c.file })),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ScipDatabase } from '../db.js';
|
|
2
|
+
import { TEST_SUPPORT_PATH_PATTERNS, testFileExclusionSql } from '../query-support.js';
|
|
3
|
+
import type { DeadOptions, DeadSymbolResult, DeadSummary } from '../types.js';
|
|
4
|
+
import { shortenSymbol } from '../symbol-parser.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find dead exports: symbols defined locally with no cross-file references.
|
|
8
|
+
* Language-agnostic — works with any SCIP index.
|
|
9
|
+
*/
|
|
10
|
+
export function dead(db: ScipDatabase, opts: DeadOptions = {}): DeadSummary {
|
|
11
|
+
const {
|
|
12
|
+
scope,
|
|
13
|
+
minLoc = 1,
|
|
14
|
+
includeTests = false,
|
|
15
|
+
skipBarrels = false,
|
|
16
|
+
includeMembers = false,
|
|
17
|
+
} = opts;
|
|
18
|
+
|
|
19
|
+
const params: unknown[] = [minLoc];
|
|
20
|
+
let testFileExclusions = '';
|
|
21
|
+
let memberExclusion = '';
|
|
22
|
+
|
|
23
|
+
if (scope) {
|
|
24
|
+
params.push(`%${scope}%`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!includeTests) {
|
|
28
|
+
testFileExclusions = `
|
|
29
|
+
AND ${testFileExclusionSql('d', TEST_SUPPORT_PATH_PATTERNS)}
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!includeMembers) {
|
|
34
|
+
memberExclusion = `AND gs.symbol NOT LIKE '%#%'`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Barrel file exclusion for the NOT EXISTS subquery
|
|
38
|
+
const barrelExclusions = skipBarrels
|
|
39
|
+
? `AND ref_d.relative_path NOT LIKE '%/index.ts'
|
|
40
|
+
AND ref_d.relative_path NOT LIKE '%/index.js'
|
|
41
|
+
AND ref_d.relative_path NOT LIKE '%/mod.rs'
|
|
42
|
+
AND ref_d.relative_path NOT LIKE '%/__init__.py'`
|
|
43
|
+
: '';
|
|
44
|
+
|
|
45
|
+
const sql = `
|
|
46
|
+
SELECT
|
|
47
|
+
d.relative_path,
|
|
48
|
+
der.start_line,
|
|
49
|
+
der.end_line,
|
|
50
|
+
(der.end_line - der.start_line + 1) AS loc,
|
|
51
|
+
gs.symbol,
|
|
52
|
+
(SELECT COUNT(*) FROM mentions m2
|
|
53
|
+
JOIN chunks c2 ON m2.chunk_id = c2.id
|
|
54
|
+
WHERE m2.symbol_id = gs.id AND m2.role = 0 AND c2.document_id = d.id
|
|
55
|
+
) AS same_file_refs
|
|
56
|
+
FROM global_symbols gs
|
|
57
|
+
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
58
|
+
JOIN documents d ON der.document_id = d.id
|
|
59
|
+
WHERE 1 = 1
|
|
60
|
+
${db.pathExclusionsFor('d')}
|
|
61
|
+
${db.symbolNoiseFor('gs')}
|
|
62
|
+
AND (der.end_line - der.start_line + 1) >= ?
|
|
63
|
+
${scope ? 'AND d.relative_path LIKE ?' : ''}
|
|
64
|
+
${testFileExclusions}
|
|
65
|
+
${memberExclusion}
|
|
66
|
+
AND NOT EXISTS (
|
|
67
|
+
SELECT 1
|
|
68
|
+
FROM mentions ref_m
|
|
69
|
+
JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
|
|
70
|
+
JOIN documents ref_d ON ref_c.document_id = ref_d.id
|
|
71
|
+
WHERE ref_m.symbol_id = gs.id
|
|
72
|
+
AND ref_m.role = 0
|
|
73
|
+
AND ref_d.id != d.id
|
|
74
|
+
${barrelExclusions}
|
|
75
|
+
)
|
|
76
|
+
ORDER BY (der.end_line - der.start_line + 1) DESC, d.relative_path, der.start_line
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const rows = db.all<{
|
|
80
|
+
relative_path: string;
|
|
81
|
+
start_line: number;
|
|
82
|
+
end_line: number;
|
|
83
|
+
loc: number;
|
|
84
|
+
symbol: string;
|
|
85
|
+
same_file_refs: number;
|
|
86
|
+
}>(sql, ...params);
|
|
87
|
+
|
|
88
|
+
let deadCodeCount = 0;
|
|
89
|
+
let fileInternalCount = 0;
|
|
90
|
+
let totalLoc = 0;
|
|
91
|
+
|
|
92
|
+
const symbols: DeadSymbolResult[] = rows
|
|
93
|
+
.filter((r) => !db.isIgnored(r.relative_path))
|
|
94
|
+
.map((r) => {
|
|
95
|
+
// dead-code: zero references anywhere (not even in same file) — safe to delete
|
|
96
|
+
// file-internal: referenced within same file but never cross-file —
|
|
97
|
+
// may be a private helper (fine) or a forgotten export (needs review)
|
|
98
|
+
const kind = r.same_file_refs === 0 ? 'dead-code' : 'file-internal';
|
|
99
|
+
if (kind === 'dead-code') deadCodeCount++;
|
|
100
|
+
else fileInternalCount++;
|
|
101
|
+
totalLoc += r.loc;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
relativePath: r.relative_path,
|
|
105
|
+
startLine: r.start_line,
|
|
106
|
+
endLine: r.end_line,
|
|
107
|
+
loc: r.loc,
|
|
108
|
+
symbol: r.symbol,
|
|
109
|
+
shortName: shortenSymbol(r.symbol),
|
|
110
|
+
sameFileRefs: r.same_file_refs,
|
|
111
|
+
kind,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
symbols,
|
|
117
|
+
totalCount: symbols.length,
|
|
118
|
+
deadCodeCount,
|
|
119
|
+
fileInternalCount,
|
|
120
|
+
totalLoc,
|
|
121
|
+
};
|
|
122
|
+
}
|