ucn 4.0.2 → 4.1.0

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.
Files changed (46) hide show
  1. package/.claude/skills/ucn/SKILL.md +31 -7
  2. package/README.md +89 -50
  3. package/cli/index.js +199 -94
  4. package/core/account.js +3 -1
  5. package/core/analysis.js +322 -304
  6. package/core/bridge.js +13 -8
  7. package/core/cache.js +109 -19
  8. package/core/callers.js +969 -77
  9. package/core/check.js +19 -8
  10. package/core/deadcode.js +368 -40
  11. package/core/discovery.js +31 -11
  12. package/core/entrypoints.js +149 -17
  13. package/core/execute.js +330 -43
  14. package/core/graph-build.js +61 -10
  15. package/core/graph.js +282 -61
  16. package/core/imports.js +72 -3
  17. package/core/output/analysis-ext.js +70 -10
  18. package/core/output/analysis.js +52 -33
  19. package/core/output/check.js +4 -1
  20. package/core/output/endpoints.js +8 -3
  21. package/core/output/extraction.js +12 -1
  22. package/core/output/find.js +23 -9
  23. package/core/output/graph.js +32 -9
  24. package/core/output/refactoring.js +147 -49
  25. package/core/output/reporting.js +104 -5
  26. package/core/output/search.js +30 -3
  27. package/core/output/shared.js +31 -4
  28. package/core/output/tracing.js +22 -11
  29. package/core/parser.js +8 -6
  30. package/core/project.js +167 -7
  31. package/core/registry.js +20 -16
  32. package/core/reporting.js +131 -13
  33. package/core/search.js +270 -55
  34. package/core/shared.js +240 -1
  35. package/core/stacktrace.js +23 -1
  36. package/core/tracing.js +278 -36
  37. package/core/verify.js +352 -349
  38. package/languages/go.js +29 -17
  39. package/languages/index.js +56 -0
  40. package/languages/java.js +133 -16
  41. package/languages/javascript.js +215 -27
  42. package/languages/python.js +113 -47
  43. package/languages/rust.js +89 -26
  44. package/languages/utils.js +35 -7
  45. package/mcp/server.js +69 -30
  46. package/package.json +4 -1
package/languages/go.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  parseStructuredParams,
13
13
  extractGoDocstring,
14
14
  visitNameNodes,
15
+ sameNode,
15
16
  } = require('./utils');
16
17
  const { PARSE_OPTIONS, safeParse } = require('./index');
17
18
 
