ucn 5.4.2 → 5.5.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 (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +40 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +457 -60
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +2113 -0
  62. package/core/rust-modules.js +185 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1161 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2237 -115
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -0,0 +1,3229 @@
1
+ /**
2
+ * core/type-references.js - Every reference to a renamed type (fix #386).
3
+ *
4
+ * Renaming a class, struct, interface, trait, enum, record, union or type
5
+ * alias changes every token that NAMES that type: base and implements
6
+ * clauses, field/parameter/return/local and generic-argument types, casts
7
+ * and type tests, static member qualifiers (`Widget.CONST`, `Widget::f`),
8
+ * constructor and destructor names, member initializers, construction
9
+ * expressions, imports and re-exports, qualified spellings (`pkg.Widget`,
10
+ * `app::Widget`, `crate::m::Widget`). Call-shaped constructor sites are only
11
+ * a fraction of them.
12
+ *
13
+ * For one pinned type definition this module walks the ground lines of the
14
+ * name (the literal-name text set), finds every identifier token spelling
15
+ * the name in the parsed tree, classifies its syntactic position per
16
+ * language and decides what the token denotes:
17
+ *
18
+ * 'edit' the token names the pinned type (or a declaration that is the
19
+ * same entity: TS class/interface merging, C# partial parts,
20
+ * configuration alternatives, Go build variants)
21
+ * 'review' it may name it and UCN cannot prove which declaration it
22
+ * names (the reason travels with it)
23
+ * 'skip' it provably names something else: another type of that name
24
+ * in another module/package/namespace, a member, a local
25
+ * binding, a type parameter, a declaration of another entity
26
+ *
27
+ * Resolution follows each language's own name lookup, conservatively:
28
+ * lexical bindings and type parameters shadow; a same-file declaration in
29
+ * scope wins; then imports (JS/TS, Python, Rust `use`, Java single-type and
30
+ * on-demand imports, C# usings), packages (Go directories, Java packages),
31
+ * namespaces (C#, C++ via cpp-scope), include closures (C/C++). Anything the
32
+ * model cannot settle is a review item, never a silent edit and never a
33
+ * silent omission. A ground line that holds the name outside any parsed
34
+ * token and outside comments/strings (macro bodies, unparsed regions) is a
35
+ * review item too.
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ const path = require('path');
41
+ const { getParser, safeParse, langTraits, LANGUAGES } = require('../languages');
42
+ const { sameNode } = require('../languages/utils');
43
+ const { classKeyOf } = require('./class-identity');
44
+
45
+ const TYPE_RENAME_KINDS = new Set([
46
+ 'class', 'struct', 'interface', 'trait', 'enum', 'record', 'union', 'type',
47
+ // TypeScript namespaces (fix #389): a container name used in type and
48
+ // value positions (`Geometry.Point`, `Geometry.origin()`), renamed with
49
+ // the same lookup.
50
+ 'namespace',
51
+ ]);
52
+ // Declarations a type NAME can resolve to (type namespace).
53
+ const TYPE_DECL_KINDS = TYPE_RENAME_KINDS;
54
+ const TOKEN_TYPES = new Set([
55
+ 'identifier', 'type_identifier', 'field_identifier', 'property_identifier',
56
+ 'shorthand_property_identifier', 'shorthand_property_identifier_pattern',
57
+ 'namespace_identifier', 'package_identifier', 'statement_identifier',
58
+ 'shorthand_field_identifier', 'private_property_identifier', 'label_name',
59
+ ]);
60
+ const MAX_QUALIFIER_HOPS = 6;
61
+
62
+ function familyOf(language) {
63
+ if (language === 'javascript' || language === 'typescript' || language === 'tsx') return 'js';
64
+ if (language === 'c' || language === 'cpp') return 'c';
65
+ return language;
66
+ }
67
+
68
+ /** Is `def` a declaration whose rename is a type rename? */
69
+ function isTypeRenamePin(def) {
70
+ return !!def && TYPE_RENAME_KINDS.has(def.type) && !def.isMethod;
71
+ }
72
+
73
+ // ── AST helpers ──────────────────────────────────────────────────────────
74
+
75
+ function fieldOf(node) {
76
+ const parent = node.parent;
77
+ if (!parent) return null;
78
+ for (let i = 0; i < parent.childCount; i++) {
79
+ const child = parent.child(i);
80
+ if (child.startIndex === node.startIndex && child.endIndex === node.endIndex &&
81
+ child.type === node.type) {
82
+ return parent.fieldNameForChild(i) || null;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function isField(node, field) {
89
+ const child = node.parent?.childForFieldName(field);
90
+ return !!child && sameNode(child, node);
91
+ }
92
+
93
+ function ancestor(node, types, stopTypes = null) {
94
+ for (let current = node.parent; current; current = current.parent) {
95
+ if (types.has(current.type)) return current;
96
+ if (stopTypes && stopTypes.has(current.type)) return null;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function contains(outer, inner) {
102
+ return outer.startIndex <= inner.startIndex && outer.endIndex >= inner.endIndex;
103
+ }
104
+
105
+ function textOf(node) {
106
+ return node ? node.text.replace(/\s+/g, '') : '';
107
+ }
108
+
109
+ // ── Token classification per language ───────────────────────────────────
110
+ //
111
+ // A role says where a token stands:
112
+ // decl the name of a type-like declaration (declNode)
113
+ // ctor a constructor/destructor name inside a class body (classNode)
114
+ // type a type position, optionally qualified (qualifier node/text)
115
+ // value an expression position, optionally qualified
116
+ // member a member access name (receiver node): a member unless the
117
+ // receiver is a module/package/namespace/type
118
+ // import the source-side name of an import/re-export (module text)
119
+ // meminit a C++ member-initializer name (base, delegating or member)
120
+ // macro a Rust macro-invocation token
121
+ // template a token of a macro definition body (binds at expansion)
122
+ // gofield a Go selector/key that may be an embedded field
123
+ // shorthand a JS shorthand property (key and value at once)
124
+ // skip not a reference (binding: true when it declares a local
125
+ // value that can shadow; typeParam: true for a type parameter)
126
+ // unknown an unmodelled position
127
+
128
+ const skip = (why, extra = {}) => ({ role: 'skip', why, ...extra });
129
+
130
+ function classifyJava(node) {
131
+ const parent = node.parent;
132
+ const field = fieldOf(node);
133
+ switch (parent.type) {
134
+ case 'class_declaration': case 'interface_declaration': case 'enum_declaration':
135
+ case 'record_declaration': case 'annotation_type_declaration':
136
+ if (field === 'name') return { role: 'decl', declNode: parent };
137
+ break;
138
+ case 'constructor_declaration': case 'compact_constructor_declaration':
139
+ if (field === 'name') return { role: 'ctor', classNode: ancestor(node, JAVA_CLASS_NODES) };
140
+ break;
141
+ case 'method_declaration': case 'annotation_type_element_declaration':
142
+ if (field === 'name') return skip('method-name');
143
+ break;
144
+ case 'variable_declarator':
145
+ if (field === 'name') return skip('variable', { binding: true });
146
+ break;
147
+ case 'formal_parameter': case 'catch_formal_parameter': case 'spread_parameter':
148
+ case 'enhanced_for_statement': case 'resource': case 'record_pattern_component':
149
+ if (field === 'name' || node.type === 'identifier') return skip('parameter', { binding: true });
150
+ break;
151
+ case 'lambda_expression': case 'inferred_parameters':
152
+ return skip('parameter', { binding: true });
153
+ case 'type_parameter':
154
+ return skip('type-parameter', { typeParam: true });
155
+ case 'enum_constant':
156
+ if (field === 'name') return skip('enum-constant');
157
+ break;
158
+ case 'scoped_type_identifier': {
159
+ const qualifier = parent.namedChild(0);
160
+ if (qualifier && !sameNode(qualifier, node)) return { role: 'type', qualifier };
161
+ return { role: 'type' };
162
+ }
163
+ case 'scoped_identifier': {
164
+ if (ancestor(node, new Set(['package_declaration']))) return skip('package');
165
+ const scope = parent.childForFieldName('scope');
166
+ if (field === 'name' && scope) {
167
+ return ancestor(node, new Set(['import_declaration']))
168
+ ? { role: 'import', qualifierText: textOf(scope) }
169
+ : { role: 'value', qualifier: scope };
170
+ }
171
+ return { role: 'value' };
172
+ }
173
+ case 'field_access':
174
+ if (field === 'field') return { role: 'member', receiver: parent.childForFieldName('object') };
175
+ return { role: 'value' };
176
+ case 'method_invocation':
177
+ if (field === 'name') return skip('method-call');
178
+ return { role: 'value' };
179
+ case 'marker_annotation': case 'annotation':
180
+ if (field === 'name') return { role: 'type' };
181
+ break;
182
+ case 'labeled_statement': case 'break_statement': case 'continue_statement':
183
+ return skip('label');
184
+ default:
185
+ break;
186
+ }
187
+ if (node.type === 'type_identifier') return { role: 'type' };
188
+ if (node.type === 'identifier') return { role: 'value' };
189
+ return { role: 'unknown', why: `java:${parent.type}` };
190
+ }
191
+ const JAVA_CLASS_NODES = new Set(['class_declaration', 'enum_declaration', 'record_declaration',
192
+ 'interface_declaration']);
193
+
194
+ const CSHARP_TYPE_DECLS = new Set(['class_declaration', 'struct_declaration', 'interface_declaration',
195
+ 'enum_declaration', 'record_declaration', 'record_struct_declaration', 'delegate_declaration']);
196
+ const CSHARP_TYPE_PARENTS = new Set(['base_list', 'type_argument_list', 'array_type', 'nullable_type',
197
+ 'pointer_type', 'ref_type', 'tuple_element', 'type_parameter_constraint', 'type_constraint',
198
+ 'typeof_expression', 'sizeof_expression', 'default_expression', 'as_expression',
199
+ 'is_expression', 'function_pointer_type', 'scoped_type', 'primary_constructor_base_type']);
200
+
201
+ // Expressions whose `{ Member = value }` initializer assigns members of
202
+ // the object they create (fix #395); array initializers assign variables.
203
+ const CSHARP_OBJECT_INITIALIZER_OWNERS = new Set(['object_creation_expression',
204
+ 'implicit_object_creation_expression', 'assignment_expression']);
205
+
206
+ function classifyCSharp(node) {
207
+ let current = node;
208
+ let parent = node.parent;
209
+ // `Widget<T>`: the generic name stands where the identifier would.
210
+ if (parent.type === 'generic_name' && sameNode(parent.namedChild(0), node)) {
211
+ current = parent;
212
+ parent = parent.parent;
213
+ }
214
+ const field = fieldOf(current);
215
+ if (CSHARP_TYPE_DECLS.has(parent.type) && field === 'name') return { role: 'decl', declNode: parent };
216
+ switch (parent.type) {
217
+ case 'constructor_declaration': case 'destructor_declaration':
218
+ if (field === 'name') return { role: 'ctor', classNode: ancestor(node, CSHARP_TYPE_DECLS) };
219
+ break;
220
+ case 'method_declaration': case 'local_function_statement': case 'property_declaration':
221
+ case 'event_declaration': case 'enum_member_declaration': case 'operator_declaration':
222
+ if (field === 'name') return skip('member-name');
223
+ break;
224
+ case 'variable_declarator':
225
+ if (field === 'name' || sameNode(parent.namedChild(0), current)) return skip('variable', { binding: true });
226
+ break;
227
+ case 'parameter':
228
+ if (field === 'name') return skip('parameter', { binding: true });
229
+ break;
230
+ case 'catch_declaration': case 'foreach_statement': case 'single_variable_designation':
231
+ if (field === 'name' || parent.type === 'single_variable_designation') {
232
+ return skip('variable', { binding: true });
233
+ }
234
+ break;
235
+ case 'type_parameter':
236
+ return skip('type-parameter', { typeParam: true });
237
+ case 'qualified_name': {
238
+ const qualifier = parent.childForFieldName('qualifier');
239
+ if (field === 'name' && qualifier) return { role: 'type', qualifier };
240
+ return { role: 'type' };
241
+ }
242
+ case 'alias_qualified_name':
243
+ if (field === 'name') return { role: 'type', qualifier: parent.childForFieldName('alias'), aliasQualified: true };
244
+ return skip('extern-alias');
245
+ case 'member_access_expression':
246
+ if (field === 'name') return { role: 'member', receiver: parent.childForFieldName('expression') };
247
+ return { role: 'value' };
248
+ case 'using_directive':
249
+ // `using W = X.Y;` names the alias W here.
250
+ return skip('using-alias');
251
+ case 'name_equals': case 'name_colon': case 'argument_name':
252
+ return skip('named-argument');
253
+ case 'assignment_expression':
254
+ // `new Address { Country = new Country() }`: the left side of an
255
+ // object initializer names a member of the created type, never
256
+ // a type (fix #395); a nested `Home = { Country = .. }` too.
257
+ if (field === 'left' && current.type === 'identifier' &&
258
+ parent.parent?.type === 'initializer_expression' &&
259
+ CSHARP_OBJECT_INITIALIZER_OWNERS.has(parent.parent.parent?.type)) {
260
+ return skip('initializer-member');
261
+ }
262
+ break;
263
+ case 'attribute':
264
+ if (field === 'name') return { role: 'type', attribute: true };
265
+ break;
266
+ case 'labeled_statement': case 'goto_statement':
267
+ return skip('label');
268
+ default:
269
+ break;
270
+ }
271
+ if (field === 'type' || field === 'returns' || CSHARP_TYPE_PARENTS.has(parent.type)) {
272
+ return { role: 'type' };
273
+ }
274
+ return { role: 'value' };
275
+ }
276
+
277
+ const JS_TYPE_DECLS = new Set(['class_declaration', 'class', 'abstract_class_declaration',
278
+ 'interface_declaration', 'type_alias_declaration', 'enum_declaration']);
279
+
280
+ function classifyJs(node) {
281
+ const parent = node.parent;
282
+ const field = fieldOf(node);
283
+ if (JS_TYPE_DECLS.has(parent.type) && field === 'name') return { role: 'decl', declNode: parent };
284
+ switch (parent.type) {
285
+ case 'import_specifier':
286
+ if (field === 'alias' && parent.childForFieldName('name')?.text !== node.text) return skip('import-alias');
287
+ if (parent.childForFieldName('alias')) {
288
+ return { role: 'import', importNode: parent, aliased: true };
289
+ }
290
+ return { role: 'import', importNode: parent };
291
+ case 'import_clause': case 'namespace_import':
292
+ return skip('import-local');
293
+ case 'export_specifier': {
294
+ if (field === 'alias' && parent.childForFieldName('name')?.text !== node.text) return skip('export-alias');
295
+ const statement = ancestor(node, new Set(['export_statement']));
296
+ if (statement?.childForFieldName('source')) return { role: 'import', importNode: parent, reexport: true };
297
+ return { role: 'value', exportLocal: true };
298
+ }
299
+ case 'nested_type_identifier':
300
+ if (field === 'name') return { role: 'type', qualifier: parent.childForFieldName('module') };
301
+ return { role: 'value' };
302
+ case 'member_expression':
303
+ if (field === 'property') return { role: 'member', receiver: parent.childForFieldName('object') };
304
+ return { role: 'value' };
305
+ case 'required_parameter': case 'optional_parameter': case 'rest_pattern':
306
+ case 'formal_parameters': case 'array_pattern': case 'assignment_pattern':
307
+ case 'object_assignment_pattern':
308
+ if (node.type === 'identifier') return skip('parameter', { binding: true });
309
+ break;
310
+ case 'variable_declarator':
311
+ if (field === 'name') return skip('variable', { binding: true });
312
+ break;
313
+ case 'catch_clause':
314
+ if (field === 'parameter') return skip('variable', { binding: true });
315
+ break;
316
+ case 'arrow_function':
317
+ if (field === 'parameter') return skip('parameter', { binding: true });
318
+ break;
319
+ case 'function_declaration': case 'generator_function_declaration':
320
+ if (field === 'name') return skip('function', { binding: true });
321
+ break;
322
+ case 'function_expression': case 'function': case 'generator_function':
323
+ if (field === 'name') return skip('function-expression-name');
324
+ break;
325
+ case 'method_definition': case 'method_signature': case 'abstract_method_signature':
326
+ case 'public_field_definition': case 'property_signature': case 'field_definition':
327
+ if (field === 'name') return skip('member-name');
328
+ break;
329
+ case 'pair': case 'pair_pattern':
330
+ if (field === 'key') {
331
+ // `const { Widget: W } = require('./w')` imports Widget.
332
+ const required = parent.type === 'pair_pattern' ? requireSourceOf(parent) : null;
333
+ if (required) return { role: 'import', requireSource: required, aliased: true };
334
+ return skip('property-key');
335
+ }
336
+ break;
337
+ case 'type_parameter':
338
+ if (field === 'name') return skip('type-parameter', { typeParam: true });
339
+ break;
340
+ case 'enum_assignment': case 'enum_body':
341
+ return skip('enum-member');
342
+ case 'labeled_statement': case 'break_statement': case 'continue_statement':
343
+ return skip('label');
344
+ default:
345
+ break;
346
+ }
347
+ if (node.type === 'shorthand_property_identifier') return { role: 'shorthand' };
348
+ if (node.type === 'shorthand_property_identifier_pattern') {
349
+ // `const { Widget } = require('./w')` is an import of Widget.
350
+ const required = requireSourceOf(node);
351
+ if (required) return { role: 'import', requireSource: required };
352
+ return skip('destructured', { binding: true });
353
+ }
354
+ if (node.type === 'type_identifier') return { role: 'type' };
355
+ if (node.type === 'identifier') return { role: 'value' };
356
+ if (node.type === 'property_identifier' || node.type === 'private_property_identifier') {
357
+ return skip('property-name');
358
+ }
359
+ if (node.type === 'statement_identifier') return skip('label');
360
+ return { role: 'unknown', why: `js:${parent.type}` };
361
+ }
362
+
363
+ /** The module a destructuring pattern takes from `require('m')`, or null. */
364
+ function requireSourceOf(node) {
365
+ let current = node.parent;
366
+ while (current && (current.type === 'object_pattern' || current.type === 'pair_pattern')) current = current.parent;
367
+ if (current?.type !== 'variable_declarator') return null;
368
+ let value = current.childForFieldName('value');
369
+ if (value?.type === 'await_expression') value = value.namedChild(0);
370
+ if (value?.type !== 'call_expression') return null;
371
+ const callee = value.childForFieldName('function');
372
+ if (!callee || (callee.text !== 'require' && callee.type !== 'import')) return null;
373
+ const arg = value.childForFieldName('arguments')?.namedChild(0);
374
+ if (arg?.type !== 'string') return null;
375
+ return arg.text.replace(/^['"`]|['"`]$/g, '');
376
+ }
377
+
378
+ function classifyPython(node) {
379
+ const parent = node.parent;
380
+ const field = fieldOf(node);
381
+ switch (parent.type) {
382
+ case 'class_definition':
383
+ if (field === 'name') return { role: 'decl', declNode: parent };
384
+ break;
385
+ case 'function_definition':
386
+ if (field === 'name') return skip('function', { binding: true });
387
+ break;
388
+ case 'attribute':
389
+ if (field === 'attribute') return { role: 'member', receiver: parent.childForFieldName('object') };
390
+ return { role: 'value' };
391
+ case 'keyword_argument':
392
+ if (field === 'name') return skip('keyword-argument');
393
+ break;
394
+ case 'parameters': case 'lambda_parameters': case 'list_splat_pattern':
395
+ case 'dictionary_splat_pattern':
396
+ return skip('parameter', { binding: true });
397
+ case 'typed_parameter':
398
+ if (sameNode(parent.namedChild(0), node)) return skip('parameter', { binding: true });
399
+ break;
400
+ case 'default_parameter': case 'typed_default_parameter':
401
+ if (field === 'name') return skip('parameter', { binding: true });
402
+ break;
403
+ case 'global_statement': case 'nonlocal_statement':
404
+ return skip('scope-declaration', { scopeDeclaration: parent.type });
405
+ case 'dotted_name': {
406
+ const from = ancestor(node, new Set(['import_from_statement']));
407
+ if (from) {
408
+ const moduleName = from.childForFieldName('module_name');
409
+ if (moduleName && contains(moduleName, node)) return skip('module-path');
410
+ if (parent.namedChildCount > 1) return { role: 'unknown', why: 'dotted-import-name' };
411
+ const aliased = parent.parent?.type === 'aliased_import';
412
+ return { role: 'import', moduleText: moduleName ? moduleName.text : '', aliased };
413
+ }
414
+ if (ancestor(node, new Set(['import_statement']))) return skip('module-path');
415
+ return { role: 'value' };
416
+ }
417
+ case 'aliased_import':
418
+ if (field === 'alias') {
419
+ // `from m import X as X` explicitly re-exports X. Both
420
+ // spellings follow the declaration; a distinct local alias
421
+ // remains the caller's binding.
422
+ const from = parent.parent;
423
+ if (from?.type === 'import_from_statement' &&
424
+ parent.childForFieldName('name')?.text === node.text) {
425
+ return { role: 'import', moduleText: from.childForFieldName('module_name')?.text || '' };
426
+ }
427
+ return skip('import-alias');
428
+ }
429
+ break;
430
+ case 'as_pattern_target': case 'named_expression':
431
+ if (parent.type === 'as_pattern_target' || field === 'name') return skip('variable', { binding: true });
432
+ break;
433
+ default:
434
+ break;
435
+ }
436
+ if (isPythonAssignmentTarget(node)) return skip('assignment', { binding: true });
437
+ if (node.type === 'identifier') return { role: 'value' };
438
+ return { role: 'unknown', why: `python:${parent.type}` };
439
+ }
440
+
441
+ function isPythonAssignmentTarget(node) {
442
+ let current = node;
443
+ for (let parent = node.parent; parent; current = parent, parent = parent.parent) {
444
+ if (parent.type === 'pattern_list' || parent.type === 'tuple_pattern' ||
445
+ parent.type === 'list_pattern' || parent.type === 'list_splat_pattern') continue;
446
+ if ((parent.type === 'assignment' || parent.type === 'augmented_assignment' ||
447
+ parent.type === 'for_statement' || parent.type === 'for_in_clause') &&
448
+ isField(current, 'left')) return true;
449
+ return false;
450
+ }
451
+ return false;
452
+ }
453
+
454
+ function classifyGo(node) {
455
+ const parent = node.parent;
456
+ const field = fieldOf(node);
457
+ switch (parent.type) {
458
+ case 'type_spec': case 'type_alias':
459
+ if (field === 'name') return { role: 'decl', declNode: parent };
460
+ break;
461
+ case 'qualified_type':
462
+ if (field === 'name') return { role: 'type', qualifier: parent.childForFieldName('package') };
463
+ return skip('package');
464
+ case 'field_declaration':
465
+ if (node.type === 'field_identifier') return skip('field-name');
466
+ if (field === 'type' && !parent.childForFieldName('name')) return { role: 'type', embedded: true };
467
+ break;
468
+ case 'selector_expression':
469
+ if (field === 'field') return { role: 'member', receiver: parent.childForFieldName('operand') };
470
+ return { role: 'value' };
471
+ case 'method_declaration': case 'method_elem': case 'method_spec':
472
+ if (field === 'name') return skip('method-name');
473
+ break;
474
+ case 'function_declaration':
475
+ if (field === 'name') return skip('function', { binding: true });
476
+ break;
477
+ case 'parameter_declaration': case 'variadic_parameter_declaration':
478
+ if (field === 'name') return skip('parameter', { binding: true });
479
+ break;
480
+ case 'var_spec': case 'const_spec':
481
+ if (field === 'name') return skip('variable', { binding: true });
482
+ break;
483
+ case 'type_parameter_declaration':
484
+ if (field === 'name') return skip('type-parameter', { typeParam: true });
485
+ break;
486
+ case 'literal_element': {
487
+ const keyed = parent.parent;
488
+ if (keyed?.type === 'keyed_element' && sameNode(keyed.namedChild(0), parent)) {
489
+ return { role: 'gofield', keyed: true };
490
+ }
491
+ break;
492
+ }
493
+ case 'labeled_statement': case 'break_statement': case 'continue_statement': case 'goto_statement':
494
+ return skip('label');
495
+ default:
496
+ break;
497
+ }
498
+ if (node.type === 'identifier' && parent.type === 'expression_list') {
499
+ const statement = parent.parent;
500
+ if ((statement?.type === 'short_var_declaration' || statement?.type === 'range_clause') &&
501
+ isField(parent, 'left')) return skip('variable', { binding: true });
502
+ }
503
+ if (node.type === 'type_identifier') return { role: 'type' };
504
+ if (node.type === 'identifier') return { role: 'value' };
505
+ if (node.type === 'field_identifier') return skip('field-name');
506
+ if (node.type === 'package_identifier') return skip('package');
507
+ if (node.type === 'label_name') return skip('label');
508
+ return { role: 'unknown', why: `go:${parent.type}` };
509
+ }
510
+
511
+ const RUST_TYPE_DECLS = new Set(['struct_item', 'enum_item', 'union_item', 'trait_item', 'type_item']);
512
+
513
+ function classifyRust(node) {
514
+ const parent = node.parent;
515
+ const field = fieldOf(node);
516
+ if (RUST_TYPE_DECLS.has(parent.type) && field === 'name') return { role: 'decl', declNode: parent };
517
+ if (ancestor(node, new Set(['macro_definition']))) return { role: 'template' };
518
+ if (parent.type === 'token_tree' || ancestor(node, new Set(['token_tree']), new Set(['macro_invocation', 'source_file']))) {
519
+ if (ancestor(node, new Set(['attribute_item', 'inner_attribute_item']))) return { role: 'unknown', why: 'attribute-token' };
520
+ return { role: 'macro' };
521
+ }
522
+ const inUse = !!ancestor(node, new Set(['use_declaration']));
523
+ switch (parent.type) {
524
+ case 'scoped_type_identifier':
525
+ if (field === 'name') return { role: 'type', qualifier: parent.childForFieldName('path') };
526
+ break;
527
+ case 'scoped_identifier': {
528
+ if (field === 'name') {
529
+ const qualifier = parent.childForFieldName('path');
530
+ return inUse ? { role: 'import', qualifier } : { role: 'value', qualifier };
531
+ }
532
+ return inUse ? { role: 'import', useHead: true } : { role: 'value' };
533
+ }
534
+ case 'use_as_clause': {
535
+ if (field === 'alias') {
536
+ const path = parent.childForFieldName('path');
537
+ const original = path?.type === 'scoped_identifier' ? path.childForFieldName('name') : path;
538
+ return original?.text === node.text ? { role: 'import', original } : skip('import-alias');
539
+ }
540
+ return { role: 'import' };
541
+ }
542
+ case 'use_list': {
543
+ const scoped = parent.parent?.type === 'scoped_use_list' ? parent.parent : null;
544
+ return { role: 'import', qualifier: scoped?.childForFieldName('path') || null, inList: true };
545
+ }
546
+ case 'use_declaration':
547
+ return { role: 'import' };
548
+ case 'function_item': case 'function_signature_item':
549
+ if (field === 'name') return skip('function', { binding: true });
550
+ break;
551
+ case 'field_expression':
552
+ if (field === 'field') return skip('member');
553
+ break;
554
+ case 'field_declaration': case 'field_initializer': case 'shorthand_field_initializer':
555
+ case 'field_pattern':
556
+ if (node.type === 'field_identifier' || node.type === 'shorthand_field_identifier') return skip('field-name');
557
+ break;
558
+ case 'enum_variant':
559
+ if (field === 'name') return skip('variant');
560
+ break;
561
+ case 'type_parameters': case 'constrained_type_parameter': case 'type_parameter':
562
+ if (node.type === 'type_identifier' && (parent.type !== 'constrained_type_parameter' || field === 'left')) {
563
+ return skip('type-parameter', { typeParam: true });
564
+ }
565
+ break;
566
+ case 'macro_invocation':
567
+ if (field === 'macro') return skip('macro-name');
568
+ break;
569
+ case 'const_item': case 'static_item':
570
+ if (field === 'name') return skip('constant', { binding: true });
571
+ break;
572
+ case 'mod_item':
573
+ return skip('module-name');
574
+ case 'label': case 'loop_label': case 'lifetime':
575
+ return skip('label');
576
+ default:
577
+ break;
578
+ }
579
+ if (node.type === 'identifier' && isRustPatternBinding(node)) return skip('pattern', { binding: true, patternBinding: true });
580
+ if (node.type === 'type_identifier') return { role: 'type' };
581
+ if (node.type === 'identifier') return { role: 'value' };
582
+ if (node.type === 'field_identifier' || node.type === 'shorthand_field_identifier') return skip('field-name');
583
+ return { role: 'unknown', why: `rust:${parent.type}` };
584
+ }
585
+
586
+ const RUST_PATTERN_OWNERS = new Set(['let_declaration', 'parameter', 'closure_parameters',
587
+ 'for_expression', 'match_arm', 'let_condition', 'if_let_expression', 'while_let_expression']);
588
+ const RUST_PATTERN_NODES = new Set(['tuple_pattern', 'tuple_struct_pattern', 'struct_pattern',
589
+ 'field_pattern', 'ref_pattern', 'mut_pattern', 'captured_pattern', 'slice_pattern', 'or_pattern',
590
+ 'reference_pattern', 'match_pattern', 'remaining_field_pattern']);
591
+
592
+ function isRustPatternBinding(node) {
593
+ let current = node;
594
+ for (let parent = node.parent; parent; current = parent, parent = parent.parent) {
595
+ if (RUST_PATTERN_NODES.has(parent.type)) {
596
+ // The path of a tuple-struct pattern names a type, not a binding.
597
+ if ((parent.type === 'tuple_struct_pattern' || parent.type === 'struct_pattern') &&
598
+ isField(current, 'type')) return false;
599
+ continue;
600
+ }
601
+ if (parent.type === 'closure_parameters') return true;
602
+ if (RUST_PATTERN_OWNERS.has(parent.type)) return isField(current, 'pattern') || parent.type === 'match_arm';
603
+ return false;
604
+ }
605
+ return false;
606
+ }
607
+
608
+ const C_TYPE_SPECIFIERS = new Set(['class_specifier', 'struct_specifier', 'union_specifier', 'enum_specifier']);
609
+
610
+ function classifyC(node, language) {
611
+ const parent = node.parent;
612
+ const field = fieldOf(node);
613
+ if (C_TYPE_SPECIFIERS.has(parent.type) && field === 'name') {
614
+ return parent.childForFieldName('body')
615
+ ? { role: 'decl', declNode: parent, tag: parent.type }
616
+ : { role: 'type', tag: parent.type, elaborated: true };
617
+ }
618
+ switch (parent.type) {
619
+ case 'qualified_identifier': {
620
+ const scope = parent.childForFieldName('scope');
621
+ if (field === 'name') {
622
+ const declarator = parent.parent?.type === 'function_declarator' &&
623
+ isField(parent, 'declarator');
624
+ if (declarator && scope && textOf(scope).replace(/<.*$/, '').split('::').pop() === node.text) {
625
+ return { role: 'ctor-qualified', qualifier: scope, qualifiedNode: parent };
626
+ }
627
+ return { role: 'type', cppQualified: true };
628
+ }
629
+ // A qualifier segment names a namespace or a class.
630
+ return { role: 'type', qualifierSegment: true, qualifiedNode: parent };
631
+ }
632
+ case 'destructor_name': {
633
+ const qualified = parent.parent?.type === 'qualified_identifier' ? parent.parent : null;
634
+ if (qualified) return { role: 'ctor-qualified', qualifier: qualified.childForFieldName('scope'), qualifiedNode: qualified };
635
+ // `obj.~Widget()` / `p->~Widget()`: an explicit destructor call
636
+ // names a type looked up at the call site, not the enclosing
637
+ // class.
638
+ if (parent.parent?.type === 'field_expression') return { role: 'type' };
639
+ return { role: 'ctor', classNode: ancestor(node, C_TYPE_SPECIFIERS) };
640
+ }
641
+ case 'function_declarator': {
642
+ if (field !== 'declarator') break;
643
+ const classNode = ancestor(node, C_TYPE_SPECIFIERS, new Set(['compound_statement']));
644
+ const body = classNode?.childForFieldName('body');
645
+ // `struct List<K>::Node { Node(..) }` names its class by the
646
+ // qualified name's last segment (fix #396).
647
+ const className = classNode?.childForFieldName('name');
648
+ const classBase = className?.type === 'qualified_identifier'
649
+ ? textOf(className).split('::').pop().replace(/<.*$/s, '') : className?.text;
650
+ if (classNode && body && contains(body, node) && classBase === node.text) {
651
+ return { role: 'ctor', classNode };
652
+ }
653
+ return skip('function', { binding: true });
654
+ }
655
+ case 'field_initializer':
656
+ if (sameNode(parent.namedChild(0), node)) return { role: 'meminit' };
657
+ break;
658
+ case 'template_type':
659
+ if (field === 'name') return { role: 'type', cppQualified: true };
660
+ break;
661
+ case 'template_function': case 'template_method':
662
+ if (field === 'name') return { role: 'value', cppQualified: true };
663
+ break;
664
+ case 'type_definition':
665
+ if (field === 'declarator') return { role: 'decl', declNode: parent, typedef: true };
666
+ break;
667
+ case 'alias_declaration':
668
+ if (field === 'name') return { role: 'decl', declNode: parent, typedef: true };
669
+ break;
670
+ case 'namespace_definition':
671
+ return skip('namespace-name');
672
+ case 'field_expression':
673
+ if (field === 'field') return skip('member');
674
+ break;
675
+ case 'field_declaration':
676
+ if (node.type === 'field_identifier') return skip('field-name');
677
+ break;
678
+ case 'enumerator':
679
+ return skip('enumerator');
680
+ case 'type_parameter_declaration': case 'optional_type_parameter_declaration':
681
+ case 'variadic_type_parameter_declaration':
682
+ // `template <typename T = Widget>`: the default is a reference.
683
+ if (field === 'default_type') return { role: 'type' };
684
+ return skip('type-parameter', { typeParam: true });
685
+ case 'preproc_def': case 'preproc_function_def': case 'preproc_params':
686
+ return skip('macro-name');
687
+ case 'init_declarator': case 'parameter_declaration': case 'optional_parameter_declaration':
688
+ case 'pointer_declarator': case 'reference_declarator': case 'array_declarator':
689
+ case 'declaration':
690
+ if (node.type === 'identifier' && (field === 'declarator' || parent.type === 'pointer_declarator' ||
691
+ parent.type === 'reference_declarator' || parent.type === 'array_declarator')) {
692
+ return skip('variable', { binding: true });
693
+ }
694
+ break;
695
+ case 'labeled_statement': case 'goto_statement':
696
+ return skip('label');
697
+ default:
698
+ break;
699
+ }
700
+ if (node.type === 'type_identifier') return { role: 'type' };
701
+ if (node.type === 'namespace_identifier') return { role: 'type', qualifierSegment: true };
702
+ if (node.type === 'identifier') return { role: 'value' };
703
+ if (node.type === 'field_identifier') return skip('member');
704
+ if (node.type === 'statement_identifier') return skip('label');
705
+ return { role: 'unknown', why: `${language}:${parent.type}` };
706
+ }
707
+
708
+ function classify(node, language) {
709
+ if (node.parent?.type === 'ERROR' || node.parent?.type === 'MISSING') {
710
+ return { role: 'unknown', why: 'parse-error' };
711
+ }
712
+ switch (familyOf(language)) {
713
+ case 'java': return classifyJava(node);
714
+ case 'csharp': return classifyCSharp(node);
715
+ case 'js': return classifyJs(node);
716
+ case 'python': return classifyPython(node);
717
+ case 'go': return classifyGo(node);
718
+ case 'rust': return classifyRust(node);
719
+ case 'c': return classifyC(node, language);
720
+ default: return { role: 'unknown', why: `language:${language}` };
721
+ }
722
+ }
723
+
724
+ // ── Scopes (bindings and type parameters that shadow the name) ───────────
725
+
726
+ const FUNCTION_SCOPES = {
727
+ js: new Set(['function_declaration', 'function_expression', 'function', 'arrow_function',
728
+ 'method_definition', 'generator_function', 'generator_function_declaration', 'program']),
729
+ python: new Set(['function_definition', 'lambda', 'module', 'list_comprehension',
730
+ 'set_comprehension', 'dictionary_comprehension', 'generator_expression']),
731
+ go: new Set(['function_declaration', 'method_declaration', 'func_literal', 'source_file']),
732
+ rust: new Set(['function_item', 'closure_expression', 'source_file']),
733
+ java: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression', 'program']),
734
+ csharp: new Set(['method_declaration', 'constructor_declaration', 'local_function_statement',
735
+ 'lambda_expression', 'anonymous_method_expression', 'compilation_unit']),
736
+ c: new Set(['function_definition', 'lambda_expression', 'translation_unit']),
737
+ };
738
+ const BLOCK_SCOPES = {
739
+ js: new Set(['statement_block', 'for_statement', 'for_in_statement', 'catch_clause', 'class_body']),
740
+ python: new Set(['class_definition']),
741
+ go: new Set(['block', 'for_statement', 'if_statement', 'switch_statement', 'type_switch_statement',
742
+ 'select_statement', 'communication_case', 'expression_case', 'type_case', 'default_case']),
743
+ rust: new Set(['block', 'match_arm', 'for_expression', 'if_expression', 'while_expression']),
744
+ java: new Set(['block', 'for_statement', 'enhanced_for_statement', 'catch_clause', 'class_body',
745
+ 'switch_block_statement_group', 'try_with_resources_statement']),
746
+ csharp: new Set(['block', 'for_statement', 'foreach_statement', 'catch_clause', 'declaration_list',
747
+ 'switch_section', 'using_statement']),
748
+ c: new Set(['compound_statement', 'for_statement', 'for_range_loop', 'field_declaration_list',
749
+ 'catch_clause', 'if_statement', 'while_statement']),
750
+ };
751
+ const TYPE_PARAM_OWNERS = new Set(['class_declaration', 'interface_declaration', 'method_declaration',
752
+ 'constructor_declaration', 'record_declaration', 'function_declaration', 'type_alias_declaration',
753
+ 'function_item', 'struct_item', 'enum_item', 'trait_item', 'impl_item', 'type_item', 'union_item',
754
+ 'type_spec', 'template_declaration', 'struct_declaration', 'delegate_declaration',
755
+ 'local_function_statement', 'method_definition', 'method_signature', 'function_signature',
756
+ 'class', 'abstract_class_declaration', 'arrow_function', 'function_expression',
757
+ 'generic_function', 'call_signature', 'construct_signature', 'function_type']);
758
+
759
+ function bindingScope(node, family, token) {
760
+ const functions = FUNCTION_SCOPES[family] || new Set();
761
+ const blocks = BLOCK_SCOPES[family] || new Set();
762
+ // JS `var` and function declarations are function-scoped; everything
763
+ // else (let/const/class, catch, parameters via their function) is
764
+ // block-scoped. Python binds per function (class bodies bind for the
765
+ // class body only).
766
+ const functionScoped = family === 'python' || (family === 'js' &&
767
+ (ancestor(node, new Set(['variable_declaration']), new Set(['statement_block', 'program'])) ||
768
+ token.why === 'function'));
769
+ for (let current = node.parent; current; current = current.parent) {
770
+ if (functions.has(current.type)) return current;
771
+ if (!functionScoped && blocks.has(current.type)) return current;
772
+ if (family === 'python' && current.type === 'class_definition') return current;
773
+ }
774
+ return null;
775
+ }
776
+
777
+ function typeParamScope(node) {
778
+ for (let current = node.parent; current; current = current.parent) {
779
+ if (TYPE_PARAM_OWNERS.has(current.type)) return current;
780
+ }
781
+ return null;
782
+ }
783
+
784
+ // ── Identity of the pinned type ──────────────────────────────────────────
785
+
786
+ function pinGroupOf(index, def, name) {
787
+ const language = index.files.get(def.file)?.language;
788
+ const family = familyOf(language);
789
+ const all = (index.symbols.get(name) || []).filter(d => TYPE_DECL_KINDS.has(d.type) &&
790
+ familyOf(index.files.get(d.file)?.language) === family);
791
+ const pinKey = classKeyOf(index, def);
792
+ const group = new Set([def]);
793
+ let crossConfig = null;
794
+ for (const other of all) {
795
+ if (other === def) continue;
796
+ if (classKeyOf(index, other) === pinKey) {
797
+ group.add(other);
798
+ continue;
799
+ }
800
+ if (family === 'rust' && other.file === def.file && other.type === def.type &&
801
+ other.lexicalScopeStartLine == null && def.lexicalScopeStartLine == null &&
802
+ rustModulePath(index, other) === rustModulePath(index, def)) {
803
+ group.add(other); // `#[cfg]` alternatives of one module path
804
+ continue;
805
+ }
806
+ if (family === 'go' && path.dirname(other.file) === path.dirname(def.file) &&
807
+ index.files.get(other.file)?.packageName === index.files.get(def.file)?.packageName &&
808
+ !other.lexicalScopeStartLine && !def.lexicalScopeStartLine && other.type === def.type) {
809
+ group.add(other); // build-constrained variants of one package type
810
+ continue;
811
+ }
812
+ if (family === 'c' && other.file !== def.file &&
813
+ (other.type === def.type || (CLASS_KEYWORD_KINDS.has(other.type) && CLASS_KEYWORD_KINDS.has(def.type))) &&
814
+ effectiveNs(index, other) === effectiveNs(index, def) &&
815
+ (other.enclosingType || '') === (def.enclosingType || '') && !other.lexicalScopeStartLine) {
816
+ // One namespace-scope class name is one entity in a program: its
817
+ // forward declarations and its definition in headers (and the
818
+ // headers a translation unit includes) are the same class; a
819
+ // same-name class local to another source file is not.
820
+ if (isHeader(def.file) && isHeader(other.file)) {
821
+ group.add(other);
822
+ continue;
823
+ }
824
+ const { includeClosure } = require('./cpp-scope');
825
+ if (includeClosure(index, def.file).has(other.file) || includeClosure(index, other.file).has(def.file)) {
826
+ group.add(other);
827
+ continue;
828
+ }
829
+ if (!crossConfig) crossConfig = require('./callers')._configurationAlternativeFiles;
830
+ if (crossConfig(index, def.file, other.file)) group.add(other);
831
+ }
832
+ }
833
+ return { group, all, family, language };
834
+ }
835
+
836
+ const CLASS_KEYWORD_KINDS = new Set(['class', 'struct']);
837
+ const SOURCE_EXTENSIONS = new Set(['.c', '.cc', '.cpp', '.cxx', '.c++', '.m', '.mm', '.cu']);
838
+
839
+ function isHeader(file) {
840
+ return !SOURCE_EXTENSIONS.has(path.extname(file).toLowerCase());
841
+ }
842
+
843
+ function effectiveNs(index, d) {
844
+ const { effectiveNamespace } = require('./cpp-scope');
845
+ return effectiveNamespace(index, d) || '';
846
+ }
847
+
848
+ function rustModulePath(index, d) {
849
+ const { rustInlineModules, rustInlineChainAt } = require('./imports');
850
+ return rustInlineChainAt(rustInlineModules(index.files.get(d.file) || {}), d.startLine).join('::');
851
+ }
852
+
853
+ // ── Resolution ───────────────────────────────────────────────────────────
854
+
855
+ class TypeReferenceResolver {
856
+ constructor(index, def, name) {
857
+ this.index = index;
858
+ this.def = def;
859
+ this.name = name;
860
+ const { group, all, family, language } = pinGroupOf(index, def, name);
861
+ this.group = group;
862
+ this.all = all;
863
+ this.family = family;
864
+ this.language = language;
865
+ this.pinFiles = new Set([...group].map(d => d.file));
866
+ this.memo = new Map();
867
+ this._embedders = undefined;
868
+ }
869
+
870
+ inGroup(d) { return this.group.has(d); }
871
+
872
+ /**
873
+ * Generic arity written at the token being decided, where arity is part
874
+ * of a type's identity (trait genericArityIsIdentity, fix #389): `X` is
875
+ * 0, `X<int>` 1, the open generic `X<>` 1 and `X<,>` 2. null when the
876
+ * site is not an identifier token (doc comments).
877
+ */
878
+ siteArity() {
879
+ const node = this.siteNode;
880
+ if (!node || node.type !== 'identifier' || !langTraits(this.language)?.genericArityIsIdentity) return null;
881
+ const parent = node.parent;
882
+ // A constructor or finalizer is named after its class without type
883
+ // arguments (fix #390: `Strategy(..)` inside `class Strategy<T>`):
884
+ // its arity is the enclosing declaration's.
885
+ if ((parent?.type === 'constructor_declaration' || parent?.type === 'destructor_declaration') &&
886
+ parent.childForFieldName('name') && sameNode(parent.childForFieldName('name'), node)) {
887
+ let owner = parent.parent;
888
+ while (owner && owner.type === 'declaration_list') owner = owner.parent;
889
+ const list = owner?.namedChildren?.find(child => child.type === 'type_parameter_list');
890
+ return list ? list.namedChildren.filter(child => child.type === 'type_parameter').length : 0;
891
+ }
892
+ if (parent?.type !== 'generic_name' || !sameNode(parent.namedChild(0), node)) return 0;
893
+ const list = parent.namedChildren.find(child => child.type === 'type_argument_list');
894
+ if (!list) return 0;
895
+ const args = list.namedChildren.filter(child => !child.type.includes('comment'));
896
+ if (args.length > 0) return args.length;
897
+ return list.children.filter(child => child.type === ',').length + 1;
898
+ }
899
+
900
+ /** Verdict over a set of declarations the lookup found. */
901
+ judge(defs, reason) {
902
+ const arity = this.siteArity();
903
+ if (arity != null) defs = defs.filter(d => (d.typeArity || 0) === arity);
904
+ if (defs.length === 0) return { verdict: 'no', reason: reason || 'other-type' };
905
+ const inside = defs.filter(d => this.inGroup(d));
906
+ if (inside.length === defs.length) return { verdict: 'yes' };
907
+ if (inside.length === 0) return { verdict: 'no', reason: reason || 'other-type' };
908
+ return { verdict: 'unknown', reason: 'ambiguous-type' };
909
+ }
910
+
911
+ /** Type-like definitions of NAME declared in `file`. */
912
+ fileTypes(file, name = this.name) {
913
+ return (this.index.symbols.get(name) || []).filter(d => d.file === file &&
914
+ TYPE_DECL_KINDS.has(d.type));
915
+ }
916
+
917
+ /** Container range of a definition inside its own file (innermost
918
+ * enclosing symbol), or null at file scope. */
919
+ containerOf(d) {
920
+ if (!this._containers) this._containers = new Map();
921
+ if (this._containers.has(d)) return this._containers.get(d);
922
+ const best = this._containerOf(d);
923
+ this._containers.set(d, best);
924
+ return best;
925
+ }
926
+
927
+ _containerOf(d) {
928
+ const entry = this.index.files.get(d.file);
929
+ let best = null;
930
+ for (const s of entry?.symbols || []) {
931
+ if (s === d || s.startLine == null || s.endLine == null) continue;
932
+ if (s.startLine > d.startLine || s.endLine < d.endLine) continue;
933
+ if (s.startLine === d.startLine && s.endLine === d.endLine) continue;
934
+ if (s.type === 'impl' || s.type === 'field') continue;
935
+ if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine)) best = s;
936
+ }
937
+ return best;
938
+ }
939
+
940
+ /** Same-file declarations of `name` visible at `line` (innermost). */
941
+ visibleInFile(file, line, name = this.name) {
942
+ const defs = this.fileTypes(file, name);
943
+ if (defs.length === 0) return [];
944
+ const entry = this.index.files.get(file);
945
+ const scored = [];
946
+ for (const d of defs) {
947
+ if (d.lexicalScopeStartLine != null && d.lexicalScopeEndLine != null) {
948
+ if (line < d.lexicalScopeStartLine || line > d.lexicalScopeEndLine) continue;
949
+ scored.push({ d, span: d.lexicalScopeEndLine - d.lexicalScopeStartLine });
950
+ continue;
951
+ }
952
+ const container = this.containerOf(d);
953
+ if (!container) {
954
+ scored.push({ d, span: Infinity });
955
+ continue;
956
+ }
957
+ if (line < container.startLine || line > container.endLine) continue;
958
+ if (this.family === 'python' && TYPE_DECL_KINDS.has(container.type)) {
959
+ // A class body's names are not visible inside its methods.
960
+ const inMethod = (entry?.symbols || []).some(s => (s.type === 'method' ||
961
+ s.type === 'function' || s.type === 'constructor') &&
962
+ s.startLine >= container.startLine && s.endLine <= container.endLine &&
963
+ s.startLine <= line && s.endLine >= line);
964
+ if (inMethod) continue;
965
+ }
966
+ scored.push({ d, span: container.endLine - container.startLine });
967
+ }
968
+ if (scored.length === 0) return [];
969
+ const best = Math.min(...scored.map(s => s.span));
970
+ return scored.filter(s => s.span === best).map(s => s.d);
971
+ }
972
+
973
+ // ── Unqualified name lookup (after local shadowing) ──
974
+
975
+ resolveName(file, line, name = this.name) {
976
+ const site = (this.family === 'c' || this.family === 'csharp') && this.siteNode ? `\0${this.siteNode.startIndex}` : '';
977
+ const key = `u\0${file}\0${line}\0${name}${site}`;
978
+ if (this.memo.has(key)) return this.memo.get(key);
979
+ let result;
980
+ try {
981
+ result = this._resolveName(file, line, name);
982
+ } catch {
983
+ result = { verdict: 'unknown', reason: 'resolution-error' };
984
+ }
985
+ this.memo.set(key, result);
986
+ return result;
987
+ }
988
+
989
+ _resolveName(file, line, name) {
990
+ const entry = this.index.files.get(file);
991
+ if (!entry) return { verdict: 'unknown', reason: 'unindexed-file' };
992
+ if (this.family === 'c') return this.resolveCFamily(file, line, name, null);
993
+ if (this.family === 'rust') return this.resolveRustName(file, line, name);
994
+ const local = this.visibleInFile(file, line, name);
995
+ if (local.length > 0) {
996
+ // A module-scope assignment of the name rebinds it, unless that
997
+ // assignment is the type's own declaration (a type alias).
998
+ const aliasDeclared = local.some(d => d.type === 'type' && this.inGroup(d));
999
+ if (this.family === 'python' && !aliasDeclared && (entry.moduleAssignedNames || []).includes(name)) {
1000
+ return { verdict: 'unknown', reason: 'module-rebinding' };
1001
+ }
1002
+ return this.judge(local);
1003
+ }
1004
+ switch (this.family) {
1005
+ case 'js': return this.resolveJsName(file, line, name);
1006
+ case 'python': return this.resolvePythonName(file, line, name);
1007
+ case 'go': return this.resolveGoName(file, line, name);
1008
+ case 'java': return this.resolveJavaName(file, line, name);
1009
+ case 'csharp': return this.resolveCSharpName(file, line, name);
1010
+ default: return { verdict: 'unknown', reason: 'unsupported-language' };
1011
+ }
1012
+ }
1013
+
1014
+ chase(startFile, name) {
1015
+ const { _nameBindingReaches } = require('./callers');
1016
+ return _nameBindingReaches(this.index, startFile, name, this.pinFiles, 8, { exactName: true });
1017
+ }
1018
+
1019
+ moduleFile(entry, module) {
1020
+ const rel = entry.moduleResolved?.[module];
1021
+ if (!rel) return null;
1022
+ return path.isAbsolute(rel) ? rel : path.join(this.index.root, rel);
1023
+ }
1024
+
1025
+ combine(verdicts) {
1026
+ if (verdicts.length === 0) return { verdict: 'no', reason: 'not-in-scope' };
1027
+ if (verdicts.every(v => v.verdict === 'yes')) return { verdict: 'yes' };
1028
+ if (verdicts.every(v => v.verdict === 'no')) return verdicts[0];
1029
+ const unknown = verdicts.find(v => v.verdict === 'unknown');
1030
+ return { verdict: 'unknown', reason: unknown?.reason || 'ambiguous-binding' };
1031
+ }
1032
+
1033
+ /** An import binding's verdict: its module binds the pin by name. */
1034
+ importBindingVerdict(file, entry, binding, name) {
1035
+ const original = binding.name;
1036
+ if (binding.alias && binding.alias !== original) return { verdict: 'no', reason: 'import-alias' };
1037
+ if (original !== name) return { verdict: 'no', reason: 'import-alias' };
1038
+ if (['default', 'namespace'].includes(binding.kind)) return { verdict: 'no', reason: 'import-alias' };
1039
+ const moduleFile = this.moduleFile(entry, binding.module);
1040
+ if (!moduleFile) {
1041
+ const { bindingIsExternal } = require('./type-denotation');
1042
+ const external = bindingIsExternal(this.index, entry, file, binding);
1043
+ return external === true ? { verdict: 'no', reason: 'external-import' }
1044
+ : { verdict: 'unknown', reason: 'unresolved-import' };
1045
+ }
1046
+ const reach = this.chase(moduleFile, name);
1047
+ return reach === 'yes' ? { verdict: 'yes' }
1048
+ : reach === 'no' ? { verdict: 'no', reason: 'other-module' }
1049
+ : { verdict: 'unknown', reason: 'import-chain' };
1050
+ }
1051
+
1052
+ resolveJsName(file, line, name) {
1053
+ const entry = this.index.files.get(file);
1054
+ const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === name &&
1055
+ b.module != null);
1056
+ if (bindings.length > 0) {
1057
+ return this.combine(bindings.map(b => this.importBindingVerdict(file, entry, b, name)));
1058
+ }
1059
+ // No binding: a global. Declarations in files without imports or
1060
+ // exports are scripts whose top level is shared.
1061
+ const isScript = fe => (fe.importBindings || []).length === 0 && (fe.exportDetails || []).length === 0;
1062
+ if ([...this.group].some(d => isScript(this.index.files.get(d.file) || {}))) {
1063
+ return { verdict: 'unknown', reason: 'global-script' };
1064
+ }
1065
+ return { verdict: 'no', reason: 'not-imported' };
1066
+ }
1067
+
1068
+ /** Does an import or class statement of this Python module bind the
1069
+ * renamed type under its name (fix #393)? */
1070
+ pythonModuleBindsPin(file, line, name = this.name) {
1071
+ const key = `pm\0${file}\0${name}`;
1072
+ if (this.memo.has(key)) return this.memo.get(key);
1073
+ const entry = this.index.files.get(file);
1074
+ let result = this.fileTypes(file, name).some(d => this.inGroup(d));
1075
+ if (!result && entry) {
1076
+ const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === name &&
1077
+ b.module != null && b.name !== '*' && b.kind !== 'import');
1078
+ result = bindings.some(b => this.importBindingVerdict(file, entry, b, name).verdict !== 'no');
1079
+ }
1080
+ this.memo.set(key, result);
1081
+ return result;
1082
+ }
1083
+
1084
+ resolvePythonName(file, line, name) {
1085
+ const entry = this.index.files.get(file);
1086
+ const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === name &&
1087
+ b.module != null && b.name !== '*');
1088
+ if ((entry.moduleAssignedNames || []).includes(name)) {
1089
+ return { verdict: 'unknown', reason: 'module-rebinding' };
1090
+ }
1091
+ if (bindings.length > 0) {
1092
+ return this.combine(bindings.map(b => {
1093
+ if (b.kind === 'import') return { verdict: 'no', reason: 'module-binding' };
1094
+ return this.importBindingVerdict(file, entry, b, name);
1095
+ }));
1096
+ }
1097
+ // Star imports (or anything else the name chase cannot model).
1098
+ const reach = this.chase(file, name);
1099
+ if (reach === 'yes') return { verdict: 'yes' };
1100
+ if (reach === 'unknown') return { verdict: 'unknown', reason: 'star-import' };
1101
+ return { verdict: 'no', reason: 'not-imported' };
1102
+ }
1103
+
1104
+ goPackageKey(file) {
1105
+ const entry = this.index.files.get(file);
1106
+ return `${path.dirname(file)}\0${entry?.packageName || ''}`;
1107
+ }
1108
+
1109
+ resolveGoName(file, line, name) {
1110
+ const key = this.goPackageKey(file);
1111
+ const packageTypes = this.all.filter(d => this.goPackageKey(d.file) === key &&
1112
+ d.lexicalScopeStartLine == null && d.name === name);
1113
+ if (packageTypes.length > 0) return this.judge(packageTypes);
1114
+ return this.resolveGoDotImports(file, name);
1115
+ }
1116
+
1117
+ /**
1118
+ * A name the file's package does not declare, looked up in the file
1119
+ * block: `import . "p"` declares there every EXPORTED package-level
1120
+ * identifier of p (Go spec, Import declarations). Each dot-imported
1121
+ * project package is searched by its directory (its `_test` files are
1122
+ * never part of the imported package); an out-of-project package cannot
1123
+ * declare a project type; an import path the index could not resolve
1124
+ * that may still be a project package leaves the token undecided.
1125
+ */
1126
+ resolveGoDotImports(file, name) {
1127
+ const entry = this.index.files.get(file);
1128
+ const dots = (entry?.importDetails || []).filter(detail => detail.type === 'dot-import' && detail.module);
1129
+ if (dots.length === 0 || !/^\p{Lu}/u.test(name)) return { verdict: 'no', reason: 'other-package' };
1130
+ const { bindingIsExternal } = require('./type-denotation');
1131
+ const verdicts = [];
1132
+ for (const detail of dots) {
1133
+ const moduleFile = this.moduleFile(entry, detail.module);
1134
+ if (!moduleFile) {
1135
+ if (bindingIsExternal(this.index, entry, file, { module: detail.module }) !== true) {
1136
+ verdicts.push({ verdict: 'unknown', reason: 'dot-import' });
1137
+ }
1138
+ continue;
1139
+ }
1140
+ const dir = path.dirname(moduleFile);
1141
+ const types = this.all.filter(d => path.dirname(d.file) === dir && d.lexicalScopeStartLine == null &&
1142
+ !d.file.endsWith('_test.go') &&
1143
+ !String(this.index.files.get(d.file)?.packageName || '').endsWith('_test'));
1144
+ if (types.length > 0) verdicts.push(this.judge(types, 'other-package'));
1145
+ }
1146
+ if (verdicts.length === 0) return { verdict: 'no', reason: 'other-package' };
1147
+ return this.combine(verdicts);
1148
+ }
1149
+
1150
+ javaFqn(d) {
1151
+ const outer = d.enclosingType ? `${d.enclosingType}.` : '';
1152
+ return `${d.namespace ? d.namespace + '.' : ''}${outer}${d.name}`;
1153
+ }
1154
+
1155
+ resolveJavaName(file, line, name) {
1156
+ const entry = this.index.files.get(file);
1157
+ const candidates = this.all.filter(d => d.name === name);
1158
+ const single = (entry.importBindings || []).filter(b => b.name === name && b.module &&
1159
+ b.kind !== 'static');
1160
+ if (single.length > 0) {
1161
+ const hits = candidates.filter(d => single.some(b => b.module === this.javaFqn(d)));
1162
+ if (hits.length > 0) return this.judge(hits);
1163
+ return { verdict: 'no', reason: 'imports-other-type' };
1164
+ }
1165
+ const sitePackage = javaPackageOf(entry, line);
1166
+ if (sitePackage === undefined) return { verdict: 'unknown', reason: 'unknown-package' };
1167
+ const samePackage = candidates.filter(d => !d.enclosingType && (d.namespace || '') === sitePackage);
1168
+ if (samePackage.length > 0) return this.judge(samePackage);
1169
+ const onDemand = (entry.importDetails || []).filter(detail => detail.module &&
1170
+ (detail.names || []).includes('*')).map(detail => String(detail.module).replace(/\.\*$/, ''));
1171
+ const viaDemand = candidates.filter(d => onDemand.some(module =>
1172
+ (!d.enclosingType && (d.namespace || '') === module) ||
1173
+ (d.enclosingType && `${d.namespace ? d.namespace + '.' : ''}${d.enclosingType}` === module)));
1174
+ if (viaDemand.length > 0) return this.judge(viaDemand);
1175
+ // A member type inherited from a supertype stays visible by simple
1176
+ // name in subclasses.
1177
+ if ([...this.group].some(d => d.enclosingType) && this.inheritsFromPinOwner(file)) {
1178
+ return { verdict: 'unknown', reason: 'inherited-member-type' };
1179
+ }
1180
+ return { verdict: 'no', reason: 'not-in-scope' };
1181
+ }
1182
+
1183
+ /**
1184
+ * The namespace enclosing the token being decided (C#): its
1185
+ * `namespace` blocks and a file-scoped `namespace X;` read from the AST
1186
+ * (a file whose later declarations fail to parse still has them), else
1187
+ * the namespace of the indexed types around the line.
1188
+ */
1189
+ csharpSiteNamespace(file, line) {
1190
+ const node = this.siteNode && this.siteFile === file && this.siteNode.startPosition.row + 1 === line
1191
+ ? this.siteNode : null;
1192
+ if (node) {
1193
+ const parts = [];
1194
+ let root = node;
1195
+ for (let current = node.parent; current; current = current.parent) {
1196
+ if (current.type === 'namespace_declaration') {
1197
+ const nameNode = current.childForFieldName('name');
1198
+ if (nameNode) parts.unshift(textOf(nameNode));
1199
+ }
1200
+ root = current;
1201
+ }
1202
+ for (let i = 0; i < root.namedChildCount; i++) {
1203
+ const child = root.namedChild(i);
1204
+ if (child.startIndex > node.startIndex) break;
1205
+ if (child.type === 'file_scoped_namespace_declaration') {
1206
+ const nameNode = child.childForFieldName('name');
1207
+ if (nameNode) parts.unshift(textOf(nameNode));
1208
+ break;
1209
+ }
1210
+ }
1211
+ if (parts.length > 0) return parts.join('.');
1212
+ }
1213
+ const { siteNamespaceOf } = require('./type-denotation');
1214
+ return siteNamespaceOf(this.index.files.get(file), line);
1215
+ }
1216
+
1217
+ inheritsFromPinOwner(file) {
1218
+ const owners = new Set([...this.group].map(d => d.enclosingType).filter(Boolean));
1219
+ const entry = this.index.files.get(file);
1220
+ return (entry?.symbols || []).some(s => TYPE_DECL_KINDS.has(s.type) &&
1221
+ [s.extends, ...(s.implements || [])].filter(Boolean).some(parent =>
1222
+ owners.has(String(parent).replace(/<.*$/, '').split('.').pop())));
1223
+ }
1224
+
1225
+ resolveCSharpName(file, line, name) {
1226
+ const entry = this.index.files.get(file);
1227
+ const { csharpUsings } = require('./type-denotation');
1228
+ const candidates = this.all.filter(d => d.name === name && !d.enclosingType);
1229
+ const alias = (entry.importBindings || []).some(b => b.name === name && b.kind === 'using');
1230
+ if (alias) return { verdict: 'no', reason: 'using-alias' };
1231
+ const site = this.csharpSiteNamespace(file, line);
1232
+ if (site === undefined) return { verdict: 'unknown', reason: 'unknown-namespace' };
1233
+ // Member types of an enclosing class come first, declared in any
1234
+ // `partial` part of it (fix #395: `readonly DepthLimiter _d;` in one
1235
+ // part of `partial class PropertyValueConverter`, the nested class
1236
+ // in another file).
1237
+ const nestedNamed = this.all.filter(d => d.name === name && d.enclosingType && d.file !== file);
1238
+ if (nestedNamed.length > 0) {
1239
+ const around = (entry.symbols || []).filter(s => TYPE_DECL_KINDS.has(s.type) &&
1240
+ s.startLine <= line && s.endLine >= line)
1241
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine));
1242
+ for (const owner of around) {
1243
+ if (!(owner.modifiers || []).includes('partial')) continue;
1244
+ const members = nestedNamed.filter(d => d.enclosingType === owner.name &&
1245
+ (d.namespace || '') === (owner.namespace || '') &&
1246
+ (this.index.files.get(d.file)?.symbols || []).some(part => part.name === owner.name &&
1247
+ TYPE_DECL_KINDS.has(part.type) && (part.modifiers || []).includes('partial') &&
1248
+ (part.namespace || '') === (owner.namespace || '') &&
1249
+ part.startLine <= d.startLine && part.endLine >= d.endLine));
1250
+ if (members.length > 0) return this.judge(members);
1251
+ }
1252
+ }
1253
+ const chain = [];
1254
+ for (let parts = site ? site.split('.') : []; parts.length > 0; parts = parts.slice(0, -1)) {
1255
+ chain.push(parts.join('.'));
1256
+ }
1257
+ chain.push('');
1258
+ for (const ns of chain) {
1259
+ const here = candidates.filter(d => (d.namespace || '') === ns);
1260
+ if (here.length > 0) return this.judge(here);
1261
+ }
1262
+ // Using directives resolved from the namespace each is written in
1263
+ // (fix #395: `namespace FluentValidation; using Results;` names
1264
+ // FluentValidation.Results); `using static` also brings nested types.
1265
+ const scope = csharpUsings(this.index, file);
1266
+ const usings = new Set([...scope.namespaces, ...scope.statics]);
1267
+ const viaUsing = candidates.filter(d => usings.has(d.namespace || ''));
1268
+ if (viaUsing.length > 0) return this.judge(viaUsing);
1269
+ if ([...this.group].some(d => d.enclosingType) && this.inheritsFromPinOwner(file)) {
1270
+ return { verdict: 'unknown', reason: 'inherited-member-type' };
1271
+ }
1272
+ return { verdict: 'no', reason: 'not-in-scope' };
1273
+ }
1274
+
1275
+ // ── Rust ──
1276
+
1277
+ rustChainAt(file, line) {
1278
+ const { rustInlineModules, rustInlineChainAt } = require('./imports');
1279
+ const entry = this.index.files.get(file);
1280
+ const memoKey = `rinline\0${file}`;
1281
+ let inline = this.memo.get(memoKey);
1282
+ if (!inline) {
1283
+ inline = rustInlineModules(entry || {});
1284
+ this.memo.set(memoKey, inline);
1285
+ }
1286
+ return rustInlineChainAt(inline, line).join('::');
1287
+ }
1288
+
1289
+ resolveRustName(file, line, name) {
1290
+ const entry = this.index.files.get(file);
1291
+ const { rustBindingsInScope } = require('./type-denotation');
1292
+ // Function-local items first (lexically scoped).
1293
+ const siteChain = this.rustChainAt(file, line);
1294
+ const local = this.fileTypes(file, name).filter(d => d.lexicalScopeStartLine != null &&
1295
+ line >= d.lexicalScopeStartLine && line <= d.lexicalScopeEndLine);
1296
+ if (local.length > 0) {
1297
+ const width = d => d.lexicalScopeEndLine - d.lexicalScopeStartLine;
1298
+ const narrowest = Math.min(...local.map(width));
1299
+ return this.judge(local.filter(d => width(d) === narrowest));
1300
+ }
1301
+ const bindings = rustBindingsInScope(entry, (entry.importBindings || []).filter(b =>
1302
+ (b.alias || b.name) === name && b.module && b.name !== '*'), line);
1303
+ const sameModule = this.fileTypes(file, name).filter(d => d.lexicalScopeStartLine == null &&
1304
+ this.rustChainAt(file, d.startLine) === siteChain);
1305
+ if (sameModule.length > 0 && bindings.length === 0) return this.judge(sameModule);
1306
+ if (bindings.length > 0) {
1307
+ // A module that declares the name and also imports it holds the
1308
+ // two under different `#[cfg]`s.
1309
+ return this.combine([...(sameModule.length > 0 ? [this.judge(sameModule)] : []), ...bindings.map(b => {
1310
+ if (b.alias && b.alias !== b.name) return { verdict: 'no', reason: 'import-alias' };
1311
+ const segments = String(b.module).split('::');
1312
+ const last = segments.pop();
1313
+ if (last !== name) return { verdict: 'no', reason: 'import-alias' };
1314
+ return this.resolveRustPath(file, b.line || line, this.rustUseQualifier(file, b.line || line, segments), name, 0);
1315
+ })]);
1316
+ }
1317
+ // Globs in scope (`use super::*`, `use crate::m::*`).
1318
+ const globs = (entry.importDetails || []).filter(detail => detail.type === 'use-glob' && detail.module);
1319
+ const visibleGlobs = rustBindingsInScope(entry, globs.map(g => ({ ...g, name: '*' })), line);
1320
+ const verdicts = visibleGlobs.map(glob => this.resolveRustPath(file, glob.line || line, glob.module, name, 0));
1321
+ const yes = verdicts.filter(v => v.verdict === 'yes');
1322
+ if (yes.length > 0 && yes.length === verdicts.filter(v => v.verdict !== 'no').length) return { verdict: 'yes' };
1323
+ if (verdicts.some(v => v.verdict !== 'no')) return { verdict: 'unknown', reason: 'glob-import' };
1324
+ // Nothing written binds the name here: it is a prelude name, or a
1325
+ // macro expansion in scope brought it in (a macro can expand to
1326
+ // `use` items, visible in its whole module).
1327
+ if (langTraits(entry.language)?.implicitPreludeNames?.has(name)) {
1328
+ return { verdict: 'no', reason: 'prelude' };
1329
+ }
1330
+ return { verdict: 'unknown', reason: 'macro-expanded-scope' };
1331
+ }
1332
+
1333
+ resolveRustPath(file, line, qualifier, name, depth) {
1334
+ if (depth > MAX_QUALIFIER_HOPS) return { verdict: 'unknown', reason: 'path-depth' };
1335
+ const key = `rp\0${file}\0${this.rustChainAt(file, line)}\0${qualifier}\0${name}`;
1336
+ if (this.memo.has(key)) return this.memo.get(key);
1337
+ const result = this._resolveRustPath(file, line, qualifier, name, depth);
1338
+ this.memo.set(key, result);
1339
+ return result;
1340
+ }
1341
+
1342
+ _resolveRustPath(file, line, qualifier, name, depth) {
1343
+ const entry = this.index.files.get(file);
1344
+ const { externalTypeDenotation, rustBindingsInScope } = require('./type-denotation');
1345
+ const { rustPathModule } = require('./rust-modules');
1346
+ if (!qualifier) return this.resolveRustName(file, line, name);
1347
+ // `::krate::..` names an extern crate, never a local module.
1348
+ const global = qualifier.startsWith('::');
1349
+ if (global) qualifier = qualifier.slice(2);
1350
+ const resolved = global ? null : rustPathModule(this.index, file, line, qualifier);
1351
+ if (resolved) return this.rustModuleVerdict(resolved, name, depth);
1352
+ // A qualifier whose first segment is a `use` binding (a module alias,
1353
+ // `use crate::widget;`, `use crate::widget::{self}`): rewrite it. A
1354
+ // binding whose own path starts with that segment (`use anyhow::
1355
+ // anyhow;` imports a macro) never names the module.
1356
+ const segments = qualifier.split('::');
1357
+ const head = segments[0];
1358
+ const bindings = global ? [] : rustBindingsInScope(entry, (entry.importBindings || []).filter(b =>
1359
+ (b.alias || b.name) === head && b.module && String(b.module).split('::')[0] !== head), line);
1360
+ if (bindings.length > 0) {
1361
+ return this.combine(bindings.map(b => this.resolveRustPath(file, b.line || line,
1362
+ [b.module, ...segments.slice(1)].join('::'), name, depth + 1)));
1363
+ }
1364
+ // A crate of this workspace named by its library name (integration
1365
+ // tests, examples and sibling crates use the package by name).
1366
+ const root = this.rustCrateRoots().get(head);
1367
+ if (root) {
1368
+ const inCrate = rustPathModule(this.index, root, 1, ['crate', ...segments.slice(1)].join('::'));
1369
+ if (inCrate) return this.rustModuleVerdict(inCrate, name, depth);
1370
+ return { verdict: 'unknown', reason: 'unresolved-path' };
1371
+ }
1372
+ if (externalTypeDenotation(this.index, file, name, qualifier, line)) {
1373
+ return { verdict: 'no', reason: 'external-type' };
1374
+ }
1375
+ // A qualifier naming a type (an associated item or enum variant).
1376
+ const typeHead = (this.index.symbols.get(segments[segments.length - 1]) || [])
1377
+ .some(d => TYPE_DECL_KINDS.has(d.type));
1378
+ if (typeHead) return { verdict: 'no', reason: 'associated-item' };
1379
+ return { verdict: 'unknown', reason: 'unresolved-path' };
1380
+ }
1381
+
1382
+ /** Project invocation sites `{file, line}` of a Rust macro by name. */
1383
+ rustMacroInvocations(macroName) {
1384
+ const key = `rmacro\0${macroName}`;
1385
+ if (this.memo.has(key)) return this.memo.get(key);
1386
+ const { computeGroundSet } = require('./account');
1387
+ const ground = computeGroundSet(this.index, macroName);
1388
+ const sites = [];
1389
+ for (const [file, lineNumbers] of ground.perFile) {
1390
+ const entry = this.index.files.get(file);
1391
+ if (entry?.language !== 'rust') continue;
1392
+ let content;
1393
+ try { content = this.index._readFile(file); } catch { continue; }
1394
+ const tree = this.index._getParsedTree?.(file, content, 'rust') || safeParse(getParser('rust'), content);
1395
+ if (!tree) continue;
1396
+ const rows = new Set(lineNumbers.map(n => n - 1));
1397
+ const stack = [tree.rootNode];
1398
+ while (stack.length > 0) {
1399
+ const current = stack.pop();
1400
+ if (current.type === 'macro_invocation' && rows.has(current.startPosition.row)) {
1401
+ const macro = current.childForFieldName('macro');
1402
+ if (macro && macro.text.split('::').pop() === macroName) {
1403
+ sites.push({ file, line: current.startPosition.row + 1 });
1404
+ }
1405
+ }
1406
+ for (let i = current.namedChildCount - 1; i >= 0; i--) stack.push(current.namedChild(i));
1407
+ }
1408
+ }
1409
+ this.memo.set(key, sites);
1410
+ return sites;
1411
+ }
1412
+
1413
+ /**
1414
+ * The qualifier of a `use` binding's path. The parser records `use
1415
+ * ::krate::X` without its leading `::`; a `use` path whose first segment
1416
+ * names both a local module and a workspace crate would not compile
1417
+ * unqualified, so such a path names the crate.
1418
+ */
1419
+ rustUseQualifier(file, line, segments) {
1420
+ const head = segments[0];
1421
+ if (!head || ['crate', 'self', 'super', ''].includes(head) || !this.rustCrateRoots().has(head)) {
1422
+ return segments.join('::');
1423
+ }
1424
+ const { rustPathModule } = require('./rust-modules');
1425
+ return rustPathModule(this.index, file, line, head) ? `::${segments.join('::')}` : segments.join('::');
1426
+ }
1427
+
1428
+ /** What NAME denotes in a resolved Rust module: its items, and the
1429
+ * names its `use` declarations re-export (a module can hold both under
1430
+ * different `#[cfg]`s). */
1431
+ rustModuleVerdict(resolved, name, depth) {
1432
+ const { rustModuleItems } = require('./rust-modules');
1433
+ const verdicts = [];
1434
+ const items = rustModuleItems(this.index, resolved, name).filter(d => TYPE_DECL_KINDS.has(d.type));
1435
+ if (items.length > 0) verdicts.push(this.judge(items));
1436
+ for (const moduleEntry of resolved.entries) {
1437
+ const target = this.index.files.get(moduleEntry.file);
1438
+ const chain = moduleEntry.chain.join('::');
1439
+ const inFunction = line => (target?.symbols || []).some(symbol =>
1440
+ (symbol.type === 'function' || symbol.type === 'method' || symbol.type === 'constructor') &&
1441
+ symbol.startLine <= line && symbol.endLine >= line);
1442
+ const reexports = (target?.importBindings || []).filter(b => (b.alias || b.name) === name &&
1443
+ b.module && this.rustChainAt(moduleEntry.file, b.line || 0) === chain && !inFunction(b.line || 0));
1444
+ for (const b of reexports) {
1445
+ if (b.alias && b.alias !== b.name) {
1446
+ verdicts.push({ verdict: 'no', reason: 'import-alias' });
1447
+ continue;
1448
+ }
1449
+ const segments = String(b.module).split('::');
1450
+ segments.pop();
1451
+ verdicts.push(this.resolveRustPath(moduleEntry.file, b.line, segments.join('::'), name, depth + 1));
1452
+ }
1453
+ if (items.length > 0) continue;
1454
+ const globs = (target?.importDetails || []).filter(detail => detail.type === 'use-glob' &&
1455
+ detail.module && this.rustChainAt(moduleEntry.file, detail.line || 0) === chain);
1456
+ for (const glob of globs) {
1457
+ const verdict = this.resolveRustPath(moduleEntry.file, glob.line, glob.module, name, depth + 1);
1458
+ if (verdict.verdict !== 'no') {
1459
+ verdicts.push(verdict.verdict === 'yes' ? verdict : { verdict: 'unknown', reason: 'glob-import' });
1460
+ }
1461
+ }
1462
+ }
1463
+ if (verdicts.length === 0) return { verdict: 'no', reason: 'not-in-module' };
1464
+ return this.combine(verdicts);
1465
+ }
1466
+
1467
+ /** Library crate roots of the workspace by crate name ([lib] name, else
1468
+ * the package name with `-` as `_`). */
1469
+ rustCrateRoots() {
1470
+ const key = 'rust-crate-roots';
1471
+ if (this.memo.has(key)) return this.memo.get(key);
1472
+ const fs = require('fs');
1473
+ const roots = new Map();
1474
+ const manifests = new Set();
1475
+ for (const [file, entry] of this.index.files) {
1476
+ if (entry.language !== 'rust') continue;
1477
+ for (let dir = path.dirname(file); dir.startsWith(this.index.root) && dir !== path.dirname(dir);
1478
+ dir = path.dirname(dir)) {
1479
+ if (manifests.has(dir)) break;
1480
+ if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
1481
+ manifests.add(dir);
1482
+ break;
1483
+ }
1484
+ }
1485
+ }
1486
+ for (const dir of manifests) {
1487
+ let text;
1488
+ try { text = fs.readFileSync(path.join(dir, 'Cargo.toml'), 'utf-8'); } catch { continue; }
1489
+ const sections = text.split(/^\s*\[/m);
1490
+ const lib = sections.find(part => /^lib\]/.test(part));
1491
+ const pkg = sections.find(part => /^package\]/.test(part));
1492
+ const nameOf = part => part?.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
1493
+ const crate = (nameOf(lib) || nameOf(pkg) || '').replace(/-/g, '_');
1494
+ const libPath = lib?.match(/^\s*path\s*=\s*"([^"]+)"/m)?.[1] || 'src/lib.rs';
1495
+ const root = path.join(dir, libPath);
1496
+ if (crate && this.index.files.has(root) && !roots.has(crate)) roots.set(crate, root);
1497
+ }
1498
+ this.memo.set(key, roots);
1499
+ return roots;
1500
+ }
1501
+
1502
+ // ── C / C++ ──
1503
+
1504
+ enclosingClassAt(file, line) {
1505
+ const entry = this.index.files.get(file);
1506
+ let best = null;
1507
+ for (const s of entry?.symbols || []) {
1508
+ if (s.startLine > line || s.endLine < line) continue;
1509
+ const isClass = TYPE_DECL_KINDS.has(s.type) && s.type !== 'type';
1510
+ const candidate = isClass ? s.name : (s.className || null);
1511
+ if (!candidate) continue;
1512
+ if (!best || (s.endLine - s.startLine) < (best.span)) {
1513
+ best = { name: candidate, span: s.endLine - s.startLine };
1514
+ }
1515
+ }
1516
+ return best?.name || null;
1517
+ }
1518
+
1519
+ resolveCFamily(file, line, name, qualifierText, options = {}) {
1520
+ const entry = this.index.files.get(file);
1521
+ const { includeClosure, resolveQualifier } = require('./cpp-scope');
1522
+ const visible = includeClosure(this.index, file);
1523
+ if (entry.language === 'c') {
1524
+ // C tags (struct/union/enum) live in their own namespace; typedef
1525
+ // names in the ordinary one.
1526
+ const tagSite = options.tag != null;
1527
+ const candidates = this.all.filter(d => d.name === name &&
1528
+ (tagSite ? !cOrdinaryName(d) : cOrdinaryName(d)));
1529
+ const seen = candidates.filter(d => d.file === file || visible.has(d.file));
1530
+ if (seen.length > 0) return this.judge(seen);
1531
+ return { verdict: 'unknown', reason: 'not-in-include-closure' };
1532
+ }
1533
+ const spelling = qualifierText ? `${qualifierText}::${name}` : name;
1534
+ if (qualifierText && this.def.enclosingType) {
1535
+ // `Acc<T>::Member` / `Acc<double>::Member` against a member of a
1536
+ // specialization: the template arguments pick the class.
1537
+ const last = String(qualifierText).split('::').pop();
1538
+ const base = s => String(s).replace(/<.*$/, '');
1539
+ const owners = [...this.group].map(d => d.enclosingType).filter(Boolean);
1540
+ if (owners.some(owner => base(owner) === base(last))) {
1541
+ if (owners.includes(last)) return { verdict: 'yes' };
1542
+ // A member of the primary template, spelled with arguments
1543
+ // (`SkipList<K, C>::Node`), while the project declares no
1544
+ // specialization of that template: every argument list
1545
+ // names the primary (fix #396).
1546
+ // Explicit specializations are indexed as further same-name
1547
+ // class definitions (`template <> struct Acc<float> {..}`):
1548
+ // one definition is the primary alone.
1549
+ const classDefinitions = (this.index.symbols.get(base(last)) || [])
1550
+ .filter(d => TYPE_DECL_KINDS.has(d.type) && d.type !== 'type');
1551
+ const specialized = classDefinitions.length !== 1 ||
1552
+ classDefinitions.some(d => d.specialization || d.isSpecialization);
1553
+ if (!specialized && owners.every(owner => !owner.includes('<')) && last.includes('<')) {
1554
+ return { verdict: 'yes' };
1555
+ }
1556
+ if (last.includes('<') || owners.some(owner => owner.includes('<'))) {
1557
+ return { verdict: 'unknown', reason: 'specialization-member' };
1558
+ }
1559
+ }
1560
+ }
1561
+ // A class declared in a function body is visible from its
1562
+ // declaration to the end of its block and shadows every outer
1563
+ // declaration of the name there (fix #389).
1564
+ const functionLocal = d => !d.enclosingType && d.lexicalScopeStartLine != null;
1565
+ const localInScope = d => d.file === file && d.lexicalScopeStartLine <= line && line <= d.lexicalScopeEndLine;
1566
+ if (!qualifierText) {
1567
+ const locals = this.all.filter(d => d.name === name && functionLocal(d) && localInScope(d));
1568
+ if (locals.length > 0) {
1569
+ const innermost = Math.max(...locals.map(d => d.lexicalScopeStartLine));
1570
+ return this.judge(locals.filter(d => d.lexicalScopeStartLine === innermost));
1571
+ }
1572
+ }
1573
+ const site = this.cppSiteContext(file, line);
1574
+ const context = { file, line, namespace: site.namespace,
1575
+ className: qualifierText && qualifierText.startsWith('::') ? null : site.className };
1576
+ const found = resolveQualifier(this.index, context, spelling, visible, 0, { declarations: true });
1577
+ if (found.kind === 'namespace') return { verdict: 'no', reason: 'namespace' };
1578
+ if (found.kind === 'type') {
1579
+ const declarations = (found.declarations || []).filter(d => !functionLocal(d) || localInScope(d));
1580
+ const siblings = this.cppSiblingMembers(declarations);
1581
+ if (siblings) {
1582
+ // Members of same-name classes (specializations of one
1583
+ // template, configuration alternatives): the class body the
1584
+ // site sits in decides; elsewhere it is not decidable here.
1585
+ const inside = d => d && d.file === file && d.startLine <= line && d.endLine >= line;
1586
+ if (siblings.pinOwners.some(inside)) return { verdict: 'yes' };
1587
+ if (siblings.otherOwners.some(inside)) return { verdict: 'no', reason: 'other-type' };
1588
+ return { verdict: 'unknown', reason: 'specialization-member' };
1589
+ }
1590
+ const verdict = this.judge(declarations);
1591
+ if (verdict.verdict === 'yes' && !found.visible) {
1592
+ return { verdict: 'unknown', reason: 'not-in-include-closure' };
1593
+ }
1594
+ // Only declarations the site cannot see answered the lookup
1595
+ // (fix #393): the site's scope was not read right (a namespace
1596
+ // a damaged parse or a macro-opened inline namespace hid), so a
1597
+ // renamed declaration the site does see may be the one named.
1598
+ if (verdict.verdict === 'no' && !found.visible &&
1599
+ [...this.group].some(d => d.file === file || visible.has(d.file))) {
1600
+ return { verdict: 'unknown', reason: 'scope-unresolved' };
1601
+ }
1602
+ return verdict;
1603
+ }
1604
+ if ((found.why === 'no-declaration' || found.why === 'not-visible') && !qualifierText) {
1605
+ // A member type (alias, nested class) of the class whose body
1606
+ // holds the site: class scope is searched before the enclosing
1607
+ // namespaces, innermost class first (fix #387: `using range_type
1608
+ // = ...` in one of two `formatter` specializations).
1609
+ const members = this.all.filter(d => d.name === name && d.file === file && d.enclosingType &&
1610
+ TYPE_DECL_KINDS.has(d.type) && d.lexicalScopeStartLine != null &&
1611
+ d.lexicalScopeStartLine <= line && line <= d.lexicalScopeEndLine);
1612
+ if (members.length > 0) {
1613
+ const innermost = Math.max(...members.map(d => d.lexicalScopeStartLine));
1614
+ return this.judge(members.filter(d => d.lexicalScopeStartLine === innermost));
1615
+ }
1616
+ }
1617
+ if ((found.why === 'no-declaration' || found.why === 'not-visible') && !qualifierText && site.className) {
1618
+ // Class scope sees the injected class names of its bases
1619
+ // (`Derived() : Base() {}`, `Base::f()` inside a member).
1620
+ const viaBase = this.cppBaseNamed(file, line, site.className, name, 0);
1621
+ if (viaBase) return viaBase;
1622
+ }
1623
+ if (found.why === 'no-declaration') {
1624
+ // A qualifier that names nothing the project declares (a macro
1625
+ // the parse kept, a namespace outside the project) decides
1626
+ // nothing about the name, unless it is the standard library.
1627
+ if (qualifierText && !qualifierText.startsWith('::')) {
1628
+ const root = qualifierText.split('::')[0];
1629
+ const outer = resolveQualifier(this.index, context, qualifierText, visible, 0, { declarations: true });
1630
+ if (outer.kind === 'unknown' && !(langTraits(entry.language)?.standardPathRoots || []).includes(root)) {
1631
+ return { verdict: 'unknown', reason: 'unresolved-qualifier' };
1632
+ }
1633
+ }
1634
+ // An unqualified name the lookup finds nowhere, while the renamed
1635
+ // type is the project's only declaration of it: the site's scope
1636
+ // was not read right (code that compiles names something), so
1637
+ // it is left for review rather than dropped.
1638
+ if (!qualifierText && this.all.filter(d => d.name === name).every(d => this.inGroup(d))) {
1639
+ return { verdict: 'unknown', reason: 'scope-unresolved' };
1640
+ }
1641
+ // No project declaration of that spelling is in scope.
1642
+ return { verdict: 'no', reason: 'not-in-scope' };
1643
+ }
1644
+ if (found.why === 'not-visible') {
1645
+ const candidates = this.all.filter(d => d.name === name);
1646
+ if (candidates.every(d => !this.inGroup(d))) return { verdict: 'no', reason: 'other-type' };
1647
+ return { verdict: 'unknown', reason: 'not-in-include-closure' };
1648
+ }
1649
+ return { verdict: 'unknown', reason: found.why || 'unresolved-qualifier' };
1650
+ }
1651
+
1652
+ pinIsTag() {
1653
+ return !cOrdinaryName(this.def);
1654
+ }
1655
+
1656
+ /** The class defining a member type (lexical container of its name). */
1657
+ ownerClassOf(d) {
1658
+ if (!d.enclosingType) return null;
1659
+ const entry = this.index.files.get(d.file);
1660
+ let best = null;
1661
+ for (const s of entry?.symbols || []) {
1662
+ if (s.name !== d.enclosingType || !TYPE_DECL_KINDS.has(s.type) || s.type === 'type') continue;
1663
+ if (s.startLine > d.startLine || s.endLine < d.endLine) continue;
1664
+ if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine)) best = s;
1665
+ }
1666
+ return best;
1667
+ }
1668
+
1669
+ /**
1670
+ * When the pin is a member type and the lookup lands on member types of
1671
+ * same-name classes (template specializations, configuration
1672
+ * alternatives) whose owners the index cannot tell apart by name: the
1673
+ * pin's owners and the other owners. Null otherwise.
1674
+ */
1675
+ cppSiblingMembers(declarations) {
1676
+ if (!this.def.enclosingType) return null;
1677
+ const pinOwners = [...this.group].map(d => this.ownerClassOf(d)).filter(Boolean);
1678
+ if (pinOwners.length === 0) return null;
1679
+ const scopeOf = d => `${effectiveNs(this.index, d)}|${d.enclosingType || ''}`;
1680
+ const pinScope = scopeOf(this.def);
1681
+ const same = this.all.filter(d => d.name === this.name && scopeOf(d) === pinScope && !this.inGroup(d));
1682
+ if (same.length === 0) return null;
1683
+ if (!declarations.some(d => scopeOf(d) === pinScope)) return null;
1684
+ const otherOwners = same.map(d => this.ownerClassOf(d)).filter(Boolean);
1685
+ return { pinOwners, otherOwners };
1686
+ }
1687
+
1688
+ /** The verdict for NAME as a base class of the class `className` seen
1689
+ * from file:line (its injected class name), or null. */
1690
+ cppBaseNamed(file, line, className, name, depth) {
1691
+ if (depth > 3) return null;
1692
+ const { includeClosure, resolveQualifier, effectiveNamespace } = require('./cpp-scope');
1693
+ const { splitParentList } = require('./graph-build');
1694
+ const visible = includeClosure(this.index, file);
1695
+ const simple = String(className).split('::').pop();
1696
+ const classes = (this.index.symbols.get(simple) || []).filter(d => TYPE_DECL_KINDS.has(d.type) &&
1697
+ d.type !== 'type' && d.extends && (d.file === file || visible.has(d.file)) &&
1698
+ familyOf(this.index.files.get(d.file)?.language) === 'c');
1699
+ const verdicts = [];
1700
+ for (const cls of classes) {
1701
+ for (const raw of splitParentList(cls.extends)) {
1702
+ const spelled = raw.replace(/\b(public|protected|private|virtual)\b/g, '').replace(/\s+/g, '');
1703
+ const plain = spelled.replace(/<.*$/, '');
1704
+ const last = plain.split('::').pop();
1705
+ const context = { file: cls.file, line: cls.startLine, namespace: effectiveNamespace(this.index, cls),
1706
+ className: cls.enclosingType || null };
1707
+ if (last === name) {
1708
+ const found = resolveQualifier(this.index, context, plain, includeClosure(this.index, cls.file), 0,
1709
+ { declarations: true });
1710
+ if (found.kind === 'type') verdicts.push(this.judge(found.declarations || []));
1711
+ else verdicts.push({ verdict: 'unknown', reason: 'base-class' });
1712
+ } else {
1713
+ const deeper = this.cppBaseNamed(cls.file, cls.startLine, last, name, depth + 1);
1714
+ if (deeper) verdicts.push(deeper);
1715
+ }
1716
+ }
1717
+ }
1718
+ return verdicts.length > 0 ? this.combine(verdicts) : null;
1719
+ }
1720
+
1721
+ /**
1722
+ * Namespace and class scope of the token being decided (C++), read from
1723
+ * the AST: enclosing `namespace` blocks (after a macro-opened prefix)
1724
+ * and enclosing class bodies, or the class an out-of-line member
1725
+ * definition qualifies (`void Widget::f() {}`). Template headers and
1726
+ * other lines outside every indexed symbol keep their real scope.
1727
+ */
1728
+ cppSiteContext(file, line) {
1729
+ const node = this.siteNode && this.siteFile === file && this.siteNode.startPosition.row + 1 === line
1730
+ ? this.siteNode : null;
1731
+ const { macroNamespacePrefixAt, namespaceAt } = require('./cpp-scope');
1732
+ if (!node) return { namespace: namespaceAt(this.index, file, line), className: this.enclosingClassAt(file, line) };
1733
+ const namespaces = [];
1734
+ const classes = [];
1735
+ let memberOf = null;
1736
+ let recovered = false;
1737
+ for (let current = node.parent; current; current = current.parent) {
1738
+ if (current.type === 'ERROR') recovered = true;
1739
+ if (current.type === 'namespace_definition') {
1740
+ const nameNode = current.childForFieldName('name');
1741
+ if (nameNode) namespaces.unshift(textOf(nameNode));
1742
+ } else if (C_TYPE_SPECIFIERS.has(current.type)) {
1743
+ const body = current.childForFieldName('body');
1744
+ const nameNode = current.childForFieldName('name');
1745
+ if (body && contains(body, node) && nameNode) {
1746
+ // Specializations keep their arguments (`Acc<double>`), as
1747
+ // the index names them; an out-of-class nested definition
1748
+ // (`struct List<K>::Node`) is in its owner's scope too
1749
+ // (fix #396).
1750
+ const segments = textOf(nameNode).split('::');
1751
+ classes.unshift(segments.slice(-2).join('::'));
1752
+ }
1753
+ } else if (current.type === 'function_definition' && !memberOf && classes.length === 0) {
1754
+ let declarator = current.childForFieldName('declarator');
1755
+ while (declarator && declarator.type !== 'function_declarator' && declarator.childForFieldName?.('declarator')) {
1756
+ declarator = declarator.childForFieldName('declarator');
1757
+ }
1758
+ const qualified = declarator?.childForFieldName?.('declarator');
1759
+ if (qualified?.type === 'qualified_identifier') {
1760
+ const scope = qualified.childForFieldName('scope');
1761
+ // `List<K>::Node::f`: Node's scope, then List's (fix #396).
1762
+ if (scope) memberOf = qualifiedScopeText(qualified) || textOf(scope).split('::').pop();
1763
+ }
1764
+ }
1765
+ }
1766
+ // A class body the raw tree lost (macro-heavy headers, conditional
1767
+ // members): the indexed nested classes around the line (parsed from
1768
+ // the recovered source) still name their owners (fix #396: `union
1769
+ // Data` inside `GenericValue`).
1770
+ if (classes.length > 0) {
1771
+ const symbols = this.index.files.get(file)?.symbols || [];
1772
+ const around = name => symbols.find(s => s.name === name && TYPE_DECL_KINDS.has(s.type) &&
1773
+ s.startLine <= line && line <= s.endLine);
1774
+ for (let owner = around(classes[0].split('::').pop())?.enclosingType, depth = 0;
1775
+ owner && depth < 8 && !classes.includes(owner); depth++) {
1776
+ classes.unshift(owner);
1777
+ owner = around(owner)?.enclosingType;
1778
+ }
1779
+ }
1780
+ const prefix = macroNamespacePrefixAt(this.index, file, line);
1781
+ const own = namespaces.join('::');
1782
+ let namespace = prefix && own ? `${prefix}::${own}` : prefix || own;
1783
+ let className = classes.length > 0 ? classes.join('::') : memberOf;
1784
+ // Macro-heavy code can break the raw tree so that an enclosing
1785
+ // namespace block closes early; the indexed declarations around the
1786
+ // line (parsed from the recovered source) still carry it.
1787
+ const indexed = namespaceAt(this.index, file, line);
1788
+ if (indexed && indexed !== namespace && (namespace === '' || own === '' ||
1789
+ indexed.endsWith(`::${own}`) || indexed.startsWith(`${namespace}::`) ||
1790
+ (recovered && indexed.startsWith(namespace)))) namespace = indexed;
1791
+ // Out-of-line member bodies: the index (parsed from the recovered
1792
+ // source) knows their class even when a macro before the return type
1793
+ // broke the literal head; the head's own qualifier is the fallback.
1794
+ if (classes.length === 0) className = this.enclosingClassAt(file, line) || memberOf;
1795
+ return { namespace, className };
1796
+ }
1797
+
1798
+ // ── Qualified names ──
1799
+
1800
+ resolveQualified(file, line, qualifierNode, qualifierText, tok) {
1801
+ const text = qualifierText != null ? qualifierText : textOf(qualifierNode);
1802
+ const site = (this.family === 'c' || this.family === 'csharp' || this.family === 'python') && this.siteNode
1803
+ ? `\0${this.siteNode.startIndex}` : '';
1804
+ const key = `q\0${file}\0${line}\0${text}\0${tok.role}${site}`;
1805
+ if (this.memo.has(key)) return this.memo.get(key);
1806
+ let result;
1807
+ try {
1808
+ result = this._resolveQualified(file, line, qualifierNode, text, tok);
1809
+ } catch {
1810
+ result = { verdict: 'unknown', reason: 'resolution-error' };
1811
+ }
1812
+ this.memo.set(key, result);
1813
+ return result;
1814
+ }
1815
+
1816
+ _resolveQualified(file, line, qualifierNode, text, tok) {
1817
+ const entry = this.index.files.get(file);
1818
+ const name = this.name;
1819
+ switch (this.family) {
1820
+ case 'js': case 'python': {
1821
+ const { _moduleAttributeBindingReaches } = require('./callers');
1822
+ let receiver = text;
1823
+ if (this.family === 'python') {
1824
+ const verdict = this.pythonModuleAttribute(file, receiver, qualifierNode);
1825
+ if (verdict) return verdict;
1826
+ }
1827
+ if (!/^[A-Za-z_$][\w$]*$/.test(receiver)) {
1828
+ return tok.role === 'member' ? { verdict: 'no', reason: 'member' }
1829
+ : { verdict: 'unknown', reason: 'qualified-name' };
1830
+ }
1831
+ const reach = _moduleAttributeBindingReaches(this.index, file, receiver, name, this.pinFiles);
1832
+ if (reach === 'yes') {
1833
+ // The module exposes the name; it must be the pin, not a
1834
+ // same-name export the file-level chase cannot tell apart.
1835
+ return this.moduleExposesPin(file, entry, receiver);
1836
+ }
1837
+ if (reach === 'no') return { verdict: 'no', reason: 'other-module' };
1838
+ if (reach === 'unknown') return { verdict: 'unknown', reason: 'module-attribute' };
1839
+ // Not a module: a class (nested type) or an ordinary value.
1840
+ const owner = [...this.group].find(d => this.ownerNameOf(d) === receiver);
1841
+ if (owner) {
1842
+ const outer = this.resolveTypeNamed(file, line, receiver);
1843
+ return outer === 'owner' ? { verdict: 'yes' }
1844
+ : outer === 'other' ? { verdict: 'no', reason: 'other-type' }
1845
+ : { verdict: 'unknown', reason: 'nested-type' };
1846
+ }
1847
+ return { verdict: 'no', reason: 'member' };
1848
+ }
1849
+ case 'go': {
1850
+ const binding = (entry.importBindings || []).find(b => (b.alias || b.name) === text && b.module);
1851
+ if (!binding) {
1852
+ if (tok.role === 'member') return this.goFieldVerdict(this.siteNode, file);
1853
+ return { verdict: 'unknown', reason: 'qualified-name' };
1854
+ }
1855
+ const moduleFile = this.moduleFile(entry, binding.module);
1856
+ if (!moduleFile) return { verdict: 'no', reason: 'external-package' };
1857
+ const dir = path.dirname(moduleFile);
1858
+ const types = this.all.filter(d => path.dirname(d.file) === dir && d.lexicalScopeStartLine == null &&
1859
+ !String(this.index.files.get(d.file)?.packageName || '').endsWith('_test'));
1860
+ return this.judge(types, 'other-package');
1861
+ }
1862
+ case 'rust':
1863
+ return this.resolveRustPath(file, line, text.replace(/<[^<>]*>/g, ''), name, 0);
1864
+ case 'java': return this.resolveJavaQualified(file, line, text, tok);
1865
+ case 'csharp': return this.resolveCSharpQualified(file, line, text, tok);
1866
+ case 'c': return this.resolveCFamily(file, line, name, text);
1867
+ default:
1868
+ return { verdict: 'unknown', reason: 'qualified-name' };
1869
+ }
1870
+ }
1871
+
1872
+ ownerNameOf(d) {
1873
+ if (d.enclosingType) return d.enclosingType;
1874
+ const container = this.containerOf(d);
1875
+ return container && TYPE_DECL_KINDS.has(container.type) ? container.name : null;
1876
+ }
1877
+
1878
+ /** Does `receiver` (a type name at file:line) name the pin's owner? */
1879
+ resolveTypeNamed(file, line, receiver) {
1880
+ const owners = [...this.group].map(d => {
1881
+ const entry = this.index.files.get(d.file);
1882
+ return (entry?.symbols || []).find(s => s.name === this.ownerNameOf(d) &&
1883
+ TYPE_DECL_KINDS.has(s.type) && s.startLine <= d.startLine && s.endLine >= d.endLine);
1884
+ }).filter(Boolean);
1885
+ if (owners.length === 0) return 'unknown';
1886
+ const sub = new TypeReferenceResolver(this.index, owners[0], receiver);
1887
+ const verdict = sub.resolveName(file, line, receiver);
1888
+ return verdict.verdict === 'yes' ? 'owner' : verdict.verdict === 'no' ? 'other' : 'unknown';
1889
+ }
1890
+
1891
+ moduleExposesPin(file, entry, receiver) {
1892
+ // `_moduleAttributeBindingReaches` is file-level; re-ask exactly.
1893
+ const starts = [];
1894
+ for (const b of entry.importBindings || []) {
1895
+ if ((b.alias || b.name) !== receiver || !b.module) continue;
1896
+ let moduleFile;
1897
+ if (this.family === 'python' && b.kind !== 'import') {
1898
+ // `from pkg import sub`: the receiver is the submodule.
1899
+ const module = String(b.module);
1900
+ moduleFile = this.moduleFile(entry, module.endsWith('.') ? module + b.name : `${module}.${b.name}`);
1901
+ } else {
1902
+ moduleFile = this.moduleFile(entry, b.module);
1903
+ }
1904
+ if (moduleFile) starts.push(moduleFile);
1905
+ }
1906
+ if (starts.length === 0) return { verdict: 'unknown', reason: 'module-attribute' };
1907
+ const verdicts = starts.map(start => this.chase(start, this.name));
1908
+ if (verdicts.every(v => v === 'yes')) return { verdict: 'yes' };
1909
+ if (verdicts.every(v => v === 'no')) return { verdict: 'no', reason: 'other-module' };
1910
+ return { verdict: 'unknown', reason: 'module-attribute' };
1911
+ }
1912
+
1913
+ pythonModuleAttribute(file, receiver, node) {
1914
+ const modules = this.pythonModulePath(file, receiver);
1915
+ if (!modules) return null; // An ordinary value or nested class.
1916
+ if (node) {
1917
+ let head = node;
1918
+ while (head?.type === 'attribute') head = head.childForFieldName('object');
1919
+ if (head?.type !== 'identifier') return { verdict: 'unknown', reason: 'module-attribute' };
1920
+ if (!this._pythonScopes) this._pythonScopes = new Map();
1921
+ if (!this._pythonScopes.has(file)) this._pythonScopes.set(file, new Map());
1922
+ const { referenceScope } = require('../languages/lexical-scope');
1923
+ // A local may hold the imported module, but its spelling alone
1924
+ // cannot prove that. Never reuse a module import through a shadow.
1925
+ if (referenceScope(head, 'python', this._pythonScopes.get(file)) !== 'module') {
1926
+ return { verdict: 'unknown', reason: 'module-receiver-shadow' };
1927
+ }
1928
+ }
1929
+ if (modules.unknown) return { verdict: 'unknown', reason: 'module-attribute' };
1930
+ if (modules.files.length === 0) return { verdict: 'no', reason: 'external-module' };
1931
+ return this.combine(modules.files.map(start => {
1932
+ const reach = this.chase(start, this.name);
1933
+ return reach === 'yes' ? { verdict: 'yes' }
1934
+ : reach === 'no' ? { verdict: 'no', reason: 'other-module' }
1935
+ : { verdict: 'unknown', reason: 'module-attribute' };
1936
+ }));
1937
+ }
1938
+
1939
+ pythonModulePath(file, receiver) {
1940
+ const { pythonModulePath } = require('./python-modules');
1941
+ return pythonModulePath(this.index, file, receiver, this.memo);
1942
+ }
1943
+
1944
+ javaPackages() {
1945
+ const key = 'java-packages';
1946
+ if (!this.memo.has(key)) {
1947
+ const packages = new Set();
1948
+ for (const entry of this.index.files.values()) {
1949
+ if (entry.language !== 'java') continue;
1950
+ for (const s of entry.symbols || []) {
1951
+ if (!s.namespace) continue;
1952
+ const parts = s.namespace.split('.');
1953
+ for (let i = 1; i <= parts.length; i++) packages.add(parts.slice(0, i).join('.'));
1954
+ }
1955
+ }
1956
+ this.memo.set(key, packages);
1957
+ }
1958
+ return this.memo.get(key);
1959
+ }
1960
+
1961
+ resolveJavaQualified(file, line, text, tok) {
1962
+ const qualifier = text.replace(/<.*?>/g, '');
1963
+ const candidates = this.all.filter(d => d.name === this.name);
1964
+ const byFqnPrefix = candidates.filter(d => {
1965
+ const prefix = this.javaFqn(d).slice(0, -(d.name.length + 1));
1966
+ return prefix === qualifier;
1967
+ });
1968
+ if (byFqnPrefix.length > 0) return this.judge(byFqnPrefix);
1969
+ if (this.javaPackages().has(qualifier)) return { verdict: 'no', reason: 'other-package' };
1970
+ const last = qualifier.split('.').pop();
1971
+ const nested = candidates.filter(d => d.enclosingType === last);
1972
+ if (nested.length > 0) {
1973
+ if (!qualifier.includes('.')) {
1974
+ const outer = this.resolveTypeNamed(file, line, last);
1975
+ if (outer === 'owner') return { verdict: 'yes' };
1976
+ if (outer === 'other') return { verdict: 'no', reason: 'other-type' };
1977
+ }
1978
+ return { verdict: 'unknown', reason: 'nested-type' };
1979
+ }
1980
+ if (tok.role === 'member') return { verdict: 'no', reason: 'member' };
1981
+ if (/^[a-z]/.test(qualifier) && !qualifier.includes('(')) {
1982
+ return { verdict: 'no', reason: 'external-package' };
1983
+ }
1984
+ return { verdict: 'unknown', reason: 'qualified-name' };
1985
+ }
1986
+
1987
+ csharpNamespaces() {
1988
+ const key = 'cs-namespaces';
1989
+ if (!this.memo.has(key)) {
1990
+ const namespaces = new Set();
1991
+ for (const entry of this.index.files.values()) {
1992
+ if (entry.language !== 'csharp') continue;
1993
+ for (const s of entry.symbols || []) {
1994
+ if (!s.namespace) continue;
1995
+ const parts = s.namespace.split('.');
1996
+ for (let i = 1; i <= parts.length; i++) namespaces.add(parts.slice(0, i).join('.'));
1997
+ }
1998
+ }
1999
+ this.memo.set(key, namespaces);
2000
+ }
2001
+ return this.memo.get(key);
2002
+ }
2003
+
2004
+ resolveCSharpQualified(file, line, text, tok) {
2005
+ const entry = this.index.files.get(file);
2006
+ // An `extern alias` qualifier (`Old::Lib.Core.Widget`, fix #401)
2007
+ // names an assembly reference: a project namespace of that spelling
2008
+ // may or may not be the assembly's.
2009
+ const externAlias = /^([A-Za-z_]\w*)::/.exec(text);
2010
+ if (externAlias && externAlias[1] !== 'global') {
2011
+ const rest = text.slice(externAlias[0].length).replace(/<.*?>/g, '');
2012
+ const spelled = this.all.some(d => d.name === this.name &&
2013
+ `${d.namespace || ''}${d.enclosingType ? (d.namespace ? '.' : '') + d.enclosingType : ''}` === rest);
2014
+ return spelled ? { verdict: 'unknown', reason: 'extern-alias' } : { verdict: 'no', reason: 'external-namespace' };
2015
+ }
2016
+ let qualifier = text.replace(/<.*?>/g, '').replace(/^global::/, '');
2017
+ // A using alias names the qualifier's head.
2018
+ const head = qualifier.split('.')[0];
2019
+ const alias = (entry.importBindings || []).find(b => b.name === head && b.kind === 'using' && b.module);
2020
+ if (alias) qualifier = [alias.module, ...qualifier.split('.').slice(1)].join('.');
2021
+ const site = text.startsWith('global::') || alias ? '' : (this.csharpSiteNamespace(file, line) || '');
2022
+ const chain = [];
2023
+ for (let parts = site ? site.split('.') : []; parts.length > 0; parts = parts.slice(0, -1)) {
2024
+ chain.push(parts.join('.'));
2025
+ }
2026
+ chain.push('');
2027
+ const candidates = this.all.filter(d => d.name === this.name);
2028
+ const namespaces = this.csharpNamespaces();
2029
+ for (const ns of chain) {
2030
+ const full = ns ? `${ns}.${qualifier}` : qualifier;
2031
+ const hits = candidates.filter(d => {
2032
+ const prefix = `${d.namespace || ''}${d.enclosingType ? (d.namespace ? '.' : '') + d.enclosingType : ''}`;
2033
+ return prefix === full;
2034
+ });
2035
+ if (hits.length > 0) return this.judge(hits);
2036
+ if (namespaces.has(full)) return { verdict: 'no', reason: 'other-namespace' };
2037
+ }
2038
+ const last = qualifier.split('.').pop();
2039
+ if (candidates.some(d => d.enclosingType === last)) {
2040
+ if (!qualifier.includes('.')) {
2041
+ const outer = this.resolveTypeNamed(file, line, last);
2042
+ if (outer === 'owner') return { verdict: 'yes' };
2043
+ if (outer === 'other') return { verdict: 'no', reason: 'other-type' };
2044
+ }
2045
+ return { verdict: 'unknown', reason: 'nested-type' };
2046
+ }
2047
+ if (tok.role === 'member') return { verdict: 'no', reason: 'member' };
2048
+ return { verdict: 'no', reason: 'external-namespace' };
2049
+ }
2050
+
2051
+ // ── Go embedded fields ──
2052
+
2053
+ embedders() {
2054
+ if (this._embedders !== undefined) return this._embedders;
2055
+ const out = [];
2056
+ for (const d of this.index.symbols.get(this.name) || []) {
2057
+ if (d.type !== 'field' || !d.embedded) continue;
2058
+ if (familyOf(this.index.files.get(d.file)?.language) !== 'go') continue;
2059
+ const spelled = String(d.fieldType || '').replace(/^\*/, '');
2060
+ const qualifier = spelled.includes('.') ? spelled.split('.').slice(0, -1).join('.') : null;
2061
+ const verdict = qualifier
2062
+ ? this.resolveQualified(d.file, d.startLine, null, qualifier, { role: 'type' })
2063
+ : this.resolveName(d.file, d.startLine);
2064
+ if (verdict.verdict !== 'no') out.push(d);
2065
+ }
2066
+ this._embedders = out;
2067
+ return out;
2068
+ }
2069
+
2070
+ /**
2071
+ * `x.Widget` or `S{Widget: v}`: when the pin is embedded somewhere, the
2072
+ * embedded field is named Widget. The struct the selector or key applies
2073
+ * to is read from the AST (a parameter, receiver, typed variable or
2074
+ * composite-literal local, or the literal's own type); its field named
2075
+ * like the type decides: the embedded pin -> edit, a named field -> not
2076
+ * a reference; anything else -> review.
2077
+ */
2078
+ goFieldVerdict(node = null, file = null) {
2079
+ const embedders = this.embedders();
2080
+ if (embedders.length === 0) return { verdict: 'no', reason: 'member' };
2081
+ const unknown = { verdict: 'unknown', reason: 'go-embedded-field' };
2082
+ if (!node || !file) return unknown;
2083
+ const typeNode = goStructTypeOf(node);
2084
+ if (!typeNode) return unknown;
2085
+ let typeName = null;
2086
+ let dir = null;
2087
+ const entry = this.index.files.get(file);
2088
+ if (typeNode.type === 'type_identifier') {
2089
+ typeName = typeNode.text;
2090
+ dir = path.dirname(file);
2091
+ } else if (typeNode.type === 'qualified_type') {
2092
+ typeName = typeNode.childForFieldName('name')?.text;
2093
+ const pkg = typeNode.childForFieldName('package')?.text;
2094
+ const binding = (entry?.importBindings || []).find(b => (b.alias || b.name) === pkg && b.module);
2095
+ const moduleFile = binding ? this.moduleFile(entry, binding.module) : null;
2096
+ dir = moduleFile ? path.dirname(moduleFile) : null;
2097
+ }
2098
+ if (!typeName || !dir) return unknown;
2099
+ const fields = (this.index.symbols.get(this.name) || []).filter(d => d.type === 'field' &&
2100
+ d.className === typeName && path.dirname(d.file) === dir);
2101
+ if (fields.length === 0) return unknown;
2102
+ if (fields.every(d => embedders.includes(d))) return { verdict: 'yes' };
2103
+ if (fields.every(d => !d.embedded)) return { verdict: 'no', reason: 'member' };
2104
+ return unknown;
2105
+ }
2106
+ }
2107
+
2108
+ const GO_FUNCTIONS = new Set(['function_declaration', 'method_declaration', 'func_literal']);
2109
+
2110
+ /** The Go type node a declaration gives an identifier in scope at `site`. */
2111
+ function goDeclaredTypeOf(site, identifier) {
2112
+ const unwrap = type => {
2113
+ while (type && (type.type === 'pointer_type' || type.type === 'parenthesized_type')) {
2114
+ type = type.namedChild(0);
2115
+ }
2116
+ if (type?.type === 'generic_type') type = type.childForFieldName('type');
2117
+ return type && (type.type === 'type_identifier' || type.type === 'qualified_type') ? type : null;
2118
+ };
2119
+ const literalType = value => {
2120
+ if (value?.type === 'unary_expression') value = value.childForFieldName('operand');
2121
+ return value?.type === 'composite_literal' ? unwrap(value.childForFieldName('type')) : null;
2122
+ };
2123
+ for (let scope = site.parent; scope; scope = scope.parent) {
2124
+ if (GO_FUNCTIONS.has(scope.type)) {
2125
+ for (const field of ['receiver', 'parameters']) {
2126
+ const list = scope.childForFieldName(field);
2127
+ for (let i = 0; i < (list?.namedChildCount || 0); i++) {
2128
+ const param = list.namedChild(i);
2129
+ const names = (param.namedChildren || []).filter(child => child.type === 'identifier');
2130
+ if (names.some(n => n.text === identifier)) return unwrap(param.childForFieldName('type'));
2131
+ }
2132
+ }
2133
+ }
2134
+ if (scope.type !== 'block' && scope.type !== 'source_file') continue;
2135
+ let found = null;
2136
+ for (let i = 0; i < scope.namedChildCount; i++) {
2137
+ const statement = scope.namedChild(i);
2138
+ if (statement.startIndex >= site.startIndex) break;
2139
+ if (statement.type === 'var_declaration') {
2140
+ for (const spec of statement.namedChildren || []) {
2141
+ if (spec.type !== 'var_spec') continue;
2142
+ const names = (spec.namedChildren || []).filter(child => child.type === 'identifier');
2143
+ const index = names.findIndex(n => n.text === identifier);
2144
+ if (index < 0) continue;
2145
+ const values = spec.childForFieldName('value');
2146
+ found = unwrap(spec.childForFieldName('type')) ||
2147
+ literalType(values?.namedChild ? values.namedChild(index) : null);
2148
+ }
2149
+ } else if (statement.type === 'short_var_declaration') {
2150
+ const left = statement.childForFieldName('left');
2151
+ const right = statement.childForFieldName('right');
2152
+ const index = (left?.namedChildren || []).findIndex(n => n.text === identifier);
2153
+ if (index >= 0) found = literalType(right?.namedChild(index));
2154
+ }
2155
+ }
2156
+ if (found) return found;
2157
+ }
2158
+ return null;
2159
+ }
2160
+
2161
+ /** The struct type a Go field selector or composite-literal key applies to. */
2162
+ function goStructTypeOf(node) {
2163
+ const parent = node.parent;
2164
+ if (parent?.type === 'selector_expression') {
2165
+ const operand = parent.childForFieldName('operand');
2166
+ if (operand?.type === 'identifier') return goDeclaredTypeOf(parent, operand.text);
2167
+ return null;
2168
+ }
2169
+ for (let current = parent; current; current = current.parent) {
2170
+ if (current.type === 'composite_literal') {
2171
+ let type = current.childForFieldName('type');
2172
+ if (type?.type === 'generic_type') type = type.childForFieldName('type');
2173
+ return type && (type.type === 'type_identifier' || type.type === 'qualified_type') ? type : null;
2174
+ }
2175
+ if (current.type !== 'literal_element' && current.type !== 'keyed_element' && current.type !== 'literal_value') {
2176
+ return null;
2177
+ }
2178
+ }
2179
+ return null;
2180
+ }
2181
+
2182
+ function javaPackageOf(entry, line) {
2183
+ const { siteNamespaceOf } = require('./type-denotation');
2184
+ const ns = siteNamespaceOf(entry, line);
2185
+ if (ns !== undefined) return ns;
2186
+ const withNs = (entry.symbols || []).find(s => s.namespace);
2187
+ return withNs ? withNs.namespace : ((entry.symbols || []).length > 0 ? '' : undefined);
2188
+ }
2189
+
2190
+ // ── The pass ─────────────────────────────────────────────────────────────
2191
+
2192
+ const TEXT_NODE = /comment|string|char_literal|heredoc|raw_string|template_string/;
2193
+ const PY_ANNOTATION = new Set(['type']);
2194
+ const PY_ANNOTATION_STOP = new Set(['block', 'module', 'class_definition', 'function_definition',
2195
+ 'call', 'argument_list']);
2196
+
2197
+ /**
2198
+ * Decide every name token on the ground lines of a type rename.
2199
+ * @returns {{ files: Map<string, {relativePath, lines: Map<number, {edits: number[], reviews: Array<{column, reason}>, skips: number, text: boolean, tokens: number}>}> }}
2200
+ */
2201
+ function typeReferenceSites(index, def, name, groundSet) {
2202
+ const resolver = new TypeReferenceResolver(index, def, name);
2203
+ const files = new Map();
2204
+ const perFile = groundSet?.perFile || new Map();
2205
+ const sortedFiles = [...perFile.keys()].sort();
2206
+ for (const file of sortedFiles) {
2207
+ const entry = index.files.get(file);
2208
+ if (!entry) continue;
2209
+ const language = entry.language;
2210
+ const htmlScripts = language === 'html' && resolver.family === 'js';
2211
+ if (familyOf(language) !== resolver.family && !htmlScripts) continue;
2212
+ const lineNumbers = [...perFile.get(file)].sort((a, b) => a - b);
2213
+ const result = scanFile(index, resolver, file, entry, language, lineNumbers);
2214
+ if (result) files.set(file, { relativePath: entry.relativePath, lines: result });
2215
+ }
2216
+ return { files, resolver };
2217
+ }
2218
+
2219
+ /**
2220
+ * An HTML page's inline scripts as JavaScript at their own lines and
2221
+ * columns (the rest blank), and the rows of `on*` handler attributes that
2222
+ * spell the name (code UCN does not parse: review).
2223
+ */
2224
+ function htmlScriptView(content, name) {
2225
+ const html = require('../languages/html');
2226
+ const htmlParser = getParser('html');
2227
+ const blocks = html.extractScriptBlocks(content, htmlParser);
2228
+ const handlerRows = new Set();
2229
+ const tree = safeParse(htmlParser, content);
2230
+ const stack = tree ? [tree.rootNode] : [];
2231
+ while (stack.length > 0) {
2232
+ const node = stack.pop();
2233
+ if (node.type === 'attribute') {
2234
+ const attrName = node.namedChild(0)?.text || '';
2235
+ if (/^on/i.test(attrName) && node.text.includes(name)) {
2236
+ for (let row = node.startPosition.row; row <= node.endPosition.row; row++) handlerRows.add(row);
2237
+ }
2238
+ continue;
2239
+ }
2240
+ for (let i = 0; i < node.namedChildCount; i++) stack.push(node.namedChild(i));
2241
+ }
2242
+ return { script: html.buildVirtualJSContent(content, blocks), handlerRows };
2243
+ }
2244
+
2245
+ function scanFile(index, resolver, file, entry, language, lineNumbers) {
2246
+ const name = resolver.name;
2247
+ let content;
2248
+ try { content = index._readFile(file); } catch { return null; }
2249
+ let htmlView = null;
2250
+ if (language === 'html') {
2251
+ htmlView = htmlScriptView(content, name);
2252
+ language = 'javascript';
2253
+ }
2254
+ const parser = getParser(language);
2255
+ if (!parser) return null;
2256
+ const tree = htmlView ? safeParse(parser, htmlView.script)
2257
+ : index._getParsedTree?.(file, content, language) || safeParse(parser, content);
2258
+ const lines = new Map();
2259
+ const lineOf = row => {
2260
+ let slot = lines.get(row + 1);
2261
+ if (!slot) {
2262
+ slot = { edits: [], reviews: [], skips: 0, skipColumns: [], text: false, tokens: 0, macroBody: false };
2263
+ lines.set(row + 1, slot);
2264
+ }
2265
+ return slot;
2266
+ };
2267
+ for (const lineNo of lineNumbers) lineOf(lineNo - 1);
2268
+ if (!tree) {
2269
+ for (const slot of lines.values()) slot.reviews.push({ column: null, reason: 'unparsed-file' });
2270
+ return lines;
2271
+ }
2272
+ const nameBytes = Buffer.byteLength(name);
2273
+ const docComments = [];
2274
+ // The nodes holding each occurrence of the name's text (one native
2275
+ // lookup per occurrence, document order): name tokens on the ground rows,
2276
+ // and the text, macro-body and opaque-error leaves overlapping them.
2277
+ const source = htmlView ? htmlView.script : content;
2278
+ const occurrences = [];
2279
+ for (let at = source.indexOf(name); at >= 0; at = source.indexOf(name, at + 1)) occurrences.push(at);
2280
+ const collect = (root, rowList, marks) => {
2281
+ const found = [];
2282
+ const seen = new Set();
2283
+ const overlaps = node => {
2284
+ const start = node.startPosition.row;
2285
+ const end = node.endPosition.row;
2286
+ let lo = 0;
2287
+ let hi = rowList.length;
2288
+ while (lo < hi) {
2289
+ const mid = (lo + hi) >> 1;
2290
+ if (rowList[mid] < start) lo = mid + 1; else hi = mid;
2291
+ }
2292
+ return lo < rowList.length && rowList[lo] <= end;
2293
+ };
2294
+ const mark = (node, field) => {
2295
+ if (!marks) return;
2296
+ for (let row = node.startPosition.row; row <= node.endPosition.row; row++) {
2297
+ if (lines.has(row + 1)) lines.get(row + 1)[field] = true;
2298
+ }
2299
+ };
2300
+ for (const at of occurrences) {
2301
+ let node = root.descendantForIndex(at, at + name.length);
2302
+ // The smallest node holding the text: a token, a text or macro
2303
+ // leaf, or (between tokens) a container the walk would not read.
2304
+ while (node && !TOKEN_TYPES.has(node.type) && !TEXT_NODE.test(node.type) &&
2305
+ node.type !== 'preproc_arg' && !(node.type === 'ERROR' && node.namedChildCount === 0)) {
2306
+ node = null;
2307
+ }
2308
+ if (!node || seen.has(node.id) || !overlaps(node)) continue;
2309
+ seen.add(node.id);
2310
+ const type = node.type;
2311
+ if (TOKEN_TYPES.has(type)) {
2312
+ const length = node.endIndex - node.startIndex;
2313
+ if ((length === name.length || length === nameBytes) && node.text === name) found.push(node);
2314
+ continue;
2315
+ }
2316
+ if (type === 'preproc_arg' || type === 'ERROR') {
2317
+ mark(node, 'macroBody');
2318
+ continue;
2319
+ }
2320
+ // Text: leaves only (a template string's substitutions are
2321
+ // code, read as tokens of their own).
2322
+ if (node.namedChildCount !== 0 && !type.includes('comment')) continue;
2323
+ mark(node, 'text');
2324
+ if (resolver.family === 'csharp' && type === 'comment' && marks) docComments.push(node);
2325
+ if (resolver.family === 'python' && type === 'string_content' &&
2326
+ (pythonTypeExpressionString(node, entry) || pythonDunderAllEntry(node, name))) found.push(node);
2327
+ }
2328
+ return found;
2329
+ };
2330
+ // C/C++: read the tree the index reads (macro recoveries applied), and
2331
+ // fall back to the literal parse for lines a recovery blanked.
2332
+ const rows = lineNumbers.map(n => n - 1);
2333
+ let tokens = collect(tree.rootNode, rows, true);
2334
+ // A clean literal parse can still be a misreading the recovery
2335
+ // corrected (`API T (name)(..)`, fix #396): a token whose statement holds
2336
+ // a persisted blank reads through the recovered tree too.
2337
+ const persistedBlanks = resolver.family === 'c' && Array.isArray(entry.recoveryBlanks) &&
2338
+ entry.recoveryBlanks.length > 0 ? entry.recoveryBlanks : null;
2339
+ if (resolver.family === 'c' && ((tree.rootNode.hasError && tokens.some(statementHasError)) ||
2340
+ (persistedBlanks && tokens.some(node => statementHoldsBlank(node, persistedBlanks))))) {
2341
+ // Only when the literal parse breaks around a token: the recovery
2342
+ // re-parses the file.
2343
+ let module;
2344
+ try { module = LANGUAGES[language]?.module(); } catch { module = null; }
2345
+ // The blank ranges the index persisted for this content rebuild the
2346
+ // recovered tree with one parse (fix #387); stale or missing ones
2347
+ // replay the recovery.
2348
+ const blanks = Array.isArray(entry.recoveryBlanks) && entry.hash &&
2349
+ require('crypto').createHash('md5').update(content).digest('hex') === entry.hash
2350
+ ? entry.recoveryBlanks : null;
2351
+ const recoveredTree = typeof module?.recoveredTree === 'function'
2352
+ ? module.recoveredTree(content, parser, blanks) : null;
2353
+ if (recoveredTree) {
2354
+ const literal = tokens;
2355
+ tokens = collect(recoveredTree.rootNode, rows, false);
2356
+ // A recovery blanks text it reads as macros; a token it blanked
2357
+ // is still a token of the source: keep the literal one.
2358
+ const seen = new Set(tokens.map(node => `${node.startPosition.row}:${node.startPosition.column}`));
2359
+ for (const node of literal) {
2360
+ if (!seen.has(`${node.startPosition.row}:${node.startPosition.column}`)) tokens.push(node);
2361
+ }
2362
+ }
2363
+ }
2364
+ if (htmlView) {
2365
+ // Page text outside scripts is not code; handler attributes are code
2366
+ // UCN does not parse.
2367
+ const scriptRows = new Set(tokens.map(node => node.startPosition.row));
2368
+ for (const row of rows) {
2369
+ if (scriptRows.has(row)) continue;
2370
+ const slot = lines.get(row + 1);
2371
+ if (htmlView.handlerRows.has(row)) {
2372
+ slot.macroBody = true;
2373
+ slot.unparsedReason = 'html-event-handler';
2374
+ }
2375
+ else slot.text = true;
2376
+ }
2377
+ }
2378
+ // A type the language declares by binding a name (a Python `Alias =
2379
+ // ...` / `Alias: TypeAlias = ...`, a JS/TS variable holding a class) is
2380
+ // declared by that binding token, not shadowed by it.
2381
+ const pinLines = new Set([...resolver.group].filter(d => d.file === file && d.type === 'type')
2382
+ .map(d => (d.nameLine || d.startLine) - 1));
2383
+ const classified = tokens.map(node => {
2384
+ let role = node.type !== 'string_content' ? classify(node, language)
2385
+ : pythonDunderAllEntry(node, name) ? { role: 'dunder-all' } : { role: 'string-annotation' };
2386
+ if (role.role === 'skip' && (role.why === 'assignment' || role.why === 'variable') &&
2387
+ (resolver.family === 'python' || resolver.family === 'js') && pinLines.has(node.startPosition.row)) {
2388
+ role = { role: 'pin-binding' };
2389
+ }
2390
+ return { node, role };
2391
+ });
2392
+ const bindings = classified.filter(t => t.role.role === 'skip' && t.role.binding)
2393
+ .map(t => ({ node: t.node, scope: bindingScope(t.node, resolver.family, t.role), role: t.role }));
2394
+ const typeParams = classified.filter(t => t.role.role === 'skip' && t.role.typeParam)
2395
+ .map(t => ({ node: t.node, scope: typeParamScope(t.node) }));
2396
+ for (const token of classified) {
2397
+ const row = token.node.startPosition.row;
2398
+ const slot = lineOf(row);
2399
+ if (token.role.role === 'string-annotation') {
2400
+ decideStringAnnotation(resolver, file, entry, token.node, slot);
2401
+ continue;
2402
+ }
2403
+ slot.tokens++;
2404
+ resolver.siteNode = token.node;
2405
+ resolver.siteFile = file;
2406
+ // A position no rule decides is a review item, never an edit or a skip.
2407
+ const decision = decide(resolver, file, entry, token, bindings, typeParams) ||
2408
+ { verdict: 'unknown', reason: 'unmodelled-position' };
2409
+ resolver.siteNode = null;
2410
+ // An `__all__` entry is a code reference: once edited it is no
2411
+ // longer a text-only review item.
2412
+ if (token.role.role === 'dunder-all') slot.stringAnnotation = true;
2413
+ if (decision.verdict === 'yes') slot.edits.push(token.node.startPosition.column);
2414
+ else if (decision.verdict === 'unknown') {
2415
+ slot.reviews.push({ column: token.node.startPosition.column, reason: decision.reason || 'unresolved' });
2416
+ } else {
2417
+ slot.skips++;
2418
+ slot.skipColumns.push(token.node.startPosition.column);
2419
+ }
2420
+ }
2421
+ for (const comment of docComments) decideDocCrefs(resolver, file, comment, lineOf(comment.startPosition.row));
2422
+ return lines;
2423
+ }
2424
+
2425
+ /**
2426
+ * C# XML documentation `cref` attributes are references the compiler
2427
+ * resolves (CS1574 when documentation is generated): each spelling of the
2428
+ * name in a cref is decided like the code reference it stands for, in the
2429
+ * scope of the documented member.
2430
+ */
2431
+ function decideDocCrefs(resolver, file, comment, slot) {
2432
+ if (comment.startPosition.row !== comment.endPosition.row) return;
2433
+ const text = comment.text;
2434
+ if (!/^\/\/\//.test(text)) return;
2435
+ const line = comment.startPosition.row + 1;
2436
+ const crefRe = /\bcref\s*=\s*"([^"]*)"/g;
2437
+ let match;
2438
+ let handled = false;
2439
+ while ((match = crefRe.exec(text)) !== null) {
2440
+ const value = match[1];
2441
+ const valueStart = match.index + match[0].indexOf('"') + 1;
2442
+ const idRe = /[A-Za-z_][A-Za-z0-9_]*/g;
2443
+ let id;
2444
+ const body = value.replace(/^[A-Z]:/, ch => ' '.repeat(ch.length));
2445
+ while ((id = idRe.exec(body)) !== null) {
2446
+ if (id[0] !== resolver.name) continue;
2447
+ // The dotted path written before the name, at the same level.
2448
+ let start = id.index;
2449
+ while (start > 1 && body[start - 1] === '.' && /[A-Za-z0-9_]/.test(body[start - 2])) {
2450
+ let s2 = start - 1;
2451
+ while (s2 > 0 && /[A-Za-z0-9_]/.test(body[s2 - 1])) s2--;
2452
+ start = s2;
2453
+ }
2454
+ const qualifier = body.slice(start, id.index).replace(/\.$/, '');
2455
+ resolver.siteNode = comment;
2456
+ resolver.siteFile = file;
2457
+ const verdict = qualifier
2458
+ ? resolver.resolveCSharpQualified(file, line, qualifier, { role: 'type' })
2459
+ : resolver.resolveName(file, line);
2460
+ resolver.siteNode = null;
2461
+ const column = comment.startPosition.column + valueStart + id.index;
2462
+ slot.tokens++;
2463
+ handled = true;
2464
+ if (verdict.verdict === 'yes') slot.edits.push(column);
2465
+ else if (verdict.verdict === 'unknown') slot.reviews.push({ column, reason: 'doc-cref' });
2466
+ else {
2467
+ slot.skips++;
2468
+ slot.skipColumns.push(column);
2469
+ }
2470
+ }
2471
+ }
2472
+ if (handled) slot.stringAnnotation = true;
2473
+ }
2474
+
2475
+ function shadowOf(token, bindings, space, family) {
2476
+ let verdict = null;
2477
+ for (const binding of bindings) {
2478
+ if (sameNode(binding.node, token.node)) continue;
2479
+ if (!binding.scope || !contains(binding.scope, token.node)) continue;
2480
+ // Type positions are a separate namespace outside Python/JS values.
2481
+ if (space === 'type' && family !== 'python') continue;
2482
+ if (binding.role.patternBinding) return 'unknown';
2483
+ // A Python module-scope binding is the module variable itself, the
2484
+ // one its imports and class statements bind too (fix #393): the
2485
+ // module's name lookup decides, not the binding alone.
2486
+ if (family === 'python' && binding.scope.type === 'module') {
2487
+ verdict = verdict || 'module';
2488
+ continue;
2489
+ }
2490
+ const before = binding.node.startIndex < token.node.startIndex;
2491
+ if (family === 'python' && binding.scope.type === 'class_definition') {
2492
+ // A class attribute is visible in the class body only (never in
2493
+ // its methods), from its assignment on: `Entry = Entry` reads
2494
+ // the module's Entry.
2495
+ const method = ancestor(token.node, new Set(['function_definition', 'lambda']));
2496
+ if (method && contains(binding.scope, method)) continue;
2497
+ const statement = ancestor(binding.node, new Set(['expression_statement']));
2498
+ if (!before || (statement && contains(statement, token.node))) continue;
2499
+ return 'no';
2500
+ }
2501
+ if (before || family === 'python' || family === 'csharp') return 'no';
2502
+ verdict = 'unknown';
2503
+ }
2504
+ return verdict;
2505
+ }
2506
+
2507
+ /** Does the literal parse break inside the declaration or statement
2508
+ * holding this token? */
2509
+ function statementHasError(node) {
2510
+ for (let current = node.parent; current; current = current.parent) {
2511
+ if (current.type === 'ERROR') return true;
2512
+ if (/(declaration|definition|statement|specifier|_list|declarator)$/.test(current.type)) {
2513
+ if (current.hasError) return true;
2514
+ if (/(declaration|definition|statement)$/.test(current.type)) return false;
2515
+ }
2516
+ }
2517
+ return false;
2518
+ }
2519
+
2520
+ /** The class path a qualified declarator's scope names, without its
2521
+ * leading namespace-looking segments: `ns::List<K>::Node::f` -> the scope
2522
+ * segments `List<K>::Node` (the last two at most). */
2523
+ function qualifiedScopeText(qualified) {
2524
+ const scopes = [];
2525
+ let current = qualified;
2526
+ while (current?.type === 'qualified_identifier') {
2527
+ const scope = current.childForFieldName('scope');
2528
+ if (scope) scopes.push(textOf(scope));
2529
+ current = current.childForFieldName('name');
2530
+ }
2531
+ return scopes.length > 0 ? scopes.slice(-2).join('::') : null;
2532
+ }
2533
+
2534
+ /** Does a persisted recovery blank fall inside the declaration or
2535
+ * statement holding this token? (`blanks`: sorted [start, end] ranges) */
2536
+ function statementHoldsBlank(node, blanks) {
2537
+ let statement = null;
2538
+ for (let current = node.parent; current; current = current.parent) {
2539
+ if (/(declaration|definition|statement)$/.test(current.type)) { statement = current; break; }
2540
+ }
2541
+ if (!statement) return false;
2542
+ const start = statement.startIndex;
2543
+ const end = statement.endIndex;
2544
+ let lo = 0;
2545
+ let hi = blanks.length;
2546
+ while (lo < hi) {
2547
+ const mid = (lo + hi) >> 1;
2548
+ if (blanks[mid][1] <= start) lo = mid + 1; else hi = mid;
2549
+ }
2550
+ return lo < blanks.length && blanks[lo][0] < end;
2551
+ }
2552
+
2553
+ function hasErrorNeighbor(node) {
2554
+ for (let current = node, depth = 0; current && depth < 2; current = current.parent, depth++) {
2555
+ const parent = current.parent;
2556
+ if (!parent) break;
2557
+ for (let i = 0; i < parent.childCount; i++) {
2558
+ if (parent.child(i).type === 'ERROR') return true;
2559
+ }
2560
+ }
2561
+ return false;
2562
+ }
2563
+
2564
+ /**
2565
+ * A token that is a whole argument of an invocation of a project
2566
+ * function-like macro whose replacement list also uses that parameter as a
2567
+ * member of another scope (`ExitCodes::name`): the argument names that
2568
+ * member too, so renaming it with the type breaks the expansion.
2569
+ */
2570
+ function macroArgumentOtherUse(resolver, file, node) {
2571
+ if (resolver.family !== 'c') return null;
2572
+ // `M(Widget)` as a call, or read as a declarator `M(Widget)` whose
2573
+ // parameter list holds the argument (a class-body invocation).
2574
+ let argument = node;
2575
+ let list = node.parent;
2576
+ const lists = new Set(['argument_list', 'parameter_list', 'parenthesized_declarator']);
2577
+ for (let hops = 0; list && hops < 2 && !lists.has(list.type); hops++) {
2578
+ argument = list;
2579
+ list = list.parent;
2580
+ }
2581
+ if (!list || !lists.has(list.type)) return null;
2582
+ const owner = list.parent;
2583
+ const callee = owner?.type === 'call_expression' ? owner.childForFieldName('function')
2584
+ : owner?.type === 'function_declarator' ? owner.childForFieldName('declarator')
2585
+ // `M(Widget);` in a class body: type `M`, declarator `(Widget)`.
2586
+ : list.type === 'parenthesized_declarator' ? owner?.childForFieldName('type') : null;
2587
+ if (!callee || !/identifier$/.test(callee.type)) return null;
2588
+ const args = list.type === 'parenthesized_declarator' ? [argument]
2589
+ : (list.namedChildren || []).filter(child => !child.type.endsWith('comment'));
2590
+ const index = args.findIndex(arg => sameNode(arg, argument));
2591
+ if (index < 0 || argument.text !== node.text) return null;
2592
+ const { includeClosure } = require('./cpp-scope');
2593
+ const visible = includeClosure(resolver.index, file);
2594
+ const macros = (resolver.index.symbols.get(callee.text) || []).filter(d => d.type === 'macro' &&
2595
+ d.functionLike && (d.file === file || visible.has(d.file)));
2596
+ const effects = macros.flatMap(d => (d.macroParamEffects || []).filter(e => e.paramIndex === index));
2597
+ const other = effects.find(e => e.kind === 'qualified' && e.qualifier !== resolver.name);
2598
+ return other ? { verdict: 'unknown', reason: 'macro-argument-other-use' } : null;
2599
+ }
2600
+
2601
+ function decide(resolver, file, entry, token, bindings, typeParams) {
2602
+ const { node, role } = token;
2603
+ const line = node.startPosition.row + 1;
2604
+ const family = resolver.family;
2605
+ const macroUse = macroArgumentOtherUse(resolver, file, node);
2606
+ if (macroUse) return macroUse;
2607
+ switch (role.role) {
2608
+ case 'pin-binding':
2609
+ return { verdict: 'yes' };
2610
+ case 'dunder-all':
2611
+ // A Python `__all__` entry names the module's own binding of the
2612
+ // name: renamed exactly when that binding is the renamed type.
2613
+ return resolver.resolveName(file, line);
2614
+ case 'skip':
2615
+ // A declarator the grammar read next to a parse error may be
2616
+ // the type itself (`MACRO Widget value` with MACRO taken for
2617
+ // the type): unknown. The C/C++ recovery blanks such
2618
+ // declaration-specifier macros before the tokens are read.
2619
+ if (role.binding && hasErrorNeighbor(node)) {
2620
+ return { verdict: 'unknown', reason: 'parse-error' };
2621
+ }
2622
+ // A Python module-scope assignment rebinding a module variable
2623
+ // an import of the renamed type also binds (`except
2624
+ // ImportError: X = None`, fix #393) is part of that variable.
2625
+ if (family === 'python' && role.binding && (role.why === 'assignment' || role.why === 'variable') &&
2626
+ bindingScope(node, family, role)?.type === 'module' &&
2627
+ resolver.pythonModuleBindsPin(file, line)) {
2628
+ return { verdict: 'unknown', reason: 'module-rebinding' };
2629
+ }
2630
+ return { verdict: 'no', reason: role.why };
2631
+ case 'unknown':
2632
+ return { verdict: 'unknown', reason: role.why || 'unmodelled-position' };
2633
+ case 'template': return decideTemplateToken(resolver, file, token);
2634
+ case 'shorthand': {
2635
+ const verdict = resolveUnqualifiedToken(resolver, file, token, bindings, typeParams, 'value');
2636
+ return verdict.verdict === 'no' ? verdict : { verdict: 'unknown', reason: 'shorthand-property' };
2637
+ }
2638
+ case 'decl': return decideDecl(resolver, file, entry, token, bindings, typeParams);
2639
+ case 'ctor': {
2640
+ // A constructor or destructor repeats the name of the class that
2641
+ // declares it: its verdict is the verdict of that class's own
2642
+ // name token (fix #394).
2643
+ const classNode = role.classNode;
2644
+ let classNameNode = classNode?.childForFieldName('name');
2645
+ // `template <> struct Box<int>`, `struct Outer::Inner`: the
2646
+ // class name is the last plain segment.
2647
+ while (classNameNode && (classNameNode.type === 'template_type' ||
2648
+ classNameNode.type === 'qualified_identifier' || classNameNode.type === 'generic_name')) {
2649
+ classNameNode = classNameNode.childForFieldName('name') || classNameNode.namedChild(0);
2650
+ }
2651
+ if (!classNameNode || textOf(classNameNode) !== node.text) {
2652
+ // No enclosing class of that name (a member the parse left
2653
+ // outside its class): the owner is not known.
2654
+ return { verdict: 'unknown', reason: 'constructor-owner' };
2655
+ }
2656
+ return decideDecl(resolver, file, entry, { node: classNameNode, role: { role: 'decl', declNode: classNode } },
2657
+ bindings, typeParams);
2658
+ }
2659
+ case 'ctor-qualified': {
2660
+ // `Widget::Widget()` / `app::Widget::~Widget()`: the member names
2661
+ // the class its qualifier spells.
2662
+ const spelled = cppPrefix(node);
2663
+ if (spelled == null) return { verdict: 'unknown', reason: 'constructor-owner' };
2664
+ const segments = spelled.split('::');
2665
+ // `Sink<Mutex>::Sink(...)`: template arguments are not part of
2666
+ // the class name the constructor repeats (fix #387).
2667
+ const last = require('./cpp-scope').stripTemplateArguments(segments.pop()).trim();
2668
+ if (last !== resolver.name) return { verdict: 'no', reason: 'other-type' };
2669
+ return resolver.resolveCFamily(file, line, last, segments.length > 0 ? segments.join('::') : null);
2670
+ }
2671
+ case 'meminit': {
2672
+ // A member named like the type is initialized as a member.
2673
+ const owner = resolver.enclosingClassAt(file, line);
2674
+ const members = (resolver.index.symbols.get(resolver.name) || []).some(d =>
2675
+ d.file && d.className === owner && (d.type === 'field' || d.memberType === 'field'));
2676
+ if (members) return { verdict: 'no', reason: 'member' };
2677
+ return resolveUnqualifiedToken(resolver, file, token, bindings, typeParams, 'type');
2678
+ }
2679
+ case 'import': return decideImport(resolver, file, entry, token);
2680
+ case 'member':
2681
+ return decideMember(resolver, file, token, bindings);
2682
+ case 'gofield':
2683
+ return resolver.goFieldVerdict(node, file);
2684
+ case 'macro': return decideMacroToken(resolver, file, token, bindings, typeParams);
2685
+ case 'type': case 'value': {
2686
+ if (role.qualifierSegment) return decideQualifierSegment(resolver, file, token, bindings, typeParams);
2687
+ if (role.cppQualified) {
2688
+ const prefix = cppPrefix(node);
2689
+ if (prefix) return resolver.resolveCFamily(file, line, resolver.name, prefix);
2690
+ }
2691
+ if (role.qualifier) {
2692
+ return resolver.resolveQualified(file, line, role.qualifier, null, { role: role.role });
2693
+ }
2694
+ if (role.aliasQualified) return { verdict: 'unknown', reason: 'extern-alias' };
2695
+ if (role.elaborated && family === 'c') {
2696
+ return resolver.resolveCFamily(file, line, resolver.name, null, { tag: role.tag });
2697
+ }
2698
+ return resolveUnqualifiedToken(resolver, file, token, bindings, typeParams, role.role);
2699
+ }
2700
+ default:
2701
+ return { verdict: 'unknown', reason: 'unmodelled-position' };
2702
+ }
2703
+ }
2704
+
2705
+ const MEMBER_SHADOW_FAMILIES = new Set(['java', 'csharp', 'c']);
2706
+ const CLASS_BODY_OWNERS = new Set(['class_declaration', 'struct_declaration', 'record_declaration',
2707
+ 'interface_declaration', 'enum_declaration', 'record_struct_declaration', 'class_specifier',
2708
+ 'struct_specifier', 'union_specifier']);
2709
+ const VALUE_MEMBER_KINDS = new Set(['field', 'property', 'event', 'constant', 'state']);
2710
+
2711
+ /**
2712
+ * A field, property or event of the enclosing class named like the type
2713
+ * hides the type in an expression (Java obscuring, C# and C++ member
2714
+ * lookup): `Alignment = value;` in a class with an `Alignment` property.
2715
+ * When the member's own type is the renamed type, `Alignment.X` may still
2716
+ * denote the type (C# "Color Color"): review. Null when no member hides it.
2717
+ */
2718
+ function memberShadowOf(resolver, file, node) {
2719
+ if (!MEMBER_SHADOW_FAMILIES.has(resolver.family)) return null;
2720
+ const owners = [];
2721
+ for (let current = node.parent; current; current = current.parent) {
2722
+ if (!CLASS_BODY_OWNERS.has(current.type)) continue;
2723
+ const nameNode = current.childForFieldName('name');
2724
+ if (nameNode) owners.push(textOf(nameNode));
2725
+ }
2726
+ if (owners.length === 0) {
2727
+ // Out-of-line C++ member definitions name their class.
2728
+ const site = resolver.family === 'c' ? resolver.cppSiteContext(file, node.startPosition.row + 1) : null;
2729
+ if (site?.className) owners.push(site.className.split('::').pop());
2730
+ }
2731
+ if (owners.length === 0) return null;
2732
+ // An invoked simple name (`LogEvent(ts, level)`) is looked up among the
2733
+ // members of the enclosing classes first: a method of that name (C#
2734
+ // `static LogEvent LogEvent(..)`, a C++ member function) hides the
2735
+ // type there (fix #393). A constructor is the class's own name.
2736
+ const invokedAt = node.parent && ((node.parent.type === 'invocation_expression' ||
2737
+ node.parent.type === 'call_expression') && isField(node, 'function'));
2738
+ if (invokedAt) {
2739
+ const scopeNames = new Set(owners);
2740
+ let level = [...owners];
2741
+ for (let hop = 0; hop < 8 && level.length > 0; hop++) {
2742
+ level = [...new Set(level.flatMap(owner => resolver.index._getInheritanceParents?.(owner, file) || []))]
2743
+ .filter(owner => owner && !scopeNames.has(owner));
2744
+ for (const owner of level) scopeNames.add(owner);
2745
+ }
2746
+ const methods = (resolver.index.symbols.get(resolver.name) || []).filter(d =>
2747
+ scopeNames.has(d.className) && d.className !== resolver.name && !d.isConstructor &&
2748
+ (d.type === 'method' || d.memberType === 'method') &&
2749
+ familyOf(resolver.index.files.get(d.file)?.language) === resolver.family);
2750
+ if (methods.length > 0) return { verdict: 'no', reason: 'member' };
2751
+ }
2752
+ const members = (resolver.index.symbols.get(resolver.name) || []).filter(d =>
2753
+ owners.includes(d.className) && (VALUE_MEMBER_KINDS.has(d.type) || VALUE_MEMBER_KINDS.has(d.memberType)) &&
2754
+ familyOf(resolver.index.files.get(d.file)?.language) === resolver.family);
2755
+ if (members.length === 0) return null;
2756
+ const accessed = node.parent && ((node.parent.type === 'member_access_expression' &&
2757
+ isField(node, 'expression')) || (node.parent.type === 'field_access' && isField(node, 'object')) ||
2758
+ (node.parent.type === 'method_invocation' && isField(node, 'object')));
2759
+ const typedAsPin = members.some(d => String(d.fieldType || d.returnType || '').replace(/[?*&\s]/g, '')
2760
+ .replace(/<.*$/, '').split(/[.:]/).pop() === resolver.name);
2761
+ if (accessed && typedAsPin) return { verdict: 'unknown', reason: 'member-named-like-type' };
2762
+ return { verdict: 'no', reason: 'member' };
2763
+ }
2764
+
2765
+ function resolveUnqualifiedToken(resolver, file, token, bindings, typeParams, space) {
2766
+ const family = resolver.family;
2767
+ if (space === 'value' || family === 'python' || family === 'js') {
2768
+ const shadow = shadowOf(token, bindings, space, family);
2769
+ if (shadow === 'no') return { verdict: 'no', reason: 'local-binding' };
2770
+ if (shadow === 'unknown') return { verdict: 'unknown', reason: 'local-binding' };
2771
+ if (shadow === 'module') {
2772
+ // The module variable holds the renamed type only when an
2773
+ // import or class statement of this module binds it too; the
2774
+ // assignments alone name another value.
2775
+ if (!resolver.pythonModuleBindsPin(file, token.node.startPosition.row + 1)) {
2776
+ return { verdict: 'no', reason: 'local-binding' };
2777
+ }
2778
+ return { verdict: 'unknown', reason: 'module-rebinding' };
2779
+ }
2780
+ }
2781
+ if (space === 'value') {
2782
+ const member = memberShadowOf(resolver, file, token.node);
2783
+ if (member) return member;
2784
+ }
2785
+ if (typeParams.some(p => p.scope && contains(p.scope, token.node) && !sameNode(p.node, token.node))) {
2786
+ return { verdict: 'no', reason: 'type-parameter' };
2787
+ }
2788
+ const line = token.node.startPosition.row + 1;
2789
+ if (family === 'c' && resolver.index.files.get(file)?.language === 'c' && resolver.pinIsTag()) {
2790
+ // Plain identifiers name typedefs and objects in C, never tags.
2791
+ return { verdict: 'no', reason: 'ordinary-namespace' };
2792
+ }
2793
+ return resolver.resolveName(file, line);
2794
+ }
2795
+
2796
+ // A C declaration whose name lives in the ordinary identifier namespace: a
2797
+ // typedef, or an anonymous struct/union/enum named by its typedef
2798
+ // (`typedef struct { .. } T;` declares no tag, fix #396).
2799
+ function cOrdinaryName(d) {
2800
+ return d.type === 'type' || !!d.typedefName;
2801
+ }
2802
+
2803
+ function declVerdict(resolver, file, declNode, role = {}, nameLine = null) {
2804
+ const endLine = declNode.endPosition.row + 1;
2805
+ const startLine = declNode.startPosition.row + 1;
2806
+ const kindOk = d => (!role.typedef || cOrdinaryName(d)) && (!role.tag || !cOrdinaryName(d));
2807
+ // A definition that records the line of its name (grouped Go specs,
2808
+ // annotated or decorated declarations) is matched there first.
2809
+ const named = nameLine != null ? resolver.fileTypes(file).filter(d => d.nameLine === nameLine && kindOk(d)) : [];
2810
+ if (named.length > 0) return resolver.judge(named);
2811
+ const defs = resolver.fileTypes(file).filter(d => d.nameLine == null && (d.endLine === endLine ||
2812
+ (d.startLine <= startLine + 1 && d.startLine >= startLine - 3 &&
2813
+ d.endLine >= endLine - 1 && d.endLine <= endLine + 1)) &&
2814
+ // A C typedef name and a struct tag of one spelling are two entities.
2815
+ (!role.typedef || cOrdinaryName(d)) && (!role.tag || !cOrdinaryName(d)));
2816
+ if (defs.length === 0) return null;
2817
+ return resolver.judge(defs);
2818
+ }
2819
+
2820
+ function decideDecl(resolver, file, entry, token, bindings, typeParams) {
2821
+ const verdict = declVerdict(resolver, file, token.role.declNode, token.role,
2822
+ token.node.startPosition.row + 1);
2823
+ if (verdict) {
2824
+ // The pin's own name token is the definition edit plan makes itself.
2825
+ return verdict;
2826
+ }
2827
+ // A declaration UCN did not index (forward declaration, ambient or
2828
+ // generated declaration): resolve it as a reference.
2829
+ const line = token.node.startPosition.row + 1;
2830
+ // A typedef name is its own declaration (`typedef struct W W;`), never
2831
+ // the struct tag it aliases.
2832
+ if (resolver.family === 'c' && token.role.typedef && resolver.pinIsTag()) {
2833
+ return { verdict: 'no', reason: 'typedef-name' };
2834
+ }
2835
+ if (resolver.family === 'c') {
2836
+ return resolver.resolveCFamily(file, line, resolver.name, null,
2837
+ token.role.tag ? { tag: token.role.tag } : {});
2838
+ }
2839
+ return { verdict: 'unknown', reason: 'unindexed-declaration' };
2840
+ }
2841
+
2842
+ function decideImport(resolver, file, entry, token) {
2843
+ const { node, role } = token;
2844
+ const line = node.startPosition.row + 1;
2845
+ const name = resolver.name;
2846
+ switch (resolver.family) {
2847
+ case 'js': {
2848
+ const statement = ancestor(node, new Set(['import_statement', 'export_statement']));
2849
+ const source = statement?.childForFieldName('source');
2850
+ const module = role.requireSource || (source ? source.text.replace(/^['"`]|['"`]$/g, '') : null);
2851
+ if (!module) return { verdict: 'unknown', reason: 'import-source' };
2852
+ return resolver.importBindingVerdict(file, entry, { name, module, kind: 'named' }, name);
2853
+ }
2854
+ case 'python': {
2855
+ const module = role.moduleText;
2856
+ if (!module) return { verdict: 'unknown', reason: 'import-source' };
2857
+ const moduleFile = resolver.moduleFile(entry, module);
2858
+ if (!moduleFile) {
2859
+ const binding = (entry.importBindings || []).find(b => b.name === name && b.line === line) ||
2860
+ { name, module, kind: 'from' };
2861
+ return resolver.importBindingVerdict(file, entry, { ...binding, alias: undefined }, name);
2862
+ }
2863
+ const reach = resolver.chase(moduleFile, name);
2864
+ return reach === 'yes' ? { verdict: 'yes' } : reach === 'no' ? { verdict: 'no', reason: 'other-module' }
2865
+ : { verdict: 'unknown', reason: 'import-chain' };
2866
+ }
2867
+ case 'rust': {
2868
+ if (role.useHead) return { verdict: 'unknown', reason: 'use-path-head' };
2869
+ const qualifier = rustUsePrefix(role.original || node);
2870
+ if (qualifier == null) return { verdict: 'unknown', reason: 'use-path' };
2871
+ if (!qualifier) return { verdict: 'unknown', reason: 'use-path-head' };
2872
+ return resolver.resolveRustPath(file, line, qualifier, name, 0);
2873
+ }
2874
+ case 'java':
2875
+ return resolver.resolveJavaQualified(file, line, role.qualifierText, { role: 'type' });
2876
+ default:
2877
+ return { verdict: 'unknown', reason: 'import' };
2878
+ }
2879
+ }
2880
+
2881
+ /**
2882
+ * The module path a `use` tree puts before a token: `crate::widget` for the
2883
+ * `Widget` of `use crate::{widget::{self, Widget as W}}`. Null when the
2884
+ * token is not the last segment of a use path.
2885
+ */
2886
+ function rustUsePrefix(node) {
2887
+ const parts = [];
2888
+ let current = node;
2889
+ if (current.parent?.type === 'scoped_identifier') {
2890
+ if (!isField(current, 'name')) return null;
2891
+ const path = current.parent.childForFieldName('path');
2892
+ if (path) parts.unshift(textOf(path));
2893
+ current = current.parent;
2894
+ }
2895
+ for (let a = current.parent; a && a.type !== 'use_declaration'; a = a.parent) {
2896
+ if (a.type === 'scoped_use_list') {
2897
+ const path = a.childForFieldName('path');
2898
+ if (path && !contains(path, node)) parts.unshift(textOf(path));
2899
+ else if (path) return null;
2900
+ }
2901
+ }
2902
+ return parts.join('::');
2903
+ }
2904
+
2905
+ function decideMember(resolver, file, token, bindings) {
2906
+ const { node, role } = token;
2907
+ const receiver = role.receiver;
2908
+ const line = node.startPosition.row + 1;
2909
+ if (!receiver) return { verdict: 'unknown', reason: 'member' };
2910
+ const family = resolver.family;
2911
+ // Receivers that are values: never a namespace for the type name. The
2912
+ // keyword receivers (`this`, `super`, C# `base`, Rust `self`) are node
2913
+ // types of their own; an identifier spelled like one is an ordinary
2914
+ // name (`from . import base` then `base.Message`, a Go package `base`).
2915
+ // Python's `self`/`cls` parameters are values by convention.
2916
+ const valueReceiver = (family === 'python' && /^(self|cls)$/.test(receiver.text)) ||
2917
+ !/^(identifier|scoped_identifier|field_access|attribute|member_expression|package_identifier|member_access_expression|qualified_name|generic_name|scoped_type_identifier|type_identifier)$/.test(receiver.type);
2918
+ if (valueReceiver) {
2919
+ if (family === 'python' && /^(self|cls)$/.test(receiver.text)) {
2920
+ // `self.Inner` inside the class that nests Inner names it.
2921
+ const owners = [...resolver.group].map(d => resolver.containerOf(d))
2922
+ .filter(owner => owner && TYPE_DECL_KINDS.has(owner.type));
2923
+ if (owners.length > 0) {
2924
+ const inside = owners.some(owner => owner.file === file && owner.startLine <= line && owner.endLine >= line);
2925
+ return inside ? { verdict: 'yes' } : { verdict: 'unknown', reason: 'nested-type' };
2926
+ }
2927
+ }
2928
+ return family === 'go' ? resolver.goFieldVerdict(node, file) : { verdict: 'no', reason: 'member' };
2929
+ }
2930
+ const verdict = resolver.resolveQualified(file, line, receiver, null, { role: 'member' });
2931
+ if (family === 'go' && verdict.verdict === 'unknown' && verdict.reason === 'qualified-name') {
2932
+ return resolver.goFieldVerdict(node, file);
2933
+ }
2934
+ return verdict;
2935
+ }
2936
+
2937
+ function decideQualifierSegment(resolver, file, token, bindings, typeParams) {
2938
+ // C++: `Widget::f` / `app::Widget::f`: the segment's own qualifier is
2939
+ // the scope of the enclosing qualified_identifier chain.
2940
+ const { node } = token;
2941
+ const line = node.startPosition.row + 1;
2942
+ const prefix = cppPrefix(node, true);
2943
+ if (prefix) return resolver.resolveCFamily(file, line, resolver.name, prefix);
2944
+ if (typeParams.some(p => p.scope && contains(p.scope, node) && !sameNode(p.node, node))) {
2945
+ return { verdict: 'no', reason: 'type-parameter' };
2946
+ }
2947
+ return resolver.resolveCFamily(file, line, resolver.name, null);
2948
+ }
2949
+
2950
+ /**
2951
+ * The qualifier written before a C++ name token, e.g. `app` for the
2952
+ * `Widget` of `app::Widget::make()` (`asScope`: the token is itself a
2953
+ * scope segment), or the full spelling up to and including the token's
2954
+ * class for a constructor name `app::Widget::Widget` (default). Null when
2955
+ * the token is not inside a qualified name.
2956
+ */
2957
+ function cppPrefix(node, asScope = false) {
2958
+ let wrapped = node;
2959
+ if ((wrapped.parent?.type === 'template_type' || wrapped.parent?.type === 'template_function') &&
2960
+ isField(wrapped, 'name')) wrapped = wrapped.parent;
2961
+ if (wrapped.parent?.type === 'destructor_name') wrapped = wrapped.parent;
2962
+ let qualified = wrapped.parent;
2963
+ if (qualified?.type !== 'qualified_identifier') return null;
2964
+ const parts = [];
2965
+ if (isField(wrapped, 'name')) {
2966
+ const scope = qualified.childForFieldName('scope');
2967
+ parts.unshift(textOf(scope));
2968
+ } else if (!isField(wrapped, 'scope')) {
2969
+ return null;
2970
+ }
2971
+ while (qualified.parent?.type === 'qualified_identifier' && isField(qualified, 'name')) {
2972
+ qualified = qualified.parent;
2973
+ parts.unshift(textOf(qualified.childForFieldName('scope')));
2974
+ }
2975
+ const spelled = parts.filter(Boolean).join('::');
2976
+ if (asScope) return spelled || null;
2977
+ return spelled || null;
2978
+ }
2979
+
2980
+ /**
2981
+ * The path written before a token inside a Rust token tree
2982
+ * (`crate::a::Widget`, `$crate::m::Widget`, `::core::x::Widget`): '' when
2983
+ * the token has no qualifier, null when the tokens before it do not form
2984
+ * a plain path (generic arguments, expressions).
2985
+ */
2986
+ function tokenTreePath(node) {
2987
+ const segments = [];
2988
+ let previous = node.previousSibling;
2989
+ while (previous && previous.type === '::') {
2990
+ const segment = previous.previousSibling;
2991
+ if (segment && (segment.type === 'identifier' || segment.type === 'crate' ||
2992
+ segment.type === 'self' || segment.type === 'super' ||
2993
+ (segment.type === 'metavariable' && segment.text === '$crate'))) {
2994
+ segments.unshift(segment.text);
2995
+ previous = segment.previousSibling;
2996
+ continue;
2997
+ }
2998
+ if (segment && (segment.text === '>' || segment.type === 'metavariable')) return null;
2999
+ segments.unshift('');
3000
+ break;
3001
+ }
3002
+ if (segments.length === 0 && node.parent?.type === 'token_tree' &&
3003
+ node.parent.child(0)?.text === '{' && node.parent.previousSibling?.type === '::') {
3004
+ // `a::b::{Widget, Other}`: the braced group's own path.
3005
+ const prior = node.previousSibling;
3006
+ if (prior && prior.type !== ',' && prior.text !== '{') return null;
3007
+ return tokenTreePath(node.parent);
3008
+ }
3009
+ return segments.join('::');
3010
+ }
3011
+
3012
+ function decideTemplateToken(resolver, file, token) {
3013
+ // A macro_rules! body binds its paths where the macro expands; `$crate`
3014
+ // always names the defining crate.
3015
+ const { node } = token;
3016
+ const line = node.startPosition.row + 1;
3017
+ const next = node.nextSibling;
3018
+ if (next?.type === '!') return { verdict: 'no', reason: 'macro-name' };
3019
+ const previous = node.previousSibling;
3020
+ if (previous && previous.type === '.') return { verdict: 'no', reason: 'member' };
3021
+ const qualifier = tokenTreePath(node);
3022
+ if (qualifier === '') {
3023
+ // A bare name binds where the macro expands: decide it at every
3024
+ // project invocation of the macro.
3025
+ const definition = ancestor(node, new Set(['macro_definition']));
3026
+ const macroName = definition?.childForFieldName('name')?.text;
3027
+ const sites = macroName ? resolver.rustMacroInvocations(macroName) : [];
3028
+ if (sites.length === 0) return { verdict: 'unknown', reason: 'macro-template' };
3029
+ const verdicts = sites.map(site => resolver.resolveRustName(site.file, site.line, resolver.name));
3030
+ const combined = resolver.combine(verdicts);
3031
+ return combined.verdict === 'unknown' ? { verdict: 'unknown', reason: 'macro-template' } : combined;
3032
+ }
3033
+ if (qualifier == null) return { verdict: 'unknown', reason: 'macro-template' };
3034
+ const crateRooted = qualifier.startsWith('$crate') || qualifier.startsWith('crate') || qualifier.startsWith('::');
3035
+ const verdict = resolver.resolveRustPath(file, line, qualifier.replace(/^\$crate/, 'crate'), resolver.name, 0);
3036
+ if (verdict.verdict === 'no' || crateRooted) return verdict;
3037
+ return { verdict: 'unknown', reason: 'macro-template' };
3038
+ }
3039
+
3040
+ function decideMacroToken(resolver, file, token, bindings, typeParams) {
3041
+ const { node } = token;
3042
+ const previous = node.previousSibling;
3043
+ const next = node.nextSibling;
3044
+ if (next?.type === '!') return { verdict: 'no', reason: 'macro-name' };
3045
+ if (previous && (previous.type === '.' || previous.text === '.')) return { verdict: 'no', reason: 'member' };
3046
+ if (previous && (previous.type === '::' || previous.text === '::')) {
3047
+ const qualifier = tokenTreePath(node);
3048
+ if (qualifier == null) return { verdict: 'unknown', reason: 'macro-path' };
3049
+ return resolver.resolveRustPath(file, node.startPosition.row + 1, qualifier, resolver.name, 0);
3050
+ }
3051
+ if (next && next.text === ':' ) return { verdict: 'unknown', reason: 'macro-argument' };
3052
+ if (bindings.some(b => b.scope && contains(b.scope, node))) {
3053
+ return { verdict: 'unknown', reason: 'local-binding' };
3054
+ }
3055
+ const verdict = resolveUnqualifiedToken(resolver, file, token, bindings, typeParams, 'value');
3056
+ return verdict;
3057
+ }
3058
+
3059
+ // Generic types of `typing` / the builtins whose subscript arguments are
3060
+ // type expressions (string arguments are forward references).
3061
+ const PY_TYPING_MODULES = new Set(['typing', 'typing_extensions', 'collections.abc']);
3062
+ const PY_BUILTIN_GENERICS = new Set(['list', 'dict', 'tuple', 'set', 'frozenset', 'type']);
3063
+
3064
+ /** Is this Python string a type expression (a forward reference)? In an
3065
+ * annotation, a subscript argument of a typing generic (`Union["Leaf"]`),
3066
+ * a TypeVar `bound=`, or the type argument of `cast`. */
3067
+ function pythonTypeExpressionString(node, entry) {
3068
+ if (ancestor(node, PY_ANNOTATION, PY_ANNOTATION_STOP)) return true;
3069
+ const string = node.parent?.type === 'string' ? node.parent : null;
3070
+ if (!string) return false;
3071
+ const typingName = (value) => {
3072
+ if (!value) return false;
3073
+ if (value.type === 'attribute') {
3074
+ const object = value.childForFieldName('object');
3075
+ return !!object && (entry.importBindings || []).some(b => b.kind === 'import' &&
3076
+ (b.alias || b.name) === object.text && PY_TYPING_MODULES.has(String(b.module)));
3077
+ }
3078
+ if (value.type !== 'identifier') return false;
3079
+ if (PY_BUILTIN_GENERICS.has(value.text)) return true;
3080
+ return (entry.importBindings || []).some(b => (b.alias || b.name) === value.text &&
3081
+ PY_TYPING_MODULES.has(String(b.module)));
3082
+ };
3083
+ for (let current = string, parent = string.parent; parent; current = parent, parent = parent.parent) {
3084
+ if (parent.type === 'subscript') {
3085
+ return !sameNode(parent.childForFieldName('value'), current) &&
3086
+ typingName(parent.childForFieldName('value'));
3087
+ }
3088
+ if (parent.type === 'assignment') {
3089
+ // `X: TypeAlias = "A | B"` (PEP 613, fix #393): the value of an
3090
+ // explicit type alias is a type expression.
3091
+ if (!sameNode(parent.childForFieldName('right'), current)) return false;
3092
+ let annotation = parent.childForFieldName('type');
3093
+ if (annotation?.type === 'type') annotation = annotation.namedChild(0);
3094
+ if (!annotation) return false;
3095
+ const aliasName = annotation.type === 'attribute'
3096
+ ? annotation.childForFieldName('attribute')?.text : annotation.text;
3097
+ return aliasName === 'TypeAlias' && typingName(annotation);
3098
+ }
3099
+ if (parent.type === 'keyword_argument') {
3100
+ const call = parent.parent?.parent;
3101
+ return parent.childForFieldName('name')?.text === 'bound' && call?.type === 'call' &&
3102
+ typingName(call.childForFieldName('function'));
3103
+ }
3104
+ if (parent.type === 'argument_list') {
3105
+ const call = parent.parent;
3106
+ return call?.type === 'call' && sameNode(parent.namedChild(0), current) &&
3107
+ /(^|\.)cast$/.test(call.childForFieldName('function')?.text || '') &&
3108
+ typingName(call.childForFieldName('function'));
3109
+ }
3110
+ if (!['tuple', 'expression_list', 'list', 'binary_operator', 'parenthesized_expression'].includes(parent.type)) {
3111
+ return false;
3112
+ }
3113
+ }
3114
+ return false;
3115
+ }
3116
+
3117
+ /** Is this Python string content an entry of the module's `__all__`
3118
+ * (`__all__ = ('A', 'B')`, `__all__ += ['C']`) spelling exactly `name`? */
3119
+ function pythonDunderAllEntry(node, name) {
3120
+ const string = node.parent?.type === 'string' ? node.parent : null;
3121
+ if (!string || node.text !== name || string.namedChildCount !== 3) return false;
3122
+ for (let current = string, parent = string.parent; parent; current = parent, parent = parent.parent) {
3123
+ if (parent.type === 'assignment' || parent.type === 'augmented_assignment') {
3124
+ if (!sameNode(parent.childForFieldName('right'), current) ||
3125
+ parent.childForFieldName('left')?.text !== '__all__') return false;
3126
+ // Module scope only: a function's or class's `__all__` is a local.
3127
+ return !ancestor(parent, new Set(['function_definition', 'class_definition', 'lambda']));
3128
+ }
3129
+ if (!['tuple', 'list', 'parenthesized_expression', 'binary_operator', 'expression_list'].includes(parent.type)) {
3130
+ return false;
3131
+ }
3132
+ }
3133
+ return false;
3134
+ }
3135
+
3136
+ function decideStringAnnotation(resolver, file, entry, node, slot) {
3137
+ // `"Widget"` / `"Widget | None"` forward references (Python): the
3138
+ // string's content is an expression evaluated in the module's scope.
3139
+ const annotation = pythonTypeExpressionString(node, entry);
3140
+ const re = new RegExp(`(^|[^A-Za-z0-9_.])(${resolver.name})(?![A-Za-z0-9_])`, 'g');
3141
+ const text = node.text;
3142
+ let match;
3143
+ const columns = [];
3144
+ while ((match = re.exec(text)) !== null) {
3145
+ columns.push(match.index + match[1].length);
3146
+ }
3147
+ if (columns.length === 0) return;
3148
+ slot.tokens += columns.length;
3149
+ const singleLine = node.startPosition.row === node.endPosition.row;
3150
+ if (!annotation || !singleLine) {
3151
+ for (const offset of columns) {
3152
+ slot.reviews.push({ column: node.startPosition.column + offset,
3153
+ reason: 'string-reference' });
3154
+ }
3155
+ return;
3156
+ }
3157
+ const line = node.startPosition.row + 1;
3158
+ const verdict = resolver.resolveName(file, line);
3159
+ for (const offset of columns) {
3160
+ const column = node.startPosition.column + offset;
3161
+ if (verdict.verdict === 'yes') slot.edits.push(column);
3162
+ else if (verdict.verdict === 'unknown') slot.reviews.push({ column, reason: verdict.reason || 'string-annotation' });
3163
+ else {
3164
+ slot.skips++;
3165
+ slot.skipColumns.push(column);
3166
+ }
3167
+ }
3168
+ slot.stringAnnotation = true;
3169
+ }
3170
+
3171
+ /**
3172
+ * C# applies an attribute class `XAttribute` as `[X]`: the short spelling
3173
+ * is outside the name's own ground lines. Each `[X]` attribute whose
3174
+ * lookup reaches the renamed class is an edit of the short token.
3175
+ * @returns {Array<{file, relativePath, line, column, verdict, reason}>}
3176
+ */
3177
+ function csharpAttributeShortSites(index, def, name) {
3178
+ const suffix = 'Attribute';
3179
+ if (index.files.get(def.file)?.language !== 'csharp' || !name.endsWith(suffix) ||
3180
+ name.length <= suffix.length) return [];
3181
+ const short = name.slice(0, -suffix.length);
3182
+ const { computeGroundSet } = require('./account');
3183
+ const ground = computeGroundSet(index, short);
3184
+ const resolver = new TypeReferenceResolver(index, def, name);
3185
+ const out = [];
3186
+ for (const [file, lineNumbers] of [...ground.perFile].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))) {
3187
+ const entry = index.files.get(file);
3188
+ if (entry?.language !== 'csharp') continue;
3189
+ let content;
3190
+ try { content = index._readFile(file); } catch { continue; }
3191
+ const tree = index._getParsedTree?.(file, content, 'csharp') || safeParse(getParser('csharp'), content);
3192
+ if (!tree) continue;
3193
+ const rows = new Set(lineNumbers.map(n => n - 1));
3194
+ const stack = [tree.rootNode];
3195
+ while (stack.length > 0) {
3196
+ const node = stack.pop();
3197
+ if (node.endPosition.row < Math.min(...rows) || node.startPosition.row > Math.max(...rows)) continue;
3198
+ if (node.type === 'attribute') {
3199
+ let nameNode = node.childForFieldName('name');
3200
+ if (nameNode?.type === 'qualified_name') nameNode = nameNode.childForFieldName('name');
3201
+ if (nameNode?.text === short && rows.has(nameNode.startPosition.row)) {
3202
+ const line = nameNode.startPosition.row + 1;
3203
+ resolver.siteNode = nameNode;
3204
+ resolver.siteFile = file;
3205
+ const qualifier = node.childForFieldName('name')?.type === 'qualified_name'
3206
+ ? node.childForFieldName('name').childForFieldName('qualifier') : null;
3207
+ const verdict = qualifier
3208
+ ? resolver.resolveCSharpQualified(file, line, textOf(qualifier), { role: 'type' })
3209
+ : resolver.resolveName(file, line, name);
3210
+ resolver.siteNode = null;
3211
+ out.push({ file, relativePath: entry.relativePath, line, column: nameNode.startPosition.column,
3212
+ verdict: verdict.verdict, reason: verdict.reason });
3213
+ }
3214
+ continue;
3215
+ }
3216
+ for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
3217
+ }
3218
+ }
3219
+ return out;
3220
+ }
3221
+
3222
+ module.exports = {
3223
+ csharpAttributeShortSites,
3224
+ TYPE_RENAME_KINDS,
3225
+ isTypeRenamePin,
3226
+ typeReferenceSites,
3227
+ TypeReferenceResolver,
3228
+ familyOf,
3229
+ };