ucn 3.7.47 → 3.8.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.
@@ -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. `trace` — Understand execution flow
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
- ### 4. `fn` / `class` — Extract without reading the whole file
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
- ### 5. `deadcode` — Find unused code
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
- 28 commands
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 28 commands ship as a single MCP tool - under 2KB of context.
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 28 commands
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 diff-impact git diff + callers
275
- smart function + helpers verify signature check
276
- trace call tree plan refactor preview
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 related sibling functions
284
- toc project overview tests find test coverage
285
- deadcode unused code stacktrace error trace context
286
- search text search api public API surface
287
- example best usage example typedef type definitions
288
- lines extract line range file-exports file's exports
289
- expand drill into context stats project stats
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
- if (needsArg[canonical]) {
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
- printOutput(result, r => output.formatSearchJson(r, arg), r => output.formatSearch(r, arg));
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
- printOutput(result,
686
- r => output.formatSearchJson(r, arg),
687
- r => output.formatSearch(r, arg)
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
 
@@ -993,7 +1040,9 @@ UNDERSTAND CODE (UCN's strength - semantic analysis)
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
- console.log(output.formatSearch(result, arg));
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
- for (const [filePath, fileEntry] of index.files) {
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
- if (!bindingId && !resolvedBySameClass &&
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
- // Build usage index once (instead of per-symbol), filtered to callable names only
176
- const usageIndex = buildUsageIndex(index, callableNames);
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
- // Use pre-built index for O(1) lookup instead of O(files) scan
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