ucn 3.8.3 → 3.8.4
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/scheduled_tasks.lock +1 -0
- package/cli/index.js +5 -3
- package/core/callers.js +14 -3
- package/core/deadcode.js +1 -2
- package/core/execute.js +43 -43
- package/core/output.js +6 -5
- package/core/project.js +161 -10
- package/core/shared.js +2 -0
- package/core/verify.js +55 -23
- package/languages/go.js +10 -2
- package/languages/java.js +23 -10
- package/languages/javascript.js +8 -0
- package/languages/python.js +4 -0
- package/languages/rust.js +20 -8
- package/languages/utils.js +29 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"5e2a28ce-7f65-4369-a922-8d1d902f9d8f","pid":18086,"acquiredAt":1773363188494}
|
package/cli/index.js
CHANGED
|
@@ -169,7 +169,9 @@ if (unknownFlags.length > 0) {
|
|
|
169
169
|
const VALUE_FLAGS = new Set([
|
|
170
170
|
'--file', '--depth', '--top', '--context', '--direction',
|
|
171
171
|
'--add-param', '--remove-param', '--rename-to', '--default',
|
|
172
|
-
'--base', '--exclude', '--not', '--in', '--max-lines', '--class-name'
|
|
172
|
+
'--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
|
|
173
|
+
'--type', '--param', '--receiver', '--returns', '--decorator',
|
|
174
|
+
'--limit', '--max-files', '--min-confidence', '--stack'
|
|
173
175
|
]);
|
|
174
176
|
|
|
175
177
|
// Remove flags from args, then add args after -- (which are all positional)
|
|
@@ -1246,7 +1248,7 @@ Flags can be added per-command: context myFunc --include-methods
|
|
|
1246
1248
|
const tokens = input.split(/\s+/);
|
|
1247
1249
|
const command = tokens[0];
|
|
1248
1250
|
// Flags that take a space-separated value (--flag value)
|
|
1249
|
-
const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack']);
|
|
1251
|
+
const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name']);
|
|
1250
1252
|
const flagTokens = [];
|
|
1251
1253
|
const argTokens = [];
|
|
1252
1254
|
const skipNext = new Set();
|
|
@@ -1456,7 +1458,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1456
1458
|
}
|
|
1457
1459
|
|
|
1458
1460
|
case 'graph': {
|
|
1459
|
-
const { ok, result, error } = execute(index, 'graph', { file: arg,
|
|
1461
|
+
const { ok, result, error } = execute(index, 'graph', { file: arg || iflags.file, direction: iflags.direction, depth: iflags.depth, all: iflags.all });
|
|
1460
1462
|
if (!ok) { console.log(error); return; }
|
|
1461
1463
|
const graphDepth = iflags.depth ? parseInt(iflags.depth) : 2;
|
|
1462
1464
|
console.log(output.formatGraph(result, { showAll: iflags.all || !!iflags.depth, maxDepth: graphDepth, file: arg }));
|
package/core/callers.js
CHANGED
|
@@ -456,6 +456,15 @@ function findCallers(index, name, options = {}) {
|
|
|
456
456
|
const receiverLower = call.receiver.toLowerCase();
|
|
457
457
|
const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
|
|
458
458
|
if (!matchesTarget) {
|
|
459
|
+
// Rust/Go path calls (Type::method() / pkg.Method()): receiver IS the type name
|
|
460
|
+
// If it doesn't match target, it's definitely a different type — filter it
|
|
461
|
+
if (call.isPathCall && /^[A-Z]/.test(call.receiver)) {
|
|
462
|
+
isUncertain = true;
|
|
463
|
+
if (!options.includeUncertain) {
|
|
464
|
+
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
459
468
|
const nonTargetClasses = new Set();
|
|
460
469
|
for (const d of definitions) {
|
|
461
470
|
const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
|
|
@@ -915,7 +924,8 @@ function findCallees(index, def, options = {}) {
|
|
|
915
924
|
callees.set(calleeKey, {
|
|
916
925
|
name: effectiveName,
|
|
917
926
|
bindingId: bindingResolved,
|
|
918
|
-
count: 1
|
|
927
|
+
count: 1,
|
|
928
|
+
...(call.isConstructor && { isConstructor: true })
|
|
919
929
|
});
|
|
920
930
|
}
|
|
921
931
|
}
|
|
@@ -1023,7 +1033,7 @@ function findCallees(index, def, options = {}) {
|
|
|
1023
1033
|
// Pre-compute import graph for callee confidence scoring
|
|
1024
1034
|
const callerImportSet = new Set(index.importGraph.get(def.file) || []);
|
|
1025
1035
|
|
|
1026
|
-
for (const { name: calleeName, bindingId, count } of callees.values()) {
|
|
1036
|
+
for (const { name: calleeName, bindingId, count, isConstructor } of callees.values()) {
|
|
1027
1037
|
const symbols = index.symbols.get(calleeName);
|
|
1028
1038
|
if (symbols && symbols.length > 0) {
|
|
1029
1039
|
let callee = symbols[0];
|
|
@@ -1122,7 +1132,8 @@ function findCallees(index, def, options = {}) {
|
|
|
1122
1132
|
if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
|
|
1123
1133
|
const isFuncField = callee.type === 'field' && callee.fieldType &&
|
|
1124
1134
|
/^func\b/.test(callee.fieldType);
|
|
1125
|
-
|
|
1135
|
+
// Constructor calls (new Foo()) are always callable regardless of type
|
|
1136
|
+
if (!isFuncField && !isConstructor) continue;
|
|
1126
1137
|
}
|
|
1127
1138
|
|
|
1128
1139
|
// Skip test-file callees when caller is production code and
|
package/core/deadcode.js
CHANGED
|
@@ -328,9 +328,8 @@ function deadcode(index, options = {}) {
|
|
|
328
328
|
|
|
329
329
|
// Rust: trait impl methods are invoked via trait dispatch, not direct calls
|
|
330
330
|
// They can never be "dead" - the trait contract requires them to exist
|
|
331
|
-
// className for trait impls contains " for " (e.g., "PartialEq for Glob")
|
|
332
331
|
const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
|
|
333
|
-
symbol.className && symbol.
|
|
332
|
+
symbol.className && symbol.traitImpl;
|
|
334
333
|
|
|
335
334
|
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
336
335
|
const isGoTestFunc = lang === 'go' &&
|
package/core/execute.js
CHANGED
|
@@ -393,36 +393,25 @@ const HANDLERS = {
|
|
|
393
393
|
if (err) return { ok: false, error: err };
|
|
394
394
|
applyClassMethodSyntax(p);
|
|
395
395
|
const exclude = applyTestExclusions(p.exclude, p.includeTests);
|
|
396
|
+
const fileErr = checkFilePatternMatch(index, p.file);
|
|
397
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
396
398
|
const result = index.usages(p.name, {
|
|
397
399
|
codeOnly: p.codeOnly || false,
|
|
398
400
|
context: num(p.context, 0),
|
|
399
401
|
className: p.className,
|
|
402
|
+
file: p.file,
|
|
400
403
|
exclude,
|
|
401
404
|
in: p.in,
|
|
402
405
|
});
|
|
403
|
-
// Apply limit to total usages
|
|
406
|
+
// Apply limit to total usages (result is a flat array)
|
|
404
407
|
const limit = num(p.limit, undefined);
|
|
405
408
|
let note;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const truncated = [];
|
|
411
|
-
for (const f of result.files) {
|
|
412
|
-
if (remaining <= 0) break;
|
|
413
|
-
if (f.usages.length <= remaining) {
|
|
414
|
-
truncated.push(f);
|
|
415
|
-
remaining -= f.usages.length;
|
|
416
|
-
} else {
|
|
417
|
-
truncated.push({ ...f, usages: f.usages.slice(0, remaining) });
|
|
418
|
-
remaining = 0;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
result.files = truncated;
|
|
422
|
-
note = limitNote(limit, total);
|
|
423
|
-
}
|
|
409
|
+
let limited = result;
|
|
410
|
+
if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
|
|
411
|
+
note = limitNote(limit, result.length);
|
|
412
|
+
limited = result.slice(0, limit);
|
|
424
413
|
}
|
|
425
|
-
return { ok: true, result, note };
|
|
414
|
+
return { ok: true, result: limited, note };
|
|
426
415
|
},
|
|
427
416
|
|
|
428
417
|
toc: (index, p) => {
|
|
@@ -432,34 +421,44 @@ const HANDLERS = {
|
|
|
432
421
|
all: p.all,
|
|
433
422
|
top: num(p.top, undefined),
|
|
434
423
|
file: p.file,
|
|
424
|
+
exclude: p.exclude,
|
|
435
425
|
});
|
|
436
|
-
// Apply limit to detailed toc entries
|
|
426
|
+
// Apply limit to detailed toc entries (symbols are in f.symbols.functions/classes arrays)
|
|
437
427
|
const limit = num(p.limit, undefined);
|
|
438
428
|
let note;
|
|
439
429
|
if (limit && limit > 0 && p.detailed && result.files) {
|
|
440
|
-
let totalEntries = result.files.reduce((s, f) =>
|
|
430
|
+
let totalEntries = result.files.reduce((s, f) => {
|
|
431
|
+
const syms = f.symbols || {};
|
|
432
|
+
return s + (syms.functions?.length || 0) + (syms.classes?.length || 0);
|
|
433
|
+
}, 0);
|
|
441
434
|
if (totalEntries > limit) {
|
|
442
435
|
let remaining = limit;
|
|
443
436
|
for (const f of result.files) {
|
|
437
|
+
const syms = f.symbols || {};
|
|
444
438
|
if (remaining <= 0) {
|
|
445
|
-
|
|
446
|
-
|
|
439
|
+
if (syms.functions) syms.functions = [];
|
|
440
|
+
if (syms.classes) syms.classes = [];
|
|
441
|
+
f.functions = 0;
|
|
442
|
+
f.classes = 0;
|
|
447
443
|
continue;
|
|
448
444
|
}
|
|
449
|
-
const fns =
|
|
450
|
-
const cls =
|
|
445
|
+
const fns = syms.functions?.length || 0;
|
|
446
|
+
const cls = syms.classes?.length || 0;
|
|
451
447
|
if (fns + cls <= remaining) {
|
|
452
448
|
remaining -= fns + cls;
|
|
453
449
|
} else {
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
remaining -=
|
|
450
|
+
if (syms.functions && remaining > 0) {
|
|
451
|
+
syms.functions = syms.functions.slice(0, remaining);
|
|
452
|
+
remaining -= syms.functions.length;
|
|
453
|
+
f.functions = syms.functions.length;
|
|
457
454
|
}
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
remaining -=
|
|
461
|
-
|
|
462
|
-
|
|
455
|
+
if (syms.classes && remaining > 0) {
|
|
456
|
+
syms.classes = syms.classes.slice(0, remaining);
|
|
457
|
+
remaining -= syms.classes.length;
|
|
458
|
+
f.classes = syms.classes.length;
|
|
459
|
+
} else if (syms.classes) {
|
|
460
|
+
syms.classes = [];
|
|
461
|
+
f.classes = 0;
|
|
463
462
|
}
|
|
464
463
|
}
|
|
465
464
|
}
|
|
@@ -549,7 +548,7 @@ const HANDLERS = {
|
|
|
549
548
|
deadcode: (index, p) => {
|
|
550
549
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
551
550
|
if (fileErr) return { ok: false, error: fileErr };
|
|
552
|
-
|
|
551
|
+
let result = index.deadcode({
|
|
553
552
|
includeExported: p.includeExported || false,
|
|
554
553
|
includeDecorated: p.includeDecorated || false,
|
|
555
554
|
includeTests: p.includeTests || false,
|
|
@@ -557,15 +556,16 @@ const HANDLERS = {
|
|
|
557
556
|
in: p.in,
|
|
558
557
|
file: p.file,
|
|
559
558
|
});
|
|
560
|
-
// Apply limit to dead code results
|
|
559
|
+
// Apply limit to dead code results (result is an array with custom properties)
|
|
561
560
|
const limit = num(p.limit, undefined);
|
|
562
561
|
let note;
|
|
563
|
-
if (limit && limit > 0 && result.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
562
|
+
if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
|
|
563
|
+
note = limitNote(limit, result.length);
|
|
564
|
+
const sliced = result.slice(0, limit);
|
|
565
|
+
// Preserve custom properties (excludedExported, excludedDecorated) from deadcode()
|
|
566
|
+
if (result.excludedExported != null) sliced.excludedExported = result.excludedExported;
|
|
567
|
+
if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
|
|
568
|
+
result = sliced;
|
|
569
569
|
}
|
|
570
570
|
return { ok: true, result, note };
|
|
571
571
|
},
|
|
@@ -866,7 +866,7 @@ const HANDLERS = {
|
|
|
866
866
|
const err = requireName(p.name);
|
|
867
867
|
if (err) return { ok: false, error: err };
|
|
868
868
|
applyClassMethodSyntax(p);
|
|
869
|
-
const result = index.typedef(p.name, { exact: p.exact || false, className: p.className });
|
|
869
|
+
const result = index.typedef(p.name, { exact: p.exact || false, className: p.className, file: p.file });
|
|
870
870
|
return { ok: true, result };
|
|
871
871
|
},
|
|
872
872
|
|
package/core/output.js
CHANGED
|
@@ -392,7 +392,7 @@ function formatSearchJson(results, term) {
|
|
|
392
392
|
const meta = results.meta;
|
|
393
393
|
const obj = {
|
|
394
394
|
term,
|
|
395
|
-
totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0),
|
|
395
|
+
totalMatches: (meta && meta.totalMatches != null) ? meta.totalMatches : results.reduce((sum, r) => sum + r.matches.length, 0),
|
|
396
396
|
files: results.map(r => ({
|
|
397
397
|
file: r.file,
|
|
398
398
|
matchCount: r.matches.length,
|
|
@@ -407,6 +407,7 @@ function formatSearchJson(results, term) {
|
|
|
407
407
|
obj.filesSkipped = meta.filesSkipped;
|
|
408
408
|
obj.totalFiles = meta.totalFiles;
|
|
409
409
|
if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
|
|
410
|
+
if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
|
|
410
411
|
}
|
|
411
412
|
return JSON.stringify(obj, null, 2);
|
|
412
413
|
}
|
|
@@ -839,7 +840,7 @@ function formatTrace(trace, options = {}) {
|
|
|
839
840
|
}
|
|
840
841
|
|
|
841
842
|
if (trace.includeMethods === false) {
|
|
842
|
-
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded
|
|
843
|
+
const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded — use --include-methods to include them';
|
|
843
844
|
lines.push(`\n${methodsHint}`);
|
|
844
845
|
}
|
|
845
846
|
|
|
@@ -1634,7 +1635,7 @@ function formatAbout(about, options = {}) {
|
|
|
1634
1635
|
}
|
|
1635
1636
|
|
|
1636
1637
|
if (about.includeMethods === false) {
|
|
1637
|
-
const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded
|
|
1638
|
+
const methodsHint = options.methodsHint || 'Note: obj.method() callers/callees excluded — use --include-methods to include them';
|
|
1638
1639
|
lines.push(`\n${methodsHint}`);
|
|
1639
1640
|
}
|
|
1640
1641
|
|
|
@@ -2322,7 +2323,7 @@ function formatCircularDeps(result) {
|
|
|
2322
2323
|
lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
|
|
2323
2324
|
}
|
|
2324
2325
|
|
|
2325
|
-
const scannedCount = result.filesWithImports
|
|
2326
|
+
const scannedCount = result.filesWithImports != null ? result.filesWithImports : result.totalFiles;
|
|
2326
2327
|
|
|
2327
2328
|
if (result.cycles.length === 0) {
|
|
2328
2329
|
lines.push('');
|
|
@@ -2413,7 +2414,7 @@ function formatSearch(results, term) {
|
|
|
2413
2414
|
}
|
|
2414
2415
|
|
|
2415
2416
|
if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
|
|
2416
|
-
lines.push(`\nNote: ${meta.filesSkipped} file${meta.filesSkipped === 1 ? '' : 's'}
|
|
2417
|
+
lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use include_tests=true to include).`);
|
|
2417
2418
|
}
|
|
2418
2419
|
|
|
2419
2420
|
return lines.join('\n');
|
package/core/project.js
CHANGED
|
@@ -365,7 +365,9 @@ class ProjectIndex {
|
|
|
365
365
|
...(item.memberType && { memberType: item.memberType }),
|
|
366
366
|
...(item.fieldType && { fieldType: item.fieldType }),
|
|
367
367
|
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
368
|
-
...(item.nameLine && { nameLine: item.nameLine })
|
|
368
|
+
...(item.nameLine && { nameLine: item.nameLine }),
|
|
369
|
+
...(item.traitImpl && { traitImpl: true }),
|
|
370
|
+
...(item.isSignature && { isSignature: true })
|
|
369
371
|
};
|
|
370
372
|
fileEntry.symbols.push(symbol);
|
|
371
373
|
fileEntry.bindings.push({
|
|
@@ -396,7 +398,7 @@ class ProjectIndex {
|
|
|
396
398
|
if (cls.members) {
|
|
397
399
|
for (const m of cls.members) {
|
|
398
400
|
const memberType = m.memberType || 'method';
|
|
399
|
-
addSymbol({ ...m, className: cls.name }, memberType);
|
|
401
|
+
addSymbol({ ...m, className: cls.name, ...(cls.traitName && { traitImpl: true }) }, memberType);
|
|
400
402
|
}
|
|
401
403
|
}
|
|
402
404
|
}
|
|
@@ -780,6 +782,8 @@ class ProjectIndex {
|
|
|
780
782
|
// Check inclusion (directory or file path)
|
|
781
783
|
if (filters.in) {
|
|
782
784
|
const inPattern = filters.in.replace(/\/$/, ''); // strip trailing slash
|
|
785
|
+
// '.' means current directory = entire project, always matches
|
|
786
|
+
if (inPattern === '.') return true;
|
|
783
787
|
// Detect if pattern looks like a file path (has an extension)
|
|
784
788
|
const looksLikeFile = /\.\w+$/.test(inPattern);
|
|
785
789
|
if (looksLikeFile) {
|
|
@@ -908,6 +912,13 @@ class ProjectIndex {
|
|
|
908
912
|
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) {
|
|
909
913
|
score += 200;
|
|
910
914
|
}
|
|
915
|
+
// Deprioritize type-only overload signatures (TypeScript function_signature)
|
|
916
|
+
if (d.isSignature) score -= 200;
|
|
917
|
+
// Prefer larger function bodies (implementation over overload signature)
|
|
918
|
+
// Only for functions/methods — not for class-level types (struct vs impl)
|
|
919
|
+
if (d.startLine && d.endLine && d.type === 'function') {
|
|
920
|
+
score += Math.min(d.endLine - d.startLine, 100);
|
|
921
|
+
}
|
|
911
922
|
return { def: d, score };
|
|
912
923
|
});
|
|
913
924
|
|
|
@@ -1257,11 +1268,17 @@ class ProjectIndex {
|
|
|
1257
1268
|
try {
|
|
1258
1269
|
const usages = [];
|
|
1259
1270
|
|
|
1271
|
+
// Resolve file pattern for --file filter
|
|
1272
|
+
const fileFilter = options.file ? this.resolveFilePathForQuery(options.file) : null;
|
|
1273
|
+
|
|
1260
1274
|
// Get definitions (filtered)
|
|
1261
1275
|
let allDefinitions = this.symbols.get(name) || [];
|
|
1262
1276
|
if (options.className) {
|
|
1263
1277
|
allDefinitions = allDefinitions.filter(d => d.className === options.className);
|
|
1264
1278
|
}
|
|
1279
|
+
if (fileFilter) {
|
|
1280
|
+
allDefinitions = allDefinitions.filter(d => d.file === fileFilter);
|
|
1281
|
+
}
|
|
1265
1282
|
const definitions = options.exclude || options.in
|
|
1266
1283
|
? allDefinitions.filter(d => this.matchesFilters(d.relativePath, options))
|
|
1267
1284
|
: allDefinitions;
|
|
@@ -1278,6 +1295,10 @@ class ProjectIndex {
|
|
|
1278
1295
|
|
|
1279
1296
|
// Scan all files for usages
|
|
1280
1297
|
for (const [filePath, fileEntry] of this.files) {
|
|
1298
|
+
// Apply --file filter
|
|
1299
|
+
if (fileFilter && filePath !== fileFilter) {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1281
1302
|
// Apply filters
|
|
1282
1303
|
if (!this.matchesFilters(fileEntry.relativePath, options)) {
|
|
1283
1304
|
continue;
|
|
@@ -2247,7 +2268,7 @@ class ProjectIndex {
|
|
|
2247
2268
|
* @returns {Array} Matching type definitions
|
|
2248
2269
|
*/
|
|
2249
2270
|
typedef(name, options = {}) {
|
|
2250
|
-
const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class'];
|
|
2271
|
+
const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class', 'record'];
|
|
2251
2272
|
const matches = this.find(name, options);
|
|
2252
2273
|
|
|
2253
2274
|
return matches.filter(m => typeKinds.includes(m.type)).map(m => ({
|
|
@@ -2277,6 +2298,15 @@ class ProjectIndex {
|
|
|
2277
2298
|
for (const [filePath, fileEntry] of this.files) {
|
|
2278
2299
|
if (isTestFile(fileEntry.relativePath, fileEntry.language)) {
|
|
2279
2300
|
testFiles.push({ path: filePath, entry: fileEntry });
|
|
2301
|
+
} else if (fileEntry.language === 'rust') {
|
|
2302
|
+
// Rust idiomatically puts tests in #[cfg(test)] modules inside source files.
|
|
2303
|
+
// Check if file has any symbols with 'test' modifier (#[test] attribute).
|
|
2304
|
+
const hasInlineTests = fileEntry.symbols?.some(s =>
|
|
2305
|
+
s.modifiers?.includes('test')
|
|
2306
|
+
);
|
|
2307
|
+
if (hasInlineTests) {
|
|
2308
|
+
testFiles.push({ path: filePath, entry: fileEntry });
|
|
2309
|
+
}
|
|
2280
2310
|
}
|
|
2281
2311
|
}
|
|
2282
2312
|
|
|
@@ -2592,6 +2622,75 @@ class ProjectIndex {
|
|
|
2592
2622
|
}
|
|
2593
2623
|
}
|
|
2594
2624
|
|
|
2625
|
+
// Python __all__ re-exports: names listed in __all__ that come from imports
|
|
2626
|
+
// e.g. __init__.py: `from .utils import helper` + `__all__ = ["helper"]`
|
|
2627
|
+
// `helper` is in fileEntry.exports but not in fileEntry.symbols
|
|
2628
|
+
if (fileEntry.language === 'python' && fileEntry.exports.length > 0) {
|
|
2629
|
+
const matchedNames = new Set(results.map(r => r.name));
|
|
2630
|
+
const unmatched = fileEntry.exports.filter(name => !matchedNames.has(name));
|
|
2631
|
+
if (unmatched.length > 0) {
|
|
2632
|
+
// Re-extract raw imports to get name→module mapping (not stored in fileEntry)
|
|
2633
|
+
const { extractImports, resolveImport } = require('./imports');
|
|
2634
|
+
try {
|
|
2635
|
+
const content = this._readFile(absPath);
|
|
2636
|
+
const { imports: rawImports } = extractImports(content, 'python');
|
|
2637
|
+
// Build name→module map from raw imports
|
|
2638
|
+
const nameToModule = new Map();
|
|
2639
|
+
for (const imp of rawImports) {
|
|
2640
|
+
if (imp.names) {
|
|
2641
|
+
for (const name of imp.names) {
|
|
2642
|
+
if (name !== '*') nameToModule.set(name, imp.module);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
for (const name of unmatched) {
|
|
2647
|
+
const sourceModule = nameToModule.get(name);
|
|
2648
|
+
if (!sourceModule) continue;
|
|
2649
|
+
const resolvedSrc = resolveImport(sourceModule, absPath, {
|
|
2650
|
+
language: 'python',
|
|
2651
|
+
root: this.root,
|
|
2652
|
+
extensions: this.extensions
|
|
2653
|
+
});
|
|
2654
|
+
if (!resolvedSrc) continue;
|
|
2655
|
+
const sourceEntry = this.files.get(resolvedSrc);
|
|
2656
|
+
const srcSymbol = sourceEntry && sourceEntry.symbols.find(s => s.name === name);
|
|
2657
|
+
if (srcSymbol) {
|
|
2658
|
+
matchedNames.add(name);
|
|
2659
|
+
results.push({
|
|
2660
|
+
name,
|
|
2661
|
+
type: srcSymbol.type,
|
|
2662
|
+
file: fileEntry.relativePath,
|
|
2663
|
+
startLine: srcSymbol.startLine,
|
|
2664
|
+
endLine: srcSymbol.endLine,
|
|
2665
|
+
params: srcSymbol.params,
|
|
2666
|
+
returnType: srcSymbol.returnType,
|
|
2667
|
+
signature: this.formatSignature(srcSymbol),
|
|
2668
|
+
reExportedFrom: sourceEntry.relativePath
|
|
2669
|
+
});
|
|
2670
|
+
} else {
|
|
2671
|
+
// Source not indexed or symbol not found — still list it
|
|
2672
|
+
matchedNames.add(name);
|
|
2673
|
+
results.push({
|
|
2674
|
+
name,
|
|
2675
|
+
type: 're-export',
|
|
2676
|
+
file: fileEntry.relativePath,
|
|
2677
|
+
startLine: undefined,
|
|
2678
|
+
endLine: undefined,
|
|
2679
|
+
params: undefined,
|
|
2680
|
+
returnType: null,
|
|
2681
|
+
signature: `re-export ${name} from '${sourceModule}'`,
|
|
2682
|
+
reExportedFrom: resolvedSrc
|
|
2683
|
+
? (sourceEntry ? sourceEntry.relativePath : resolvedSrc)
|
|
2684
|
+
: sourceModule
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
} catch (_) {
|
|
2689
|
+
// File read failure — skip Python re-export resolution
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2595
2694
|
return results;
|
|
2596
2695
|
}
|
|
2597
2696
|
|
|
@@ -3181,8 +3280,8 @@ class ProjectIndex {
|
|
|
3181
3280
|
const content = this._readFile(c.callerFile);
|
|
3182
3281
|
const lines = content.split('\n');
|
|
3183
3282
|
const line = lines[call.line - 1] || '';
|
|
3184
|
-
// Match "var = ClassName(...)"
|
|
3185
|
-
const m = line.match(
|
|
3283
|
+
// Match "var = ClassName(...)" or "var = new ClassName(...)" or "Type var = new ClassName<>(...)"
|
|
3284
|
+
const m = line.match(/(\w+)\s*=\s*(?:await\s+)?(?:new\s+)?(\w+)\s*(?:<[^>]*>)?\s*\(/);
|
|
3186
3285
|
if (m && m[2] === call.name) {
|
|
3187
3286
|
localTypes.set(m[1], call.name);
|
|
3188
3287
|
}
|
|
@@ -3198,6 +3297,31 @@ class ProjectIndex {
|
|
|
3198
3297
|
}
|
|
3199
3298
|
}
|
|
3200
3299
|
}
|
|
3300
|
+
// Check class field declarations for receiver type: private DataService service
|
|
3301
|
+
if (c.callerFile) {
|
|
3302
|
+
const callerEnclosing = this.findEnclosingFunction(c.callerFile, c.line, true);
|
|
3303
|
+
if (callerEnclosing?.className) {
|
|
3304
|
+
const classSyms = this.symbols.get(callerEnclosing.className);
|
|
3305
|
+
if (classSyms) {
|
|
3306
|
+
const classDef = classSyms.find(s => s.type === 'class' || s.type === 'struct' || s.type === 'interface');
|
|
3307
|
+
if (classDef) {
|
|
3308
|
+
const content = this._readFile(c.callerFile);
|
|
3309
|
+
const lines = content.split('\n');
|
|
3310
|
+
// Scan class body for field declarations matching the receiver
|
|
3311
|
+
for (let li = classDef.startLine - 1; li < (classDef.endLine || classDef.startLine + 50) && li < lines.length; li++) {
|
|
3312
|
+
const line = lines[li];
|
|
3313
|
+
// Match Java/TS field: [modifiers] TypeName<...> receiverName [= ...]
|
|
3314
|
+
const fieldMatch = line.match(new RegExp(`\\b(\\w+)(?:<[^>]*>)?\\s+${r.replace(/[.*+?^${}()|[\]\\]/g, '\\\\$&')}\\s*[;=]`));
|
|
3315
|
+
if (fieldMatch) {
|
|
3316
|
+
const fieldType = fieldMatch[1];
|
|
3317
|
+
if (fieldType === targetClassName) return true;
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3201
3325
|
// Check parameter type annotations: def foo(tracker: SourceTracker) → tracker.record()
|
|
3202
3326
|
if (c.callerFile && c.callerStartLine) {
|
|
3203
3327
|
const callerSymbol = this.findEnclosingFunction(c.callerFile, c.line, true);
|
|
@@ -3357,7 +3481,7 @@ class ProjectIndex {
|
|
|
3357
3481
|
const classNames = [...new Set(allDefs
|
|
3358
3482
|
.filter(d => d.className && d.className !== def.className)
|
|
3359
3483
|
.map(d => d.className))];
|
|
3360
|
-
if (classNames.length > 0) {
|
|
3484
|
+
if (classNames.length > 0 && !options.className && !options.file) {
|
|
3361
3485
|
scopeWarning = {
|
|
3362
3486
|
targetClass: def.className || '(unknown)',
|
|
3363
3487
|
otherClasses: classNames,
|
|
@@ -3678,6 +3802,19 @@ class ProjectIndex {
|
|
|
3678
3802
|
node.entryPoint = true;
|
|
3679
3803
|
entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
|
|
3680
3804
|
}
|
|
3805
|
+
} else if (currentDepth > 0) {
|
|
3806
|
+
// At depth limit: check if this node is an entry point
|
|
3807
|
+
const callers = this.findCallers(funcDef.name, {
|
|
3808
|
+
includeMethods,
|
|
3809
|
+
includeUncertain,
|
|
3810
|
+
targetDefinitions: funcDef.bindingId ? [funcDef] : undefined,
|
|
3811
|
+
});
|
|
3812
|
+
const hasCallers = callers.some(c => c.callerName &&
|
|
3813
|
+
(exclude.length === 0 || this.matchesFilters(c.relativePath, { exclude })));
|
|
3814
|
+
if (!hasCallers) {
|
|
3815
|
+
node.entryPoint = true;
|
|
3816
|
+
entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
|
|
3817
|
+
}
|
|
3681
3818
|
}
|
|
3682
3819
|
|
|
3683
3820
|
return node;
|
|
@@ -3764,7 +3901,12 @@ class ProjectIndex {
|
|
|
3764
3901
|
const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
|
|
3765
3902
|
const results = [];
|
|
3766
3903
|
for (const [filePath, fileEntry] of this.files) {
|
|
3767
|
-
|
|
3904
|
+
let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
|
|
3905
|
+
// Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
|
|
3906
|
+
if (!isTest && fileEntry.language === 'rust') {
|
|
3907
|
+
isTest = fileEntry.symbols?.some(s => s.modifiers?.includes('test'));
|
|
3908
|
+
}
|
|
3909
|
+
if (!isTest) continue;
|
|
3768
3910
|
if (excludeArr.length > 0 && !this.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
3769
3911
|
try {
|
|
3770
3912
|
const content = this._readFile(filePath);
|
|
@@ -4082,6 +4224,7 @@ class ProjectIndex {
|
|
|
4082
4224
|
const results = [];
|
|
4083
4225
|
let filesScanned = 0;
|
|
4084
4226
|
let filesSkipped = 0;
|
|
4227
|
+
let filesFilteredByFlag = 0;
|
|
4085
4228
|
const regexFlags = options.caseSensitive ? 'g' : 'gi';
|
|
4086
4229
|
const useRegex = options.regex !== false; // Default: regex ON
|
|
4087
4230
|
let regex;
|
|
@@ -4103,7 +4246,7 @@ class ProjectIndex {
|
|
|
4103
4246
|
if (options.file) {
|
|
4104
4247
|
const fp = fileEntry.relativePath;
|
|
4105
4248
|
if (!fp.includes(options.file) && !fp.endsWith(options.file)) {
|
|
4106
|
-
|
|
4249
|
+
filesFilteredByFlag++;
|
|
4107
4250
|
continue;
|
|
4108
4251
|
}
|
|
4109
4252
|
}
|
|
@@ -4228,7 +4371,7 @@ class ProjectIndex {
|
|
|
4228
4371
|
results.push(...truncated);
|
|
4229
4372
|
}
|
|
4230
4373
|
|
|
4231
|
-
results.meta = { filesScanned, filesSkipped, totalFiles: this.files.size, regexFallback, totalMatches, truncatedMatches };
|
|
4374
|
+
results.meta = { filesScanned, filesSkipped, filesFilteredByFlag, totalFiles: this.files.size, regexFallback, totalMatches, truncatedMatches };
|
|
4232
4375
|
return results;
|
|
4233
4376
|
} finally { this._endOp(); }
|
|
4234
4377
|
}
|
|
@@ -4330,7 +4473,7 @@ class ProjectIndex {
|
|
|
4330
4473
|
}
|
|
4331
4474
|
} else {
|
|
4332
4475
|
// Search symbols (functions, classes, methods, types)
|
|
4333
|
-
const functionTypes = new Set(['function', 'constructor', 'method', 'arrow', 'static', 'classmethod']);
|
|
4476
|
+
const functionTypes = new Set(['function', 'constructor', 'method', 'arrow', 'static', 'classmethod', 'abstract']);
|
|
4334
4477
|
const classTypes = new Set(['class', 'struct', 'interface', 'impl', 'trait']);
|
|
4335
4478
|
const typeTypes = new Set(['type', 'enum', 'interface', 'trait']);
|
|
4336
4479
|
const methodTypes = new Set(['method', 'constructor']);
|
|
@@ -4361,6 +4504,11 @@ class ProjectIndex {
|
|
|
4361
4504
|
if (!hasMatch) continue;
|
|
4362
4505
|
}
|
|
4363
4506
|
|
|
4507
|
+
// Receiver filter: match className for methods
|
|
4508
|
+
if (receiver) {
|
|
4509
|
+
if (!def.className || !matchesSubstring(def.className, receiver, options.caseSensitive)) continue;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4364
4512
|
// Return type filter
|
|
4365
4513
|
if (returnType) {
|
|
4366
4514
|
if (!def.returnType || !matchesSubstring(def.returnType, returnType, options.caseSensitive)) continue;
|
|
@@ -4546,6 +4694,9 @@ class ProjectIndex {
|
|
|
4546
4694
|
|
|
4547
4695
|
for (const [filePath, fileEntry] of this.files) {
|
|
4548
4696
|
if (fileFilter && !fileFilter.has(filePath)) continue;
|
|
4697
|
+
if (options.exclude && options.exclude.length > 0) {
|
|
4698
|
+
if (!this.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) continue;
|
|
4699
|
+
}
|
|
4549
4700
|
let functions = fileEntry.symbols.filter(s =>
|
|
4550
4701
|
s.type === 'function' || s.type === 'method' || s.type === 'static' ||
|
|
4551
4702
|
s.type === 'constructor' || s.type === 'public' || s.type === 'abstract' ||
|
package/core/shared.js
CHANGED
|
@@ -19,6 +19,8 @@ function pickBestDefinition(matches) {
|
|
|
19
19
|
if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
|
|
20
20
|
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
21
21
|
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
22
|
+
// Deprioritize type-only overload signatures (TypeScript function_signature)
|
|
23
|
+
if (m.isSignature) score -= 200;
|
|
22
24
|
// Tiebreaker: prefer larger function bodies (more important/complex)
|
|
23
25
|
if (m.startLine && m.endLine) {
|
|
24
26
|
score += Math.min(m.endLine - m.startLine, 100);
|
package/core/verify.js
CHANGED
|
@@ -29,11 +29,17 @@ function findCallNode(node, callTypes, targetRow, funcName) {
|
|
|
29
29
|
}
|
|
30
30
|
} else {
|
|
31
31
|
// Check if this call is for our target function
|
|
32
|
-
|
|
32
|
+
let funcNode = node.childForFieldName('function') ||
|
|
33
33
|
node.childForFieldName('name'); // Java method_invocation uses 'name'
|
|
34
|
+
// Unwrap turbofish/generic_function: process::<T>() wraps the function in generic_function
|
|
35
|
+
if (funcNode && funcNode.type === 'generic_function') {
|
|
36
|
+
funcNode = funcNode.childForFieldName('function') || funcNode.namedChild(0);
|
|
37
|
+
}
|
|
34
38
|
if (funcNode) {
|
|
35
39
|
const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
36
40
|
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
41
|
+
: funcNode.type === 'scoped_identifier'
|
|
42
|
+
? (funcNode.childForFieldName('name') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
37
43
|
: funcNode.text;
|
|
38
44
|
if (funcText === funcName) return node;
|
|
39
45
|
}
|
|
@@ -190,7 +196,7 @@ function verify(index, name, options = {}) {
|
|
|
190
196
|
let params = def.paramsStructured || [];
|
|
191
197
|
if ((lang === 'python' || lang === 'rust') && params.length > 0) {
|
|
192
198
|
const firstName = params[0].name;
|
|
193
|
-
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
199
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
|
|
194
200
|
params = params.slice(1);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
@@ -420,7 +426,7 @@ function verify(index, name, options = {}) {
|
|
|
420
426
|
const classNames = [...new Set(allDefs
|
|
421
427
|
.filter(d => d.className && d.className !== def.className)
|
|
422
428
|
.map(d => d.className))];
|
|
423
|
-
if (classNames.length > 0) {
|
|
429
|
+
if (classNames.length > 0 && !options.className && !options.file) {
|
|
424
430
|
scopeWarning = {
|
|
425
431
|
targetClass: def.className || '(unknown)',
|
|
426
432
|
otherClasses: classNames,
|
|
@@ -494,31 +500,39 @@ function plan(index, name, options = {}) {
|
|
|
494
500
|
...(options.defaultValue && { default: options.defaultValue })
|
|
495
501
|
};
|
|
496
502
|
|
|
497
|
-
// When adding a
|
|
498
|
-
// optional
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
// When adding a param, insert before rest params (*args/**kwargs) and
|
|
504
|
+
// before optional params (required must precede optional in Python/TS).
|
|
505
|
+
{
|
|
506
|
+
const selfNames = ['self', 'cls', '&self', '&mut self', 'mut self'];
|
|
507
|
+
const minIdx = (newParams.length > 0 && selfNames.includes(newParams[0].name)) ? 1 : 0;
|
|
508
|
+
const firstRestIdx = newParams.findIndex(p => p.rest || (p.name && (p.name.startsWith('*') || p.name.startsWith('...'))));
|
|
509
|
+
if (firstRestIdx !== -1) {
|
|
510
|
+
// Always insert before rest params (*args, **kwargs, ...rest)
|
|
511
|
+
const insertIdx = Math.max(firstRestIdx, minIdx);
|
|
506
512
|
newParams.splice(insertIdx, 0, newParam);
|
|
513
|
+
} else if (!options.defaultValue) {
|
|
514
|
+
const firstOptIdx = newParams.findIndex(p => p.optional || p.default !== undefined);
|
|
515
|
+
if (firstOptIdx !== -1) {
|
|
516
|
+
const insertIdx = Math.max(firstOptIdx, minIdx);
|
|
517
|
+
newParams.splice(insertIdx, 0, newParam);
|
|
518
|
+
} else {
|
|
519
|
+
newParams.push(newParam);
|
|
520
|
+
}
|
|
507
521
|
} else {
|
|
508
522
|
newParams.push(newParam);
|
|
509
523
|
}
|
|
510
|
-
} else {
|
|
511
|
-
newParams.push(newParam);
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
// Generate new signature
|
|
515
527
|
const paramsList = newParams.map(p => {
|
|
516
|
-
let str = p.name;
|
|
528
|
+
let str = (p.rest && !p.name.startsWith('*')) ? `...${p.name}` : p.name;
|
|
529
|
+
if (p.optional && !p.default) str += '?';
|
|
517
530
|
if (p.type) str += `: ${p.type}`;
|
|
518
531
|
if (p.default) str += ` = ${p.default}`;
|
|
519
532
|
return str;
|
|
520
533
|
}).join(', ');
|
|
521
|
-
|
|
534
|
+
const asyncPrefix = (def.async || def.isAsync || def.modifiers?.includes('async')) ? 'async ' : '';
|
|
535
|
+
newSignature = `${asyncPrefix}${name}(${paramsList})`;
|
|
522
536
|
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
523
537
|
|
|
524
538
|
// Describe changes needed at each call site
|
|
@@ -540,7 +554,13 @@ function plan(index, name, options = {}) {
|
|
|
540
554
|
|
|
541
555
|
if (options.removeParam) {
|
|
542
556
|
operation = 'remove-param';
|
|
543
|
-
|
|
557
|
+
// Normalize self-parameter lookup: 'self' matches '&self', '&mut self', 'mut self'
|
|
558
|
+
let removeTarget = options.removeParam;
|
|
559
|
+
let paramIndex = currentParams.findIndex(p => p.name === removeTarget);
|
|
560
|
+
if (paramIndex === -1 && removeTarget === 'self') {
|
|
561
|
+
paramIndex = currentParams.findIndex(p => /^&?(?:mut )?self$/.test(p.name));
|
|
562
|
+
if (paramIndex !== -1) removeTarget = currentParams[paramIndex].name;
|
|
563
|
+
}
|
|
544
564
|
if (paramIndex === -1) {
|
|
545
565
|
return {
|
|
546
566
|
found: true,
|
|
@@ -549,16 +569,18 @@ function plan(index, name, options = {}) {
|
|
|
549
569
|
};
|
|
550
570
|
}
|
|
551
571
|
|
|
552
|
-
newParams = currentParams.filter(p => p.name !==
|
|
572
|
+
newParams = currentParams.filter(p => p.name !== removeTarget);
|
|
553
573
|
|
|
554
574
|
// Generate new signature
|
|
555
575
|
const paramsList = newParams.map(p => {
|
|
556
|
-
let str = p.name;
|
|
576
|
+
let str = (p.rest && !p.name.startsWith('*')) ? `...${p.name}` : p.name;
|
|
577
|
+
if (p.optional && !p.default) str += '?';
|
|
557
578
|
if (p.type) str += `: ${p.type}`;
|
|
558
579
|
if (p.default) str += ` = ${p.default}`;
|
|
559
580
|
return str;
|
|
560
581
|
}).join(', ');
|
|
561
|
-
|
|
582
|
+
const asyncPrefix = (def.async || def.isAsync || def.modifiers?.includes('async')) ? 'async ' : '';
|
|
583
|
+
newSignature = `${asyncPrefix}${name}(${paramsList})`;
|
|
562
584
|
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
563
585
|
|
|
564
586
|
// For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
|
|
@@ -568,7 +590,7 @@ function plan(index, name, options = {}) {
|
|
|
568
590
|
let selfOffset = 0;
|
|
569
591
|
if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
|
|
570
592
|
const firstName = currentParams[0].name;
|
|
571
|
-
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
593
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
|
|
572
594
|
selfOffset = 1;
|
|
573
595
|
}
|
|
574
596
|
}
|
|
@@ -643,11 +665,21 @@ function plan(index, name, options = {}) {
|
|
|
643
665
|
operation,
|
|
644
666
|
before: {
|
|
645
667
|
signature: currentSignature,
|
|
646
|
-
params: currentParams.map(p =>
|
|
668
|
+
params: currentParams.map(p => {
|
|
669
|
+
let n = p.name;
|
|
670
|
+
if (p.optional && !p.default) n += '?';
|
|
671
|
+
if (p.type) n += `: ${p.type}`;
|
|
672
|
+
return n;
|
|
673
|
+
})
|
|
647
674
|
},
|
|
648
675
|
after: {
|
|
649
676
|
signature: newSignature,
|
|
650
|
-
params: newParams.map(p =>
|
|
677
|
+
params: newParams.map(p => {
|
|
678
|
+
let n = p.name;
|
|
679
|
+
if (p.optional && !p.default) n += '?';
|
|
680
|
+
if (p.type) n += `: ${p.type}`;
|
|
681
|
+
return n;
|
|
682
|
+
})
|
|
651
683
|
},
|
|
652
684
|
totalChanges: changes.length,
|
|
653
685
|
filesAffected: new Set(changes.map(c => c.file)).size,
|
package/languages/go.js
CHANGED
|
@@ -290,6 +290,7 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
290
290
|
// Name is in a field_identifier child
|
|
291
291
|
let nameText = null;
|
|
292
292
|
let paramsText = null;
|
|
293
|
+
let paramsNode = null;
|
|
293
294
|
let returnType = null;
|
|
294
295
|
let hasParams = false;
|
|
295
296
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
@@ -300,6 +301,7 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
300
301
|
hasParams = true;
|
|
301
302
|
if (!paramsText) {
|
|
302
303
|
paramsText = sub.text.slice(1, -1); // strip parens
|
|
304
|
+
paramsNode = sub;
|
|
303
305
|
} else {
|
|
304
306
|
// Second parameter_list is the return type tuple
|
|
305
307
|
returnType = sub.text;
|
|
@@ -312,10 +314,15 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
312
314
|
if (nameNode) nameText = nameNode.text;
|
|
313
315
|
}
|
|
314
316
|
if (!returnType) {
|
|
315
|
-
// Single return type
|
|
317
|
+
// Single return type — can be type_identifier, pointer_type, slice_type, map_type, etc.
|
|
318
|
+
const returnTypeNodes = new Set([
|
|
319
|
+
'type_identifier', 'pointer_type', 'slice_type', 'map_type',
|
|
320
|
+
'array_type', 'channel_type', 'function_type', 'interface_type',
|
|
321
|
+
'struct_type', 'generic_type', 'qualified_type',
|
|
322
|
+
]);
|
|
316
323
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
317
324
|
const sub = child.namedChild(j);
|
|
318
|
-
if (sub.type
|
|
325
|
+
if (returnTypeNodes.has(sub.type) && sub.text !== nameText) {
|
|
319
326
|
returnType = sub.text;
|
|
320
327
|
}
|
|
321
328
|
}
|
|
@@ -341,6 +348,7 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
341
348
|
endLine,
|
|
342
349
|
memberType: 'method',
|
|
343
350
|
...(paramsText !== null && { params: paramsText }),
|
|
351
|
+
...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'go') }),
|
|
344
352
|
...(returnType && { returnType })
|
|
345
353
|
});
|
|
346
354
|
}
|
package/languages/java.js
CHANGED
|
@@ -51,7 +51,11 @@ function extractModifiers(node) {
|
|
|
51
51
|
if (mod.type === 'marker_annotation' || mod.type === 'annotation') {
|
|
52
52
|
// Store annotation name (without @) as modifier (e.g., @Test -> 'test', @Override -> 'override')
|
|
53
53
|
const annoText = mod.text.replace(/^@/, '').split('(')[0].toLowerCase();
|
|
54
|
-
|
|
54
|
+
// Skip noise annotations that don't carry semantic meaning
|
|
55
|
+
const SKIP_ANNOTATIONS = new Set(['suppresswarnings', 'safevarargs', 'serial', 'generated']);
|
|
56
|
+
if (!SKIP_ANNOTATIONS.has(annoText)) {
|
|
57
|
+
modifiers.push(annoText);
|
|
58
|
+
}
|
|
55
59
|
continue;
|
|
56
60
|
}
|
|
57
61
|
modifiers.push(mod.text);
|
|
@@ -59,10 +63,11 @@ function extractModifiers(node) {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// Also check text before parameter list for modifiers (avoid matching keywords in params)
|
|
66
|
+
// Use the parameters node position to find the real paren (not annotation parens)
|
|
62
67
|
const text = node.text;
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const preParams =
|
|
68
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
69
|
+
const paramOffset = paramsNode ? paramsNode.startIndex - node.startIndex : -1;
|
|
70
|
+
const preParams = paramOffset >= 0 ? text.substring(0, paramOffset) : text.split('\n').slice(0, 3).join(' ');
|
|
66
71
|
const keywords = ['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default'];
|
|
67
72
|
for (const kw of keywords) {
|
|
68
73
|
if (preParams.includes(kw + ' ') && !modifiers.includes(kw)) {
|
|
@@ -130,10 +135,10 @@ function findFunctions(code, parser) {
|
|
|
130
135
|
if (processedRanges.has(rangeKey)) return true;
|
|
131
136
|
processedRanges.add(rangeKey);
|
|
132
137
|
|
|
133
|
-
// Skip methods inside a class body (they're extracted as class members)
|
|
138
|
+
// Skip methods inside a class/interface/enum body (they're extracted as class members)
|
|
134
139
|
let parent = node.parent;
|
|
135
|
-
if (parent && parent.type === 'class_body') {
|
|
136
|
-
return true; // Skip - this is a class method
|
|
140
|
+
if (parent && (parent.type === 'class_body' || parent.type === 'interface_body' || parent.type === 'enum_body' || parent.type === 'enum_body_declarations')) {
|
|
141
|
+
return true; // Skip - this is a class/interface/enum method
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
const nameNode = node.childForFieldName('name');
|
|
@@ -172,10 +177,10 @@ function findFunctions(code, parser) {
|
|
|
172
177
|
if (processedRanges.has(rangeKey)) return true;
|
|
173
178
|
processedRanges.add(rangeKey);
|
|
174
179
|
|
|
175
|
-
// Skip constructors inside a class body (they're extracted as class members)
|
|
180
|
+
// Skip constructors inside a class/enum body (they're extracted as class members)
|
|
176
181
|
let parent = node.parent;
|
|
177
|
-
if (parent && parent.type === 'class_body') {
|
|
178
|
-
return true; // Skip - this is a class constructor
|
|
182
|
+
if (parent && (parent.type === 'class_body' || parent.type === 'enum_body' || parent.type === 'enum_body_declarations')) {
|
|
183
|
+
return true; // Skip - this is a class/enum constructor
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
const nameNode = node.childForFieldName('name');
|
|
@@ -524,6 +529,7 @@ function extractClassMembers(classNode, code) {
|
|
|
524
529
|
const members = [];
|
|
525
530
|
const bodyNode = classNode.childForFieldName('body');
|
|
526
531
|
if (!bodyNode) return members;
|
|
532
|
+
const isInterface = bodyNode.type === 'interface_body';
|
|
527
533
|
|
|
528
534
|
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
529
535
|
const child = bodyNode.namedChild(i);
|
|
@@ -536,6 +542,13 @@ function extractClassMembers(classNode, code) {
|
|
|
536
542
|
if (nameNode) {
|
|
537
543
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
538
544
|
const modifiers = extractModifiers(child);
|
|
545
|
+
// Interface methods are implicitly public and abstract in Java
|
|
546
|
+
if (isInterface) {
|
|
547
|
+
if (!modifiers.includes('public')) modifiers.push('public');
|
|
548
|
+
if (!modifiers.includes('abstract') && !modifiers.includes('default') && !modifiers.includes('static')) {
|
|
549
|
+
modifiers.push('abstract');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
539
552
|
const returnType = extractReturnType(child);
|
|
540
553
|
const docstring = extractJavaDocstring(code, startLine);
|
|
541
554
|
const nameLine = nameNode.startPosition.row + 1;
|
package/languages/javascript.js
CHANGED
|
@@ -181,6 +181,7 @@ function findFunctions(code, parser) {
|
|
|
181
181
|
indent,
|
|
182
182
|
isArrow: false,
|
|
183
183
|
isGenerator: false,
|
|
184
|
+
isSignature: true,
|
|
184
185
|
modifiers: [],
|
|
185
186
|
...(returnType && { returnType }),
|
|
186
187
|
...(generics && { generics }),
|
|
@@ -1950,6 +1951,13 @@ function findExportsInCode(code, parser) {
|
|
|
1950
1951
|
exports.push({ name: propNode.text, type: 'exports', line });
|
|
1951
1952
|
return true;
|
|
1952
1953
|
}
|
|
1954
|
+
|
|
1955
|
+
// module.exports.name = ...
|
|
1956
|
+
if (objNode.type === 'member_expression' && objNode.text === 'module.exports') {
|
|
1957
|
+
const line = node.startPosition.row + 1;
|
|
1958
|
+
exports.push({ name: propNode.text, type: 'module.exports', line });
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1953
1961
|
}
|
|
1954
1962
|
}
|
|
1955
1963
|
return true;
|
package/languages/python.js
CHANGED
|
@@ -231,6 +231,10 @@ function extractBases(classNode) {
|
|
|
231
231
|
const arg = argsNode.namedChild(i);
|
|
232
232
|
if (arg.type === 'identifier' || arg.type === 'attribute') {
|
|
233
233
|
bases.push(arg.text);
|
|
234
|
+
} else if (arg.type === 'subscript') {
|
|
235
|
+
// Parameterized base: Generic[T], Protocol[T], Dict[str, int]
|
|
236
|
+
const baseNode = arg.childForFieldName('value');
|
|
237
|
+
if (baseNode) bases.push(baseNode.text);
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
}
|
package/languages/rust.js
CHANGED
|
@@ -77,7 +77,11 @@ function extractAttributes(node, code) {
|
|
|
77
77
|
const attrContent = match[1];
|
|
78
78
|
// Get just the attribute name (without arguments)
|
|
79
79
|
const attrName = attrContent.split('(')[0].trim();
|
|
80
|
-
attributes
|
|
80
|
+
// Skip compiler hint attributes that aren't semantically meaningful for display
|
|
81
|
+
const SKIP_ATTRS = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
|
|
82
|
+
if (!SKIP_ATTRS.has(attrName)) {
|
|
83
|
+
attributes.push(attrName);
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
} else if (!line.startsWith('//')) {
|
|
83
87
|
// Stop at non-comment, non-attribute lines
|
|
@@ -491,19 +495,25 @@ function extractImplInfo(implNode) {
|
|
|
491
495
|
typeName = typeNode.text;
|
|
492
496
|
}
|
|
493
497
|
|
|
494
|
-
|
|
498
|
+
// Strip generic type arguments from typeName and traitName for lookup
|
|
499
|
+
// e.g., "CacheService<T>" → "CacheService", "Entity" stays "Entity"
|
|
500
|
+
const stripGenerics = (s) => s ? s.replace(/<[^>]*>/g, '').trim() : s;
|
|
501
|
+
const bareTypeName = stripGenerics(typeName);
|
|
502
|
+
const bareTraitName = stripGenerics(traitName);
|
|
503
|
+
|
|
495
504
|
let name;
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
505
|
+
if (bareTraitName && bareTypeName) {
|
|
506
|
+
// Use the concrete type as className so Task.get_id works for `impl Entity for Task`
|
|
507
|
+
name = bareTypeName;
|
|
508
|
+
} else if (bareTypeName) {
|
|
509
|
+
name = bareTypeName;
|
|
500
510
|
} else {
|
|
501
511
|
const text = implNode.text;
|
|
502
512
|
const match = text.match(/impl\s*(?:<[^>]+>\s*)?(\w+(?:\s+for\s+\w+)?)/);
|
|
503
|
-
name = match ?
|
|
513
|
+
name = match ? match[1] : 'impl';
|
|
504
514
|
}
|
|
505
515
|
|
|
506
|
-
return { name, traitName, typeName };
|
|
516
|
+
return { name, traitName, typeName: bareTypeName, generics: typeParams || undefined };
|
|
507
517
|
}
|
|
508
518
|
|
|
509
519
|
/**
|
|
@@ -565,7 +575,9 @@ function extractTraitMembers(traitNode, code) {
|
|
|
565
575
|
endLine,
|
|
566
576
|
memberType: 'method',
|
|
567
577
|
isMethod: true,
|
|
578
|
+
modifiers: ['public'], // Trait methods are implicitly public
|
|
568
579
|
...(paramsNode && { params: extractRustParams(paramsNode) }),
|
|
580
|
+
...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
|
|
569
581
|
...(returnType && { returnType }),
|
|
570
582
|
...(hasSelf && { receiver: 'self' })
|
|
571
583
|
});
|
package/languages/utils.js
CHANGED
|
@@ -100,9 +100,37 @@ function parseJSParam(param, info) {
|
|
|
100
100
|
} else if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
|
|
101
101
|
const patternNode = param.childForFieldName('pattern');
|
|
102
102
|
const typeNode = param.childForFieldName('type');
|
|
103
|
-
if (patternNode)
|
|
103
|
+
if (patternNode) {
|
|
104
|
+
// Check if pattern is a rest_pattern (e.g., ...args inside required_parameter)
|
|
105
|
+
if (patternNode.type === 'rest_pattern') {
|
|
106
|
+
const innerName = patternNode.namedChild(0);
|
|
107
|
+
info.name = innerName ? innerName.text : patternNode.text.replace(/^\.\.\./, '');
|
|
108
|
+
info.rest = true;
|
|
109
|
+
} else {
|
|
110
|
+
info.name = patternNode.text;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
104
113
|
if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
|
|
105
114
|
if (param.type === 'optional_parameter') info.optional = true;
|
|
115
|
+
// Check for default value (e.g., priority: number = 1)
|
|
116
|
+
const valueNode = param.childForFieldName('value');
|
|
117
|
+
if (valueNode) {
|
|
118
|
+
info.default = valueNode.text;
|
|
119
|
+
info.optional = true;
|
|
120
|
+
} else if (!info.rest) {
|
|
121
|
+
// Also check for bare number/string/etc. children as defaults
|
|
122
|
+
for (let i = 0; i < param.namedChildCount; i++) {
|
|
123
|
+
const child = param.namedChild(i);
|
|
124
|
+
if (child !== patternNode && child !== (typeNode && typeNode.parent === param ? typeNode : null) &&
|
|
125
|
+
child.type !== 'type_annotation' && child.type !== 'rest_pattern' &&
|
|
126
|
+
!['identifier', 'type_annotation'].includes(child.type)) {
|
|
127
|
+
// This is likely a default value node
|
|
128
|
+
info.default = child.text;
|
|
129
|
+
info.optional = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
106
134
|
} else if (param.type === 'rest_parameter' || param.type === 'rest_pattern') {
|
|
107
135
|
// rest_parameter = TypeScript, rest_pattern = JavaScript
|
|
108
136
|
const patternNode = param.childForFieldName('pattern') || param.namedChild(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.4",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|