ucn 3.7.46 → 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.
@@ -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
- { regex: /^\s*([^\s(]+)\(\)$/, extract: null }, // Skip function-only lines
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 && pattern.extract) {
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
- members.push({
300
- name: nameText,
301
- startLine,
302
- endLine,
303
- memberType: 'method',
304
- ...(paramsText !== null && { params: paramsText }),
305
- ...(returnType && { returnType })
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
- if (nameNode && valueNode && isCompositeLiteral(valueNode)) {
342
- const name = nameNode.text;
343
- if (statePattern.test(name)) {
344
- const { startLine, endLine } = nodeToLocation(spec, code);
345
- objects.push({ name, startLine, endLine });
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 && valueNode && isCompositeLiteral(valueNode)) {
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
- functionStack.push({
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: true,
544
- receiver: operandNode?.type === 'identifier' ? operandNode.text : undefined,
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) closureScopes.delete(leaving.startLine);
863
+ if (leaving) {
864
+ closureScopes.delete(leaving.startLine);
865
+ scopeTypes.delete(leaving.startLine);
866
+ }
559
867
  }
560
868
  }
561
869
  });
@@ -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
- return parser.parse(content, oldTree, { ...options, bufferSize });
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