ucn 3.7.24 → 3.7.26

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/core/verify.js ADDED
@@ -0,0 +1,533 @@
1
+ /**
2
+ * core/verify.js - Signature verification, refactoring planning, call site analysis
3
+ *
4
+ * Extracted from project.js. All functions take an `index` (ProjectIndex)
5
+ * as the first argument instead of using `this`.
6
+ */
7
+
8
+ const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
9
+ const { escapeRegExp } = require('./shared');
10
+
11
+ /**
12
+ * Find a call expression node at the target line matching funcName
13
+ */
14
+ function findCallNode(node, callTypes, targetRow, funcName) {
15
+ if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
16
+ return null; // Skip nodes that don't contain the target line
17
+ }
18
+
19
+ if (callTypes.has(node.type) && node.startPosition.row <= targetRow && node.endPosition.row >= targetRow) {
20
+ // Java constructor: new ClassName(args) — name is in 'type' field
21
+ if (node.type === 'object_creation_expression') {
22
+ const typeNode = node.childForFieldName('type');
23
+ if (typeNode) {
24
+ // Strip generics and package qualifiers: com.foo.Bar<T> -> Bar
25
+ const typeName = typeNode.text.replace(/<.*>$/, '').split('.').pop();
26
+ if (typeName === funcName) return node;
27
+ }
28
+ } else {
29
+ // Check if this call is for our target function
30
+ const funcNode = node.childForFieldName('function') ||
31
+ node.childForFieldName('name'); // Java method_invocation uses 'name'
32
+ if (funcNode) {
33
+ const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
34
+ ? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
35
+ : funcNode.text;
36
+ if (funcText === funcName) return node;
37
+ }
38
+ }
39
+ }
40
+
41
+ // Recurse into children
42
+ for (let i = 0; i < node.childCount; i++) {
43
+ const result = findCallNode(node.child(i), callTypes, targetRow, funcName);
44
+ if (result) return result;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Clear the AST tree cache (call after batch operations)
51
+ * @param {object} index - ProjectIndex instance
52
+ */
53
+ function clearTreeCache(index) {
54
+ index._treeCache = null;
55
+ }
56
+
57
+ /**
58
+ * Analyze a call site to understand how it's being called (AST-based)
59
+ * @param {object} index - ProjectIndex instance
60
+ * @param {object} call - Usage object with file, line, content
61
+ * @param {string} funcName - Function name to find
62
+ * @returns {object} { args, argCount, hasSpread, hasVariable }
63
+ */
64
+ function analyzeCallSite(index, call, funcName) {
65
+ try {
66
+ const language = detectLanguage(call.file);
67
+ if (!language) return { args: null, argCount: 0 };
68
+
69
+ // Use tree cache to avoid re-parsing the same file in batch operations
70
+ let tree = index._treeCache?.get(call.file);
71
+ if (!tree) {
72
+ const content = index._readFile(call.file);
73
+ // HTML files need special handling: parse script blocks as JS
74
+ if (language === 'html') {
75
+ const htmlModule = getLanguageModule('html');
76
+ const htmlParser = getParser('html');
77
+ const jsParser = getParser('javascript');
78
+ if (!htmlParser || !jsParser) return { args: null, argCount: 0 };
79
+ const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
80
+ if (blocks.length === 0) return { args: null, argCount: 0 };
81
+ const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
82
+ tree = safeParse(jsParser, virtualJS);
83
+ } else {
84
+ const parser = getParser(language);
85
+ if (!parser) return { args: null, argCount: 0 };
86
+ tree = safeParse(parser, content);
87
+ }
88
+ if (!tree) return { args: null, argCount: 0 };
89
+ if (!index._treeCache) index._treeCache = new Map();
90
+ index._treeCache.set(call.file, tree);
91
+ }
92
+
93
+ // Call node types vary by language
94
+ const callTypes = new Set(['call_expression', 'call', 'method_invocation', 'object_creation_expression']);
95
+ const targetRow = call.line - 1; // tree-sitter is 0-indexed
96
+
97
+ // Find the call expression at the target line matching funcName
98
+ const callNode = findCallNode(tree.rootNode, callTypes, targetRow, funcName);
99
+ if (!callNode) return { args: null, argCount: 0 };
100
+
101
+ // Check if this is a method call (obj.func()) vs a direct call (func())
102
+ const funcNode = callNode.childForFieldName('function') ||
103
+ callNode.childForFieldName('name');
104
+ let isMethodCall = false;
105
+ if (funcNode) {
106
+ // member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
107
+ if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
108
+ isMethodCall = true;
109
+ }
110
+ // Java method_invocation with object
111
+ if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
112
+ isMethodCall = true;
113
+ }
114
+ }
115
+
116
+ const argsNode = callNode.childForFieldName('arguments');
117
+ if (!argsNode) return { args: [], argCount: 0, isMethodCall };
118
+
119
+ const args = [];
120
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
121
+ args.push(argsNode.namedChild(i).text.trim());
122
+ }
123
+
124
+ return {
125
+ args,
126
+ argCount: args.length,
127
+ hasSpread: args.some(a => a.startsWith('...')),
128
+ hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
129
+ isMethodCall
130
+ };
131
+ } catch (e) {
132
+ return { args: null, argCount: 0 };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Identify common calling patterns
138
+ * @param {Array} callSites - Array of call site objects
139
+ * @param {string} funcName - Function name
140
+ * @returns {object} Pattern counts
141
+ */
142
+ function identifyCallPatterns(callSites, funcName) {
143
+ const patterns = {
144
+ constantArgs: 0, // Call sites with literal/constant arguments
145
+ variableArgs: 0, // Call sites passing variables
146
+ chainedCalls: 0, // Calls that are part of method chains
147
+ awaitedCalls: 0, // Async calls with await
148
+ spreadCalls: 0 // Calls using spread operator
149
+ };
150
+
151
+ for (const site of callSites) {
152
+ const expr = site.expression;
153
+
154
+ if (site.hasSpread) patterns.spreadCalls++;
155
+ if (/await\s/.test(expr)) patterns.awaitedCalls++;
156
+ if (new RegExp('\\.' + escapeRegExp(funcName) + '\\s*\\(').test(expr)) patterns.chainedCalls++;
157
+
158
+ if (site.args && site.args.length > 0) {
159
+ const hasLiteral = site.args.some(a =>
160
+ /^[\d'"{\[]/.test(a) || a === 'true' || a === 'false' || a === 'null'
161
+ );
162
+ if (hasLiteral) patterns.constantArgs++;
163
+ if (site.hasVariable) patterns.variableArgs++;
164
+ }
165
+ }
166
+
167
+ return patterns;
168
+ }
169
+
170
+ /**
171
+ * Verify that all call sites match a function's signature
172
+ * @param {object} index - ProjectIndex instance
173
+ * @param {string} name - Function name
174
+ * @param {object} options - { file }
175
+ * @returns {object} Verification results with mismatches
176
+ */
177
+ function verify(index, name, options = {}) {
178
+ index._beginOp();
179
+ try {
180
+ const { def } = index.resolveSymbol(name, { file: options.file });
181
+ if (!def) {
182
+ return { found: false, function: name };
183
+ }
184
+ // For Python/Rust methods, exclude self/cls from parameter count
185
+ // (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
186
+ const fileEntry = index.files.get(def.file);
187
+ const lang = fileEntry?.language;
188
+ let params = def.paramsStructured || [];
189
+ if ((lang === 'python' || lang === 'rust') && params.length > 0) {
190
+ const firstName = params[0].name;
191
+ if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
192
+ params = params.slice(1);
193
+ }
194
+ }
195
+ const hasRest = params.some(p => p.rest);
196
+ // Rest params don't count toward expected/min — they accept 0+ extra args
197
+ const nonRestParams = params.filter(p => !p.rest);
198
+ const expectedParamCount = nonRestParams.length;
199
+ const optionalCount = nonRestParams.filter(p => p.optional || p.default !== undefined).length;
200
+ const minArgs = expectedParamCount - optionalCount;
201
+
202
+ // Get all call sites
203
+ const usages = index.usages(name, { codeOnly: true });
204
+ const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
205
+
206
+ const valid = [];
207
+ const mismatches = [];
208
+ const uncertain = [];
209
+
210
+ // If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
211
+ // This prevents false positives where a standalone function name matches method calls
212
+ const defIsMethod = def.isMethod || def.type === 'method' || def.className;
213
+
214
+ for (const call of calls) {
215
+ const analysis = analyzeCallSite(index, call, name);
216
+
217
+ // Skip method calls when verifying a non-method definition
218
+ if (analysis.isMethodCall && !defIsMethod) {
219
+ continue;
220
+ }
221
+
222
+ if (analysis.args === null) {
223
+ // Couldn't parse arguments
224
+ uncertain.push({
225
+ file: call.relativePath,
226
+ line: call.line,
227
+ expression: call.content.trim(),
228
+ reason: 'Could not parse call arguments'
229
+ });
230
+ continue;
231
+ }
232
+
233
+ if (analysis.hasSpread) {
234
+ // Spread args - can't verify count
235
+ uncertain.push({
236
+ file: call.relativePath,
237
+ line: call.line,
238
+ expression: call.content.trim(),
239
+ reason: 'Uses spread operator'
240
+ });
241
+ continue;
242
+ }
243
+
244
+ const argCount = analysis.argCount;
245
+
246
+ // Check if arg count is valid
247
+ if (hasRest) {
248
+ // With rest param, need at least minArgs
249
+ if (argCount >= minArgs) {
250
+ valid.push({ file: call.relativePath, line: call.line });
251
+ } else {
252
+ mismatches.push({
253
+ file: call.relativePath,
254
+ line: call.line,
255
+ expression: call.content.trim(),
256
+ expected: `at least ${minArgs} arg(s)`,
257
+ actual: argCount,
258
+ args: analysis.args
259
+ });
260
+ }
261
+ } else {
262
+ // Without rest, need between minArgs and expectedParamCount
263
+ if (argCount >= minArgs && argCount <= expectedParamCount) {
264
+ valid.push({ file: call.relativePath, line: call.line });
265
+ } else {
266
+ mismatches.push({
267
+ file: call.relativePath,
268
+ line: call.line,
269
+ expression: call.content.trim(),
270
+ expected: minArgs === expectedParamCount
271
+ ? `${expectedParamCount} arg(s)`
272
+ : `${minArgs}-${expectedParamCount} arg(s)`,
273
+ actual: argCount,
274
+ args: analysis.args
275
+ });
276
+ }
277
+ }
278
+ }
279
+ clearTreeCache(index);
280
+
281
+ return {
282
+ found: true,
283
+ function: name,
284
+ file: def.relativePath,
285
+ startLine: def.startLine,
286
+ signature: index.formatSignature(def),
287
+ params: params.map(p => ({
288
+ name: p.name,
289
+ optional: p.optional || p.default !== undefined,
290
+ hasDefault: p.default !== undefined
291
+ })),
292
+ expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
293
+ totalCalls: valid.length + mismatches.length + uncertain.length,
294
+ valid: valid.length,
295
+ mismatches: mismatches.length,
296
+ uncertain: uncertain.length,
297
+ mismatchDetails: mismatches,
298
+ uncertainDetails: uncertain
299
+ };
300
+ } finally { index._endOp(); }
301
+ }
302
+
303
+ /**
304
+ * Plan a refactoring operation
305
+ * @param {object} index - ProjectIndex instance
306
+ * @param {string} name - Function name
307
+ * @param {object} options - { addParam, removeParam, renameTo, defaultValue }
308
+ * @returns {object} Plan with before/after signatures and affected call sites
309
+ */
310
+ function plan(index, name, options = {}) {
311
+ index._beginOp();
312
+ try {
313
+ const definitions = index.symbols.get(name);
314
+ if (!definitions || definitions.length === 0) {
315
+ return { found: false, function: name };
316
+ }
317
+
318
+ const resolved = index.resolveSymbol(name, { file: options.file });
319
+ const def = resolved.def || definitions[0];
320
+ const impact = index.impact(name, { file: options.file });
321
+ const currentParams = def.paramsStructured || [];
322
+ const currentSignature = index.formatSignature(def);
323
+
324
+ let newParams = [...currentParams];
325
+ let newSignature = currentSignature;
326
+ let operation = null;
327
+ let changes = [];
328
+
329
+ if (options.addParam) {
330
+ operation = 'add-param';
331
+ const newParam = {
332
+ name: options.addParam,
333
+ ...(options.defaultValue && { default: options.defaultValue })
334
+ };
335
+ newParams.push(newParam);
336
+
337
+ // Generate new signature
338
+ const paramsList = newParams.map(p => {
339
+ let str = p.name;
340
+ if (p.type) str += `: ${p.type}`;
341
+ if (p.default) str += ` = ${p.default}`;
342
+ return str;
343
+ }).join(', ');
344
+ newSignature = `${name}(${paramsList})`;
345
+ if (def.returnType) newSignature += `: ${def.returnType}`;
346
+
347
+ // Describe changes needed at each call site
348
+ for (const fileGroup of impact.byFile) {
349
+ for (const site of fileGroup.sites) {
350
+ const suggestion = options.defaultValue
351
+ ? `No change needed (has default value)`
352
+ : `Add argument: ${options.addParam}`;
353
+ changes.push({
354
+ file: site.file,
355
+ line: site.line,
356
+ expression: site.expression,
357
+ suggestion,
358
+ args: site.args
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ if (options.removeParam) {
365
+ operation = 'remove-param';
366
+ const paramIndex = currentParams.findIndex(p => p.name === options.removeParam);
367
+ if (paramIndex === -1) {
368
+ return {
369
+ found: true,
370
+ error: `Parameter "${options.removeParam}" not found in ${name}`,
371
+ currentParams: currentParams.map(p => p.name)
372
+ };
373
+ }
374
+
375
+ newParams = currentParams.filter(p => p.name !== options.removeParam);
376
+
377
+ // Generate new signature
378
+ const paramsList = newParams.map(p => {
379
+ let str = p.name;
380
+ if (p.type) str += `: ${p.type}`;
381
+ if (p.default) str += ` = ${p.default}`;
382
+ return str;
383
+ }).join(', ');
384
+ newSignature = `${name}(${paramsList})`;
385
+ if (def.returnType) newSignature += `: ${def.returnType}`;
386
+
387
+ // For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
388
+ // but callers don't pass it. Adjust paramIndex to caller-side position.
389
+ const fileEntry = index.files.get(def.file);
390
+ const lang = fileEntry?.language;
391
+ let selfOffset = 0;
392
+ if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
393
+ const firstName = currentParams[0].name;
394
+ if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
395
+ selfOffset = 1;
396
+ }
397
+ }
398
+ const callerArgIndex = paramIndex - selfOffset;
399
+
400
+ // Describe changes at each call site
401
+ for (const fileGroup of impact.byFile) {
402
+ for (const site of fileGroup.sites) {
403
+ if (site.args && site.argCount > callerArgIndex) {
404
+ changes.push({
405
+ file: site.file,
406
+ line: site.line,
407
+ expression: site.expression,
408
+ suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
409
+ args: site.args
410
+ });
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ if (options.renameTo) {
417
+ operation = 'rename';
418
+ newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
419
+
420
+ // All call sites need renaming
421
+ for (const fileGroup of impact.byFile) {
422
+ for (const site of fileGroup.sites) {
423
+ const newExpression = site.expression.replace(
424
+ new RegExp('\\b' + escapeRegExp(name) + '\\b'),
425
+ options.renameTo
426
+ );
427
+ changes.push({
428
+ file: site.file,
429
+ line: site.line,
430
+ expression: site.expression,
431
+ suggestion: `Rename to: ${newExpression}`,
432
+ newExpression
433
+ });
434
+ }
435
+ }
436
+ }
437
+
438
+ return {
439
+ found: true,
440
+ function: name,
441
+ file: def.relativePath,
442
+ startLine: def.startLine,
443
+ operation,
444
+ before: {
445
+ signature: currentSignature,
446
+ params: currentParams.map(p => p.name)
447
+ },
448
+ after: {
449
+ signature: newSignature,
450
+ params: newParams.map(p => p.name)
451
+ },
452
+ totalChanges: changes.length,
453
+ filesAffected: new Set(changes.map(c => c.file)).size,
454
+ changes
455
+ };
456
+ } finally { index._endOp(); }
457
+ }
458
+
459
+ /**
460
+ * Analyze a call site using AST for example scoring.
461
+ * @param {object} index - ProjectIndex instance
462
+ * @param {string} filePath - File path
463
+ * @param {number} lineNum - Line number
464
+ * @param {string} funcName - Function name
465
+ * @returns {object} Analysis results
466
+ * @private
467
+ */
468
+ function analyzeCallSiteAST(index, filePath, lineNum, funcName) {
469
+ const result = {
470
+ isAwait: false, isDestructured: false, isTypedAssignment: false,
471
+ isInReturn: false, isInCatch: false, isInConditional: false,
472
+ hasComment: false, isStandalone: false
473
+ };
474
+
475
+ try {
476
+ const language = detectLanguage(filePath);
477
+ if (!language) return result;
478
+
479
+ const parser = getParser(language);
480
+ const content = index._readFile(filePath);
481
+ const tree = safeParse(parser, content);
482
+ if (!tree) return result;
483
+
484
+ const row = lineNum - 1;
485
+ const node = tree.rootNode.descendantForPosition({ row, column: 0 });
486
+ if (!node) return result;
487
+
488
+ let current = node;
489
+ let foundCall = false;
490
+
491
+ while (current) {
492
+ const type = current.type;
493
+
494
+ if (!foundCall && (type === 'call_expression' || type === 'call')) {
495
+ const calleeNode = current.childForFieldName('function') || current.namedChild(0);
496
+ if (calleeNode && calleeNode.text === funcName) {
497
+ foundCall = true;
498
+ }
499
+ }
500
+
501
+ if (foundCall) {
502
+ if (type === 'await_expression') result.isAwait = true;
503
+ if (type === 'variable_declarator' || type === 'assignment_expression') {
504
+ const parent = current.parent;
505
+ if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration')) {
506
+ result.isTypedAssignment = true;
507
+ }
508
+ }
509
+ if (type === 'array_pattern' || type === 'object_pattern') result.isDestructured = true;
510
+ if (type === 'return_statement') result.isInReturn = true;
511
+ if (type === 'catch_clause' || type === 'except_clause') result.isInCatch = true;
512
+ if (type === 'if_statement' || type === 'conditional_expression' || type === 'ternary_expression') result.isInConditional = true;
513
+ if (type === 'expression_statement') result.isStandalone = true;
514
+ }
515
+
516
+ current = current.parent;
517
+ }
518
+
519
+ const contentLines = content.split('\n');
520
+ if (lineNum > 1) {
521
+ const prevLine = contentLines[lineNum - 2].trim();
522
+ if (prevLine.startsWith('//') || prevLine.startsWith('#') || prevLine.endsWith('*/')) {
523
+ result.hasComment = true;
524
+ }
525
+ }
526
+ } catch (e) {
527
+ // Return default result on error
528
+ }
529
+
530
+ return result;
531
+ }
532
+
533
+ module.exports = { verify, plan, analyzeCallSite, analyzeCallSiteAST, findCallNode, clearTreeCache, identifyCallPatterns };
package/languages/go.js CHANGED
@@ -45,8 +45,13 @@ function extractGoParams(paramsNode) {
45
45
  function extractReceiver(receiverNode) {
46
46
  if (!receiverNode) return null;
47
47
  const text = receiverNode.text;
48
- const match = text.match(/\(\s*\w*\s*(\*?\w+)\s*\)/);
49
- return match ? match[1] : text.replace(/^\(|\)$/g, '').trim();
48
+ // Match named receiver: (r *Router) or (r Router[T])
49
+ const namedMatch = text.match(/\(\s*\w+\s+(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
50
+ if (namedMatch) return namedMatch[1];
51
+ // Match unnamed receiver: (Router) or (*Router) or (Router[T])
52
+ const unnamedMatch = text.match(/\(\s*(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
53
+ if (unnamedMatch) return unnamedMatch[1];
54
+ return text.replace(/^\(|\)$/g, '').trim();
50
55
  }
51
56
 
52
57
  /**
@@ -215,7 +220,17 @@ function findClasses(code, parser) {
215
220
  */
216
221
  function extractStructFields(structNode, code) {
217
222
  const fields = [];
218
- const fieldListNode = structNode.childForFieldName('body') || structNode;
223
+ // struct_type contains a field_declaration_list child (not a 'body' field)
224
+ let fieldListNode = structNode.childForFieldName('body');
225
+ if (!fieldListNode) {
226
+ for (let i = 0; i < structNode.namedChildCount; i++) {
227
+ if (structNode.namedChild(i).type === 'field_declaration_list') {
228
+ fieldListNode = structNode.namedChild(i);
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ if (!fieldListNode) fieldListNode = structNode;
219
234
 
220
235
  for (let i = 0; i < fieldListNode.namedChildCount; i++) {
221
236
  const field = fieldListNode.namedChild(i);
@@ -444,9 +459,9 @@ function findCallsInCode(code, parser) {
444
459
  });
445
460
  }
446
461
 
447
- // Track local closures: atoi := func(...) { ... }
462
+ // Track local closures: atoi := func(...) { ... } or var handler = func(...) { ... }
448
463
  if (node.type === 'short_var_declaration' || node.type === 'var_declaration') {
449
- // Check if RHS contains a func_literal
464
+ // Check if a subtree contains a func_literal
450
465
  const hasFunc = (n) => {
451
466
  if (!n) return false;
452
467
  if (n.type === 'func_literal') return true;
@@ -455,20 +470,41 @@ function findCallsInCode(code, parser) {
455
470
  }
456
471
  return false;
457
472
  };
458
- if (hasFunc(node)) {
459
- // Extract the variable name from the LHS
460
- const left = node.childForFieldName('left');
461
- if (left) {
462
- const names = left.type === 'expression_list'
463
- ? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
464
- .filter(n => n.type === 'identifier').map(n => n.text)
465
- : left.type === 'identifier' ? [left.text] : [];
466
- if (names.length > 0 && functionStack.length > 0) {
467
- const scopeKey = functionStack[functionStack.length - 1].startLine;
468
- if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Set());
469
- for (const n of names) closureScopes.get(scopeKey).add(n);
473
+ let names = [];
474
+ if (node.type === 'short_var_declaration') {
475
+ // short_var_declaration checks the whole RHS
476
+ if (hasFunc(node)) {
477
+ const left = node.childForFieldName('left');
478
+ if (left) {
479
+ names = left.type === 'expression_list'
480
+ ? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
481
+ .filter(n => n.type === 'identifier').map(n => n.text)
482
+ : left.type === 'identifier' ? [left.text] : [];
470
483
  }
471
484
  }
485
+ } else {
486
+ // var_declaration: check per-spec so only names with func_literal values are tracked
487
+ // Handle both: var x = func(){} (var_declaration > var_spec)
488
+ // and: var (\n x = func(){} \n) (var_declaration > var_spec_list > var_spec)
489
+ const collectClosureNames = (parent) => {
490
+ for (let i = 0; i < parent.namedChildCount; i++) {
491
+ const child = parent.namedChild(i);
492
+ if (child.type === 'var_spec' && hasFunc(child)) {
493
+ const nameNode = child.childForFieldName('name');
494
+ if (nameNode && nameNode.type === 'identifier') {
495
+ names.push(nameNode.text);
496
+ }
497
+ } else if (child.type === 'var_spec_list') {
498
+ collectClosureNames(child);
499
+ }
500
+ }
501
+ };
502
+ collectClosureNames(node);
503
+ }
504
+ if (names.length > 0 && functionStack.length > 0) {
505
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
506
+ if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Set());
507
+ for (const n of names) closureScopes.get(scopeKey).add(n);
472
508
  }
473
509
  }
474
510
 
@@ -546,8 +582,8 @@ function findImportsInCode(code, parser) {
546
582
 
547
583
  for (let i = 0; i < spec.namedChildCount; i++) {
548
584
  const child = spec.namedChild(i);
549
- if (child.type === 'interpreted_string_literal') {
550
- // Remove quotes
585
+ if (child.type === 'interpreted_string_literal' || child.type === 'raw_string_literal') {
586
+ // Remove quotes (double quotes or backticks)
551
587
  modulePath = child.text.slice(1, -1);
552
588
  } else if (child.type === 'package_identifier') {
553
589
  alias = child.text;
@@ -647,7 +683,7 @@ function findExportsInCode(code, parser) {
647
683
  exports.push({
648
684
  name: nameNode.text,
649
685
  type: 'type',
650
- line: node.startPosition.row + 1
686
+ line: spec.startPosition.row + 1
651
687
  });
652
688
  }
653
689
  }
@@ -665,7 +701,7 @@ function findExportsInCode(code, parser) {
665
701
  exports.push({
666
702
  name: nameNode.text,
667
703
  type: node.type === 'const_declaration' ? 'const' : 'var',
668
- line: node.startPosition.row + 1
704
+ line: spec.startPosition.row + 1
669
705
  });
670
706
  }
671
707
  }