@@ -34,11 +35,13 @@ function extractReturnType(node) {
34
35
  * Extract Go parameters
35
36
  */
36
37
  function extractGoParams(paramsNode) {
38
+ // Distinguish "we have no node" (genuinely unknown) from "node is empty".
39
+ // Returning '...' for empty parens conflated zero-param functions with
40
+ // unknown signatures in JSON output (fix #238; the shared
41
+ // utils.extractParams already had this fix).
37
42
  if (!paramsNode) return '...';
38
43
  const text = paramsNode.text;
39
- let params = text.replace(/^\(|\)$/g, '').trim();
40
- if (!params) return '...';
41
- return params;
44
+ return text.replace(/^\(|\)$/g, '').trim();
42
45
  }
43
46
 
44
47
  /**
@@ -49,11 +52,15 @@ function extractReceiver(receiverNode) {
49
52
  // receiverNode is a parameter_list: (r *Router)
50
53
  // Find the parameter_declaration child
51
54
  const param = receiverNode.namedChildren.find(c => c.type === 'parameter_declaration');
52
- if (!param) return receiverNode.text.replace(/^\(|\)$/g, '').trim();
55
+ if (!param) return receiverNode.text.replace(/^\(|\)$/g, '').trim().replace(/\[.*\]$/, '');
53
56
  // The type is the last named child (name is first for named receivers)
54
57
  const typeNode = param.namedChildren[param.namedChildren.length - 1];
55
58
  if (!typeNode) return null;
56
- return typeNode.text;
59
+ // Strip generic type parameters: `(p *Pair[K, V])` receives on the TYPE
60
+ // Pair — the bracket spelling made fn Pair.First/--class-name/
61
+ // findMethodsForType all miss (fix #248; Rust/Java already normalize,
62
+ // and a Go receiver type is a defined type, never a slice/map literal).
63
+ return typeNode.text.replace(/\[.*\]$/, '');
57
64
  }
58
65
 
59
66
  // --- Single-pass helpers: extracted from find* callbacks ---
@@ -1634,10 +1641,11 @@ function findExportsInCode(code, parser) {
1634
1641
  * @param {string} code - Source code
1635
1642
  * @param {string} name - Symbol name to find
1636
1643
  * @param {object} parser - Tree-sitter parser instance
1644
+ * @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
1637
1645
  * @returns {Array<{line: number, column: number, usageType: string}>}
1638
1646
  */
1639
- function findUsagesInCode(code, name, parser) {
1640
- const tree = parseTree(parser, code);
1647
+ function findUsagesInCode(code, name, parser, tree) {
1648
+ tree = tree || parseTree(parser, code);
1641
1649
  const usages = [];
1642
1650
 
1643
1651
  visitNameNodes(tree, code, name, (node) => {
@@ -1662,52 +1670,52 @@ function findUsagesInCode(code, name, parser) {
1662
1670
  }
1663
1671
  // Call: identifier is function in call_expression
1664
1672
  else if (parent.type === 'call_expression' &&
1665
- parent.childForFieldName('function') === node) {
1673
+ sameNode(parent.childForFieldName('function'), node)) {
1666
1674
  usageType = 'call';
1667
1675
  }
1668
1676
  // Definition: function name
1669
1677
  else if (parent.type === 'function_declaration' &&
1670
- parent.childForFieldName('name') === node) {
1678
+ sameNode(parent.childForFieldName('name'), node)) {
1671
1679
  usageType = 'definition';
1672
1680
  }
1673
1681
  // Definition: method name
1674
1682
  else if (parent.type === 'method_declaration' &&
1675
- parent.childForFieldName('name') === node) {
1683
+ sameNode(parent.childForFieldName('name'), node)) {
1676
1684
  usageType = 'definition';
1677
1685
  }
1678
1686
  // Definition: type name
1679
1687
  else if (parent.type === 'type_spec' &&
1680
- parent.childForFieldName('name') === node) {
1688
+ sameNode(parent.childForFieldName('name'), node)) {
1681
1689
  usageType = 'definition';
1682
1690
  }
1683
1691
  // Definition: variable name in short var declaration
1684
1692
  else if (parent.type === 'short_var_declaration') {
1685
1693
  const left = parent.childForFieldName('left');
1686
- if (left && (left === node || left.namedChildren?.some(c => c === node))) {
1694
+ if (left && (sameNode(left, node) || left.namedChildren?.some(c => sameNode(c, node)))) {
1687
1695
  usageType = 'definition';
1688
1696
  }
1689
1697
  }
1690
1698
  // Multi-var: x, err := foo() — identifier parent is expression_list
1691
1699
  else if (parent.type === 'expression_list' &&
1692
1700
  parent.parent?.type === 'short_var_declaration' &&
1693
- parent.parent.childForFieldName('left') === parent) {
1701
+ sameNode(parent.parent.childForFieldName('left'), parent)) {
1694
1702
  usageType = 'definition';
1695
1703
  }
1696
1704
  // Definition: const/var spec
1697
1705
  else if (parent.type === 'const_spec' || parent.type === 'var_spec') {
1698
1706
  const nameNode = parent.childForFieldName('name');
1699
- if (nameNode === node) {
1707
+ if (sameNode(nameNode, node)) {
1700
1708
  usageType = 'definition';
1701
1709
  }
1702
1710
  }
1703
1711
  // Definition: parameter name (not the type)
1704
1712
  else if (parent.type === 'parameter_declaration' &&
1705
- parent.childForFieldName('name') === node) {
1713
+ sameNode(parent.childForFieldName('name'), node)) {
1706
1714
  usageType = 'definition';
1707
1715
  }
1708
1716
  // Composite literal: Type{} — type_identifier is the type of composite_literal
1709
1717
  else if (parent.type === 'composite_literal' &&
1710
- parent.childForFieldName('type') === node) {
1718
+ sameNode(parent.childForFieldName('type'), node)) {
1711
1719
  usageType = 'call';
1712
1720
  }
1713
1721
  // Method call: selector_expression followed by call (field_identifier case)
@@ -1745,8 +1753,12 @@ function findUsagesInCode(code, name, parser) {
1745
1753
  */
1746
1754
  function getEntryPointKind(symbol) {
1747
1755
  const { name } = symbol;
1756
+ // Test* stays receiver-agnostic — testify suite METHODS (func (s *Suite)
1757
+ // TestFoo) are genuinely harness-invoked.
1748
1758
  if (/^(Test|Benchmark|Example|Fuzz)[A-Z_]/.test(name)) return 'test';
1749
- if (name === 'main' || name === 'init') return 'main';
1759
+ // Only FREE functions main/init are runtime entries a method named
1760
+ // main/init on a receiver is an ordinary method (fix #243).
1761
+ if ((name === 'main' || name === 'init') && !symbol.className && !symbol.receiver) return 'main';
1750
1762
  return null;
1751
1763
  }
1752
1764
 
@@ -21,8 +21,18 @@ const STRUCTURAL_TRAITS = {
21
21
  exportVisibility: 'keyword',
22
22
  hasDynamicImports: true,
23
23
  testDirs: [],
24
+ // Whether the language supports default parameter values in function
25
+ // signatures (JS/TS/Python: yes). Drives plan --add-param --default:
26
+ // without them, every call site needs the new argument and the rendered
27
+ // signature must not use `= value` syntax (Go/Java/Rust).
28
+ hasDefaultParams: true,
24
29
  allMethodsVirtual: false,
25
30
  hasArityOverloads: false,
31
+ // Whether `from pkg import name` can bind a SUBMODULE file as a plain
32
+ // name (fix #224). Python sets true: graph-build resolves the composed
33
+ // dotted specifier and records it in moduleResolved, making the receiver
34
+ // a module receiver at query time. JS from-imports bind values only.
35
+ submoduleImports: false,
26
36
  // Class members are public unless marked otherwise (#name, _name,
27
37
  // `private`). An exported class therefore exposes its non-private methods
28
38
  // as public API — deadcode treats them as exported (fix #211). Languages
@@ -49,6 +59,8 @@ const NOMINAL_TRAITS = {
49
59
  exportVisibility: 'keyword',
50
60
  hasDynamicImports: true,
51
61
  testDirs: [],
62
+ // Go/Java/Rust have no default parameter values — see STRUCTURAL_TRAITS.
63
+ hasDefaultParams: false,
52
64
  // Whether ANY instance method call can dynamically dispatch to a subtype
53
65
  // override (Java: all instance methods are virtual). Go struct method
54
66
  // sets and Rust inherent methods bind statically — only interface/trait
@@ -153,6 +165,11 @@ const LANGUAGES = {
153
165
  traits: {
154
166
  ...STRUCTURAL_TRAITS,
155
167
  selfParam: ['self', 'cls'],
168
+ // fix #224: `from pkg import name` may bind a SUBMODULE file, not
169
+ // a symbol — graph-build resolves the composed dotted specifier
170
+ // and query code treats such receivers as module receivers. JS
171
+ // from-imports bind values only (`import * as ns` is parser-marked).
172
+ submoduleImports: true,
156
173
  testFileCandidates: (base, ext) => [`test_${base}.py`, `${base}_test.py`],
157
174
  testDirs: ['tests'],
158
175
  },
@@ -244,10 +261,49 @@ function loadTreeSitter() {
244
261
  { cause: e }
245
262
  );
246
263
  }
264
+ _installNodeTypeCache(TreeSitter);
247
265
  }
248
266
  return TreeSitter;
249
267
  }
250
268
 
269
+ /**
270
+ * Make SyntaxNode#type a cheap data-property read.
271
+ *
272
+ * The binding generates one node subclass per grammar type id and intends
273
+ * `nodeSubclass.prototype.type = typeName` to shadow the base getter — but
274
+ * SyntaxNode.prototype.type is a getter-only accessor, so that plain
275
+ * assignment silently no-ops and EVERY `.type` read marshals into native
276
+ * code. Hot walks read `.type` many times per node; on a ~300-file build
277
+ * this getter alone costs seconds.
278
+ *
279
+ * A node's type is immutable and each generated subclass maps to exactly
280
+ * one grammar type, so the first native read per CLASS plants the string as
281
+ * a data property on that class's prototype — all later reads on any node
282
+ * of that type are plain property hits. Nodes using the base class itself
283
+ * (anonymous tokens, ERROR) keep the native getter.
284
+ */
285
+ function _installNodeTypeCache(TS) {
286
+ try {
287
+ const base = TS?.SyntaxNode?.prototype;
288
+ const desc = base && Object.getOwnPropertyDescriptor(base, 'type');
289
+ if (!desc?.get || desc.set) return; // shape changed upstream — leave it alone
290
+ const nativeGet = desc.get;
291
+ Object.defineProperty(base, 'type', {
292
+ configurable: true,
293
+ get() {
294
+ const t = nativeGet.call(this);
295
+ const ctor = this.constructor;
296
+ if (ctor && ctor !== TS.SyntaxNode && ctor.prototype instanceof TS.SyntaxNode) {
297
+ Object.defineProperty(ctor.prototype, 'type', {
298
+ value: t, configurable: true
299
+ });
300
+ }
301
+ return t;
302
+ }
303
+ });
304
+ } catch { /* stock getter keeps working */ }
305
+ }
306
+
251
307
  /**
252
308
  * Get or create parser for a language
253
309
  * @param {string} language - Language name
package/languages/java.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  parseStructuredParams,
13
13
  extractJavaDocstring,
14
14
  visitNameNodes,
15
+ sameNode,
15
16
  } = require('./utils');
16
17
  const { PARSE_OPTIONS, safeParse } = require('./index');
17
18
 
@@ -23,10 +24,13 @@ function parseTree(parser, code) {
23
24
  * Extract Java parameters
24
25
  */
25
26
  function extractJavaParams(paramsNode) {
27
+ // Distinguish "we have no node" (genuinely unknown) from "node is empty".
28
+ // Returning '...' for empty parens conflated zero-param methods with
29
+ // unknown signatures in JSON output (fix #241; go/rust got this in #238,
30
+ // the shared utils.extractParams already had it).
26
31
  if (!paramsNode) return '...';
27
32
  const text = paramsNode.text;
28
33
  let params = text.replace(/^\(|\)$/g, '').trim();
29
- if (!params) return '...';
30
34
  return params;
31
35
  }
32
36
 
@@ -585,6 +589,9 @@ function extractEnumConstants(enumNode, codeOrLines) {
585
589
  startLine,
586
590
  endLine,
587
591
  memberType: 'constant',
592
+ // JLS: enum constants are implicitly public static final
593
+ // (fix #251 — api omitted them for lack of a modifier).
594
+ modifiers: ['public', 'static', 'final'],
588
595
  ...(argsNode && { params: argsNode.text.slice(1, -1) })
589
596
  });
590
597
  }
@@ -608,6 +615,7 @@ function extractEnumConstants(enumNode, codeOrLines) {
608
615
  constants.push({
609
616
  name: nameNode.text,
610
617
  params: extractJavaParams(paramsNode),
618
+ paramsStructured: parseStructuredParams(paramsNode, 'java'),
611
619
  startLine,
612
620
  endLine,
613
621
  memberType: modifiers.includes('static') ? 'static' : 'method',
@@ -625,6 +633,10 @@ function extractEnumConstants(enumNode, codeOrLines) {
625
633
  constants.push({
626
634
  name: nameNode.text,
627
635
  params: extractJavaParams(paramsNode),
636
+ // paramsStructured drives verify's arg-check
637
+ // (fix #230 — enum constant args LOW(1) were
638
+ // checked against an empty list).
639
+ paramsStructured: parseStructuredParams(paramsNode, 'java'),
628
640
  startLine,
629
641
  endLine,
630
642
  memberType: 'constructor',
@@ -681,6 +693,7 @@ function extractClassMembers(classNode, codeOrLines) {
681
693
  memberType = 'abstract';
682
694
  }
683
695
 
696
+ const memberGenerics = extractGenerics(child);
684
697
  members.push({
685
698
  name: nameNode.text,
686
699
  params: extractJavaParams(paramsNode),
@@ -693,7 +706,10 @@ function extractClassMembers(classNode, codeOrLines) {
693
706
  ...(returnType && { returnType }),
694
707
  ...(docstring && { docstring }),
695
708
  ...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
696
- ...(nameLine !== startLine && { nameLine })
709
+ ...(nameLine !== startLine && { nameLine }),
710
+ // Method-level type params (fix #229): generic-param receiver
711
+ // types inside the method resolve against this declaration.
712
+ ...(memberGenerics && { generics: memberGenerics })
697
713
  });
698
714
  }
699
715
  }
@@ -711,6 +727,11 @@ function extractClassMembers(classNode, codeOrLines) {
711
727
  if (child.type === 'field_declaration') {
712
728
  const typeNode = child.childForFieldName('type');
713
729
  const fieldTypeText = typeNode ? typeNode.text : null;
730
+ // Visibility travels with the member (fix #251 — public
731
+ // instance fields were invisible to api/fileExports because
732
+ // the member had no modifiers for the #240 discipline to read;
733
+ // the #241 Rust-field twin).
734
+ const fieldModifiers = extractModifiers(child);
714
735
  for (let j = 0; j < child.namedChildCount; j++) {
715
736
  const decl = child.namedChild(j);
716
737
  if (decl.type === 'variable_declarator') {
@@ -722,6 +743,7 @@ function extractClassMembers(classNode, codeOrLines) {
722
743
  startLine,
723
744
  endLine,
724
745
  memberType: 'field',
746
+ ...(fieldModifiers.length > 0 && { modifiers: fieldModifiers }),
725
747
  fieldType: fieldTypeText
726
748
  });
727
749
  }
@@ -1144,6 +1166,80 @@ function findCallsInCode(code, parser) {
1144
1166
  }
1145
1167
 
1146
1168
  // Handle constructor calls: new Foo(), new pkg.Bar()
1169
+ // super(x) / this(x) — constructor delegation (fix #238: these
1170
+ // sites were invisible to every command). Resolve to the parent
1171
+ // class's constructor (super) or a same-class overload (this);
1172
+ // Java constructors are indexed under the CLASS name, so the
1173
+ // record carries the target class as its name.
1174
+ if (node.type === 'explicit_constructor_invocation') {
1175
+ let cls = node.parent;
1176
+ while (cls && cls.type !== 'class_declaration' && cls.type !== 'enum_declaration') {
1177
+ cls = cls.parent;
1178
+ }
1179
+ const isSuperCall = node.children.some(c => c.type === 'super');
1180
+ let targetClass = null;
1181
+ if (cls) {
1182
+ if (isSuperCall) {
1183
+ const sup = cls.childForFieldName('superclass');
1184
+ targetClass = sup?.namedChild(0)?.text || null;
1185
+ } else {
1186
+ targetClass = cls.childForFieldName('name')?.text || null;
1187
+ }
1188
+ }
1189
+ if (targetClass) {
1190
+ const genericIdx = targetClass.indexOf('<');
1191
+ if (genericIdx > 0) targetClass = targetClass.substring(0, genericIdx);
1192
+ const dotIdx = targetClass.lastIndexOf('.');
1193
+ if (dotIdx > 0) targetClass = targetClass.substring(dotIdx + 1);
1194
+ const enclosingFunction = getCurrentEnclosingFunction();
1195
+ const ctorArgs = getCallArgs(node);
1196
+ calls.push({
1197
+ name: targetClass,
1198
+ line: node.startPosition.row + 1,
1199
+ isMethod: false,
1200
+ isConstructor: true,
1201
+ // 'this' delegation names the ENCLOSING class by
1202
+ // construction — an intra-class mechanism, never a
1203
+ // caller edge for the class (jdtls-measured, fix #238).
1204
+ ctorDelegation: isSuperCall ? 'super' : 'this',
1205
+ argCount: ctorArgs.argCount,
1206
+ ...(ctorArgs.argKinds && { argKinds: ctorArgs.argKinds }),
1207
+ enclosingFunction
1208
+ });
1209
+ }
1210
+ return true;
1211
+ }
1212
+
1213
+ // Enum constants with arguments (RED(1)) invoke the enum's own
1214
+ // constructor (fix #238: the constructor had no call records, so
1215
+ // search --unused / deadcode flagged it dead in every enum).
1216
+ // Argument-less constants still construct — they call the implicit
1217
+ // or 0-arg constructor.
1218
+ if (node.type === 'enum_constant') {
1219
+ let enclosingEnum = node.parent;
1220
+ while (enclosingEnum && enclosingEnum.type !== 'enum_declaration') {
1221
+ enclosingEnum = enclosingEnum.parent;
1222
+ }
1223
+ const enumName = enclosingEnum?.childForFieldName('name')?.text;
1224
+ if (enumName) {
1225
+ const ctorArgs = getCallArgs(node);
1226
+ calls.push({
1227
+ name: enumName,
1228
+ line: node.startPosition.row + 1,
1229
+ isMethod: false,
1230
+ isConstructor: true,
1231
+ // Part of the enum's own declaration — keeps the
1232
+ // constructor alive for deadcode/--unused, but never a
1233
+ // caller edge for the enum (jdtls-measured, fix #238).
1234
+ enumConstant: true,
1235
+ argCount: ctorArgs.argCount,
1236
+ ...(ctorArgs.argKinds && { argKinds: ctorArgs.argKinds }),
1237
+ enclosingFunction: getCurrentEnclosingFunction()
1238
+ });
1239
+ }
1240
+ return true;
1241
+ }
1242
+
1147
1243
  if (node.type === 'object_creation_expression') {
1148
1244
  const typeNode = node.childForFieldName('type');
1149
1245
  if (typeNode) {
@@ -1230,6 +1326,26 @@ function findCallsInCode(code, parser) {
1230
1326
  }
1231
1327
  }
1232
1328
 
1329
+ // Try-with-resources declarations are declared-type locals too (fix
1330
+ // #231): `try (Res r = new Res())` types r exactly like `Res r = ...`
1331
+ // — the resource node carries type/name/value fields directly.
1332
+ if (node.type === 'resource' && functionStack.length > 0) {
1333
+ const resTypeNode = node.childForFieldName('type');
1334
+ const resNameNode = node.childForFieldName('name');
1335
+ const resValueNode = node.childForFieldName('value');
1336
+ let typeName = resValueNode?.type === 'object_creation_expression'
1337
+ ? extractTypeName(resValueNode.childForFieldName('type'))
1338
+ : null;
1339
+ if (!typeName && resTypeNode && resTypeNode.text !== 'var') {
1340
+ typeName = extractTypeName(resTypeNode);
1341
+ }
1342
+ if (resNameNode && typeName) {
1343
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
1344
+ const typeMap = scopeTypes.get(scopeKey);
1345
+ if (typeMap) typeMap.set(resNameNode.text, typeName);
1346
+ }
1347
+ }
1348
+
1233
1349
  return true;
1234
1350
  }, {
1235
1351
  onLeave: (node) => {
@@ -1375,10 +1491,11 @@ function findExportsInCode(code, parser) {
1375
1491
  * @param {string} code - Source code
1376
1492
  * @param {string} name - Symbol name to find
1377
1493
  * @param {object} parser - Tree-sitter parser instance
1494
+ * @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
1378
1495
  * @returns {Array<{line: number, column: number, usageType: string}>}
1379
1496
  */
1380
- function findUsagesInCode(code, name, parser) {
1381
- const tree = parseTree(parser, code);
1497
+ function findUsagesInCode(code, name, parser, tree) {
1498
+ tree = tree || parseTree(parser, code);
1382
1499
  const usages = [];
1383
1500
 
1384
1501
  visitNameNodes(tree, code, name, (node) => {
@@ -1410,7 +1527,7 @@ function findUsagesInCode(code, name, parser) {
1410
1527
  }
1411
1528
  // Call: method_invocation with name field
1412
1529
  else if (parent.type === 'method_invocation' &&
1413
- parent.childForFieldName('name') === node) {
1530
+ sameNode(parent.childForFieldName('name'), node)) {
1414
1531
  usageType = 'call';
1415
1532
  // Track receiver for method invocations (obj.name() → receiver = 'obj')
1416
1533
  const object = parent.childForFieldName('object');
@@ -1421,50 +1538,50 @@ function findUsagesInCode(code, name, parser) {
1421
1538
  }
1422
1539
  // Definition: method name
1423
1540
  else if (parent.type === 'method_declaration' &&
1424
- parent.childForFieldName('name') === node) {
1541
+ sameNode(parent.childForFieldName('name'), node)) {
1425
1542
  usageType = 'definition';
1426
1543
  }
1427
1544
  // Definition: class name
1428
1545
  else if (parent.type === 'class_declaration' &&
1429
- parent.childForFieldName('name') === node) {
1546
+ sameNode(parent.childForFieldName('name'), node)) {
1430
1547
  usageType = 'definition';
1431
1548
  }
1432
1549
  // Definition: interface name
1433
1550
  else if (parent.type === 'interface_declaration' &&
1434
- parent.childForFieldName('name') === node) {
1551
+ sameNode(parent.childForFieldName('name'), node)) {
1435
1552
  usageType = 'definition';
1436
1553
  }
1437
1554
  // Definition: enum name
1438
1555
  else if (parent.type === 'enum_declaration' &&
1439
- parent.childForFieldName('name') === node) {
1556
+ sameNode(parent.childForFieldName('name'), node)) {
1440
1557
  usageType = 'definition';
1441
1558
  }
1442
1559
  // Definition: constructor
1443
1560
  else if (parent.type === 'constructor_declaration' &&
1444
- parent.childForFieldName('name') === node) {
1561
+ sameNode(parent.childForFieldName('name'), node)) {
1445
1562
  usageType = 'definition';
1446
1563
  }
1447
1564
  // Definition: local variable
1448
1565
  else if (parent.type === 'variable_declarator' &&
1449
- parent.childForFieldName('name') === node) {
1566
+ sameNode(parent.childForFieldName('name'), node)) {
1450
1567
  usageType = 'definition';
1451
1568
  }
1452
1569
  // Definition: parameter (name field only, not the type)
1453
1570
  else if ((parent.type === 'formal_parameter' ||
1454
1571
  parent.type === 'spread_parameter') &&
1455
- parent.childForFieldName('name') === node) {
1572
+ sameNode(parent.childForFieldName('name'), node)) {
1456
1573
  usageType = 'definition';
1457
1574
  }
1458
1575
  // Definition: field (declarator name only, not the type)
1459
1576
  else if (parent.type === 'field_declaration' &&
1460
1577
  node.type === 'identifier' &&
1461
- parent.descendantsOfType('variable_declarator').some(d => d.childForFieldName('name') === node)) {
1578
+ parent.descendantsOfType('variable_declarator').some(d => sameNode(d.childForFieldName('name'), node))) {
1462
1579
  usageType = 'definition';
1463
1580
  }
1464
1581
  // Object creation: new ClassName()
1465
1582
  else if (parent.type === 'object_creation_expression') {
1466
1583
  const typeNode = parent.childForFieldName('type');
1467
- if (typeNode === node || typeNode?.text === name) {
1584
+ if (sameNode(typeNode, node) || typeNode?.text === name) {
1468
1585
  usageType = 'call';
1469
1586
  }
1470
1587
  }
@@ -1472,12 +1589,12 @@ function findUsagesInCode(code, name, parser) {
1472
1589
  // (variable or ClassName), referenced, not called. The call belongs
1473
1590
  // to the name field, handled above.
1474
1591
  else if (parent.type === 'method_invocation' &&
1475
- parent.childForFieldName('object') === node) {
1592
+ sameNode(parent.childForFieldName('object'), node)) {
1476
1593
  usageType = 'reference';
1477
1594
  }
1478
1595
  // Field access: obj.field
1479
1596
  else if (parent.type === 'field_access' &&
1480
- parent.childForFieldName('field') === node) {
1597
+ sameNode(parent.childForFieldName('field'), node)) {
1481
1598
  usageType = 'reference';
1482
1599
  // Track receiver for field access (obj.field → receiver = 'obj')
1483
1600
  const object = parent.childForFieldName('object');