ucn 3.7.45 → 3.7.47
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 +6 -4
- package/README.md +23 -24
- package/cli/index.js +14 -6
- package/core/cache.js +176 -51
- package/core/callers.js +315 -51
- package/core/deadcode.js +42 -16
- package/core/discovery.js +1 -1
- package/core/execute.js +148 -11
- package/core/output.js +26 -4
- package/core/project.js +290 -52
- package/core/registry.js +1 -0
- package/core/shared.js +1 -1
- package/core/stacktrace.js +31 -2
- package/core/verify.js +11 -0
- package/languages/go.js +331 -23
- package/languages/index.js +20 -1
- package/languages/java.js +109 -4
- package/languages/rust.js +93 -4
- package/mcp/server.js +33 -16
- package/package.json +11 -10
package/core/stacktrace.js
CHANGED
|
@@ -293,7 +293,8 @@ function parseStackTrace(index, stackText) {
|
|
|
293
293
|
// Go: "file.go:line" or "package/file.go:line +0x..."
|
|
294
294
|
{ regex: /^\s*([^\s:]+\.go):(\d+)(?:\s|$)/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), funcName: null, col: null }) },
|
|
295
295
|
// Go with function: "package.FunctionName()\n\tfile.go:line"
|
|
296
|
-
|
|
296
|
+
// Also handles method syntax: "package.(*Type).Method(...)"
|
|
297
|
+
{ regex: /^\s*((?:[^\s(]|\([^)]*\))+)\(.*\)$/, extract: null }, // Skip function-only lines
|
|
297
298
|
// Java: "at package.Class.method(File.java:line)"
|
|
298
299
|
{ regex: /at\s+([^\(]+)\(([^:]+):(\d+)\)/, extract: (m) => ({ funcName: m[1].split('.').pop(), file: m[2], line: parseInt(m[3]), col: null }) },
|
|
299
300
|
// Rust: "at src/main.rs:line:col" or panic location
|
|
@@ -302,16 +303,40 @@ function parseStackTrace(index, stackText) {
|
|
|
302
303
|
{ regex: /([^\s:]+\.\w+):(\d+)(?::(\d+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) }
|
|
303
304
|
];
|
|
304
305
|
|
|
306
|
+
// Track Go function names that appear on a line before the file:line
|
|
307
|
+
let pendingGoFuncName = null;
|
|
308
|
+
|
|
305
309
|
for (const line of lines) {
|
|
306
310
|
const trimmed = line.trim();
|
|
307
311
|
if (!trimmed) continue;
|
|
308
312
|
|
|
309
313
|
// Try each pattern until one matches
|
|
314
|
+
let matched = false;
|
|
310
315
|
for (const pattern of patterns) {
|
|
311
316
|
const match = pattern.regex.exec(trimmed);
|
|
312
|
-
if (match
|
|
317
|
+
if (match) {
|
|
318
|
+
if (pattern.extract === null) {
|
|
319
|
+
// Go function-only line (e.g. "package.FunctionName()")
|
|
320
|
+
// Extract the function name and carry it forward to the next file:line
|
|
321
|
+
const fullName = match[1];
|
|
322
|
+
// Go uses fully-qualified names: pkg/path.FuncName or pkg/path.(*Type).Method
|
|
323
|
+
const lastDot = fullName.lastIndexOf('.');
|
|
324
|
+
pendingGoFuncName = lastDot >= 0 ? fullName.slice(lastDot + 1) : fullName;
|
|
325
|
+
// Strip Go method receiver syntax: (*Type).Method → Method
|
|
326
|
+
if (pendingGoFuncName.startsWith('(*')) {
|
|
327
|
+
const parenClose = pendingGoFuncName.indexOf(').');
|
|
328
|
+
if (parenClose >= 0) pendingGoFuncName = pendingGoFuncName.slice(parenClose + 2);
|
|
329
|
+
}
|
|
330
|
+
matched = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
313
333
|
const extracted = pattern.extract(match);
|
|
314
334
|
if (extracted && extracted.file && extracted.line) {
|
|
335
|
+
// Use pending Go function name if no function name extracted
|
|
336
|
+
if (!extracted.funcName && pendingGoFuncName) {
|
|
337
|
+
extracted.funcName = pendingGoFuncName;
|
|
338
|
+
}
|
|
339
|
+
pendingGoFuncName = null;
|
|
315
340
|
frames.push(createStackFrame(
|
|
316
341
|
index,
|
|
317
342
|
extracted.file,
|
|
@@ -320,10 +345,14 @@ function parseStackTrace(index, stackText) {
|
|
|
320
345
|
extracted.col,
|
|
321
346
|
trimmed
|
|
322
347
|
));
|
|
348
|
+
matched = true;
|
|
323
349
|
break; // Move to next line
|
|
324
350
|
}
|
|
325
351
|
}
|
|
326
352
|
}
|
|
353
|
+
if (!matched) {
|
|
354
|
+
pendingGoFuncName = null; // Reset if line doesn't match any pattern
|
|
355
|
+
}
|
|
327
356
|
}
|
|
328
357
|
|
|
329
358
|
return {
|
package/core/verify.js
CHANGED
|
@@ -301,6 +301,8 @@ function verify(index, name, options = {}) {
|
|
|
301
301
|
// and the function lives in jobs.py).
|
|
302
302
|
const defIsMethod = !!(def.isMethod || def.type === 'method' || def.className);
|
|
303
303
|
const targetBasename = path.basename(def.file, path.extname(def.file));
|
|
304
|
+
const defFileEntry = index.files.get(def.file);
|
|
305
|
+
const defLang = defFileEntry?.language;
|
|
304
306
|
|
|
305
307
|
// Build import-name lookup for receiver checking (module.func() vs dict.get())
|
|
306
308
|
const importNameCache = new Map();
|
|
@@ -337,6 +339,15 @@ function verify(index, name, options = {}) {
|
|
|
337
339
|
const importedNames = getImportedNames(call.file);
|
|
338
340
|
if (!importedNames.has(callReceiver)) continue;
|
|
339
341
|
// Receiver matches target module and is imported — keep it
|
|
342
|
+
} else if (callReceiver && defLang === 'go') {
|
|
343
|
+
// Go: receiver is package alias (last segment of import path, e.g., "controller"
|
|
344
|
+
// from "k8s.io/.../pkg/controller"), not the filename ("controller_utils").
|
|
345
|
+
// Check if receiver matches the directory name of the target file.
|
|
346
|
+
const targetDir = path.basename(path.dirname(def.file));
|
|
347
|
+
if (callReceiver !== targetDir) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
// Receiver matches package directory — keep it
|
|
340
351
|
} else {
|
|
341
352
|
continue;
|
|
342
353
|
}
|
package/languages/go.js
CHANGED
|
@@ -247,6 +247,23 @@ function extractStructFields(structNode, code) {
|
|
|
247
247
|
memberType: 'field',
|
|
248
248
|
...(typeNode && { fieldType: typeNode.text })
|
|
249
249
|
});
|
|
250
|
+
} else if (typeNode) {
|
|
251
|
+
// Embedded field: has type but no name (e.g., `Base` in `type Child struct { Base; Name string }`)
|
|
252
|
+
// Use the type name as the field name
|
|
253
|
+
let embeddedName = typeNode.text;
|
|
254
|
+
// Strip pointer prefix: *Base → Base
|
|
255
|
+
if (embeddedName.startsWith('*')) embeddedName = embeddedName.slice(1);
|
|
256
|
+
// Strip package prefix: pkg.Base → Base
|
|
257
|
+
const dotIdx = embeddedName.indexOf('.');
|
|
258
|
+
if (dotIdx >= 0) embeddedName = embeddedName.slice(dotIdx + 1);
|
|
259
|
+
fields.push({
|
|
260
|
+
name: embeddedName,
|
|
261
|
+
startLine,
|
|
262
|
+
endLine,
|
|
263
|
+
memberType: 'field',
|
|
264
|
+
embedded: true,
|
|
265
|
+
fieldType: typeNode.text
|
|
266
|
+
});
|
|
250
267
|
}
|
|
251
268
|
}
|
|
252
269
|
}
|
|
@@ -268,11 +285,13 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
268
285
|
let nameText = null;
|
|
269
286
|
let paramsText = null;
|
|
270
287
|
let returnType = null;
|
|
288
|
+
let hasParams = false;
|
|
271
289
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
272
290
|
const sub = child.namedChild(j);
|
|
273
291
|
if (sub.type === 'field_identifier' || sub.type === 'type_identifier') {
|
|
274
292
|
if (!nameText) nameText = sub.text;
|
|
275
293
|
} else if (sub.type === 'parameter_list') {
|
|
294
|
+
hasParams = true;
|
|
276
295
|
if (!paramsText) {
|
|
277
296
|
paramsText = sub.text.slice(1, -1); // strip parens
|
|
278
297
|
} else {
|
|
@@ -296,14 +315,63 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
296
315
|
}
|
|
297
316
|
}
|
|
298
317
|
if (nameText) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
318
|
+
// Distinguish between method signatures and embedded interfaces:
|
|
319
|
+
// method_elem with parameter_list → method
|
|
320
|
+
// method_elem with only type_identifier → embedded interface
|
|
321
|
+
if (!hasParams && child.namedChildCount === 1 && child.namedChild(0).type === 'type_identifier') {
|
|
322
|
+
// Embedded interface
|
|
323
|
+
members.push({
|
|
324
|
+
name: nameText,
|
|
325
|
+
startLine,
|
|
326
|
+
endLine,
|
|
327
|
+
memberType: 'field',
|
|
328
|
+
embedded: true,
|
|
329
|
+
fieldType: nameText
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
members.push({
|
|
333
|
+
name: nameText,
|
|
334
|
+
startLine,
|
|
335
|
+
endLine,
|
|
336
|
+
memberType: 'method',
|
|
337
|
+
...(paramsText !== null && { params: paramsText }),
|
|
338
|
+
...(returnType && { returnType })
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else if (child.type === 'type_identifier' || child.type === 'qualified_type') {
|
|
343
|
+
// Standalone type identifier inside interface body — embedded interface
|
|
344
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
345
|
+
let embName = child.text;
|
|
346
|
+
const dotIdx = embName.indexOf('.');
|
|
347
|
+
if (dotIdx >= 0) embName = embName.slice(dotIdx + 1);
|
|
348
|
+
members.push({
|
|
349
|
+
name: embName,
|
|
350
|
+
startLine,
|
|
351
|
+
endLine,
|
|
352
|
+
memberType: 'field',
|
|
353
|
+
embedded: true,
|
|
354
|
+
fieldType: child.text
|
|
355
|
+
});
|
|
356
|
+
} else if (child.type === 'type_elem') {
|
|
357
|
+
// type_elem wrapping a type_identifier — embedded interface
|
|
358
|
+
// e.g., `Reader` in `type ReadWriter interface { Reader; Write(...) }`
|
|
359
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
360
|
+
const sub = child.namedChild(j);
|
|
361
|
+
if (sub.type === 'type_identifier' || sub.type === 'qualified_type') {
|
|
362
|
+
const { startLine, endLine } = nodeToLocation(sub, code);
|
|
363
|
+
let embName = sub.text;
|
|
364
|
+
const dotIdx = embName.indexOf('.');
|
|
365
|
+
if (dotIdx >= 0) embName = embName.slice(dotIdx + 1);
|
|
366
|
+
members.push({
|
|
367
|
+
name: embName,
|
|
368
|
+
startLine,
|
|
369
|
+
endLine,
|
|
370
|
+
memberType: 'field',
|
|
371
|
+
embedded: true,
|
|
372
|
+
fieldType: sub.text
|
|
373
|
+
});
|
|
374
|
+
}
|
|
307
375
|
}
|
|
308
376
|
}
|
|
309
377
|
}
|
|
@@ -318,6 +386,8 @@ function findStateObjects(code, parser) {
|
|
|
318
386
|
const objects = [];
|
|
319
387
|
|
|
320
388
|
const statePattern = /^(CONFIG|SETTINGS|[A-Z][A-Z0-9_]+|Default[A-Z][a-zA-Z]*|[A-Z][a-zA-Z]*(?:Config|Settings|Options))$/;
|
|
389
|
+
// All exported (^[A-Z]) package-level const/var are indexed as state objects
|
|
390
|
+
const isExportedName = (name) => /^[A-Z]/.test(name);
|
|
321
391
|
|
|
322
392
|
// Check if a value node is a composite literal
|
|
323
393
|
function isCompositeLiteral(valueNode) {
|
|
@@ -329,21 +399,51 @@ function findStateObjects(code, parser) {
|
|
|
329
399
|
return false;
|
|
330
400
|
}
|
|
331
401
|
|
|
402
|
+
// Check if a const block uses iota (enum-like pattern)
|
|
403
|
+
function blockHasIota(constDecl) {
|
|
404
|
+
for (let i = 0; i < constDecl.namedChildCount; i++) {
|
|
405
|
+
const spec = constDecl.namedChild(i);
|
|
406
|
+
if (spec.type === 'const_spec') {
|
|
407
|
+
const valueNode = spec.childForFieldName('value');
|
|
408
|
+
if (valueNode) {
|
|
409
|
+
// Check if any child is 'iota'
|
|
410
|
+
const checkIota = (n) => {
|
|
411
|
+
if (n.type === 'iota') return true;
|
|
412
|
+
for (let j = 0; j < n.childCount; j++) {
|
|
413
|
+
if (checkIota(n.child(j))) return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
};
|
|
417
|
+
if (checkIota(valueNode)) return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
332
424
|
traverseTree(tree.rootNode, (node) => {
|
|
333
425
|
// Handle const declarations
|
|
334
426
|
if (node.type === 'const_declaration') {
|
|
427
|
+
const isIotaBlock = blockHasIota(node);
|
|
335
428
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
336
429
|
const spec = node.namedChild(i);
|
|
337
430
|
if (spec.type === 'const_spec') {
|
|
338
431
|
const nameNode = spec.childForFieldName('name');
|
|
339
432
|
const valueNode = spec.childForFieldName('value');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
433
|
+
if (!nameNode) continue;
|
|
434
|
+
const name = nameNode.text;
|
|
435
|
+
|
|
436
|
+
// Include if: composite literal matching state pattern, OR exported const in iota block,
|
|
437
|
+
// OR any exported (^[A-Z]) package-level const
|
|
438
|
+
if (valueNode && isCompositeLiteral(valueNode) && statePattern.test(name)) {
|
|
439
|
+
const { startLine, endLine } = nodeToLocation(spec, code);
|
|
440
|
+
objects.push({ name, startLine, endLine });
|
|
441
|
+
} else if (isIotaBlock && /^[A-Z]/.test(name)) {
|
|
442
|
+
const { startLine, endLine } = nodeToLocation(spec, code);
|
|
443
|
+
objects.push({ name, startLine, endLine, isConst: true });
|
|
444
|
+
} else if (isExportedName(name)) {
|
|
445
|
+
const { startLine, endLine } = nodeToLocation(spec, code);
|
|
446
|
+
objects.push({ name, startLine, endLine, isConst: true });
|
|
347
447
|
}
|
|
348
448
|
}
|
|
349
449
|
}
|
|
@@ -358,9 +458,12 @@ function findStateObjects(code, parser) {
|
|
|
358
458
|
const nameNode = spec.childForFieldName('name');
|
|
359
459
|
const valueNode = spec.childForFieldName('value');
|
|
360
460
|
|
|
361
|
-
if (nameNode
|
|
461
|
+
if (nameNode) {
|
|
362
462
|
const name = nameNode.text;
|
|
363
|
-
if (statePattern.test(name)) {
|
|
463
|
+
if (valueNode && isCompositeLiteral(valueNode) && statePattern.test(name)) {
|
|
464
|
+
const { startLine, endLine } = nodeToLocation(spec, code);
|
|
465
|
+
objects.push({ name, startLine, endLine });
|
|
466
|
+
} else if (isExportedName(name)) {
|
|
364
467
|
const { startLine, endLine } = nodeToLocation(spec, code);
|
|
365
468
|
objects.push({ name, startLine, endLine });
|
|
366
469
|
}
|
|
@@ -405,18 +508,108 @@ const GO_BUILTINS = new Set([
|
|
|
405
508
|
'println', 'real', 'recover'
|
|
406
509
|
]);
|
|
407
510
|
|
|
408
|
-
function findCallsInCode(code, parser) {
|
|
511
|
+
function findCallsInCode(code, parser, options = {}) {
|
|
409
512
|
const tree = parseTree(parser, code);
|
|
410
513
|
const calls = [];
|
|
411
514
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
412
515
|
// Track local closure names per function scope (scopeStartLine -> Set<name>)
|
|
413
516
|
const closureScopes = new Map();
|
|
517
|
+
// Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
|
|
518
|
+
const scopeTypes = new Map();
|
|
519
|
+
// Track function-typed parameter names per scope (scopeStartLine -> Set<name>)
|
|
520
|
+
const funcParamScopes = new Map();
|
|
521
|
+
|
|
522
|
+
// Build set of import aliases for distinguishing pkg.Func() from obj.Method()
|
|
523
|
+
// options.imports contains resolved alias names (e.g., 'utilversion' for renamed imports,
|
|
524
|
+
// 'fmt' for standard imports). These come from fileEntry.importNames.
|
|
525
|
+
const importAliases = new Set();
|
|
526
|
+
if (options.imports) {
|
|
527
|
+
for (const name of options.imports) {
|
|
528
|
+
if (name && name !== '_' && name !== '.') importAliases.add(name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
414
531
|
|
|
415
532
|
// Helper to check if a node creates a function scope
|
|
416
533
|
const isFunctionNode = (node) => {
|
|
417
534
|
return ['function_declaration', 'method_declaration', 'func_literal'].includes(node.type);
|
|
418
535
|
};
|
|
419
536
|
|
|
537
|
+
// Extract the base type name from a type node (strips pointer, qualified, etc.)
|
|
538
|
+
const extractTypeName = (typeNode) => {
|
|
539
|
+
if (!typeNode) return null;
|
|
540
|
+
if (typeNode.type === 'type_identifier') return typeNode.text;
|
|
541
|
+
if (typeNode.type === 'pointer_type') {
|
|
542
|
+
// *Framework -> Framework
|
|
543
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
544
|
+
const r = extractTypeName(typeNode.namedChild(i));
|
|
545
|
+
if (r) return r;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (typeNode.type === 'qualified_type') {
|
|
549
|
+
// pkg.Type -> Type
|
|
550
|
+
const tn = typeNode.childForFieldName('name');
|
|
551
|
+
if (tn) return tn.text;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// Build type map from function/method parameters and receiver.
|
|
557
|
+
// Also returns funcParamNames: parameter names with function types (func(...) ...)
|
|
558
|
+
// so calls to them can be skipped (they're local parameter calls, not global function calls).
|
|
559
|
+
const buildScopeTypeMap = (node) => {
|
|
560
|
+
const typeMap = new Map();
|
|
561
|
+
const funcParamNames = new Set();
|
|
562
|
+
|
|
563
|
+
// Method receiver: func (f *Framework) Method()
|
|
564
|
+
if (node.type === 'method_declaration') {
|
|
565
|
+
const receiverNode = node.childForFieldName('receiver');
|
|
566
|
+
if (receiverNode) {
|
|
567
|
+
for (let i = 0; i < receiverNode.namedChildCount; i++) {
|
|
568
|
+
const param = receiverNode.namedChild(i);
|
|
569
|
+
if (param.type === 'parameter_declaration') {
|
|
570
|
+
const nameNode = param.childForFieldName('name');
|
|
571
|
+
const typeNode = param.childForFieldName('type');
|
|
572
|
+
const typeName = extractTypeName(typeNode);
|
|
573
|
+
if (nameNode && typeName) {
|
|
574
|
+
typeMap.set(nameNode.text, typeName);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Function/method parameters
|
|
582
|
+
// Go allows shared-type declarations: (adopt, release func(...) error)
|
|
583
|
+
// childForFieldName('name') returns only the first name — iterate all identifier children
|
|
584
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
585
|
+
if (paramsNode) {
|
|
586
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
587
|
+
const param = paramsNode.namedChild(i);
|
|
588
|
+
if (param.type === 'parameter_declaration') {
|
|
589
|
+
const typeNode = param.childForFieldName('type');
|
|
590
|
+
if (!typeNode) continue;
|
|
591
|
+
// Collect all name identifiers in this declaration
|
|
592
|
+
const nameNodes = [];
|
|
593
|
+
for (let j = 0; j < param.namedChildCount; j++) {
|
|
594
|
+
const child = param.namedChild(j);
|
|
595
|
+
if (child.type === 'identifier') nameNodes.push(child);
|
|
596
|
+
}
|
|
597
|
+
if (nameNodes.length === 0) continue;
|
|
598
|
+
if (typeNode.type === 'function_type') {
|
|
599
|
+
for (const nn of nameNodes) funcParamNames.add(nn.text);
|
|
600
|
+
} else {
|
|
601
|
+
const typeName = extractTypeName(typeNode);
|
|
602
|
+
if (typeName) {
|
|
603
|
+
for (const nn of nameNodes) typeMap.set(nn.text, typeName);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { typeMap, funcParamNames };
|
|
611
|
+
};
|
|
612
|
+
|
|
420
613
|
// Helper to extract function name from a function node
|
|
421
614
|
const extractFunctionName = (node) => {
|
|
422
615
|
if (node.type === 'function_declaration') {
|
|
@@ -449,14 +642,91 @@ function findCallsInCode(code, parser) {
|
|
|
449
642
|
return false;
|
|
450
643
|
};
|
|
451
644
|
|
|
645
|
+
// Check if name is a function-typed parameter (e.g., match func(Object) bool)
|
|
646
|
+
// Calls to these are local parameter invocations, not global function calls
|
|
647
|
+
const isFuncTypedParam = (name) => {
|
|
648
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
649
|
+
const scope = funcParamScopes.get(functionStack[i].startLine);
|
|
650
|
+
if (scope?.has(name)) return true;
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Look up variable type from scope chain
|
|
656
|
+
const getReceiverType = (varName) => {
|
|
657
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
658
|
+
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
659
|
+
if (typeMap?.has(varName)) return typeMap.get(varName);
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
};
|
|
663
|
+
|
|
452
664
|
traverseTree(tree.rootNode, (node) => {
|
|
453
665
|
// Track function entry
|
|
454
666
|
if (isFunctionNode(node)) {
|
|
455
|
-
|
|
667
|
+
const entry = {
|
|
456
668
|
name: extractFunctionName(node),
|
|
457
669
|
startLine: node.startPosition.row + 1,
|
|
458
670
|
endLine: node.endPosition.row + 1
|
|
459
|
-
}
|
|
671
|
+
};
|
|
672
|
+
functionStack.push(entry);
|
|
673
|
+
const { typeMap, funcParamNames } = buildScopeTypeMap(node);
|
|
674
|
+
scopeTypes.set(entry.startLine, typeMap);
|
|
675
|
+
if (funcParamNames.size > 0) {
|
|
676
|
+
funcParamScopes.set(entry.startLine, funcParamNames);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Track local variable types from composite literals and typed assignments
|
|
681
|
+
// e.g., s := &Status{...} → s has type Status
|
|
682
|
+
// registry := Registry{...} → registry has type Registry
|
|
683
|
+
if (node.type === 'short_var_declaration' && functionStack.length > 0) {
|
|
684
|
+
const left = node.childForFieldName('left');
|
|
685
|
+
const right = node.childForFieldName('right');
|
|
686
|
+
if (left && right) {
|
|
687
|
+
const names = left.type === 'expression_list'
|
|
688
|
+
? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
|
|
689
|
+
.filter(n => n.type === 'identifier').map(n => n.text)
|
|
690
|
+
: left.type === 'identifier' ? [left.text] : [];
|
|
691
|
+
const values = right.type === 'expression_list'
|
|
692
|
+
? Array.from({ length: right.namedChildCount }, (_, i) => right.namedChild(i))
|
|
693
|
+
: [right];
|
|
694
|
+
const scopeKey = functionStack[functionStack.length - 1].startLine;
|
|
695
|
+
const typeMap = scopeTypes.get(scopeKey);
|
|
696
|
+
if (typeMap && names.length > 0 && values.length > 0) {
|
|
697
|
+
for (let vi = 0; vi < Math.min(names.length, values.length); vi++) {
|
|
698
|
+
const val = values[vi];
|
|
699
|
+
let typeName = null;
|
|
700
|
+
// &Type{...} or Type{...}
|
|
701
|
+
if (val.type === 'composite_literal') {
|
|
702
|
+
typeName = extractTypeName(val.childForFieldName('type'));
|
|
703
|
+
} else if (val.type === 'unary_expression' && val.childCount > 0) {
|
|
704
|
+
for (let ci = 0; ci < val.namedChildCount; ci++) {
|
|
705
|
+
const ch = val.namedChild(ci);
|
|
706
|
+
if (ch.type === 'composite_literal') {
|
|
707
|
+
typeName = extractTypeName(ch.childForFieldName('type'));
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} else if (val.type === 'call_expression') {
|
|
712
|
+
// NewFoo() or pkg.NewFoo() → infer type as Foo
|
|
713
|
+
const callFuncNode = val.childForFieldName('function');
|
|
714
|
+
if (callFuncNode) {
|
|
715
|
+
const callName = callFuncNode.type === 'identifier'
|
|
716
|
+
? callFuncNode.text
|
|
717
|
+
: callFuncNode.type === 'selector_expression'
|
|
718
|
+
? callFuncNode.childForFieldName('field')?.text
|
|
719
|
+
: null;
|
|
720
|
+
if (callName && /^New[A-Z]/.test(callName)) {
|
|
721
|
+
typeName = callName.slice(3);
|
|
722
|
+
if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (typeName) typeMap.set(names[vi], typeName);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
460
730
|
}
|
|
461
731
|
|
|
462
732
|
// Track local closures: atoi := func(...) { ... } or var handler = func(...) { ... }
|
|
@@ -522,6 +792,9 @@ function findCallsInCode(code, parser) {
|
|
|
522
792
|
if (GO_BUILTINS.has(callName)) return true;
|
|
523
793
|
// Skip calls to local closures (they shadow package-level functions)
|
|
524
794
|
if (isLocalClosure(callName)) return true;
|
|
795
|
+
// Skip calls to function-typed parameters (e.g., match func(Object) bool)
|
|
796
|
+
// These are local parameter invocations, not calls to global functions
|
|
797
|
+
if (isFuncTypedParam(callName)) return true;
|
|
525
798
|
|
|
526
799
|
// Direct call: foo()
|
|
527
800
|
calls.push({
|
|
@@ -537,11 +810,17 @@ function findCallsInCode(code, parser) {
|
|
|
537
810
|
const operandNode = funcNode.childForFieldName('operand');
|
|
538
811
|
|
|
539
812
|
if (fieldNode) {
|
|
813
|
+
const receiver = operandNode?.type === 'identifier' ? operandNode.text : undefined;
|
|
814
|
+
// Distinguish pkg.Func() (package-qualified) from obj.Method()
|
|
815
|
+
// If receiver is a known import alias, this is a package call, not a method call
|
|
816
|
+
const isPkgCall = receiver && importAliases.has(receiver);
|
|
817
|
+
const receiverType = (!isPkgCall && receiver) ? getReceiverType(receiver) : undefined;
|
|
540
818
|
calls.push({
|
|
541
819
|
name: fieldNode.text,
|
|
542
820
|
line: node.startPosition.row + 1,
|
|
543
|
-
isMethod:
|
|
544
|
-
receiver
|
|
821
|
+
isMethod: !isPkgCall,
|
|
822
|
+
receiver,
|
|
823
|
+
...(receiverType && { receiverType }),
|
|
545
824
|
enclosingFunction,
|
|
546
825
|
uncertain
|
|
547
826
|
});
|
|
@@ -550,12 +829,41 @@ function findCallsInCode(code, parser) {
|
|
|
550
829
|
return true;
|
|
551
830
|
}
|
|
552
831
|
|
|
832
|
+
// Detect function references passed as arguments: dc.worker passed to UntilWithContext(ctx, dc.worker, ...)
|
|
833
|
+
// selector_expression inside argument_list (not inside call_expression as the function)
|
|
834
|
+
if (node.type === 'selector_expression' && node.parent?.type === 'argument_list') {
|
|
835
|
+
// Only if this selector_expression is NOT the function being called
|
|
836
|
+
const grandparent = node.parent?.parent;
|
|
837
|
+
if (!grandparent || grandparent.type !== 'call_expression' || grandparent.childForFieldName('function') !== node) {
|
|
838
|
+
const fieldNode = node.childForFieldName('field');
|
|
839
|
+
const operandNode = node.childForFieldName('operand');
|
|
840
|
+
if (fieldNode && operandNode) {
|
|
841
|
+
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
842
|
+
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
843
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
844
|
+
calls.push({
|
|
845
|
+
name: fieldNode.text,
|
|
846
|
+
line: node.startPosition.row + 1,
|
|
847
|
+
isMethod: true,
|
|
848
|
+
receiver,
|
|
849
|
+
...(receiverType && { receiverType }),
|
|
850
|
+
enclosingFunction,
|
|
851
|
+
isPotentialCallback: true,
|
|
852
|
+
uncertain: false
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
553
858
|
return true;
|
|
554
859
|
}, {
|
|
555
860
|
onLeave: (node) => {
|
|
556
861
|
if (isFunctionNode(node)) {
|
|
557
862
|
const leaving = functionStack.pop();
|
|
558
|
-
if (leaving)
|
|
863
|
+
if (leaving) {
|
|
864
|
+
closureScopes.delete(leaving.startLine);
|
|
865
|
+
scopeTypes.delete(leaving.startLine);
|
|
866
|
+
}
|
|
559
867
|
}
|
|
560
868
|
}
|
|
561
869
|
});
|
package/languages/index.js
CHANGED
|
@@ -228,7 +228,19 @@ function getParseOptions(contentLength = 0) {
|
|
|
228
228
|
* @param {object} options - Additional parse options
|
|
229
229
|
* @returns {object} Parsed tree
|
|
230
230
|
*/
|
|
231
|
+
// Single-entry parse cache: during indexFile(), the same (parser, content) is parsed
|
|
232
|
+
// 5 times (findFunctions + findClasses + findStateObjects + findImports + findExports).
|
|
233
|
+
// Caching the last result eliminates 4 out of 5 parses per file (80% reduction).
|
|
234
|
+
let _lastParseParser = null;
|
|
235
|
+
let _lastParseContent = null;
|
|
236
|
+
let _lastParseTree = null;
|
|
237
|
+
|
|
231
238
|
function safeParse(parser, content, oldTree = undefined, options = {}) {
|
|
239
|
+
// Fast path: return cached tree if same parser and content (no oldTree override)
|
|
240
|
+
if (!oldTree && parser === _lastParseParser && content === _lastParseContent && _lastParseTree) {
|
|
241
|
+
return _lastParseTree;
|
|
242
|
+
}
|
|
243
|
+
|
|
232
244
|
const contentLength = content.length;
|
|
233
245
|
|
|
234
246
|
// Try with escalating buffer sizes
|
|
@@ -243,7 +255,14 @@ function safeParse(parser, content, oldTree = undefined, options = {}) {
|
|
|
243
255
|
let lastError;
|
|
244
256
|
for (const bufferSize of bufferSizes) {
|
|
245
257
|
try {
|
|
246
|
-
|
|
258
|
+
const tree = parser.parse(content, oldTree, { ...options, bufferSize });
|
|
259
|
+
// Cache the result for same-(parser, content) reuse
|
|
260
|
+
if (!oldTree) {
|
|
261
|
+
_lastParseParser = parser;
|
|
262
|
+
_lastParseContent = content;
|
|
263
|
+
_lastParseTree = tree;
|
|
264
|
+
}
|
|
265
|
+
return tree;
|
|
247
266
|
} catch (e) {
|
|
248
267
|
lastError = e;
|
|
249
268
|
// Only retry on buffer-related errors
|