ucn 3.7.47 → 3.8.1
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/.claude/skills/ucn/SKILL.md +32 -4
- package/README.md +16 -13
- package/cli/index.js +100 -11
- package/core/callers.js +36 -2
- package/core/deadcode.js +103 -3
- package/core/execute.js +97 -0
- package/core/output.js +399 -0
- package/core/project.js +931 -52
- package/core/registry.js +17 -7
- package/languages/go.js +7 -1
- package/languages/java.js +36 -2
- package/languages/javascript.js +199 -8
- package/languages/python.js +8 -2
- package/languages/rust.js +75 -4
- package/mcp/server.js +48 -3
- package/package.json +1 -1
|
@@ -49,7 +49,17 @@ ucn impact score_trend --exclude=test # Only production callers
|
|
|
49
49
|
|
|
50
50
|
Replaces: grep for the function name → manually filtering definitions vs calls vs imports → reading context around each match.
|
|
51
51
|
|
|
52
|
-
### 3. `
|
|
52
|
+
### 3. `blast` — Transitive blast radius
|
|
53
|
+
|
|
54
|
+
Walks UP the caller chain recursively. Shows the full tree of functions affected transitively if you change something. Like `impact` but recursive — answers "what breaks if I change this, including indirect callers?"
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ucn blast helper # callers of callers (depth 3)
|
|
58
|
+
ucn blast helper --depth=5 # deeper chain
|
|
59
|
+
ucn blast helper --exclude=test # skip test callers
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4. `trace` — Understand execution flow (downward)
|
|
53
63
|
|
|
54
64
|
Draws the call tree downward from any function. Compact by default; setting `--depth=N` shows the full tree to that depth with all children expanded.
|
|
55
65
|
|
|
@@ -61,7 +71,7 @@ ucn trace generate_report --all # all children at default depth
|
|
|
61
71
|
|
|
62
72
|
Shows the entire pipeline — what `generate_report` calls, what those functions call, etc. — as an indented tree. No file reading needed. Invaluable for understanding orchestrator functions or entry points.
|
|
63
73
|
|
|
64
|
-
###
|
|
74
|
+
### 5. `fn` / `class` — Extract without reading the whole file
|
|
65
75
|
|
|
66
76
|
Pull one or more functions out of a large file. Supports comma-separated names for bulk extraction.
|
|
67
77
|
|
|
@@ -71,7 +81,7 @@ ucn fn parse,format,validate # Extract multiple functions in one call
|
|
|
71
81
|
ucn class MarketDataFetcher
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
###
|
|
84
|
+
### 6. `deadcode` — Find unused code
|
|
75
85
|
|
|
76
86
|
Lists all functions and classes with zero callers across the project.
|
|
77
87
|
|
|
@@ -86,6 +96,9 @@ ucn deadcode --exclude=test # Skip test files (most useful)
|
|
|
86
96
|
|-----------|---------|-------------|
|
|
87
97
|
| Quick callers + callees list | `ucn context <name>` | Who calls it and what it calls. Results are numbered for `expand`. Use instead of `about` when you just need the call graph, not source code |
|
|
88
98
|
| Need function + all its helpers inline | `ucn smart <name>` | Returns function source with every helper it calls expanded below it. Use instead of `about` when you need code, not metadata |
|
|
99
|
+
| Full transitive blast radius | `ucn blast <name> --depth=5` | Callers of callers — the full chain of what breaks if you change something |
|
|
100
|
+
| How execution reaches a function | `ucn reverse-trace <name>` | Walk UP callers to entry points (★ marked). Shows how code flows to this function. Default depth=5 |
|
|
101
|
+
| Which tests to run after a change | `ucn affected-tests <name>` | Blast + test detection: shows test files, coverage %, uncovered functions. Use `--depth=N` to control depth |
|
|
89
102
|
| What changed and who's affected | `ucn diff-impact --base=main` | Shows changed functions + their callers from git diff |
|
|
90
103
|
| Checking if a refactor broke signatures | `ucn verify <name>` | Validates all call sites match the function's parameter count |
|
|
91
104
|
| Understanding a file's role in the project | `ucn imports <file>` | What it depends on |
|
|
@@ -97,10 +110,16 @@ ucn deadcode --exclude=test # Skip test files (most useful)
|
|
|
97
110
|
| Text search with context | `ucn search term --context=3` | Like grep -C 3, shows surrounding lines |
|
|
98
111
|
| Regex search (default) | `ucn search '\d+'` | Search supports regex by default (alternation, character classes, etc.) |
|
|
99
112
|
| Text search filtered | `ucn search term --exclude=test` | Search only in matching files |
|
|
113
|
+
| Structural search (index) | `ucn search --type=function --param=Request` | Query the symbol table, not text. Finds functions by param, return type, decorator, etc. |
|
|
114
|
+
| Find all db.* calls | `ucn search --type=call --receiver=db` | Search call sites by receiver — something grep can't do |
|
|
115
|
+
| Find exported functions | `ucn search --type=function --exported` | Only exported/public symbols |
|
|
116
|
+
| Find unused symbols | `ucn search --type=function --unused` | Mini deadcode: zero callers |
|
|
117
|
+
| Find decorated functions | `ucn search --decorator=Route` | Functions/classes with a specific decorator/annotation |
|
|
100
118
|
| Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
|
|
101
119
|
| Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
|
|
102
120
|
| Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
|
|
103
121
|
| File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Setting `--depth=N` expands all children. Can be noisy — use depth=1 for large projects. For function-level flow, use `trace` instead |
|
|
122
|
+
| Are there circular dependencies? | `ucn circular-deps` | Detect circular import chains. `--file=<pattern>` filters to cycles involving a file. `--exclude=test` skips test files |
|
|
104
123
|
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
|
|
105
124
|
| Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
|
|
106
125
|
| Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
|
|
@@ -153,6 +172,13 @@ ucn [target] <command> [name] [--flags]
|
|
|
153
172
|
| `--include-uncertain` | Include ambiguous/uncertain matches in `context`/`smart`/`about` |
|
|
154
173
|
| `--include-exported` | Include exported symbols in `deadcode` results |
|
|
155
174
|
| `--include-decorated` | Include decorated/annotated symbols in `deadcode` results |
|
|
175
|
+
| `--type=<kind>` | Structural search: `function`, `class`, `call`, `method`, `type`. Triggers index query instead of text grep |
|
|
176
|
+
| `--param=<name>` | Structural search: filter by parameter name or type (e.g., `--param=Request`) |
|
|
177
|
+
| `--receiver=<name>` | Structural search: filter calls by receiver (e.g., `--receiver=db` for all db.* calls) |
|
|
178
|
+
| `--returns=<type>` | Structural search: filter by return type (e.g., `--returns=error`) |
|
|
179
|
+
| `--decorator=<name>` | Structural search: filter by decorator/annotation (e.g., `--decorator=Route`) |
|
|
180
|
+
| `--exported` | Structural search: only exported/public symbols |
|
|
181
|
+
| `--unused` | Structural search: only symbols with zero callers |
|
|
156
182
|
|
|
157
183
|
## Workflow Integration
|
|
158
184
|
|
|
@@ -164,7 +190,9 @@ ucn trace problematic_function --depth=2 # See what it calls
|
|
|
164
190
|
|
|
165
191
|
**Before modifying a function:**
|
|
166
192
|
```bash
|
|
167
|
-
ucn impact the_function # Who will break?
|
|
193
|
+
ucn impact the_function # Who will break? (direct callers)
|
|
194
|
+
ucn blast the_function # Who will break? (full transitive chain)
|
|
195
|
+
ucn affected-tests the_function # Which tests to run after the change?
|
|
168
196
|
ucn smart the_function # See it + its helpers
|
|
169
197
|
# ... make changes ...
|
|
170
198
|
ucn verify the_function # Did all call sites survive?
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Supports JS/TS, Python, Go, Rust, Java, and HTML. Runs locally.
|
|
|
38
38
|
│
|
|
39
39
|
┌──────┴──────┐
|
|
40
40
|
│ UCN Engine │
|
|
41
|
-
│
|
|
41
|
+
│ commands │
|
|
42
42
|
│ tree-sitter │
|
|
43
43
|
└─────────────┘
|
|
44
44
|
```
|
|
@@ -138,7 +138,7 @@ VS Code uses `.vscode/mcp.json`:
|
|
|
138
138
|
|
|
139
139
|
</details>
|
|
140
140
|
|
|
141
|
-
All
|
|
141
|
+
All commands ship as a single MCP tool - under 2KB of context.
|
|
142
142
|
|
|
143
143
|
### Agent Skill (no server needed)
|
|
144
144
|
|
|
@@ -265,28 +265,31 @@ ucn deadcode --exclude=test # what can be deleted?
|
|
|
265
265
|
|
|
266
266
|
---
|
|
267
267
|
|
|
268
|
-
## All
|
|
268
|
+
## All commands
|
|
269
269
|
|
|
270
270
|
```
|
|
271
271
|
UNDERSTAND MODIFY SAFELY
|
|
272
272
|
───────────────────── ─────────────────────
|
|
273
273
|
about full picture impact all call sites
|
|
274
|
-
context callers + callees
|
|
275
|
-
smart function + helpers
|
|
276
|
-
trace call tree
|
|
274
|
+
context callers + callees blast transitive impact
|
|
275
|
+
smart function + helpers diff-impact git diff + callers
|
|
276
|
+
trace call tree verify signature check
|
|
277
|
+
reverse-trace callers → root plan refactor preview
|
|
277
278
|
|
|
278
279
|
FIND & EXTRACT ARCHITECTURE
|
|
279
280
|
───────────────────── ─────────────────────
|
|
280
281
|
find locate definitions imports file dependencies
|
|
281
282
|
usages all occurrences exporters reverse dependencies
|
|
282
283
|
fn extract function graph dependency tree
|
|
283
|
-
class extract class
|
|
284
|
-
toc project overview
|
|
285
|
-
deadcode unused code
|
|
286
|
-
search text search
|
|
287
|
-
example best usage example
|
|
288
|
-
lines extract line range
|
|
289
|
-
expand drill into context
|
|
284
|
+
class extract class circular-deps import cycles
|
|
285
|
+
toc project overview related sibling functions
|
|
286
|
+
deadcode unused code tests find test coverage
|
|
287
|
+
search text search affected-tests tests for changes
|
|
288
|
+
example best usage example stacktrace error trace context
|
|
289
|
+
lines extract line range api public API surface
|
|
290
|
+
expand drill into context typedef type definitions
|
|
291
|
+
file-exports file's exports
|
|
292
|
+
stats project stats
|
|
290
293
|
```
|
|
291
294
|
|
|
292
295
|
---
|
package/cli/index.js
CHANGED
|
@@ -104,6 +104,14 @@ function parseFlags(tokens) {
|
|
|
104
104
|
className: getValueFlag('--class-name'),
|
|
105
105
|
limit: parseInt(getValueFlag('--limit') || '0') || undefined,
|
|
106
106
|
maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
|
|
107
|
+
// Structural search flags
|
|
108
|
+
type: getValueFlag('--type'),
|
|
109
|
+
param: getValueFlag('--param'),
|
|
110
|
+
receiver: getValueFlag('--receiver'),
|
|
111
|
+
returns: getValueFlag('--returns'),
|
|
112
|
+
decorator: getValueFlag('--decorator'),
|
|
113
|
+
exported: tokens.includes('--exported'),
|
|
114
|
+
unused: tokens.includes('--unused'),
|
|
107
115
|
};
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -128,7 +136,8 @@ const knownFlags = new Set([
|
|
|
128
136
|
'--default', '--top', '--no-follow-symlinks',
|
|
129
137
|
'--base', '--staged', '--stack',
|
|
130
138
|
'--regex', '--no-regex', '--functions',
|
|
131
|
-
'--max-lines', '--class-name', '--limit', '--max-files'
|
|
139
|
+
'--max-lines', '--class-name', '--limit', '--max-files',
|
|
140
|
+
'--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused'
|
|
132
141
|
]);
|
|
133
142
|
|
|
134
143
|
// Handle help flag
|
|
@@ -287,7 +296,9 @@ function runFileCommand(filePath, command, arg) {
|
|
|
287
296
|
|
|
288
297
|
// Require arg for commands that need it
|
|
289
298
|
const needsArg = { fn: 'fn <name>', class: 'class <name>', find: 'find <name>', usages: 'usages <name>', search: 'search <term>', lines: 'lines <start-end>', typedef: 'typedef <name>' };
|
|
290
|
-
|
|
299
|
+
// Structural search doesn't require term
|
|
300
|
+
const isStructural = flags.type || flags.param || flags.receiver || flags.returns || flags.decorator || flags.exported || flags.unused;
|
|
301
|
+
if (needsArg[canonical] && !(canonical === 'search' && isStructural)) {
|
|
291
302
|
requireArg(arg, `Usage: ucn <file> ${needsArg[canonical]}`);
|
|
292
303
|
}
|
|
293
304
|
|
|
@@ -342,7 +353,11 @@ function runFileCommand(filePath, command, arg) {
|
|
|
342
353
|
printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
|
|
343
354
|
break;
|
|
344
355
|
case 'search':
|
|
345
|
-
|
|
356
|
+
if (result && result.meta && result.meta.mode === 'structural') {
|
|
357
|
+
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
358
|
+
} else {
|
|
359
|
+
printOutput(result, r => output.formatSearchJson(r, arg), r => output.formatSearch(r, arg));
|
|
360
|
+
}
|
|
346
361
|
break;
|
|
347
362
|
case 'typedef':
|
|
348
363
|
printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
|
|
@@ -537,6 +552,13 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
537
552
|
break;
|
|
538
553
|
}
|
|
539
554
|
|
|
555
|
+
case 'blast': {
|
|
556
|
+
const { ok, result, error } = execute(index, 'blast', { name: arg, ...flags });
|
|
557
|
+
if (!ok) fail(error);
|
|
558
|
+
printOutput(result, output.formatBlastJson, output.formatBlast);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
|
|
540
562
|
case 'plan': {
|
|
541
563
|
const { ok, result, error } = execute(index, 'plan', { name: arg, ...flags });
|
|
542
564
|
if (!ok) fail(error);
|
|
@@ -551,6 +573,13 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
551
573
|
break;
|
|
552
574
|
}
|
|
553
575
|
|
|
576
|
+
case 'reverseTrace': {
|
|
577
|
+
const { ok, result, error } = execute(index, 'reverseTrace', { name: arg, ...flags });
|
|
578
|
+
if (!ok) fail(error);
|
|
579
|
+
printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
554
583
|
case 'stacktrace': {
|
|
555
584
|
const { ok, result, error } = execute(index, 'stacktrace', { stack: arg });
|
|
556
585
|
if (!ok) fail(error);
|
|
@@ -646,6 +675,13 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
646
675
|
break;
|
|
647
676
|
}
|
|
648
677
|
|
|
678
|
+
case 'circularDeps': {
|
|
679
|
+
const { ok, result, error } = execute(index, 'circularDeps', { file: flags.file, exclude: flags.exclude });
|
|
680
|
+
if (!ok) fail(error);
|
|
681
|
+
printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
|
|
649
685
|
// ── Remaining commands ──────────────────────────────────────────
|
|
650
686
|
|
|
651
687
|
case 'typedef': {
|
|
@@ -668,6 +704,13 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
668
704
|
break;
|
|
669
705
|
}
|
|
670
706
|
|
|
707
|
+
case 'affectedTests': {
|
|
708
|
+
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...flags });
|
|
709
|
+
if (!ok) fail(error);
|
|
710
|
+
printOutput(result, output.formatAffectedTestsJson, output.formatAffectedTests);
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
|
|
671
714
|
case 'api': {
|
|
672
715
|
const { ok, result, error, note } = execute(index, 'api', { file: arg || flags.file, limit: flags.limit });
|
|
673
716
|
if (!ok) fail(error);
|
|
@@ -680,12 +723,16 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
680
723
|
}
|
|
681
724
|
|
|
682
725
|
case 'search': {
|
|
683
|
-
const { ok, result, error } = execute(index, 'search', { term: arg, ...flags });
|
|
726
|
+
const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...flags });
|
|
684
727
|
if (!ok) fail(error);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
728
|
+
if (structural) {
|
|
729
|
+
printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
|
|
730
|
+
} else {
|
|
731
|
+
printOutput(result,
|
|
732
|
+
r => output.formatSearchJson(r, arg),
|
|
733
|
+
r => output.formatSearch(r, arg)
|
|
734
|
+
);
|
|
735
|
+
}
|
|
689
736
|
break;
|
|
690
737
|
}
|
|
691
738
|
|
|
@@ -987,13 +1034,15 @@ Usage:
|
|
|
987
1034
|
(Default output is text; add --json for machine-readable JSON)
|
|
988
1035
|
|
|
989
1036
|
═══════════════════════════════════════════════════════════════════════════════
|
|
990
|
-
UNDERSTAND CODE
|
|
1037
|
+
UNDERSTAND CODE
|
|
991
1038
|
═══════════════════════════════════════════════════════════════════════════════
|
|
992
1039
|
about <name> RECOMMENDED: Full picture (definition, callers, callees, tests, code)
|
|
993
1040
|
context <name> Who calls this + what it calls (numbered for expand)
|
|
994
1041
|
smart <name> Function + all dependencies inline
|
|
995
1042
|
impact <name> What breaks if changed (call sites grouped by file)
|
|
1043
|
+
blast <name> Transitive blast radius (callers of callers, --depth=N)
|
|
996
1044
|
trace <name> Call tree visualization (--depth=N expands all children)
|
|
1045
|
+
reverse-trace <name> Upward call chain to entry points (--depth=N, default 5)
|
|
997
1046
|
related <name> Find similar functions (same file, shared deps)
|
|
998
1047
|
example <name> Best usage example with context
|
|
999
1048
|
|
|
@@ -1004,7 +1053,9 @@ FIND CODE
|
|
|
1004
1053
|
usages <name> All usages grouped: definitions, calls, imports, references
|
|
1005
1054
|
toc Table of contents (compact; --detailed lists all symbols)
|
|
1006
1055
|
search <term> Text search (regex default, --context=N, --exclude=, --in=)
|
|
1056
|
+
Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
|
|
1007
1057
|
tests <name> Find test files for a function
|
|
1058
|
+
affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
|
|
1008
1059
|
|
|
1009
1060
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1010
1061
|
EXTRACT CODE
|
|
@@ -1021,6 +1072,7 @@ FILE DEPENDENCIES
|
|
|
1021
1072
|
exporters <file> Who imports this file
|
|
1022
1073
|
file-exports <file> What does file export
|
|
1023
1074
|
graph <file> Full dependency tree (--depth=N, --direction=imports|importers|both)
|
|
1075
|
+
circular-deps Detect circular import chains (--file=, --exclude=)
|
|
1024
1076
|
|
|
1025
1077
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1026
1078
|
REFACTORING HELPERS
|
|
@@ -1128,18 +1180,23 @@ Commands:
|
|
|
1128
1180
|
expand <N> Show code for item N from context
|
|
1129
1181
|
smart <name> Function + dependencies
|
|
1130
1182
|
impact <name> What breaks if changed
|
|
1183
|
+
blast <name> Transitive blast radius (--depth=N)
|
|
1131
1184
|
trace <name> Call tree (--depth=N)
|
|
1185
|
+
reverse-trace <name> Upward to entry points (--depth=N)
|
|
1132
1186
|
example <name> Best usage example
|
|
1133
1187
|
related <name> Sibling functions
|
|
1134
1188
|
fn <name>[,n2,...] Extract function(s) (--file=)
|
|
1135
1189
|
class <name> Extract class code (--file=)
|
|
1136
1190
|
lines <range> Extract lines (--file= required)
|
|
1137
1191
|
graph <file> File dependency tree (--direction=, --depth=)
|
|
1192
|
+
circular-deps Circular import chains (--file=, --exclude=)
|
|
1138
1193
|
file-exports <file> File's exported symbols
|
|
1139
1194
|
imports <file> What file imports
|
|
1140
1195
|
exporters <file> Who imports file
|
|
1141
1196
|
tests <name> Find tests (--calls-only)
|
|
1197
|
+
affected-tests <n> Tests affected by a change (--depth=N)
|
|
1142
1198
|
search <term> Text search (--context=N, --exclude=, --in=)
|
|
1199
|
+
Structural: --type= --param= --returns= --decorator= --exported --unused
|
|
1143
1200
|
typedef <name> Find type definitions
|
|
1144
1201
|
deadcode Find unused functions/classes
|
|
1145
1202
|
verify <name> Check call sites match signature
|
|
@@ -1349,6 +1406,13 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1349
1406
|
break;
|
|
1350
1407
|
}
|
|
1351
1408
|
|
|
1409
|
+
case 'blast': {
|
|
1410
|
+
const { ok, result, error } = execute(index, 'blast', { name: arg, ...iflags });
|
|
1411
|
+
if (!ok) { console.log(error); return; }
|
|
1412
|
+
console.log(output.formatBlast(result));
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1352
1416
|
case 'trace': {
|
|
1353
1417
|
const { ok, result, error } = execute(index, 'trace', { name: arg, ...iflags });
|
|
1354
1418
|
if (!ok) { console.log(error); return; }
|
|
@@ -1356,6 +1420,13 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1356
1420
|
break;
|
|
1357
1421
|
}
|
|
1358
1422
|
|
|
1423
|
+
case 'reverseTrace': {
|
|
1424
|
+
const { ok, result, error } = execute(index, 'reverseTrace', { name: arg, ...iflags });
|
|
1425
|
+
if (!ok) { console.log(error); return; }
|
|
1426
|
+
console.log(output.formatReverseTrace(result));
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1359
1430
|
case 'graph': {
|
|
1360
1431
|
const { ok, result, error } = execute(index, 'graph', { file: arg, ...iflags });
|
|
1361
1432
|
if (!ok) { console.log(error); return; }
|
|
@@ -1364,6 +1435,13 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1364
1435
|
break;
|
|
1365
1436
|
}
|
|
1366
1437
|
|
|
1438
|
+
case 'circularDeps': {
|
|
1439
|
+
const { ok, result, error } = execute(index, 'circularDeps', { file: iflags.file, exclude: iflags.exclude });
|
|
1440
|
+
if (!ok) { console.log(error); return; }
|
|
1441
|
+
console.log(output.formatCircularDeps(result));
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1367
1445
|
case 'fileExports': {
|
|
1368
1446
|
const { ok, result, error } = execute(index, 'fileExports', { file: arg });
|
|
1369
1447
|
if (!ok) { console.log(error); return; }
|
|
@@ -1392,10 +1470,21 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1392
1470
|
break;
|
|
1393
1471
|
}
|
|
1394
1472
|
|
|
1473
|
+
case 'affectedTests': {
|
|
1474
|
+
const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...iflags });
|
|
1475
|
+
if (!ok) { console.log(error); return; }
|
|
1476
|
+
console.log(output.formatAffectedTests(result));
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1395
1480
|
case 'search': {
|
|
1396
|
-
const { ok, result, error } = execute(index, 'search', { term: arg, ...iflags });
|
|
1481
|
+
const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...iflags });
|
|
1397
1482
|
if (!ok) { console.log(error); return; }
|
|
1398
|
-
|
|
1483
|
+
if (structural) {
|
|
1484
|
+
console.log(output.formatStructuralSearch(result));
|
|
1485
|
+
} else {
|
|
1486
|
+
console.log(output.formatSearch(result, arg));
|
|
1487
|
+
}
|
|
1399
1488
|
break;
|
|
1400
1489
|
}
|
|
1401
1490
|
|
package/core/callers.js
CHANGED
|
@@ -136,7 +136,13 @@ function findCallers(index, name, options = {}) {
|
|
|
136
136
|
let pendingCount = 0;
|
|
137
137
|
const maxResults = options.maxResults;
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
// Use inverted callee index to skip files that don't contain calls to this name
|
|
140
|
+
const calleeFiles = index.getCalleeFiles(name);
|
|
141
|
+
const fileIterator = calleeFiles
|
|
142
|
+
? [...calleeFiles].map(fp => [fp, index.files.get(fp)]).filter(([, fe]) => fe)
|
|
143
|
+
: index.files;
|
|
144
|
+
|
|
145
|
+
for (const [filePath, fileEntry] of fileIterator) {
|
|
140
146
|
// Early exit when maxResults is reached
|
|
141
147
|
if (maxResults && pendingCount >= maxResults) break;
|
|
142
148
|
try {
|
|
@@ -361,6 +367,32 @@ function findCallers(index, name, options = {}) {
|
|
|
361
367
|
continue;
|
|
362
368
|
}
|
|
363
369
|
|
|
370
|
+
// Import-graph disambiguation for JS/TS/Python: when multiple definitions of
|
|
371
|
+
// the same name exist and this call has no bindingId, check whether the calling
|
|
372
|
+
// file imports from the target definition's file. Skips false positives like
|
|
373
|
+
// user_b importing from b.js being reported as a caller of a.js:process.
|
|
374
|
+
// Go/Java/Rust are excluded — they use package/module scoping, not file imports.
|
|
375
|
+
if (!bindingId && options.targetDefinitions && definitions.length > 1 &&
|
|
376
|
+
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
377
|
+
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
378
|
+
if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
|
|
379
|
+
const imports = index.importGraph.get(filePath) || [];
|
|
380
|
+
const importsTarget = imports.some(imp => targetFiles.has(imp));
|
|
381
|
+
if (!importsTarget) {
|
|
382
|
+
// Check one level of re-exports (barrel files)
|
|
383
|
+
let foundViaReexport = false;
|
|
384
|
+
for (const imp of imports) {
|
|
385
|
+
const transImports = index.importGraph.get(imp) || [];
|
|
386
|
+
if (transImports.some(ti => targetFiles.has(ti))) {
|
|
387
|
+
foundViaReexport = true;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!foundViaReexport) continue;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
364
396
|
// Go unexported visibility: lowercase functions are package-private.
|
|
365
397
|
// Only allow callers from the same package directory.
|
|
366
398
|
if (fileEntry.language === 'go' && /^[a-z]/.test(name)) {
|
|
@@ -375,7 +407,9 @@ function findCallers(index, name, options = {}) {
|
|
|
375
407
|
// Go/Java/Rust: method vs non-method cross-matching filter.
|
|
376
408
|
// Prevents t.Errorf() (method call) from matching standalone func Errorf,
|
|
377
409
|
// and cli.Run() (package call, isMethod:false) from matching DeploymentController.Run.
|
|
378
|
-
|
|
410
|
+
// Rust path calls (module::func(), Type::new()) bypass this filter — they're
|
|
411
|
+
// scoped_identifier calls that can target both standalone functions and impl methods.
|
|
412
|
+
if (!bindingId && !resolvedBySameClass && !call.isPathCall &&
|
|
379
413
|
(fileEntry.language === 'go' || fileEntry.language === 'java' || fileEntry.language === 'rust')) {
|
|
380
414
|
const targetHasClass = targetDefs.some(d => d.className);
|
|
381
415
|
if (call.isMethod && !targetHasClass) {
|
package/core/deadcode.js
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
9
9
|
const { isTestFile } = require('./discovery');
|
|
10
|
+
const { escapeRegExp } = require('./shared');
|
|
11
|
+
|
|
12
|
+
/** Check if a position in a line is inside a string literal (quotes/backticks) */
|
|
13
|
+
function isInsideString(line, pos) {
|
|
14
|
+
let inSingle = false, inDouble = false, inBacktick = false;
|
|
15
|
+
for (let j = 0; j < pos; j++) {
|
|
16
|
+
const ch = line[j];
|
|
17
|
+
if (ch === '\\') { j++; continue; }
|
|
18
|
+
if (ch === '"' && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
19
|
+
if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
|
|
20
|
+
if (ch === '`' && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
21
|
+
}
|
|
22
|
+
return inSingle || inDouble || inBacktick;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
/**
|
|
12
26
|
* Build a usage index for identifiers in the codebase (optimized for deadcode)
|
|
@@ -25,6 +39,19 @@ function buildUsageIndex(index, filterNames) {
|
|
|
25
39
|
|
|
26
40
|
const content = index._readFile(filePath);
|
|
27
41
|
|
|
42
|
+
// Text pre-filter: skip files that don't contain any target names
|
|
43
|
+
// (avoids expensive tree-sitter parse + AST traversal for irrelevant files)
|
|
44
|
+
if (filterNames && filterNames.size > 0) {
|
|
45
|
+
let hasAny = false;
|
|
46
|
+
for (const name of filterNames) {
|
|
47
|
+
if (content.includes(name)) {
|
|
48
|
+
hasAny = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!hasAny) continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
// For HTML files, parse the virtual JS content instead of raw HTML
|
|
29
56
|
// (HTML tree-sitter sees script content as raw_text, not JS identifiers)
|
|
30
57
|
let tree;
|
|
@@ -163,6 +190,11 @@ function deadcode(index, options = {}) {
|
|
|
163
190
|
let excludedDecorated = 0;
|
|
164
191
|
let excludedExported = 0;
|
|
165
192
|
|
|
193
|
+
// Ensure callee index is built (lazy, reused across operations)
|
|
194
|
+
if (!index.calleeIndex) {
|
|
195
|
+
index.buildCalleeIndex();
|
|
196
|
+
}
|
|
197
|
+
|
|
166
198
|
// Collect callable symbol names to reduce usage index scope
|
|
167
199
|
const callableTypes = ['function', 'method', 'static', 'public', 'abstract', 'constructor'];
|
|
168
200
|
const callableNames = new Set();
|
|
@@ -172,8 +204,71 @@ function deadcode(index, options = {}) {
|
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
|
|
175
|
-
//
|
|
176
|
-
const
|
|
207
|
+
// Pre-filter: names in the callee index have call sites → definitely used → not dead.
|
|
208
|
+
const potentiallyDeadNames = new Set();
|
|
209
|
+
for (const name of callableNames) {
|
|
210
|
+
if (!index.calleeIndex.has(name)) {
|
|
211
|
+
potentiallyDeadNames.add(name);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build usage index for potentially dead names using text scan (no tree-sitter reparsing).
|
|
216
|
+
// The callee index already covers all call-based usages. For remaining names, a word-boundary
|
|
217
|
+
// text scan catches imports, exports, shorthand properties, type refs, and variable refs.
|
|
218
|
+
// Trade-off: may match names in comments/strings (false "used" → fewer dead code reports),
|
|
219
|
+
// but avoids ~1.9s of tree-sitter re-parsing. buildUsageIndex() is kept for direct callers.
|
|
220
|
+
const usageIndex = new Map();
|
|
221
|
+
if (potentiallyDeadNames.size > 0) {
|
|
222
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
223
|
+
try {
|
|
224
|
+
const content = index._readFile(filePath);
|
|
225
|
+
const lines = content.split('\n');
|
|
226
|
+
for (const name of potentiallyDeadNames) {
|
|
227
|
+
if (!content.includes(name)) continue;
|
|
228
|
+
const nameLen = name.length;
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
const line = lines[i];
|
|
231
|
+
if (!line.includes(name)) continue;
|
|
232
|
+
// Skip line if entirely inside a line comment (// or #)
|
|
233
|
+
const commentIdx = line.indexOf('//');
|
|
234
|
+
const hashIdx = line.indexOf('#');
|
|
235
|
+
let searchFrom = 0;
|
|
236
|
+
while (searchFrom < line.length) {
|
|
237
|
+
const pos = line.indexOf(name, searchFrom);
|
|
238
|
+
if (pos === -1) break;
|
|
239
|
+
searchFrom = pos + 1;
|
|
240
|
+
// Word boundary check
|
|
241
|
+
if (pos > 0 && /\w/.test(line[pos - 1])) continue;
|
|
242
|
+
if (pos + nameLen < line.length && /\w/.test(line[pos + nameLen])) continue;
|
|
243
|
+
// Skip if inside a // comment (not :// URL)
|
|
244
|
+
if (commentIdx !== -1 && commentIdx < pos &&
|
|
245
|
+
(commentIdx === 0 || line[commentIdx - 1] !== ':')) continue;
|
|
246
|
+
// Skip if inside a # comment (Python — # preceded by whitespace or at start)
|
|
247
|
+
if (hashIdx !== -1 && hashIdx < pos &&
|
|
248
|
+
(hashIdx === 0 || /\s/.test(line[hashIdx - 1]))) continue;
|
|
249
|
+
// Skip if inside a string literal
|
|
250
|
+
if (isInsideString(line, pos)) continue;
|
|
251
|
+
// Skip property/field access: preceded by '.' unless followed by '(' (method call)
|
|
252
|
+
if (pos > 0 && line[pos - 1] === '.' &&
|
|
253
|
+
(pos + nameLen >= line.length || line[pos + nameLen] !== '(')) continue;
|
|
254
|
+
// Skip object literal key: name followed by ':' (not '::' for Rust paths)
|
|
255
|
+
const afterChar = pos + nameLen < line.length ? line[pos + nameLen] : '';
|
|
256
|
+
const afterChar2 = pos + nameLen + 1 < line.length ? line[pos + nameLen + 1] : '';
|
|
257
|
+
if (afterChar === ':' && afterChar2 !== ':') continue;
|
|
258
|
+
// Valid reference found
|
|
259
|
+
if (!usageIndex.has(name)) usageIndex.set(name, []);
|
|
260
|
+
usageIndex.get(name).push({
|
|
261
|
+
file: filePath,
|
|
262
|
+
line: i + 1,
|
|
263
|
+
relativePath: fileEntry.relativePath
|
|
264
|
+
});
|
|
265
|
+
break; // one match per line is enough for deadcode
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
177
272
|
|
|
178
273
|
for (const [name, symbols] of index.symbols) {
|
|
179
274
|
for (const symbol of symbols) {
|
|
@@ -308,7 +403,12 @@ function deadcode(index, options = {}) {
|
|
|
308
403
|
continue;
|
|
309
404
|
}
|
|
310
405
|
|
|
311
|
-
//
|
|
406
|
+
// Fast path: name has call sites in callee index → definitely used → not dead
|
|
407
|
+
if (index.calleeIndex.has(name)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Slow path: check AST-based usage index for remaining names
|
|
312
412
|
const allUsages = usageIndex.get(name) || [];
|
|
313
413
|
|
|
314
414
|
// Filter out usages that are at the definition location
|