ucn 3.8.13 → 3.8.15
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 +3 -1
- package/.github/workflows/ci.yml +13 -1
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/imports.js +50 -16
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- package/package.json +8 -3
package/core/callers.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
|
-
const { detectLanguage, getParser, getLanguageModule } = require('../languages');
|
|
11
|
+
const { detectLanguage, getParser, getLanguageModule, langTraits } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
13
|
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
14
14
|
const { scoreEdge } = require('./confidence');
|
|
@@ -83,7 +83,7 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
83
83
|
// Pass import alias names to Go parser for package vs method call disambiguation
|
|
84
84
|
// importNames contains resolved alias names (e.g., 'utilversion' for renamed imports)
|
|
85
85
|
const callOpts = {};
|
|
86
|
-
if (language
|
|
86
|
+
if (langTraits(language)?.hasReceiverPackageCalls) {
|
|
87
87
|
const fileEntry = index.files.get(filePath);
|
|
88
88
|
if (fileEntry?.importNames) {
|
|
89
89
|
callOpts.imports = fileEntry.importNames;
|
|
@@ -136,6 +136,7 @@ function findCallers(index, name, options = {}) {
|
|
|
136
136
|
const pendingByFile = new Map(); // filePath -> [{ call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver }]
|
|
137
137
|
let pendingCount = 0;
|
|
138
138
|
const maxResults = options.maxResults;
|
|
139
|
+
const localTypeCache = new Map(); // `${filePath}:${startLine}` -> localTypes Map or null
|
|
139
140
|
|
|
140
141
|
// Use inverted callee index to skip files that don't contain calls to this name
|
|
141
142
|
const calleeFiles = index.getCalleeFiles(name);
|
|
@@ -163,7 +164,7 @@ function findCallers(index, name, options = {}) {
|
|
|
163
164
|
|
|
164
165
|
// Go unexported visibility: lowercase functions are package-private.
|
|
165
166
|
// Only allow callers from the same package directory.
|
|
166
|
-
if (fileEntry.language === '
|
|
167
|
+
if (langTraits(fileEntry.language)?.exportVisibility === 'capitalization' && /^[a-z]/.test(name)) {
|
|
167
168
|
const targetDefs = options.targetDefinitions || definitions;
|
|
168
169
|
const targetPkgDirs = new Set(
|
|
169
170
|
targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
|
|
@@ -173,9 +174,9 @@ function findCallers(index, name, options = {}) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
//
|
|
177
|
+
// Nominal type receiver disambiguation for callbacks (e.g. dc.worker)
|
|
177
178
|
if (call.isMethod && call.receiver &&
|
|
178
|
-
(fileEntry.language === '
|
|
179
|
+
langTraits(fileEntry.language)?.typeSystem === 'nominal') {
|
|
179
180
|
const targetDefs = options.targetDefinitions || definitions;
|
|
180
181
|
const targetTypes = new Set();
|
|
181
182
|
for (const td of targetDefs) {
|
|
@@ -211,12 +212,16 @@ function findCallers(index, name, options = {}) {
|
|
|
211
212
|
if (!bindingId && !skipLocalBinding) {
|
|
212
213
|
let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
213
214
|
// For Go, also check sibling files in same directory (same package scope)
|
|
214
|
-
if (bindings.length === 0 && fileEntry.language === '
|
|
215
|
+
if (bindings.length === 0 && langTraits(fileEntry.language)?.packageScope === 'directory') {
|
|
215
216
|
const dir = path.dirname(filePath);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
const siblings = index.dirToFiles?.get(dir) || [];
|
|
218
|
+
for (const fp of siblings) {
|
|
219
|
+
if (fp !== filePath) {
|
|
220
|
+
const fe = index.files.get(fp);
|
|
221
|
+
if (fe) {
|
|
222
|
+
const sibling = (fe.bindings || []).filter(b => b.name === call.name);
|
|
223
|
+
bindings = bindings.concat(sibling);
|
|
224
|
+
}
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
}
|
|
@@ -278,7 +283,7 @@ function findCallers(index, name, options = {}) {
|
|
|
278
283
|
// over-reporting is preferred to losing callers. These languages' nominal
|
|
279
284
|
// type systems also make method links more reliable.
|
|
280
285
|
if (bindings.length === 0 && call.isMethod &&
|
|
281
|
-
fileEntry.language
|
|
286
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
282
287
|
const hasReceiverEvidence = call.receiver &&
|
|
283
288
|
(fileEntry.bindings || []).some(b => b.name === call.receiver);
|
|
284
289
|
if (!hasReceiverEvidence) {
|
|
@@ -348,7 +353,7 @@ function findCallers(index, name, options = {}) {
|
|
|
348
353
|
// Java method calls are always obj.method() - include by default
|
|
349
354
|
// Rust Type::method() calls - include by default (associated functions)
|
|
350
355
|
// For other languages, skip method calls unless explicitly requested
|
|
351
|
-
if (fileEntry.language
|
|
356
|
+
if (langTraits(fileEntry.language)?.methodCallInclusion === 'explicit' && !options.includeMethods) continue;
|
|
352
357
|
}
|
|
353
358
|
}
|
|
354
359
|
|
|
@@ -375,7 +380,7 @@ function findCallers(index, name, options = {}) {
|
|
|
375
380
|
// user_b importing from b.js being reported as a caller of a.js:process.
|
|
376
381
|
// Go/Java/Rust are excluded — they use package/module scoping, not file imports.
|
|
377
382
|
if (!bindingId && options.targetDefinitions && definitions.length > 1 &&
|
|
378
|
-
fileEntry.language
|
|
383
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
379
384
|
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
380
385
|
if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
|
|
381
386
|
const imports = index.importGraph.get(filePath) || [];
|
|
@@ -397,7 +402,7 @@ function findCallers(index, name, options = {}) {
|
|
|
397
402
|
|
|
398
403
|
// Go unexported visibility: lowercase functions are package-private.
|
|
399
404
|
// Only allow callers from the same package directory.
|
|
400
|
-
if (fileEntry.language === '
|
|
405
|
+
if (langTraits(fileEntry.language)?.exportVisibility === 'capitalization' && /^[a-z]/.test(name)) {
|
|
401
406
|
const targetPkgDirs = new Set(
|
|
402
407
|
targetDefs.filter(d => d.file).map(d => path.dirname(d.file))
|
|
403
408
|
);
|
|
@@ -412,7 +417,7 @@ function findCallers(index, name, options = {}) {
|
|
|
412
417
|
// Rust path calls (module::func(), Type::new()) bypass this filter — they're
|
|
413
418
|
// scoped_identifier calls that can target both standalone functions and impl methods.
|
|
414
419
|
if (!bindingId && !resolvedBySameClass && !call.isPathCall &&
|
|
415
|
-
(fileEntry.language === '
|
|
420
|
+
langTraits(fileEntry.language)?.typeSystem === 'nominal') {
|
|
416
421
|
const targetHasClass = targetDefs.some(d => d.className);
|
|
417
422
|
if (call.isMethod && !targetHasClass) {
|
|
418
423
|
// Method call but target is a standalone function — skip
|
|
@@ -424,19 +429,65 @@ function findCallers(index, name, options = {}) {
|
|
|
424
429
|
}
|
|
425
430
|
}
|
|
426
431
|
|
|
432
|
+
// Go package-qualified call filter: when a non-method call has a receiver
|
|
433
|
+
// that is an import alias (e.g., fmt.Errorf()), verify the caller imports
|
|
434
|
+
// a project file containing the target. Catches stdlib (single-segment imports
|
|
435
|
+
// like "fmt", "os") and third-party calls (import graph has no edge to target).
|
|
436
|
+
if (!call.isMethod && call.receiver && !bindingId &&
|
|
437
|
+
langTraits(fileEntry.language)?.hasReceiverPackageCalls) {
|
|
438
|
+
const callerFileImports = fileEntry.imports || [];
|
|
439
|
+
const importModule = callerFileImports.find(mod => {
|
|
440
|
+
const parts = mod.split('/');
|
|
441
|
+
const last = parts[parts.length - 1];
|
|
442
|
+
const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
443
|
+
return pkgName === call.receiver;
|
|
444
|
+
});
|
|
445
|
+
if (importModule) {
|
|
446
|
+
if (!importModule.includes('/')) {
|
|
447
|
+
// Single-segment import — Go stdlib, always external
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Multi-segment import — verify via import graph
|
|
451
|
+
const callerImportedFiles = index.importGraph.get(filePath) || [];
|
|
452
|
+
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
453
|
+
if (!targetFiles.has(filePath)) {
|
|
454
|
+
const hasImportEdge = callerImportedFiles.some(imp => targetFiles.has(imp));
|
|
455
|
+
if (!hasImportEdge) {
|
|
456
|
+
// No import edge — allow same-package (same directory) calls
|
|
457
|
+
const callerDir = path.dirname(filePath);
|
|
458
|
+
const samePackage = targetDefs.some(d => d.file && path.dirname(d.file) === callerDir);
|
|
459
|
+
if (!samePackage) continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
427
465
|
// Receiver-class disambiguation:
|
|
428
466
|
// When the target definition has a class/receiver type, filter callers
|
|
429
467
|
// whose receiverType is known to be a different type.
|
|
430
468
|
// All languages use receiverType when available (constructor/annotation inference).
|
|
431
469
|
// Go/Java/Rust additionally fall back to variable name matching.
|
|
432
470
|
if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
|
|
433
|
-
(call.receiverType || fileEntry.language === '
|
|
471
|
+
(call.receiverType || langTraits(fileEntry.language)?.typeSystem === 'nominal')) {
|
|
434
472
|
// Build target type set from both className (Java) and receiver (Go/Rust)
|
|
435
473
|
const targetTypes = new Set();
|
|
436
474
|
for (const td of targetDefs) {
|
|
437
475
|
if (td.className) targetTypes.add(td.className);
|
|
438
476
|
if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
|
|
439
477
|
}
|
|
478
|
+
// Expand targetTypes with types that embed the target (Go/Java/Rust)
|
|
479
|
+
// e.g., if target is Base.Start() and Child embeds Base, accept Child.Start() callers
|
|
480
|
+
if (targetTypes.size > 0 && langTraits(fileEntry.language)?.typeSystem === 'nominal') {
|
|
481
|
+
for (const tt of [...targetTypes]) {
|
|
482
|
+
const children = index.extendedByGraph?.get(tt);
|
|
483
|
+
if (children) {
|
|
484
|
+
for (const child of children) {
|
|
485
|
+
const cName = typeof child === 'string' ? child : child.name;
|
|
486
|
+
if (cName) targetTypes.add(cName);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
440
491
|
if (targetTypes.size > 0) {
|
|
441
492
|
// Use inferred receiverType when available (Go/Java/Rust parameter type tracking)
|
|
442
493
|
const knownType = call.receiverType;
|
|
@@ -450,32 +501,68 @@ function findCallers(index, name, options = {}) {
|
|
|
450
501
|
continue;
|
|
451
502
|
}
|
|
452
503
|
}
|
|
453
|
-
} else
|
|
454
|
-
// No inferred type —
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
465
|
-
|
|
504
|
+
} else {
|
|
505
|
+
// No parser-inferred type — try local type inference
|
|
506
|
+
// for Go/Java/Rust (nominal type systems)
|
|
507
|
+
let inferredMatch = false;
|
|
508
|
+
let inferredMismatch = false;
|
|
509
|
+
if (langTraits(fileEntry.language)?.typeSystem === 'nominal') {
|
|
510
|
+
const callerSym = index.findEnclosingFunction(filePath, call.line, true);
|
|
511
|
+
if (callerSym && callerSym.startLine != null && callerSym.endLine != null) {
|
|
512
|
+
const cacheKey = `${filePath}:${callerSym.startLine}`;
|
|
513
|
+
let localTypes = localTypeCache.get(cacheKey);
|
|
514
|
+
if (localTypes === undefined) {
|
|
515
|
+
const callsForFile = getCachedCalls(index, filePath);
|
|
516
|
+
localTypes = callsForFile ? _buildTypedLocalTypeMap(index,
|
|
517
|
+
{ file: filePath, startLine: callerSym.startLine, endLine: callerSym.endLine },
|
|
518
|
+
callsForFile) : null;
|
|
519
|
+
localTypeCache.set(cacheKey, localTypes);
|
|
520
|
+
}
|
|
521
|
+
if (localTypes) {
|
|
522
|
+
const inferredType = localTypes.get(call.receiver);
|
|
523
|
+
if (inferredType) {
|
|
524
|
+
if (targetTypes.has(inferredType)) {
|
|
525
|
+
inferredMatch = true;
|
|
526
|
+
} else {
|
|
527
|
+
inferredMismatch = true;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
466
530
|
}
|
|
467
531
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
532
|
+
}
|
|
533
|
+
if (inferredMismatch) {
|
|
534
|
+
isUncertain = true;
|
|
535
|
+
if (!options.includeUncertain) {
|
|
536
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
537
|
+
continue;
|
|
472
538
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
539
|
+
}
|
|
540
|
+
// Still no type — fall back to receiver name matching when multiple defs exist
|
|
541
|
+
if (!inferredMatch && !inferredMismatch && definitions.length > 1) {
|
|
542
|
+
const receiverLower = call.receiver.toLowerCase();
|
|
543
|
+
const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
|
|
544
|
+
if (!matchesTarget) {
|
|
545
|
+
// Rust/Go path calls (Type::method() / pkg.Method()): receiver IS the type name
|
|
546
|
+
// If it doesn't match target, it's definitely a different type — filter it
|
|
547
|
+
if (call.isPathCall && /^[A-Z]/.test(call.receiver)) {
|
|
548
|
+
isUncertain = true;
|
|
549
|
+
if (!options.includeUncertain) {
|
|
550
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const nonTargetClasses = new Set();
|
|
555
|
+
for (const d of definitions) {
|
|
556
|
+
const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
|
|
557
|
+
if (t && !targetTypes.has(t)) nonTargetClasses.add(t);
|
|
558
|
+
}
|
|
559
|
+
const matchesOther = [...nonTargetClasses].some(cn => cn.toLowerCase() === receiverLower);
|
|
560
|
+
if (matchesOther) {
|
|
561
|
+
isUncertain = true;
|
|
562
|
+
if (!options.includeUncertain) {
|
|
563
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
479
566
|
}
|
|
480
567
|
}
|
|
481
568
|
}
|
|
@@ -515,7 +602,7 @@ function findCallers(index, name, options = {}) {
|
|
|
515
602
|
// Method calls where binding resolution was skipped (non-self receiver)
|
|
516
603
|
// and the receiver has no binding evidence → uncertain (JS/TS/Python only)
|
|
517
604
|
skipLocalBinding && call.isMethod && !resolvedBySameClass &&
|
|
518
|
-
fileEntry.language
|
|
605
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
519
606
|
!(call.receiver && (fileEntry.bindings || []).some(b => b.name === call.receiver))
|
|
520
607
|
),
|
|
521
608
|
hasReceiverType: !!call.receiverType,
|
|
@@ -604,9 +691,9 @@ function findCallees(index, def, options = {}) {
|
|
|
604
691
|
// Build local variable type map for receiver resolution
|
|
605
692
|
// Scans for patterns like: bt = Backtester(...) → bt maps to Backtester
|
|
606
693
|
let localTypes = null;
|
|
607
|
-
if (language === '
|
|
694
|
+
if (langTraits(language)?.typeSystem === 'structural') {
|
|
608
695
|
localTypes = _buildLocalTypeMap(index, def, calls);
|
|
609
|
-
} else if (language === '
|
|
696
|
+
} else if (langTraits(language)?.typeSystem === 'nominal') {
|
|
610
697
|
localTypes = _buildTypedLocalTypeMap(index, def, calls);
|
|
611
698
|
}
|
|
612
699
|
|
|
@@ -644,10 +731,23 @@ function findCallees(index, def, options = {}) {
|
|
|
644
731
|
const symbols = index.symbols.get(call.name);
|
|
645
732
|
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
646
733
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
647
|
-
|
|
734
|
+
let match = symbols?.find(s =>
|
|
648
735
|
isCallable(s) && (
|
|
649
736
|
s.className === typeName ||
|
|
650
737
|
(s.receiver && s.receiver.replace(/^\*/, '') === typeName)));
|
|
738
|
+
// Walk embedding/inheritance chain if no direct match (nominal type systems)
|
|
739
|
+
if (!match && langTraits(language)?.typeSystem === 'nominal') {
|
|
740
|
+
const parentNames = index._getInheritanceParents?.(typeName, def.file);
|
|
741
|
+
if (parentNames) {
|
|
742
|
+
for (const pName of parentNames) {
|
|
743
|
+
match = symbols?.find(s =>
|
|
744
|
+
isCallable(s) && (
|
|
745
|
+
s.className === pName ||
|
|
746
|
+
(s.receiver && s.receiver.replace(/^\*/, '') === pName)));
|
|
747
|
+
if (match) break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
651
751
|
if (match) {
|
|
652
752
|
const key = match.bindingId || `${typeName}.${call.name}`;
|
|
653
753
|
const existing = callees.get(key);
|
|
@@ -667,10 +767,23 @@ function findCallees(index, def, options = {}) {
|
|
|
667
767
|
const symbols = index.symbols.get(call.name);
|
|
668
768
|
const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
669
769
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
670
|
-
|
|
770
|
+
let match = symbols?.find(s =>
|
|
671
771
|
isCallableRT(s) && (
|
|
672
772
|
(s.receiver && s.receiver.replace(/^\*/, '') === typeName) ||
|
|
673
773
|
s.className === typeName));
|
|
774
|
+
// Walk embedding/inheritance chain if no direct match (nominal type systems)
|
|
775
|
+
if (!match && langTraits(language)?.typeSystem === 'nominal') {
|
|
776
|
+
const parentNames = index._getInheritanceParents?.(typeName, def.file);
|
|
777
|
+
if (parentNames) {
|
|
778
|
+
for (const pName of parentNames) {
|
|
779
|
+
match = symbols?.find(s =>
|
|
780
|
+
isCallableRT(s) && (
|
|
781
|
+
(s.receiver && s.receiver.replace(/^\*/, '') === pName) ||
|
|
782
|
+
s.className === pName));
|
|
783
|
+
if (match) break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
674
787
|
if (match) {
|
|
675
788
|
const key = match.bindingId || `${typeName}.${call.name}`;
|
|
676
789
|
const existing = callees.get(key);
|
|
@@ -682,30 +795,44 @@ function findCallees(index, def, options = {}) {
|
|
|
682
795
|
continue;
|
|
683
796
|
}
|
|
684
797
|
// No match found with inferred type — fall through to include as unresolved
|
|
685
|
-
} else if (language
|
|
798
|
+
} else if (langTraits(language)?.hasReceiverPackageCalls && call.receiver) {
|
|
686
799
|
// Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
|
|
687
800
|
// Check if receiver is an import alias and resolve to correct package
|
|
688
801
|
const goImports = fileEntry?.imports || [];
|
|
689
802
|
// Find import whose package name matches the receiver
|
|
803
|
+
// Handle Go version suffixes: k8s.io/klog/v2 → klog, not v2
|
|
690
804
|
const importModule = goImports.find(mod => {
|
|
691
|
-
const
|
|
805
|
+
const parts = mod.split('/');
|
|
806
|
+
const last = parts[parts.length - 1];
|
|
807
|
+
const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
692
808
|
return pkgName === call.receiver;
|
|
693
809
|
});
|
|
694
810
|
if (importModule) {
|
|
695
811
|
// Receiver is an import alias — resolve to definitions from that package
|
|
696
812
|
const symbols = index.symbols.get(call.name);
|
|
697
813
|
if (symbols) {
|
|
698
|
-
// Match by checking if the definition's directory path matches the import path suffix
|
|
814
|
+
// Match by checking if the definition's directory path matches the import path suffix.
|
|
815
|
+
// Pick the symbol with the LONGEST suffix match to avoid false positives
|
|
816
|
+
// (e.g., import "k8s.io/client-go/kubernetes/scheme" should prefer a definition
|
|
817
|
+
// in .../client-go/kubernetes/scheme/ over one in .../kubeadm/scheme/).
|
|
699
818
|
const importParts = importModule.split('/');
|
|
700
|
-
|
|
819
|
+
let bestMatch = null;
|
|
820
|
+
let bestMatchLen = 0;
|
|
821
|
+
for (const s of symbols) {
|
|
701
822
|
const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
|
|
702
|
-
|
|
703
|
-
for (let i = importParts.length - 1; i >= 0; i--) {
|
|
823
|
+
for (let i = 0; i < importParts.length; i++) {
|
|
704
824
|
const suffix = importParts.slice(i).join('/');
|
|
705
|
-
if (sDir === suffix || sDir.endsWith('/' + suffix))
|
|
825
|
+
if (sDir === suffix || sDir.endsWith('/' + suffix)) {
|
|
826
|
+
const matchLen = importParts.length - i;
|
|
827
|
+
if (matchLen > bestMatchLen) {
|
|
828
|
+
bestMatchLen = matchLen;
|
|
829
|
+
bestMatch = s;
|
|
830
|
+
}
|
|
831
|
+
break; // this symbol's best suffix found, try next
|
|
832
|
+
}
|
|
706
833
|
}
|
|
707
|
-
|
|
708
|
-
|
|
834
|
+
}
|
|
835
|
+
const match = bestMatch;
|
|
709
836
|
if (match) {
|
|
710
837
|
const key = match.bindingId || `${call.receiver}.${call.name}`;
|
|
711
838
|
const existing = callees.get(key);
|
|
@@ -717,8 +844,10 @@ function findCallees(index, def, options = {}) {
|
|
|
717
844
|
continue;
|
|
718
845
|
}
|
|
719
846
|
}
|
|
847
|
+
// Import resolved but no project definition matches — external call, skip
|
|
848
|
+
continue;
|
|
720
849
|
}
|
|
721
|
-
} else if (language
|
|
850
|
+
} else if (langTraits(language)?.methodCallInclusion === 'explicit' && !options.includeMethods) {
|
|
722
851
|
continue;
|
|
723
852
|
}
|
|
724
853
|
}
|
|
@@ -781,7 +910,7 @@ function findCallees(index, def, options = {}) {
|
|
|
781
910
|
if (!call.bindingId && fileEntry?.bindings) {
|
|
782
911
|
let bindings = fileEntry.bindings.filter(b => b.name === call.name);
|
|
783
912
|
// For Go, also check sibling files in same directory (same package scope)
|
|
784
|
-
if (bindings.length === 0 && language === '
|
|
913
|
+
if (bindings.length === 0 && langTraits(language)?.packageScope === 'directory') {
|
|
785
914
|
const dir = path.dirname(def.file);
|
|
786
915
|
for (const [fp, fe] of index.files) {
|
|
787
916
|
if (fp !== def.file && path.dirname(fp) === dir) {
|
|
@@ -793,7 +922,7 @@ function findCallees(index, def, options = {}) {
|
|
|
793
922
|
// Method call with no binding for the method name:
|
|
794
923
|
// Different strategies by language family:
|
|
795
924
|
if (bindings.length === 0 && call.isMethod) {
|
|
796
|
-
if (language
|
|
925
|
+
if (langTraits(language)?.typeSystem === 'structural') {
|
|
797
926
|
// JS/TS/Python: mark uncertain unless receiver has import/binding
|
|
798
927
|
// evidence in file scope AND that binding can plausibly have this method.
|
|
799
928
|
// Prevents false positives like m.get() → repository.get() when m is
|
|
@@ -818,7 +947,7 @@ function findCallees(index, def, options = {}) {
|
|
|
818
947
|
// Go: if receiverType is known, check if it matches exactly one def
|
|
819
948
|
// This resolves ambiguity like Framework.Run vs Scheduler.Run
|
|
820
949
|
const rType = call.receiverType || localTypes?.get(call.receiver);
|
|
821
|
-
if (rType && (language === '
|
|
950
|
+
if (rType && langTraits(language)?.typeSystem === 'nominal') {
|
|
822
951
|
const matchingDef = defs.find(d =>
|
|
823
952
|
d.className === rType ||
|
|
824
953
|
(d.receiver && d.receiver.replace(/^\*/, '') === rType));
|
|
@@ -840,7 +969,7 @@ function findCallees(index, def, options = {}) {
|
|
|
840
969
|
// matches the binding's class. Prevents plt.close() → ReportGenerator.close()
|
|
841
970
|
// when close is defined in the same file as a class method.
|
|
842
971
|
if (call.isMethod && call.receiver && bindings[0].type === 'method' &&
|
|
843
|
-
language
|
|
972
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
844
973
|
// The binding is a class method — check if the receiver could be an instance
|
|
845
974
|
const bindingSym = index.symbols.get(call.name)?.find(
|
|
846
975
|
s => s.bindingId === bindings[0].id);
|
|
@@ -1269,6 +1398,7 @@ function _buildLocalTypeMap(index, def, calls) {
|
|
|
1269
1398
|
*/
|
|
1270
1399
|
function _buildTypedLocalTypeMap(index, def, calls) {
|
|
1271
1400
|
const localTypes = new Map();
|
|
1401
|
+
let _cachedLines = null;
|
|
1272
1402
|
|
|
1273
1403
|
for (const call of calls) {
|
|
1274
1404
|
if (call.line < def.startLine || call.line > def.endLine) continue;
|
|
@@ -1282,12 +1412,12 @@ function _buildTypedLocalTypeMap(index, def, calls) {
|
|
|
1282
1412
|
// Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()
|
|
1283
1413
|
const newName = call.isMethod ? call.name : call.name;
|
|
1284
1414
|
if (/^New[A-Z]/.test(newName) && !call.isPotentialCallback) {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const sourceLine =
|
|
1415
|
+
if (!_cachedLines) {
|
|
1416
|
+
try {
|
|
1417
|
+
_cachedLines = index._readFile(def.file).split('\n');
|
|
1418
|
+
} catch { continue; }
|
|
1419
|
+
}
|
|
1420
|
+
const sourceLine = _cachedLines[call.line - 1];
|
|
1291
1421
|
if (!sourceLine) continue;
|
|
1292
1422
|
// Match: x := [pkg.]NewFoo( or x, err := [pkg.]NewFoo( or x, _ := [pkg.]NewFoo(
|
|
1293
1423
|
const assignMatch = sourceLine.match(
|
package/core/deadcode.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* as the first argument instead of using `this`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
8
|
+
const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } = require('../languages');
|
|
9
9
|
const { isTestFile } = require('./discovery');
|
|
10
|
-
const { escapeRegExp } = require('./shared');
|
|
11
10
|
const { isFrameworkEntrypoint } = require('./entrypoints');
|
|
12
11
|
|
|
13
12
|
/** Check if a position in a line is inside a string literal (quotes/backticks) */
|
|
@@ -206,13 +205,27 @@ function deadcode(index, options = {}) {
|
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
// Pre-filter: names in the callee index have call sites → definitely used → not dead.
|
|
209
|
-
|
|
208
|
+
let potentiallyDeadNames = new Set();
|
|
210
209
|
for (const name of callableNames) {
|
|
211
210
|
if (!index.calleeIndex.has(name)) {
|
|
212
211
|
potentiallyDeadNames.add(name);
|
|
213
212
|
}
|
|
214
213
|
}
|
|
215
214
|
|
|
215
|
+
// When --file is provided, pre-filter to only names of symbols in the target scope.
|
|
216
|
+
// The text scan below is O(potentiallyDeadNames × files) — narrowing the name set
|
|
217
|
+
// avoids scanning all files for names that will be filtered out at the result stage.
|
|
218
|
+
if (options.file) {
|
|
219
|
+
const filteredNames = new Set();
|
|
220
|
+
for (const name of potentiallyDeadNames) {
|
|
221
|
+
const syms = index.symbols.get(name) || [];
|
|
222
|
+
if (syms.some(s => s.relativePath && s.relativePath.includes(options.file))) {
|
|
223
|
+
filteredNames.add(name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
potentiallyDeadNames = filteredNames;
|
|
227
|
+
}
|
|
228
|
+
|
|
216
229
|
// Build usage index for potentially dead names using text scan (no tree-sitter reparsing).
|
|
217
230
|
// The callee index already covers all call-based usages. For remaining names, a word-boundary
|
|
218
231
|
// text scan catches imports, exports, shorthand properties, type refs, and variable refs.
|
|
@@ -267,7 +280,7 @@ function deadcode(index, options = {}) {
|
|
|
267
280
|
}
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
|
-
} catch {}
|
|
283
|
+
} catch { /* skip unreadable files */ }
|
|
271
284
|
}
|
|
272
285
|
}
|
|
273
286
|
|
|
@@ -305,67 +318,10 @@ function deadcode(index, options = {}) {
|
|
|
305
318
|
|
|
306
319
|
const mods = symbol.modifiers || [];
|
|
307
320
|
|
|
308
|
-
// Language-specific entry points (called by runtime,
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
// Java: public static void main(String[] args) is the entry point
|
|
313
|
-
const isJavaEntryPoint = lang === 'java' && name === 'main' &&
|
|
314
|
-
mods.includes('public') && mods.includes('static');
|
|
315
|
-
|
|
316
|
-
// Python: Magic/dunder methods are called by the interpreter, not user code
|
|
317
|
-
// test_* functions/methods are called by pytest/unittest via reflection
|
|
318
|
-
// setUp/tearDown are unittest.TestCase framework methods called by test runner
|
|
319
|
-
// pytest_* are pytest plugin hooks called by the framework
|
|
320
|
-
const isPythonEntryPoint = lang === 'python' &&
|
|
321
|
-
(/^__\w+__$/.test(name) || /^test_/.test(name) ||
|
|
322
|
-
/^(setUp|tearDown)(Class|Module)?$/.test(name) ||
|
|
323
|
-
/^pytest_/.test(name));
|
|
324
|
-
|
|
325
|
-
// Rust: main() is entry point, #[test] and #[bench] functions are called by test/bench runner
|
|
326
|
-
const isRustEntryPoint = lang === 'rust' &&
|
|
327
|
-
(name === 'main' || mods.includes('test') || mods.includes('bench'));
|
|
328
|
-
|
|
329
|
-
// Rust: trait impl methods are invoked via trait dispatch, not direct calls
|
|
330
|
-
// They can never be "dead" - the trait contract requires them to exist
|
|
331
|
-
const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
|
|
332
|
-
symbol.className && symbol.traitImpl;
|
|
333
|
-
|
|
334
|
-
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
335
|
-
const isGoTestFunc = lang === 'go' &&
|
|
336
|
-
/^(Test|Benchmark|Example)[A-Z]/.test(name);
|
|
337
|
-
|
|
338
|
-
// Java: @Test annotated methods are called by JUnit
|
|
339
|
-
const isJavaTestMethod = lang === 'java' && mods.includes('test');
|
|
340
|
-
|
|
341
|
-
// Java: @Override methods are invoked via polymorphic dispatch
|
|
342
|
-
// They implement interface/superclass contracts and can't be dead
|
|
343
|
-
const isJavaOverride = lang === 'java' && mods.includes('override');
|
|
344
|
-
|
|
345
|
-
// Skip trait impl / @Override methods entirely - they're required by the type system
|
|
346
|
-
if (isRustTraitImpl || isJavaOverride) {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// JavaScript/TypeScript: framework lifecycle methods called by runtime
|
|
351
|
-
// React class components, Web Components, Angular, Vue
|
|
352
|
-
const jsLifecycleMethods = new Set([
|
|
353
|
-
// React class component lifecycle
|
|
354
|
-
'render', 'componentDidMount', 'componentDidUpdate', 'componentWillUnmount',
|
|
355
|
-
'getDerivedStateFromProps', 'getDerivedStateFromError', 'componentDidCatch',
|
|
356
|
-
'getSnapshotBeforeUpdate', 'shouldComponentUpdate',
|
|
357
|
-
// Web Components lifecycle
|
|
358
|
-
'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
|
|
359
|
-
]);
|
|
360
|
-
const isJsEntryPoint = (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') &&
|
|
361
|
-
symbol.isMethod && jsLifecycleMethods.has(name);
|
|
362
|
-
|
|
363
|
-
const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
|
|
364
|
-
isJavaEntryPoint || isJavaTestMethod ||
|
|
365
|
-
isPythonEntryPoint || isRustEntryPoint || isJsEntryPoint;
|
|
366
|
-
|
|
367
|
-
// Entry points are always excluded — they're invoked by the runtime, not user code
|
|
368
|
-
if (isEntryPoint) {
|
|
321
|
+
// Language-specific entry points (called by runtime/test runner, not user code)
|
|
322
|
+
// Each language module declares its own isEntryPoint() rules.
|
|
323
|
+
const langModule = getLanguageModule(lang);
|
|
324
|
+
if (langModule.isEntryPoint?.(symbol)) {
|
|
369
325
|
continue;
|
|
370
326
|
}
|
|
371
327
|
|
|
@@ -384,7 +340,7 @@ function deadcode(index, options = {}) {
|
|
|
384
340
|
fileEntry.exports.includes(name) ||
|
|
385
341
|
mods.includes('export') ||
|
|
386
342
|
mods.includes('public') ||
|
|
387
|
-
(lang === '
|
|
343
|
+
(langTraits(lang)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(name))
|
|
388
344
|
);
|
|
389
345
|
|
|
390
346
|
// Skip exported unless requested
|