ucn 3.8.1 → 3.8.3
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 +17 -1
- package/cli/index.js +34 -6
- package/core/callers.js +60 -13
- package/core/confidence.js +116 -0
- package/core/deadcode.js +10 -18
- package/core/entrypoints.js +416 -0
- package/core/execute.js +16 -2
- package/core/output.js +101 -3
- package/core/parser.js +1 -1
- package/core/project.js +36 -4
- package/core/registry.js +3 -1
- package/languages/go.js +20 -0
- package/languages/javascript.js +48 -0
- package/languages/python.js +40 -0
- package/languages/rust.js +62 -0
- package/mcp/server.js +13 -2
- package/package.json +1 -1
|
@@ -83,11 +83,23 @@ ucn class MarketDataFetcher
|
|
|
83
83
|
|
|
84
84
|
### 6. `deadcode` — Find unused code
|
|
85
85
|
|
|
86
|
-
Lists all functions and classes with zero callers across the project.
|
|
86
|
+
Lists all functions and classes with zero callers across the project. Framework entry points (Express routes, Spring controllers, Celery tasks, etc.) are automatically excluded.
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
ucn deadcode # Everything
|
|
90
90
|
ucn deadcode --exclude=test # Skip test files (most useful)
|
|
91
|
+
ucn deadcode --include-decorated # Include framework-registered functions
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 7. `entrypoints` — Detect framework entry points
|
|
95
|
+
|
|
96
|
+
Lists functions registered as framework handlers (HTTP routes, DI beans, job schedulers, etc.). Detects patterns across Express, FastAPI, Flask, Spring, Gin, Actix, Celery, pytest, and more.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
ucn entrypoints # All detected entry points
|
|
100
|
+
ucn entrypoints --type=http # HTTP routes only
|
|
101
|
+
ucn entrypoints --framework=express # Specific framework
|
|
102
|
+
ucn entrypoints --file=routes/ # Scoped to files
|
|
91
103
|
```
|
|
92
104
|
|
|
93
105
|
## When to Use the Other Commands
|
|
@@ -120,6 +132,7 @@ ucn deadcode --exclude=test # Skip test files (most useful)
|
|
|
120
132
|
| Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
|
|
121
133
|
| 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
134
|
| Are there circular dependencies? | `ucn circular-deps` | Detect circular import chains. `--file=<pattern>` filters to cycles involving a file. `--exclude=test` skips test files |
|
|
135
|
+
| What are the framework entry points? | `ucn entrypoints` | Lists all detected routes, DI beans, tasks, etc. Filter: `--type=http`, `--framework=express` |
|
|
123
136
|
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
|
|
124
137
|
| Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
|
|
125
138
|
| Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
|
|
@@ -170,8 +183,11 @@ ucn [target] <command> [name] [--flags]
|
|
|
170
183
|
| `--case-sensitive` | Case-sensitive text search (default: case-insensitive) |
|
|
171
184
|
| `--exact` | Exact name match only in `find`/`typedef` (no substring) |
|
|
172
185
|
| `--include-uncertain` | Include ambiguous/uncertain matches in `context`/`smart`/`about` |
|
|
186
|
+
| `--show-confidence` | Show confidence scores (0.0–1.0) per caller/callee edge in `context`/`about` |
|
|
187
|
+
| `--min-confidence=N` | Filter edges below confidence threshold (e.g., `--min-confidence=0.7` keeps only high-confidence edges) |
|
|
173
188
|
| `--include-exported` | Include exported symbols in `deadcode` results |
|
|
174
189
|
| `--include-decorated` | Include decorated/annotated symbols in `deadcode` results |
|
|
190
|
+
| `--framework=X` | Filter `entrypoints` by framework (e.g., `express`, `spring`, `celery`) |
|
|
175
191
|
| `--type=<kind>` | Structural search: `function`, `class`, `call`, `method`, `type`. Triggers index query instead of text grep |
|
|
176
192
|
| `--param=<name>` | Structural search: filter by parameter name or type (e.g., `--param=Request`) |
|
|
177
193
|
| `--receiver=<name>` | Structural search: filter calls by receiver (e.g., `--receiver=db` for all db.* calls) |
|
package/cli/index.js
CHANGED
|
@@ -112,6 +112,9 @@ function parseFlags(tokens) {
|
|
|
112
112
|
decorator: getValueFlag('--decorator'),
|
|
113
113
|
exported: tokens.includes('--exported'),
|
|
114
114
|
unused: tokens.includes('--unused'),
|
|
115
|
+
showConfidence: tokens.includes('--show-confidence'),
|
|
116
|
+
minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
|
|
117
|
+
framework: getValueFlag('--framework'),
|
|
115
118
|
};
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -137,7 +140,9 @@ const knownFlags = new Set([
|
|
|
137
140
|
'--base', '--staged', '--stack',
|
|
138
141
|
'--regex', '--no-regex', '--functions',
|
|
139
142
|
'--max-lines', '--class-name', '--limit', '--max-files',
|
|
140
|
-
'--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused'
|
|
143
|
+
'--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
|
|
144
|
+
'--show-confidence', '--min-confidence',
|
|
145
|
+
'--framework'
|
|
141
146
|
]);
|
|
142
147
|
|
|
143
148
|
// Handle help flag
|
|
@@ -476,7 +481,8 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
476
481
|
const { text, expandable } = output.formatContext(ctx, {
|
|
477
482
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
478
483
|
expandHint: 'Use "ucn . expand <N>" to see code for item N',
|
|
479
|
-
uncertainHint: 'use --include-uncertain to include all'
|
|
484
|
+
uncertainHint: 'use --include-uncertain to include all',
|
|
485
|
+
showConfidence: flags.showConfidence,
|
|
480
486
|
});
|
|
481
487
|
console.log(text);
|
|
482
488
|
|
|
@@ -540,7 +546,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
540
546
|
if (!ok) fail(error);
|
|
541
547
|
printOutput(result,
|
|
542
548
|
output.formatAboutJson,
|
|
543
|
-
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
|
|
549
|
+
r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence })
|
|
544
550
|
);
|
|
545
551
|
break;
|
|
546
552
|
}
|
|
@@ -751,6 +757,16 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
751
757
|
break;
|
|
752
758
|
}
|
|
753
759
|
|
|
760
|
+
case 'entrypoints': {
|
|
761
|
+
const { ok, result, error } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file });
|
|
762
|
+
if (!ok) fail(error);
|
|
763
|
+
printOutput(result,
|
|
764
|
+
output.formatEntrypointsJson,
|
|
765
|
+
r => output.formatEntrypoints(r)
|
|
766
|
+
);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
|
|
754
770
|
case 'stats': {
|
|
755
771
|
const { ok, result, error } = execute(index, 'stats', { functions: flags.functions });
|
|
756
772
|
if (!ok) fail(error);
|
|
@@ -1036,7 +1052,7 @@ Usage:
|
|
|
1036
1052
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1037
1053
|
UNDERSTAND CODE
|
|
1038
1054
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1039
|
-
about <name>
|
|
1055
|
+
about <name> Full picture (definition, callers, callees, tests, code)
|
|
1040
1056
|
context <name> Who calls this + what it calls (numbered for expand)
|
|
1041
1057
|
smart <name> Function + all dependencies inline
|
|
1042
1058
|
impact <name> What breaks if changed (call sites grouped by file)
|
|
@@ -1081,6 +1097,7 @@ REFACTORING HELPERS
|
|
|
1081
1097
|
verify <name> Check all call sites match signature
|
|
1082
1098
|
diff-impact What changed in git diff and who calls it (--base, --staged)
|
|
1083
1099
|
deadcode Find unused functions/classes
|
|
1100
|
+
entrypoints Detect framework entry points (routes, DI, tasks)
|
|
1084
1101
|
|
|
1085
1102
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1086
1103
|
OTHER
|
|
@@ -1107,10 +1124,13 @@ Common Flags:
|
|
|
1107
1124
|
--include-tests Include test files
|
|
1108
1125
|
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
1109
1126
|
--include-uncertain Include ambiguous/uncertain matches
|
|
1127
|
+
--show-confidence Show confidence scores per caller/callee edge
|
|
1128
|
+
--min-confidence=N Filter edges below confidence threshold (0.0-1.0)
|
|
1110
1129
|
--include-exported Include exported symbols in deadcode
|
|
1111
1130
|
--no-regex Force plain text search (regex is default)
|
|
1112
1131
|
--functions Show per-function line counts (stats command)
|
|
1113
1132
|
--include-decorated Include decorated/annotated symbols in deadcode
|
|
1133
|
+
--framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
|
|
1114
1134
|
--exact Exact name match only (find)
|
|
1115
1135
|
--calls-only Only show call/test-case matches (tests)
|
|
1116
1136
|
--case-sensitive Case-sensitive text search (search)
|
|
@@ -1340,7 +1360,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1340
1360
|
const { text, expandable } = output.formatContext(result, {
|
|
1341
1361
|
methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
|
|
1342
1362
|
expandHint: 'Use "expand <N>" to see code for item N',
|
|
1343
|
-
uncertainHint: 'use --include-uncertain to include all'
|
|
1363
|
+
uncertainHint: 'use --include-uncertain to include all',
|
|
1364
|
+
showConfidence: iflags.showConfidence,
|
|
1344
1365
|
});
|
|
1345
1366
|
console.log(text);
|
|
1346
1367
|
if (cache) {
|
|
@@ -1364,6 +1385,13 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1364
1385
|
break;
|
|
1365
1386
|
}
|
|
1366
1387
|
|
|
1388
|
+
case 'entrypoints': {
|
|
1389
|
+
const { ok, result, error } = execute(index, 'entrypoints', { type: iflags.type, framework: iflags.framework, file: iflags.file });
|
|
1390
|
+
if (!ok) { console.log(error); return; }
|
|
1391
|
+
console.log(output.formatEntrypoints(result));
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1367
1395
|
// ── Standard commands routed through execute() ───────────────────
|
|
1368
1396
|
|
|
1369
1397
|
case 'toc': {
|
|
@@ -1379,7 +1407,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1379
1407
|
case 'about': {
|
|
1380
1408
|
const { ok, result, error } = execute(index, 'about', { name: arg, ...iflags });
|
|
1381
1409
|
if (!ok) { console.log(error); return; }
|
|
1382
|
-
console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth }));
|
|
1410
|
+
console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth, showConfidence: iflags.showConfidence }));
|
|
1383
1411
|
break;
|
|
1384
1412
|
}
|
|
1385
1413
|
|
package/core/callers.js
CHANGED
|
@@ -11,6 +11,7 @@ const crypto = require('crypto');
|
|
|
11
11
|
const { detectLanguage, getParser, getLanguageModule } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
13
|
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
14
|
+
const { scoreEdge } = require('./confidence');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Extract a single line from content without splitting the entire string.
|
|
@@ -191,7 +192,8 @@ function findCallers(index, name, options = {}) {
|
|
|
191
192
|
if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
|
|
192
193
|
pendingByFile.get(filePath).push({
|
|
193
194
|
call, fileEntry, callerSymbol,
|
|
194
|
-
isMethod: false, isFunctionReference: true, receiver: undefined
|
|
195
|
+
isMethod: false, isFunctionReference: true, receiver: undefined,
|
|
196
|
+
_evidence: { isFunctionReference: true }
|
|
195
197
|
});
|
|
196
198
|
pendingCount++;
|
|
197
199
|
continue;
|
|
@@ -422,12 +424,13 @@ function findCallers(index, name, options = {}) {
|
|
|
422
424
|
}
|
|
423
425
|
}
|
|
424
426
|
|
|
425
|
-
//
|
|
427
|
+
// Receiver-class disambiguation:
|
|
426
428
|
// When the target definition has a class/receiver type, filter callers
|
|
427
429
|
// whose receiverType is known to be a different type.
|
|
428
|
-
//
|
|
430
|
+
// All languages use receiverType when available (constructor/annotation inference).
|
|
431
|
+
// Go/Java/Rust additionally fall back to variable name matching.
|
|
429
432
|
if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
|
|
430
|
-
(fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
|
|
433
|
+
(call.receiverType || fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
|
|
431
434
|
// Build target type set from both className (Java) and receiver (Go/Rust)
|
|
432
435
|
const targetTypes = new Set();
|
|
433
436
|
for (const td of targetDefs) {
|
|
@@ -474,12 +477,43 @@ function findCallers(index, name, options = {}) {
|
|
|
474
477
|
// Find the enclosing function (get full symbol info)
|
|
475
478
|
const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
|
|
476
479
|
|
|
480
|
+
// Check import graph evidence: does this file import from the target definition's file?
|
|
481
|
+
const targetDefs2 = options.targetDefinitions || definitions;
|
|
482
|
+
const targetFiles2 = new Set(targetDefs2.map(d => d.file).filter(Boolean));
|
|
483
|
+
const callerImports = index.importGraph.get(filePath) || [];
|
|
484
|
+
let hasImportLink = targetFiles2.has(filePath) || callerImports.some(imp => targetFiles2.has(imp));
|
|
485
|
+
// Check one level of re-exports (barrel files) for import evidence
|
|
486
|
+
if (!hasImportLink) {
|
|
487
|
+
for (const imp of callerImports) {
|
|
488
|
+
const transImports = index.importGraph.get(imp) || [];
|
|
489
|
+
if (transImports.some(ti => targetFiles2.has(ti))) {
|
|
490
|
+
hasImportLink = true;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
477
496
|
if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
|
|
478
497
|
pendingByFile.get(filePath).push({
|
|
479
498
|
call, fileEntry, callerSymbol,
|
|
480
499
|
isMethod: call.isMethod || false, isFunctionReference: false,
|
|
481
500
|
receiver: call.receiver,
|
|
482
|
-
receiverType: call.receiverType
|
|
501
|
+
receiverType: call.receiverType,
|
|
502
|
+
_evidence: {
|
|
503
|
+
hasBindingId: !!bindingId,
|
|
504
|
+
resolvedBySameClass: !!resolvedBySameClass,
|
|
505
|
+
isUncertain: !!isUncertain || (
|
|
506
|
+
// Method calls where binding resolution was skipped (non-self receiver)
|
|
507
|
+
// and the receiver has no binding evidence → uncertain (JS/TS/Python only)
|
|
508
|
+
skipLocalBinding && call.isMethod && !resolvedBySameClass &&
|
|
509
|
+
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust' &&
|
|
510
|
+
!(call.receiver && (fileEntry.bindings || []).some(b => b.name === call.receiver))
|
|
511
|
+
),
|
|
512
|
+
hasReceiverType: !!call.receiverType,
|
|
513
|
+
hasReceiverEvidence: !!(call.receiver &&
|
|
514
|
+
(fileEntry.bindings || []).some(b => b.name === call.receiver)),
|
|
515
|
+
hasImportEvidence: !!bindingId || hasImportLink,
|
|
516
|
+
}
|
|
483
517
|
});
|
|
484
518
|
pendingCount++;
|
|
485
519
|
}
|
|
@@ -493,7 +527,8 @@ function findCallers(index, name, options = {}) {
|
|
|
493
527
|
for (const [filePath, pending] of pendingByFile) {
|
|
494
528
|
try {
|
|
495
529
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
496
|
-
for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType } of pending) {
|
|
530
|
+
for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType, _evidence } of pending) {
|
|
531
|
+
const scored = scoreEdge(_evidence || {});
|
|
497
532
|
callers.push({
|
|
498
533
|
file: filePath,
|
|
499
534
|
relativePath: fileEntry.relativePath,
|
|
@@ -506,7 +541,9 @@ function findCallers(index, name, options = {}) {
|
|
|
506
541
|
isMethod,
|
|
507
542
|
...(isFunctionReference && { isFunctionReference: true }),
|
|
508
543
|
...(receiver !== undefined && { receiver }),
|
|
509
|
-
...(receiverType && { receiverType })
|
|
544
|
+
...(receiverType && { receiverType }),
|
|
545
|
+
confidence: scored.confidence,
|
|
546
|
+
resolution: scored.resolution,
|
|
510
547
|
});
|
|
511
548
|
}
|
|
512
549
|
} catch (e) {
|
|
@@ -612,9 +649,11 @@ function findCallees(index, def, options = {}) {
|
|
|
612
649
|
}
|
|
613
650
|
}
|
|
614
651
|
continue;
|
|
615
|
-
} else if (call.receiverType
|
|
652
|
+
} else if (call.receiverType) {
|
|
616
653
|
// Use parser-inferred receiverType for method resolution
|
|
617
|
-
//
|
|
654
|
+
// Go/Java/Rust: from param/receiver type declarations
|
|
655
|
+
// JS/TS: from `new Foo()` assignments or TypeScript type annotations
|
|
656
|
+
// Python: from constructor calls or type annotations
|
|
618
657
|
const typeName = call.receiverType;
|
|
619
658
|
const symbols = index.symbols.get(call.name);
|
|
620
659
|
const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
@@ -981,6 +1020,8 @@ function findCallees(index, def, options = {}) {
|
|
|
981
1020
|
const defReceiver = def.receiver;
|
|
982
1021
|
const defFileEntry = fileEntry;
|
|
983
1022
|
const callerIsTest = defFileEntry && isTestFile(defFileEntry.relativePath, defFileEntry.language);
|
|
1023
|
+
// Pre-compute import graph for callee confidence scoring
|
|
1024
|
+
const callerImportSet = new Set(index.importGraph.get(def.file) || []);
|
|
984
1025
|
|
|
985
1026
|
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
986
1027
|
const symbols = index.symbols.get(calleeName);
|
|
@@ -1009,9 +1050,7 @@ function findCallees(index, def, options = {}) {
|
|
|
1009
1050
|
} else {
|
|
1010
1051
|
// Priority 2.5: Imported file — check if the caller's file imports
|
|
1011
1052
|
// from any of the candidate callee files (using importGraph)
|
|
1012
|
-
const
|
|
1013
|
-
const importedFileSet = new Set(callerImportedFiles);
|
|
1014
|
-
const importedCallee = symbols.find(s => importedFileSet.has(s.file));
|
|
1053
|
+
const importedCallee = symbols.find(s => callerImportSet.has(s.file));
|
|
1015
1054
|
if (importedCallee) {
|
|
1016
1055
|
callee = importedCallee;
|
|
1017
1056
|
} else if (defReceiver) {
|
|
@@ -1095,10 +1134,18 @@ function findCallees(index, def, options = {}) {
|
|
|
1095
1134
|
}
|
|
1096
1135
|
}
|
|
1097
1136
|
|
|
1137
|
+
const calleeScored = scoreEdge({
|
|
1138
|
+
hasBindingId: !!bindingId,
|
|
1139
|
+
hasImportEvidence: !!bindingId || (symbols && symbols.length === 1) ||
|
|
1140
|
+
(callee.file === def.file) || callerImportSet.has(callee.file),
|
|
1141
|
+
isUncertain: false, // uncertain callees already filtered above
|
|
1142
|
+
});
|
|
1098
1143
|
result.push({
|
|
1099
1144
|
...callee,
|
|
1100
1145
|
callCount: count,
|
|
1101
|
-
weight: index.calculateWeight(count)
|
|
1146
|
+
weight: index.calculateWeight(count),
|
|
1147
|
+
confidence: calleeScored.confidence,
|
|
1148
|
+
resolution: calleeScored.resolution,
|
|
1102
1149
|
});
|
|
1103
1150
|
}
|
|
1104
1151
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/confidence.js - Deterministic edge confidence scoring
|
|
3
|
+
*
|
|
4
|
+
* Assigns a 0.0-1.0 confidence score to each caller/callee edge based on
|
|
5
|
+
* how the call was resolved. Rule-based, no ML.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Resolution types ordered from most to least confident
|
|
11
|
+
const RESOLUTION = {
|
|
12
|
+
EXACT_BINDING: 'exact-binding',
|
|
13
|
+
SAME_CLASS: 'same-class',
|
|
14
|
+
RECEIVER_HINT: 'receiver-hint',
|
|
15
|
+
SCOPE_MATCH: 'scope-match',
|
|
16
|
+
NAME_ONLY: 'name-only',
|
|
17
|
+
UNCERTAIN: 'uncertain',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Seed scores per resolution type (tunable)
|
|
21
|
+
const SCORES = {
|
|
22
|
+
[RESOLUTION.EXACT_BINDING]: 0.98,
|
|
23
|
+
[RESOLUTION.SAME_CLASS]: 0.92,
|
|
24
|
+
[RESOLUTION.RECEIVER_HINT]: 0.80,
|
|
25
|
+
[RESOLUTION.SCOPE_MATCH]: 0.65,
|
|
26
|
+
[RESOLUTION.NAME_ONLY]: 0.40,
|
|
27
|
+
[RESOLUTION.UNCERTAIN]: 0.25,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Score a caller/callee edge based on resolution evidence.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} evidence - Resolution evidence collected during call resolution
|
|
34
|
+
* @param {boolean} [evidence.hasBindingId] - Call resolved to a specific bindingId
|
|
35
|
+
* @param {boolean} [evidence.resolvedBySameClass] - Resolved via self/this/super/cls
|
|
36
|
+
* @param {boolean} [evidence.resolvedByReceiverHint] - Receiver type narrowed via local hints
|
|
37
|
+
* @param {boolean} [evidence.hasImportEvidence] - File imports the target definition
|
|
38
|
+
* @param {boolean} [evidence.hasReceiverEvidence] - Receiver variable has binding in file scope
|
|
39
|
+
* @param {boolean} [evidence.isUncertain] - Marked uncertain by resolution logic
|
|
40
|
+
* @param {boolean} [evidence.isFunctionReference] - Passed as callback argument
|
|
41
|
+
* @param {boolean} [evidence.hasReceiverType] - Go/Java/Rust parser-inferred receiverType
|
|
42
|
+
* @returns {{ confidence: number, resolution: string, evidence: string[] }}
|
|
43
|
+
*/
|
|
44
|
+
function scoreEdge(evidence) {
|
|
45
|
+
const reasons = [];
|
|
46
|
+
|
|
47
|
+
// Exact binding match (highest confidence)
|
|
48
|
+
if (evidence.hasBindingId) {
|
|
49
|
+
reasons.push('binding-id match');
|
|
50
|
+
if (evidence.hasImportEvidence) reasons.push('import-verified');
|
|
51
|
+
return { confidence: SCORES[RESOLUTION.EXACT_BINDING], resolution: RESOLUTION.EXACT_BINDING, evidence: reasons };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Same-class resolution (self/this/super/cls)
|
|
55
|
+
if (evidence.resolvedBySameClass) {
|
|
56
|
+
reasons.push('same-class method');
|
|
57
|
+
if (evidence.hasInheritanceChain) reasons.push('via inheritance');
|
|
58
|
+
return { confidence: SCORES[RESOLUTION.SAME_CLASS], resolution: RESOLUTION.SAME_CLASS, evidence: reasons };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Receiver hint narrowed to specific type
|
|
62
|
+
if (evidence.resolvedByReceiverHint || evidence.hasReceiverType) {
|
|
63
|
+
reasons.push(evidence.hasReceiverType ? 'parser receiver-type' : 'local type inference');
|
|
64
|
+
return { confidence: SCORES[RESOLUTION.RECEIVER_HINT], resolution: RESOLUTION.RECEIVER_HINT, evidence: reasons };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Scope/import-supported match
|
|
68
|
+
if (evidence.hasImportEvidence || evidence.hasReceiverEvidence) {
|
|
69
|
+
if (evidence.hasImportEvidence) reasons.push('import-supported');
|
|
70
|
+
if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
|
|
71
|
+
return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Function reference (callback)
|
|
75
|
+
if (evidence.isFunctionReference) {
|
|
76
|
+
reasons.push('function reference');
|
|
77
|
+
return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Uncertain
|
|
81
|
+
if (evidence.isUncertain) {
|
|
82
|
+
reasons.push('ambiguous resolution');
|
|
83
|
+
return { confidence: SCORES[RESOLUTION.UNCERTAIN], resolution: RESOLUTION.UNCERTAIN, evidence: reasons };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Name-only match (no additional evidence)
|
|
87
|
+
reasons.push('name match only');
|
|
88
|
+
return { confidence: SCORES[RESOLUTION.NAME_ONLY], resolution: RESOLUTION.NAME_ONLY, evidence: reasons };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Filter edges by minimum confidence threshold.
|
|
93
|
+
* @param {Array} edges - Array of objects with .confidence property
|
|
94
|
+
* @param {number} minConfidence - Minimum confidence (0.0-1.0)
|
|
95
|
+
* @returns {{ kept: Array, filtered: number }}
|
|
96
|
+
*/
|
|
97
|
+
function filterByConfidence(edges, minConfidence) {
|
|
98
|
+
if (!minConfidence || minConfidence <= 0) return { kept: edges, filtered: 0 };
|
|
99
|
+
const kept = [];
|
|
100
|
+
let filtered = 0;
|
|
101
|
+
for (const edge of edges) {
|
|
102
|
+
if ((edge.confidence || 0) >= minConfidence) {
|
|
103
|
+
kept.push(edge);
|
|
104
|
+
} else {
|
|
105
|
+
filtered++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { kept, filtered };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
RESOLUTION,
|
|
113
|
+
SCORES,
|
|
114
|
+
scoreEdge,
|
|
115
|
+
filterByConfidence,
|
|
116
|
+
};
|
package/core/deadcode.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
9
9
|
const { isTestFile } = require('./discovery');
|
|
10
10
|
const { escapeRegExp } = require('./shared');
|
|
11
|
+
const { isFrameworkEntrypoint } = require('./entrypoints');
|
|
11
12
|
|
|
12
13
|
/** Check if a position in a line is inside a string literal (quotes/backticks) */
|
|
13
14
|
function isInsideString(line, pos) {
|
|
@@ -369,23 +370,13 @@ function deadcode(index, options = {}) {
|
|
|
369
370
|
continue;
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
// Framework
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// These functions are invoked by frameworks, not by user code
|
|
376
|
-
const
|
|
377
|
-
const hasRegistrationDecorator = (() => {
|
|
378
|
-
if (lang === 'python') {
|
|
379
|
-
const decorators = symbol.decorators || [];
|
|
380
|
-
return decorators.some(d => d.includes('.'));
|
|
381
|
-
}
|
|
382
|
-
if (lang === 'java') {
|
|
383
|
-
return mods.some(m => !javaKeywords.has(m));
|
|
384
|
-
}
|
|
385
|
-
return false;
|
|
386
|
-
})();
|
|
373
|
+
// Framework entry point detection — excluded by default to reduce noise
|
|
374
|
+
// Detects decorator/annotation patterns (Python, Java, Rust, JS/TS) and
|
|
375
|
+
// call-pattern-based registration (Express routes, Gin handlers, etc.)
|
|
376
|
+
// These functions are invoked by frameworks, not by user code.
|
|
377
|
+
const hasFrameworkEntrypoint = isFrameworkEntrypoint(symbol, index);
|
|
387
378
|
|
|
388
|
-
if (
|
|
379
|
+
if (hasFrameworkEntrypoint && !options.includeDecorated) {
|
|
389
380
|
excludedDecorated++;
|
|
390
381
|
continue;
|
|
391
382
|
}
|
|
@@ -447,9 +438,10 @@ function deadcode(index, options = {}) {
|
|
|
447
438
|
// Python: symbol.decorators (e.g., ['app.route("/path")', 'login_required'])
|
|
448
439
|
// Java/Rust/Go: symbol.modifiers may contain annotations (e.g., 'bean', 'scheduled')
|
|
449
440
|
const decorators = symbol.decorators || [];
|
|
450
|
-
// For Java, extract annotation-like modifiers
|
|
441
|
+
// For Java, extract annotation-like modifiers
|
|
442
|
+
const javaKw = new Set(['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default']);
|
|
451
443
|
const annotations = lang === 'java'
|
|
452
|
-
? mods.filter(m => !
|
|
444
|
+
? mods.filter(m => !javaKw.has(m))
|
|
453
445
|
: [];
|
|
454
446
|
|
|
455
447
|
results.push({
|