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
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
4
+
3
5
  const {
4
6
  traverseTree,
5
7
  nodeTextWithoutComments,
@@ -9,7 +11,22 @@ const {
9
11
  extractStringArg,
10
12
  visitNameNodes,
11
13
  sameNode,
14
+ containsOwnNode,
15
+ parseErrorRegions,
16
+ genericArityOf,
12
17
  } = require('./utils');
18
+
19
+ // An iterator method (`yield return` in its OWN body) returns an enumerable /
20
+ // async enumerable, never a Task: `async IAsyncEnumerable<T>` is consumed by
21
+ // `await foreach`, not awaited.
22
+ const CS_YIELD_TYPES = new Set(['yield_statement']);
23
+ const CS_SCOPE_BOUNDARIES = new Set(['local_function_statement', 'lambda_expression',
24
+ 'anonymous_method_expression', 'class_declaration', 'struct_declaration', 'record_declaration']);
25
+ function isCSharpIterator(fnNode) {
26
+ const body = fnNode.childForFieldName('body');
27
+ return !!body && body.text.includes('yield') &&
28
+ containsOwnNode(body, CS_YIELD_TYPES, CS_SCOPE_BOUNDARIES);
29
+ }
13
30
  const { PARSE_OPTIONS, safeParse } = require('./index');
14
31
 
15
32
  const TYPE_DECLARATIONS = new Map([
@@ -53,24 +70,359 @@ function isControlFlowLocalArtifact(node) {
53
70
  }
54
71
 
55
72
  function parseTree(parser, code) {
56
- return safeParse(parser, code, undefined, PARSE_OPTIONS);
73
+ const views = csharpViews(parser, code);
74
+ return views ? views.primary.tree : safeParse(parser, code, undefined, PARSE_OPTIONS);
75
+ }
76
+
77
+ /*
78
+ * Conditional compilation (fix #391). tree-sitter-c-sharp models `#if` as a
79
+ * node wrapping whole declarations or statements. A conditional that splits
80
+ * one (`ILogger F(..)` then `#if X => body #endif ;`, `#if X , IFoo #endif`
81
+ * in a base list, `else` before `#endif`, alternative method heads or
82
+ * parameters) leaves its directive tokens inside ERROR nodes and derails the
83
+ * parse, often of the rest of the file: serilog's ILogger.cs parsed as one
84
+ * ERROR, with no interface and no members. The C# preprocessor is line based
85
+ * and has no macros, so each conditional the damage touches is read in one
86
+ * configuration,
87
+ * keeping every row and column: its directive lines are blanked and one
88
+ * branch stays (a lone `#if` branch always; among alternatives the one with
89
+ * the most call sites, the first on ties). That is the primary view, kept
90
+ * when it has fewer syntax errors than the literal parse. The rows of the
91
+ * other alternatives are read from secondary views that keep those branches
92
+ * instead. Conditionals the grammar placed stay as they are (both branches
93
+ * of a structured `#if` are already in the tree), and a file whose literal
94
+ * parse is clean is read as before.
95
+ */
96
+ const CS_CONDITIONAL_LINE = /^[ \t]*#[ \t]*(if|elif|else|endif)\b/;
97
+ const CS_HAS_CONDITIONAL = /^[ \t]*#[ \t]*if\b/m;
98
+ const CS_DIRECTIVE_TOKENS = new Set(['#if', '#elif', '#else', '#endif']);
99
+ const CS_CALL_SHAPE = /[A-Za-z_]\w*\s*[<(]/g;
100
+ const CS_VIEW_MEMO_MAX = 8;
101
+ const csViewMemo = new Map();
102
+ const csViewsByTree = new WeakMap();
103
+ let csLastConditionalCode = null;
104
+ let csLastConditional = false;
105
+
106
+ function csHasConditional(code) {
107
+ if (code !== csLastConditionalCode) {
108
+ csLastConditionalCode = code;
109
+ csLastConditional = CS_HAS_CONDITIONAL.test(code);
110
+ }
111
+ return csLastConditional;
112
+ }
113
+
114
+ function csCountErrors(node) {
115
+ let count = node.type === 'ERROR' ? 1 : 0;
116
+ for (const child of node.children) {
117
+ if (child.isMissing) count++;
118
+ else if (child.hasError) count += csCountErrors(child);
119
+ }
120
+ return count;
121
+ }
122
+
123
+ function csBlankLine(line) {
124
+ return line.replace(/[^\r\n]/g, ' ');
125
+ }
126
+
127
+ /**
128
+ * 0-based rows where the literal parse is damaged: directive tokens left in
129
+ * ERROR nodes, the first and last rows of ERROR nodes, and MISSING tokens
130
+ * (a declaration head the grammar closed before a conditional body).
131
+ */
132
+ function csDamagedRows(root) {
133
+ const rows = new Set();
134
+ // The syntax errors counted as csCountErrors counts them.
135
+ rows.errors = 0;
136
+ const visit = node => {
137
+ const error = node.type === 'ERROR';
138
+ if (error) {
139
+ rows.errors++;
140
+ rows.add(node.startPosition.row);
141
+ rows.add(node.endPosition.row);
142
+ }
143
+ for (const child of node.children) {
144
+ if (child.isMissing) {
145
+ rows.errors++;
146
+ rows.add(child.startPosition.row);
147
+ } else if (error && CS_DIRECTIVE_TOKENS.has(child.type)) {
148
+ rows.add(child.startPosition.row);
149
+ }
150
+ if (!child.isMissing && child.hasError) visit(child);
151
+ }
152
+ };
153
+ if (root.hasError) visit(root);
154
+ return rows;
155
+ }
156
+
157
+ /**
158
+ * The views of a C# file whose literal parse misplaces conditional
159
+ * directives (see above), memoized per content; null when the literal tree
160
+ * is the file's view. { primary: { tree }, secondary: [{ tree, owns }] }
161
+ * with `owns` the 0-based rows a secondary view supplies.
162
+ */
163
+ function csharpViews(parser, code) {
164
+ if (!csHasConditional(code)) return null;
165
+ if (csViewMemo.has(code)) return csViewMemo.get(code);
166
+ const views = csBuildViews(parser, code);
167
+ csViewMemo.set(code, views);
168
+ if (views) csViewsByTree.set(views.primary.tree, { code, views });
169
+ if (csViewMemo.size > CS_VIEW_MEMO_MAX) csViewMemo.delete(csViewMemo.keys().next().value);
170
+ return views;
171
+ }
172
+
173
+ function csBuildViews(parser, code) {
174
+ const literal = safeParse(parser, code, undefined, PARSE_OPTIONS);
175
+ if (!literal.rootNode.hasError) return null;
176
+ const damaged = csDamagedRows(literal.rootNode);
177
+ const lines = code.match(/.*(?:\r\n|\n|\r|$)/g)?.filter(Boolean) || [];
178
+ // Conditional groups: directive rows of each branch, in nesting order.
179
+ const groups = [];
180
+ const stack = [];
181
+ for (let row = 0; row < lines.length; row++) {
182
+ const match = lines[row].match(CS_CONDITIONAL_LINE);
183
+ if (!match) continue;
184
+ if (match[1] === 'if') stack.push({ branches: [row] });
185
+ else if (match[1] === 'elif' || match[1] === 'else') {
186
+ if (!stack.length) return null;
187
+ stack[stack.length - 1].branches.push(row);
188
+ } else {
189
+ const group = stack.pop();
190
+ if (!group) return null;
191
+ group.end = row;
192
+ groups.push(group);
193
+ }
194
+ }
195
+ if (stack.length > 0) return null;
196
+ // Conditionals the damage touches, from the row before `#if` (a head
197
+ // left open for the conditional body) to the `#endif`.
198
+ const resolved = groups.filter(group => {
199
+ for (let row = group.branches[0] - 1; row <= group.end; row++) {
200
+ if (damaged.has(row)) return true;
201
+ }
202
+ return false;
203
+ });
204
+ if (resolved.length === 0) return null;
205
+ for (const group of resolved) group.primary = csPrimaryBranch(group, lines);
206
+ return csViewsFromGroups(parser, code, lines, resolved, damaged.errors);
207
+ }
208
+
209
+ // Rows of branch b of a group (between its directive and the next).
210
+ function csBranchRows(group, b) {
211
+ const bounds = [...group.branches, group.end];
212
+ const rows = [];
213
+ for (let row = bounds[b] + 1; row < bounds[b + 1]; row++) rows.push(row);
214
+ return rows;
215
+ }
216
+
217
+ // The branch a view keeps by default: the one with the most call shapes,
218
+ // the first on ties.
219
+ function csPrimaryBranch(group, lines) {
220
+ let keep = 0;
221
+ let keepCalls = -1;
222
+ for (let b = 0; b < group.branches.length; b++) {
223
+ let calls = 0;
224
+ for (const row of csBranchRows(group, b)) calls += (lines[row].match(CS_CALL_SHAPE) || []).length;
225
+ if (calls > keepCalls) {
226
+ keep = b;
227
+ keepCalls = calls;
228
+ }
229
+ }
230
+ return keep;
231
+ }
232
+
233
+ /**
234
+ * The views of `code` for its resolved conditional groups, one parse each.
235
+ * With the literal parse's error count (build) the views are kept only when
236
+ * the primary view has fewer syntax errors; a query rebuilds the views the
237
+ * index recorded for this content from `groups` (persisted per file as
238
+ * [branch rows..., end row, primary branch]) without the literal parse.
239
+ */
240
+ function csViewsFromGroups(parser, code, lines, resolved, literalErrors = null) {
241
+ // One view per alternative position: view 0 keeps each group's primary
242
+ // branch, view k its k-th other branch (the primary one when it has no
243
+ // more). A lone `#if` branch is kept in every view.
244
+ const alternatives = Math.max(...resolved.map(group => group.branches.length));
245
+ const viewSource = k => {
246
+ const blank = new Set();
247
+ const kept = new Set();
248
+ for (const group of resolved) {
249
+ for (const row of [...group.branches, group.end]) blank.add(row);
250
+ const others = group.branches.map((_, b) => b).filter(b => b !== group.primary);
251
+ const chosen = k === 0 || others.length === 0 ? group.primary : others[(k - 1) % others.length];
252
+ for (let b = 0; b < group.branches.length; b++) {
253
+ for (const row of csBranchRows(group, b)) {
254
+ if (b === chosen) {
255
+ if (k > 0 && b !== group.primary) kept.add(row);
256
+ } else {
257
+ blank.add(row);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return {
263
+ source: lines.map((line, row) => (blank.has(row) ? csBlankLine(line) : line)).join(''),
264
+ owns: kept,
265
+ };
266
+ };
267
+ const reparse = source => safeParse(parser, source, undefined, PARSE_OPTIONS);
268
+ const primaryTree = reparse(viewSource(0).source);
269
+ if (literalErrors != null && primaryTree.rootNode.hasError &&
270
+ csCountErrors(primaryTree.rootNode) >= literalErrors) return null;
271
+ const secondary = [];
272
+ for (let k = 1; k < alternatives; k++) {
273
+ const view = viewSource(k);
274
+ // Only rows that hold code are worth a parse.
275
+ const owns = new Set([...view.owns].filter(row => /\w/.test(lines[row])));
276
+ if (owns.size === 0) continue;
277
+ secondary.push({ tree: reparse(view.source), owns, reaches: csRowReach(owns) });
278
+ }
279
+ return {
280
+ primary: { tree: primaryTree },
281
+ secondary,
282
+ groups: resolved.map(group => [...group.branches, group.end, group.primary]),
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Items of every view: the primary view's, then each secondary view's
288
+ * items on the rows it owns. `rowOf` gives an item's 1-based row.
289
+ */
290
+ function csMergeViewItems(views, extract, rowOf) {
291
+ const merged = extract(views.primary.tree, null);
292
+ for (const view of views.secondary) {
293
+ for (const item of extract(view.tree, view.reaches)) {
294
+ if (view.owns.has(rowOf(item) - 1)) merged.push(item);
295
+ }
296
+ }
297
+ return merged;
57
298
  }
58
299
 
300
+ /**
301
+ * Whether a node's rows reach a secondary view's owned rows: extractors
302
+ * skip every other subtree of that view, since only items on owned rows are
303
+ * taken from it (the primary view supplies the rest).
304
+ */
305
+ function csRowReach(owns) {
306
+ const rows = [...owns].sort((a, b) => a - b);
307
+ return node => {
308
+ const start = node.startPosition.row;
309
+ const end = node.endPosition.row;
310
+ let lo = 0;
311
+ let hi = rows.length;
312
+ while (lo < hi) {
313
+ const mid = (lo + hi) >> 1;
314
+ if (rows[mid] < start) lo = mid + 1;
315
+ else hi = mid;
316
+ }
317
+ return lo < rows.length && rows[lo] <= end;
318
+ };
319
+ }
320
+
321
+ /**
322
+ * The tree queries read for a C# file: its primary view (fix #391). With the
323
+ * file's index entry for this content, the views are rebuilt from the groups
324
+ * the index recorded (`conditionalViews`) and a file the index read literally
325
+ * is parsed once, without repeating the damage analysis.
326
+ */
327
+ function queryTree(code, parser, entry = null) {
328
+ if (!entry?.hash || !csHasConditional(code) ||
329
+ crypto.createHash('md5').update(code).digest('hex') !== entry.hash) {
330
+ return parseTree(parser, code);
331
+ }
332
+ if (!Array.isArray(entry.conditionalViews)) {
333
+ const tree = safeParse(parser, code, undefined, PARSE_OPTIONS);
334
+ csViewsByTree.set(tree, { code, views: null });
335
+ return tree;
336
+ }
337
+ let views = csViewMemo.get(code);
338
+ if (!views) {
339
+ const lines = code.match(/.*(?:\r\n|\n|\r|$)/g)?.filter(Boolean) || [];
340
+ const resolved = entry.conditionalViews.map(group => ({
341
+ branches: group.slice(0, -2),
342
+ end: group[group.length - 2],
343
+ primary: group[group.length - 1],
344
+ }));
345
+ views = csViewsFromGroups(parser, code, lines, resolved);
346
+ csViewMemo.set(code, views);
347
+ if (csViewMemo.size > CS_VIEW_MEMO_MAX) csViewMemo.delete(csViewMemo.keys().next().value);
348
+ }
349
+ csViewsByTree.set(views.primary.tree, { code, views });
350
+ return views.primary.tree;
351
+ }
352
+
353
+ /**
354
+ * Whether queryTree for this source is its plain parse (fix #395): a file
355
+ * without a conditional directive, or one its index entry (same content)
356
+ * read literally, has one view.
357
+ */
358
+ function queryTreeIsPlain(code, entry = null) {
359
+ if (!csHasConditional(code)) return true;
360
+ return !!entry?.hash && !Array.isArray(entry.conditionalViews) &&
361
+ crypto.createHash('md5').update(code).digest('hex') === entry.hash;
362
+ }
363
+
364
+ function namespaceDeclarationName(declaration) {
365
+ const nameNode = declaration.childForFieldName('name') ||
366
+ declaration.namedChildren.find(child =>
367
+ child.type === 'identifier' || child.type === 'qualified_name');
368
+ return nameNode ? nameNode.text.replace(/\s+/g, '') : null;
369
+ }
370
+
371
+ // The full namespace a node is declared in (fix #395): every enclosing
372
+ // `namespace A { namespace B { .. } }` block contributes its name, outermost
373
+ // first, under the file-scoped `namespace X;` that precedes the node. C#
374
+ // names a nested block's members A.B, never B. Per tree, the file-scoped
375
+ // declaration and each block's full path are computed once.
376
+ const namespaceMemo = new WeakMap();
59
377
  function namespaceOf(node, tree) {
378
+ let memo = tree ? namespaceMemo.get(tree) : null;
379
+ if (tree && !memo) {
380
+ const fileScoped = fileScopedNamespace(tree.rootNode);
381
+ memo = {
382
+ fileScoped: fileScoped ? { name: namespaceDeclarationName(fileScoped), start: fileScoped.startIndex } : null,
383
+ blocks: new Map(),
384
+ };
385
+ namespaceMemo.set(tree, memo);
386
+ }
387
+ let block = null;
60
388
  for (let parent = node?.parent; parent; parent = parent.parent) {
61
- if (parent.type === 'namespace_declaration') {
62
- return parent.childForFieldName('name')?.text ||
63
- parent.namedChildren.find(child =>
64
- child.type === 'identifier' || child.type === 'qualified_name')?.text ||
65
- null;
389
+ if (parent.type === 'namespace_declaration') { block = parent; break; }
390
+ }
391
+ let blockPath = null;
392
+ if (block) {
393
+ blockPath = memo?.blocks.get(block.id);
394
+ if (blockPath === undefined) {
395
+ const parts = [];
396
+ for (let current = block; current; current = current.parent) {
397
+ if (current.type !== 'namespace_declaration') continue;
398
+ const name = namespaceDeclarationName(current);
399
+ if (name) parts.unshift(name);
400
+ }
401
+ blockPath = parts.length > 0 ? parts.join('.') : null;
402
+ memo?.blocks.set(block.id, blockPath);
403
+ }
404
+ }
405
+ const fileScoped = memo ? memo.fileScoped : (() => {
406
+ const declaration = fileScopedNamespace(tree?.rootNode);
407
+ return declaration ? { name: namespaceDeclarationName(declaration), start: declaration.startIndex } : null;
408
+ })();
409
+ const scopedName = fileScoped?.name && (!node || fileScoped.start <= node.startIndex) ? fileScoped.name : null;
410
+ if (scopedName && blockPath) return `${scopedName}.${blockPath}`;
411
+ return scopedName || blockPath || null;
412
+ }
413
+
414
+ // The file-scoped namespace declaration of a compilation unit, also when a
415
+ // conditional wraps the whole file (`#if X` ... `namespace N;` ... `#endif`,
416
+ // fix #391).
417
+ function fileScopedNamespace(root) {
418
+ for (const child of root?.namedChildren || []) {
419
+ if (child.type === 'file_scoped_namespace_declaration') return child;
420
+ if (child.type.startsWith('preproc_')) {
421
+ const nested = fileScopedNamespace(child);
422
+ if (nested) return nested;
66
423
  }
67
424
  }
68
- const fileScoped = (tree?.rootNode?.namedChildren || []).find(child =>
69
- child.type === 'file_scoped_namespace_declaration');
70
- return fileScoped?.childForFieldName('name')?.text ||
71
- fileScoped?.namedChildren.find(child =>
72
- child.type === 'identifier' || child.type === 'qualified_name')?.text ||
73
- null;
425
+ return null;
74
426
  }
75
427
 
76
428
  function modifiersOf(node) {
@@ -96,6 +448,11 @@ function attributeData(node) {
96
448
  arg: stringArg.value,
97
449
  interp: stringArg.interp,
98
450
  }),
451
+ // fix #366: non-literal arguments (constants, concatenation,
452
+ // nameof) keep their source so route extraction can fold them.
453
+ ...(!stringArg && args && args.namedChildCount > 0 && {
454
+ args: args.text.replace(/^\(|\)$/g, '').trim(),
455
+ }),
99
456
  });
100
457
  }
101
458
  }
@@ -143,7 +500,7 @@ function structuredParams(paramsNode) {
143
500
  if (modifiersOf(param).includes('this')) info.extensionReceiver = true;
144
501
  if (param.type === 'parameter_array') info.rest = true;
145
502
  const value = param.childForFieldName('value') ||
146
- param.namedChildren.find(child => child !== nameNode && child !== typeNode &&
503
+ param.namedChildren.find(child => !sameNode(child, nameNode) && !sameNode(child, typeNode) &&
147
504
  !['attribute_list', 'modifier', 'comment'].includes(child.type));
148
505
  if (value) {
149
506
  info.default = nodeTextWithoutComments(value);
@@ -202,11 +559,31 @@ function memberFromNode(node, className, lines) {
202
559
  if (node.type === 'destructor_declaration') modifiers.push('destructor');
203
560
  const conversion = conversionKind(node);
204
561
  if (conversion) modifiers.push(conversion);
205
- const explicitInterfaceNode = node.namedChildren.find(child =>
206
- child.type === 'explicit_interface_specifier');
207
- const explicitInterface = explicitInterfaceNode?.text
208
- .replace(/\.$/, '').trim() || null;
562
+ const explicitInterface = explicitInterfaceOf(node);
209
563
  const paramsStructured = structuredParams(paramsNode);
564
+ const generics = typeParameterNames(node);
565
+ // fix #380: an extension receiver typed by a METHOD type parameter
566
+ // (`static T Ext<T>(this T e) where T : Exception`) accepts any type
567
+ // satisfying the parameter's constraints.
568
+ const receiverParam = paramsStructured[0];
569
+ if (receiverParam?.extensionReceiver && receiverParam.type) {
570
+ const typeParams = node.childForFieldName('type_parameters');
571
+ const isTypeParam = (typeParams?.namedChildren || []).some(child =>
572
+ child.type === 'type_parameter' && child.childForFieldName('name')?.text === receiverParam.type);
573
+ if (isTypeParam) {
574
+ const constraints = [];
575
+ for (const clause of node.namedChildren) {
576
+ if (clause.type !== 'type_parameter_constraints_clause') continue;
577
+ if (clause.namedChild(0)?.text !== receiverParam.type) continue;
578
+ for (const constraint of clause.namedChildren.slice(1)) {
579
+ const typeNode = constraint.childForFieldName('type');
580
+ if (typeNode) constraints.push(nodeTextWithoutComments(typeNode));
581
+ }
582
+ }
583
+ paramsStructured[0] = { ...receiverParam, typeParameter: true,
584
+ ...(constraints.length > 0 && { constraints }) };
585
+ }
586
+ }
210
587
  return {
211
588
  name,
212
589
  params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
@@ -214,14 +591,17 @@ function memberFromNode(node, className, lines) {
214
591
  returnType: isConstructor ? null : nodeTextWithoutComments(returnNode).trim() || null,
215
592
  startLine,
216
593
  endLine,
594
+ ...nameLineOf(nameNode, startLine),
217
595
  indent,
218
596
  modifiers,
597
+ ...(generics && { generics }),
219
598
  memberType: isConstructor ? 'constructor' : 'method',
220
599
  isMethod: true,
221
600
  isConstructor,
222
601
  className,
223
602
  ...(explicitInterface && { explicitInterface }),
224
603
  isAsync: modifiers.includes('async'),
604
+ ...(isCSharpIterator(node) && { isGenerator: true }),
225
605
  ...(modifiers.includes('static') &&
226
606
  paramsStructured[0]?.extensionReceiver && {
227
607
  isExtensionMethod: true,
@@ -257,11 +637,22 @@ function fieldMembers(node, lines) {
257
637
  return members;
258
638
  }
259
639
 
640
+ // The interface an explicit implementation names (`void IDisposable.Dispose()`,
641
+ // `object ICollection.SyncRoot`, `int IList<int>.this[int i]`, events):
642
+ // such a member is reached only through that interface (fix #391 extends it
643
+ // from methods to properties, indexers and events).
644
+ function explicitInterfaceOf(node) {
645
+ const specifier = node.namedChildren.find(child =>
646
+ child.type === 'explicit_interface_specifier');
647
+ return specifier?.text.replace(/\.$/, '').trim() || null;
648
+ }
649
+
260
650
  function indexerMember(node, className, lines) {
261
651
  if (node.type !== 'indexer_declaration') return null;
262
652
  const paramsNode = node.childForFieldName('parameters');
263
653
  const typeNode = node.childForFieldName('type');
264
654
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
655
+ const explicitInterface = explicitInterfaceOf(node);
265
656
  return {
266
657
  name: 'this[]',
267
658
  params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\[|\]$/g, '').trim() : '...',
@@ -269,11 +660,13 @@ function indexerMember(node, className, lines) {
269
660
  returnType: nodeTextWithoutComments(typeNode).trim() || null,
270
661
  startLine,
271
662
  endLine,
663
+ ...nameLineOf((node.children || []).find(child => child.type === 'this') || null, startLine),
272
664
  indent,
273
665
  modifiers: modifiersOf(node),
274
666
  memberType: 'property',
275
667
  isMethod: true,
276
668
  className,
669
+ ...(explicitInterface && { explicitInterface }),
277
670
  docstring: extractJSDocstring(lines, startLine),
278
671
  };
279
672
  }
@@ -288,17 +681,53 @@ function propertyMember(node, lines) {
288
681
  // artifact instead of letting one invalid symbol discard the whole file.
289
682
  if (!nameNode?.text) return null;
290
683
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
684
+ const explicitInterface = explicitInterfaceOf(node);
291
685
  return {
292
686
  name: nameNode.text,
293
687
  startLine,
294
688
  endLine,
689
+ ...nameLineOf(nameNode, startLine),
295
690
  indent,
296
691
  modifiers: modifiersOf(node),
297
692
  memberType: 'property',
298
693
  fieldType: typeNode?.text || null,
694
+ ...(explicitInterface && { explicitInterface }),
299
695
  };
300
696
  }
301
697
 
698
+ // fix #380: a C# type's generic arity is part of its identity (`Outcome`,
699
+ // `Outcome<T>` and `Outcome<T1, T2>` are three types).
700
+ function typeParameterCount(node) {
701
+ const list = node?.namedChildren?.find(child => child.type === 'type_parameter_list');
702
+ if (!list) return 0;
703
+ return list.namedChildren.filter(child => child.type === 'type_parameter').length;
704
+ }
705
+
706
+ // fix #390: the declared type parameter NAMES, in order (`<TState, TResult>`;
707
+ // variance and attributes dropped), so a base clause `: Visitor<TextWriter,
708
+ // bool>` can be read as a substitution of those names.
709
+ function typeParameterNames(node) {
710
+ const list = node?.childForFieldName?.('type_parameters') ||
711
+ node?.namedChildren?.find(child => child.type === 'type_parameter_list');
712
+ if (!list) return null;
713
+ const names = [];
714
+ for (const child of list.namedChildren) {
715
+ if (child.type !== 'type_parameter') continue;
716
+ const name = child.childForFieldName('name') ||
717
+ child.namedChildren.find(part => part.type === 'identifier');
718
+ if (name?.text) names.push(name.text);
719
+ }
720
+ return names.length > 0 ? `<${names.join(', ')}>` : null;
721
+ }
722
+
723
+ // fix #390: the line of the declared name when attributes on their own lines
724
+ // start the declaration (`[Obsolete]` above `void M()`): definition edits and
725
+ // definition-line classification use it.
726
+ function nameLineOf(nameNode, startLine) {
727
+ const line = nameNode ? nameNode.startPosition.row + 1 : null;
728
+ return line != null && line !== startLine ? { nameLine: line } : {};
729
+ }
730
+
302
731
  function baseHeadName(text) {
303
732
  return text.replace(/<.*$/, '').split('.').pop().trim();
304
733
  }
@@ -331,7 +760,43 @@ function classifyBases(bases, type, fileTypeKinds) {
331
760
  }
332
761
 
333
762
  function findClasses(code, parser) {
334
- const tree = parseTree(parser, code);
763
+ const views = csharpViews(parser, code);
764
+ if (!views) return findClassesInTree(code, safeParse(parser, code, undefined, PARSE_OPTIONS));
765
+ const classes = findClassesInTree(code, views.primary.tree);
766
+ for (const view of views.secondary) {
767
+ csMergeSecondaryClasses(classes, findClassesInTree(code, view.tree), view.owns);
768
+ }
769
+ return classes;
770
+ }
771
+
772
+ /**
773
+ * Declarations a secondary view owns (fix #391): a type whose head it owns
774
+ * joins whole; members it owns join the primary view's type of the same
775
+ * name and head row.
776
+ */
777
+ function csMergeSecondaryClasses(classes, secondary, owns) {
778
+ for (const cls of secondary) {
779
+ if (owns.has(cls.startLine - 1)) {
780
+ if (!classes.some(existing => existing.name === cls.name && existing.startLine === cls.startLine)) {
781
+ classes.push(cls);
782
+ }
783
+ continue;
784
+ }
785
+ const target = classes.find(existing => existing.name === cls.name && existing.startLine === cls.startLine);
786
+ if (!target) continue;
787
+ for (const member of cls.members || []) {
788
+ if (!owns.has(member.startLine - 1)) continue;
789
+ if (target.members.some(existing => existing.name === member.name &&
790
+ existing.startLine === member.startLine)) continue;
791
+ target.members.push(member);
792
+ target.endLine = Math.max(target.endLine, member.endLine);
793
+ }
794
+ target.members.sort((a, b) => a.startLine - b.startLine);
795
+ }
796
+ classes.sort((a, b) => a.startLine - b.startLine);
797
+ }
798
+
799
+ function findClassesInTree(code, tree) {
335
800
  const lines = code.split('\n');
336
801
  const classes = [];
337
802
  // Same-file type kinds override the I-prefix convention when classifying
@@ -351,11 +816,24 @@ function findClasses(code, parser) {
351
816
  const delegateName = node.childForFieldName('name');
352
817
  if (delegateName) {
353
818
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
819
+ const delegateArity = typeParameterCount(node);
820
+ const delegateGenerics = typeParameterNames(node);
821
+ // fix #393: the delegate's parameter count, the count a
822
+ // lambda converting to it must have (C# 10.7.1).
823
+ const delegateParamList = node.childForFieldName('parameters') ||
824
+ node.namedChildren.find(child => child.type === 'parameter_list');
825
+ const delegateParams = delegateParamList
826
+ ? delegateParamList.namedChildren.filter(child => child.type === 'parameter').length
827
+ : null;
354
828
  classes.push({
355
829
  name: delegateName.text,
356
830
  type: 'type',
831
+ ...(delegateArity > 0 && { typeArity: delegateArity }),
832
+ ...(delegateGenerics && { generics: delegateGenerics }),
833
+ ...(Number.isInteger(delegateParams) && { delegateParams }),
357
834
  startLine,
358
835
  endLine,
836
+ ...nameLineOf(delegateName, startLine),
359
837
  indent,
360
838
  modifiers: modifiersOf(node),
361
839
  ...(namespaceOf(node, tree) && { namespace: namespaceOf(node, tree) }),
@@ -395,6 +873,7 @@ function findClasses(code, parser) {
395
873
  name: enumName.text,
396
874
  startLine,
397
875
  endLine,
876
+ ...nameLineOf(enumName, startLine),
398
877
  indent,
399
878
  modifiers: ['public', 'static'],
400
879
  memberType: 'field',
@@ -422,13 +901,25 @@ function findClasses(code, parser) {
422
901
  enclosingType = parent.childForFieldName('name')?.text;
423
902
  if (enclosingType) break;
424
903
  }
904
+ const typeArity = typeParameterCount(node);
905
+ if (typeArity > 0) {
906
+ for (const member of members) member.ownerTypeArity = typeArity;
907
+ }
908
+ const generics = typeParameterNames(node);
909
+ // A primary constructor's parameters (`class C(int a)`, positional
910
+ // records): the constructor the type declares in its header (fix #400).
911
+ const primaryList = node.namedChildren.find(child => child.type === 'parameter_list');
425
912
  classes.push({
426
913
  name: nameNode.text,
427
914
  type,
915
+ ...(primaryList && { primaryParams: structuredParams(primaryList) }),
428
916
  startLine,
429
917
  endLine,
918
+ ...nameLineOf(nameNode, startLine),
430
919
  indent,
431
920
  modifiers: modifiersOf(node),
921
+ ...(typeArity > 0 && { typeArity }),
922
+ ...(generics && { generics }),
432
923
  ...(enclosingType && { enclosingType }),
433
924
  ...(namespaceOf(node, tree) && { namespace: namespaceOf(node, tree) }),
434
925
  members,
@@ -463,6 +954,7 @@ function findClasses(code, parser) {
463
954
  .sort((a, b) => b.startLine - a.startLine)[0];
464
955
  if (!candidate) return false;
465
956
  const member = memberFromNode(node, candidate.name, lines);
957
+ if (member && candidate.typeArity) member.ownerTypeArity = candidate.typeArity;
466
958
  if (member && !candidate.members.some(existing =>
467
959
  existing.startLine === member.startLine &&
468
960
  existing.name === member.name)) {
@@ -475,10 +967,17 @@ function findClasses(code, parser) {
475
967
  }
476
968
 
477
969
  function findFunctions(code, parser) {
478
- const tree = parseTree(parser, code);
970
+ const views = csharpViews(parser, code);
971
+ if (!views) return findFunctionsInTree(code, safeParse(parser, code, undefined, PARSE_OPTIONS));
972
+ return csMergeViewItems(views, (tree, reaches) => findFunctionsInTree(code, tree, reaches), fn => fn.startLine)
973
+ .sort((a, b) => a.startLine - b.startLine);
974
+ }
975
+
976
+ function findFunctionsInTree(code, tree, reaches = null) {
479
977
  const lines = code.split('\n');
480
978
  const functions = [];
481
979
  traverseTreeCached(tree.rootNode, node => {
980
+ if (reaches && !reaches(node)) return false;
482
981
  if (node.type !== 'local_function_statement') return true;
483
982
  const nameNode = node.childForFieldName('name');
484
983
  if (!nameNode) return true;
@@ -494,6 +993,7 @@ function findFunctions(code, parser) {
494
993
  const returnNode = node.childForFieldName('returns') || node.childForFieldName('type');
495
994
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
496
995
  const modifiers = modifiersOf(node);
996
+ const localGenerics = typeParameterNames(node);
497
997
  functions.push({
498
998
  name: nameNode.text,
499
999
  params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
@@ -501,13 +1001,18 @@ function findFunctions(code, parser) {
501
1001
  returnType: nodeTextWithoutComments(returnNode).trim() || null,
502
1002
  startLine,
503
1003
  endLine,
1004
+ ...nameLineOf(nameNode, startLine),
504
1005
  indent,
505
1006
  modifiers,
1007
+ ...(localGenerics && { generics: localGenerics }),
506
1008
  isAsync: modifiers.includes('async'),
1009
+ ...(isCSharpIterator(node) && { isGenerator: true }),
507
1010
  isNested: true,
508
1011
  docstring: extractJSDocstring(lines, startLine),
509
1012
  });
510
- return false;
1013
+ // A local function may declare local functions of its own (fix
1014
+ // #395: AutoMapper's `MapCollectionCore` holds `GetDestinationType`).
1015
+ return true;
511
1016
  });
512
1017
  const topLevel = (tree.rootNode.namedChildren || []).filter(child =>
513
1018
  child.type === 'global_statement');
@@ -528,10 +1033,17 @@ function findFunctions(code, parser) {
528
1033
  }
529
1034
 
530
1035
  function findStateObjects(code, parser) {
531
- const tree = parseTree(parser, code);
1036
+ const views = csharpViews(parser, code);
1037
+ if (!views) return findStateObjectsInTree(code, safeParse(parser, code, undefined, PARSE_OPTIONS));
1038
+ return csMergeViewItems(views, (tree, reaches) => findStateObjectsInTree(code, tree, reaches), state => state.startLine)
1039
+ .sort((a, b) => a.startLine - b.startLine);
1040
+ }
1041
+
1042
+ function findStateObjectsInTree(code, tree, reaches = null) {
532
1043
  const lines = code.split('\n');
533
1044
  const states = [];
534
1045
  traverseTreeCached(tree.rootNode, node => {
1046
+ if (reaches && !reaches(node)) return false;
535
1047
  if (node.type !== 'global_statement') return true;
536
1048
  const declaration = node.namedChildren.find(child =>
537
1049
  child.type === 'local_declaration_statement')?.namedChild(0);
@@ -553,6 +1065,208 @@ function findStateObjects(code, parser) {
553
1065
  return states;
554
1066
  }
555
1067
 
1068
+ const TASK_WRAPPERS = new Set(['Task', 'ValueTask']);
1069
+
1070
+ /** The written type a target-typed `new(...)` constructs, or null (fix #393). */
1071
+ function implicitCreationTarget(node) {
1072
+ const typeOf = typeNode => {
1073
+ if (!typeNode) return null;
1074
+ let current = typeNode;
1075
+ if (current.type === 'nullable_type') current = current.namedChild(0);
1076
+ if (!current || !['identifier', 'qualified_name', 'generic_name'].includes(current.type)) return null;
1077
+ if (current.text === 'var' || current.text === 'dynamic') return null;
1078
+ return current;
1079
+ };
1080
+ const parent = node.parent;
1081
+ if (!parent) return null;
1082
+ // An element of a collection initializer or collection expression (fix
1083
+ // #401): the element type of the collection's written type.
1084
+ const element = collectionElementSlot(node);
1085
+ if (element) return typeOf(collectionElementType(element.collectionType, element.position));
1086
+ if (parent.type === 'variable_declarator') {
1087
+ const declaration = parent.parent;
1088
+ return declaration?.type === 'variable_declaration' ? typeOf(declaration.childForFieldName('type')) : null;
1089
+ }
1090
+ if (parent.type === 'equals_value_clause' && parent.parent?.type === 'property_declaration') {
1091
+ return typeOf(parent.parent.childForFieldName('type'));
1092
+ }
1093
+ // An auto-property initializer (`List<T> Items { get; } = new();`, fix #395).
1094
+ if (parent.type === 'property_declaration' && sameNode(parent.childForFieldName('value'), node)) {
1095
+ return typeOf(parent.childForFieldName('type'));
1096
+ }
1097
+ if (parent.type !== 'return_statement' && parent.type !== 'arrow_expression_clause') return null;
1098
+ for (let owner = parent.parent; owner; owner = owner.parent) {
1099
+ if (owner.type === 'lambda_expression' || owner.type === 'anonymous_method_expression') return null;
1100
+ if (owner.type === 'accessor_declaration') {
1101
+ const keyword = owner.childForFieldName('name')?.text || owner.child(0)?.text;
1102
+ if (keyword !== 'get') return null;
1103
+ continue;
1104
+ }
1105
+ if (owner.type === 'property_declaration' || owner.type === 'indexer_declaration') {
1106
+ return typeOf(owner.childForFieldName('type'));
1107
+ }
1108
+ if (owner.type === 'method_declaration' || owner.type === 'local_function_statement') {
1109
+ const returns = owner.childForFieldName('returns') || owner.childForFieldName('type');
1110
+ const isAsync = (owner.children || []).some(child => child.type === 'modifier' && child.text === 'async');
1111
+ if (isAsync) {
1112
+ const generic = returns?.type === 'generic_name' ? returns
1113
+ : returns?.type === 'qualified_name' && returns.namedChildren.at(-1)?.type === 'generic_name'
1114
+ ? returns.namedChildren.at(-1) : null;
1115
+ const wrapper = generic?.namedChild(0)?.text;
1116
+ const args = generic?.namedChildren.find(child => child.type === 'type_argument_list');
1117
+ if (!TASK_WRAPPERS.has(wrapper) || args?.namedChildCount !== 1) return null;
1118
+ return typeOf(args.namedChild(0));
1119
+ }
1120
+ return typeOf(returns);
1121
+ }
1122
+ if (owner.type === 'constructor_declaration' || TYPE_DECLARATIONS.has(owner.type)) return null;
1123
+ }
1124
+ return null;
1125
+ }
1126
+
1127
+ // Collection types whose initializer elements and collection-expression
1128
+ // elements are their single type argument (the Add(T) / builder element),
1129
+ // and dictionaries whose `{ k, v }` element pairs are their two (fix #401).
1130
+ const CSHARP_ELEMENT_COLLECTIONS = new Set(['List', 'IList', 'ICollection', 'IEnumerable',
1131
+ 'IReadOnlyList', 'IReadOnlyCollection', 'HashSet', 'ISet', 'IReadOnlySet', 'SortedSet', 'Collection',
1132
+ 'ReadOnlyCollection', 'ImmutableArray', 'ImmutableList', 'ImmutableHashSet', 'Span', 'ReadOnlySpan']);
1133
+ const CSHARP_PAIR_COLLECTIONS = new Set(['Dictionary', 'SortedDictionary', 'SortedList']);
1134
+
1135
+ /**
1136
+ * The collection a target-typed `new(..)` is an element of (fix #401):
1137
+ * `new List<T> { new(..) }`, `new Dictionary<K, V> { { new(..), new(..) } }`
1138
+ * and collection expressions (`List<T> xs = [new(..)]`, parsed as an element
1139
+ * binding by the grammar). { collectionType, position } or null; position is
1140
+ * the index inside a dictionary's `{ k, v }` pair, else 0.
1141
+ */
1142
+ function collectionElementSlot(node) {
1143
+ const parent = node.parent;
1144
+ if (!parent) return null;
1145
+ if (parent.type === 'initializer_expression') {
1146
+ const owner = parent.parent;
1147
+ if (owner?.type === 'object_creation_expression') {
1148
+ return { collectionType: owner.childForFieldName('type'), position: -1 };
1149
+ }
1150
+ if (owner?.type === 'initializer_expression' && owner.parent?.type === 'object_creation_expression') {
1151
+ const position = parent.namedChildren.findIndex(child => sameNode(child, node));
1152
+ return { collectionType: owner.parent.childForFieldName('type'), position, pair: true };
1153
+ }
1154
+ return null;
1155
+ }
1156
+ const holder = parent.type === 'argument' ? parent.parent
1157
+ : parent.type === 'collection_expression' ? parent : null;
1158
+ if (!holder || (holder.type !== 'element_binding_expression' && holder.type !== 'collection_expression')) return null;
1159
+ const declarator = holder.parent;
1160
+ if (declarator?.type === 'variable_declarator') {
1161
+ const declaration = declarator.parent;
1162
+ return declaration?.type === 'variable_declaration'
1163
+ ? { collectionType: declaration.childForFieldName('type'), position: -1 } : null;
1164
+ }
1165
+ const value = holder.parent?.type === 'equals_value_clause' ? holder.parent.parent : holder.parent;
1166
+ if (value?.type === 'property_declaration') {
1167
+ return { collectionType: value.childForFieldName('type'), position: -1 };
1168
+ }
1169
+ return null;
1170
+ }
1171
+
1172
+ /** The element type node of a written collection type, or null. */
1173
+ function collectionElementType(typeNode, position) {
1174
+ let current = typeNode;
1175
+ if (current?.type === 'nullable_type') current = current.namedChild(0);
1176
+ if (!current) return null;
1177
+ if (current.type === 'array_type') {
1178
+ return position === -1 && !current.namedChildren.some(child =>
1179
+ child.type === 'array_rank_specifier' && child.text.includes(',')) ? current.namedChild(0) : null;
1180
+ }
1181
+ const generic = current.type === 'generic_name' ? current
1182
+ : current.type === 'qualified_name' && current.namedChildren.at(-1)?.type === 'generic_name'
1183
+ ? current.namedChildren.at(-1) : null;
1184
+ const base = generic?.namedChild(0)?.text;
1185
+ const args = (generic?.namedChildren.find(child => child.type === 'type_argument_list')?.namedChildren || [])
1186
+ .filter(child => !child.type.endsWith('comment'));
1187
+ if (CSHARP_ELEMENT_COLLECTIONS.has(base) && args.length === 1 && position === -1) return args[0];
1188
+ if (CSHARP_PAIR_COLLECTIONS.has(base) && args.length === 2 && (position === 0 || position === 1)) {
1189
+ return args[position];
1190
+ }
1191
+ return null;
1192
+ }
1193
+
1194
+ /**
1195
+ * Where a target-typed `new(..)` no declaration types sits (fix #400): an
1196
+ * argument of a call, a construction or a constructor initializer (its type
1197
+ * is the parameter type of the overload the compiler picks), or the value
1198
+ * assigned to a member (the member's type). Null elsewhere.
1199
+ */
1200
+ function implicitCreationSlot(node) {
1201
+ const parent = node.parent;
1202
+ if (parent?.type === 'argument') {
1203
+ const list = parent.parent;
1204
+ const owner = list?.type === 'argument_list' ? list.parent : null;
1205
+ if (!owner) return null;
1206
+ const args = list.namedChildren.filter(child => child.type === 'argument');
1207
+ const index = args.findIndex(argument => sameNode(argument, parent));
1208
+ const nameNode = parent.childForFieldName('name');
1209
+ let ownerKind;
1210
+ let ownerStart;
1211
+ let ownerName;
1212
+ let receiverName = null;
1213
+ let instance = false;
1214
+ if (owner.type === 'invocation_expression') {
1215
+ const functionNode = owner.childForFieldName('function');
1216
+ const identity = invocationIdentity(functionNode);
1217
+ if (!identity.name) return null;
1218
+ ownerKind = 'invocation';
1219
+ ownerStart = (identity.nameNode || owner).startIndex;
1220
+ ownerName = identity.name;
1221
+ // `x.M(..)`: an extension method's first parameter is `x`.
1222
+ instance = functionNode?.type === 'member_access_expression' ||
1223
+ functionNode?.type === 'conditional_access_expression';
1224
+ // `field.M(..)` / `this.field.M(..)`: the receiver's member name.
1225
+ let receiver = functionNode?.type === 'member_access_expression'
1226
+ ? unwrapReceiverNode(functionNode.childForFieldName('expression')) : null;
1227
+ if (receiver?.type === 'member_access_expression' &&
1228
+ unwrapReceiverNode(receiver.childForFieldName('expression'))?.type === 'this') {
1229
+ receiver = receiver.childForFieldName('name');
1230
+ }
1231
+ if (receiver?.type === 'identifier') receiverName = receiver.text;
1232
+ } else if (owner.type === 'object_creation_expression') {
1233
+ const typeNode = owner.childForFieldName('type');
1234
+ if (!typeNode) return null;
1235
+ ownerKind = 'creation';
1236
+ ownerStart = typeNode.startIndex;
1237
+ ownerName = typeNode.text.replace(/<.*$/s, '').split('.').pop().trim();
1238
+ } else if (owner.type === 'implicit_object_creation_expression') {
1239
+ ownerKind = 'creation';
1240
+ ownerStart = (owner.child(0) || owner).startIndex;
1241
+ ownerName = 'new';
1242
+ } else if (owner.type === 'constructor_initializer') {
1243
+ const keyword = owner.children.find(child => child.type === 'this' || child.type === 'base');
1244
+ if (!keyword) return null;
1245
+ ownerKind = keyword.type;
1246
+ ownerStart = owner.startIndex;
1247
+ ownerName = keyword.type;
1248
+ } else {
1249
+ return null;
1250
+ }
1251
+ return {
1252
+ kind: 'argument', owner: ownerKind, ownerStart, ownerName, index, argCount: args.length,
1253
+ ...(instance && { instance: true }),
1254
+ ...(receiverName && { receiverName }),
1255
+ ...(nameNode && { name: nameNode.text }),
1256
+ };
1257
+ }
1258
+ if (parent?.type === 'assignment_expression' && sameNode(parent.childForFieldName('right'), node) &&
1259
+ parent.children.some(child => child.type === '=')) {
1260
+ const left = parent.childForFieldName('left');
1261
+ if (left?.type === 'identifier') return { kind: 'member', member: left.text };
1262
+ if (left?.type === 'member_access_expression' && left.childForFieldName('expression')?.type === 'this') {
1263
+ const member = left.childForFieldName('name');
1264
+ if (member?.type === 'identifier') return { kind: 'member', member: member.text, this: true };
1265
+ }
1266
+ }
1267
+ return null;
1268
+ }
1269
+
556
1270
  function enclosingFunctionOf(node) {
557
1271
  for (let parent = node?.parent; parent; parent = parent.parent) {
558
1272
  if (parent.type === 'method_declaration' ||
@@ -634,6 +1348,85 @@ function enclosingTypeDeclaresMember(node, memberName) {
634
1348
  return false;
635
1349
  }
636
1350
 
1351
+ const _memberTypesByType = new WeakMap();
1352
+
1353
+ /**
1354
+ * fix #391: the declared type of a bare identifier that names a field or
1355
+ * property of its enclosing type (`Write(level, NoPropertyValues)`), or
1356
+ * null. A name any local, parameter or pattern variable of the enclosing
1357
+ * member binds is never read as the member; a name the type declares more
1358
+ * than once (or only through a nested type) has no single type.
1359
+ */
1360
+ function enclosingMemberValueType(identifier) {
1361
+ const name = identifier.text;
1362
+ let typeNode = null;
1363
+ let callable = null;
1364
+ for (let parent = identifier.parent; parent; parent = parent.parent) {
1365
+ if (!callable && CALLABLE_SCOPE_NODES.has(parent.type)) callable = parent;
1366
+ if (TYPE_DECLARATIONS.has(parent.type)) {
1367
+ typeNode = parent;
1368
+ break;
1369
+ }
1370
+ }
1371
+ if (!typeNode) return null;
1372
+ let members = _memberTypesByType.get(typeNode);
1373
+ if (!members) {
1374
+ members = new Map();
1375
+ const record = (memberName, typeText) => {
1376
+ if (!memberName || !typeText) return;
1377
+ members.set(memberName, members.has(memberName) ? null : typeText);
1378
+ };
1379
+ const body = typeNode.childForFieldName('body') ||
1380
+ typeNode.namedChildren.find(child => child.type === 'declaration_list');
1381
+ const stack = [...(body?.namedChildren || [])];
1382
+ while (stack.length > 0) {
1383
+ const current = stack.pop();
1384
+ if (current.type === 'property_declaration') {
1385
+ record(current.childForFieldName('name')?.text, current.childForFieldName('type')?.text);
1386
+ } else if (current.type === 'field_declaration') {
1387
+ const declaration = current.namedChildren.find(child => child.type === 'variable_declaration');
1388
+ const typeText = declaration?.childForFieldName('type')?.text;
1389
+ for (const variable of declaration?.namedChildren || []) {
1390
+ if (variable.type !== 'variable_declarator') continue;
1391
+ record((variable.childForFieldName('name') || variable.namedChild(0))?.text, typeText);
1392
+ }
1393
+ } else if (current.type.startsWith('preproc_') || current.type === 'declaration_list') {
1394
+ stack.push(...(current.namedChildren || []));
1395
+ }
1396
+ }
1397
+ _memberTypesByType.set(typeNode, members);
1398
+ }
1399
+ const typeText = members.get(name);
1400
+ if (!typeText || typeText === 'var') return null;
1401
+ if (callable && _localNamesOfCallable(callable).has(name)) return null;
1402
+ return typeText;
1403
+ }
1404
+
1405
+ const _localNamesByCallable = new WeakMap();
1406
+
1407
+ // Every name a member binds locally (parameters, locals, lambda and pattern
1408
+ // variables, foreach and catch variables), one native walk per member.
1409
+ function _localNamesOfCallable(callable) {
1410
+ let names = _localNamesByCallable.get(callable);
1411
+ if (names) return names;
1412
+ names = new Set();
1413
+ for (const node of callable.descendantsOfType(['variable_declarator', 'parameter', 'implicit_parameter',
1414
+ 'declaration_expression', 'declaration_pattern', 'foreach_statement', 'catch_declaration'])) {
1415
+ const bound = node.type === 'implicit_parameter' ? node
1416
+ : node.type === 'foreach_statement' ? node.childForFieldName('left')
1417
+ : node.childForFieldName('name') || node.namedChildren.at(-1);
1418
+ if (bound?.text) names.add(bound.text);
1419
+ }
1420
+ // `params T[] name` parameters are siblings of the list (structuredParams).
1421
+ const list = callable.childForFieldName('parameters');
1422
+ for (let i = 0; list && i < list.childCount; i++) {
1423
+ const child = list.child(i);
1424
+ if (child.type === 'identifier') names.add(child.text);
1425
+ }
1426
+ _localNamesByCallable.set(callable, names);
1427
+ return names;
1428
+ }
1429
+
637
1430
  const CALLABLE_SCOPE_NODES = new Set([
638
1431
  'method_declaration', 'constructor_declaration', 'destructor_declaration',
639
1432
  'operator_declaration', 'conversion_operator_declaration',
@@ -651,6 +1444,130 @@ function variableScopeKey(node) {
651
1444
  return 'global';
652
1445
  }
653
1446
 
1447
+ const localBindingFactsByTree = new WeakMap();
1448
+
1449
+ /**
1450
+ * Local binding facts of one callable (fix #381): how many times each name
1451
+ * is declared (locals, parameters, pattern/out variables, foreach and catch
1452
+ * variables, lambda parameters) and which names are assigned after their
1453
+ * declaration. A one-hop `var` alias needs one declaration and no
1454
+ * assignment.
1455
+ */
1456
+ function csharpLocalBindingFacts(fnNode) {
1457
+ let byId = localBindingFactsByTree.get(fnNode.tree);
1458
+ if (!byId) { byId = new Map(); localBindingFactsByTree.set(fnNode.tree, byId); }
1459
+ let facts = byId.get(fnNode.id);
1460
+ if (facts) return facts;
1461
+ facts = { declared: new Map(), assigned: new Set(), shapes: new Map() };
1462
+ const declare = name => { if (name) facts.declared.set(name, (facts.declared.get(name) || 0) + 1); };
1463
+ const walk = (node) => {
1464
+ for (const child of node.namedChildren || []) {
1465
+ if (child.type === 'variable_declarator' || child.type === 'parameter' ||
1466
+ child.type === 'foreach_statement' || child.type === 'catch_declaration') {
1467
+ const name = child.childForFieldName('name')?.text;
1468
+ declare(name);
1469
+ // Each declaration's `Declared x = new Constructed(..)` shape
1470
+ // (fix #394): several declarations of one name in separate
1471
+ // blocks hold the same exact type only when every shape agrees.
1472
+ if (name) {
1473
+ if (!facts.shapes.has(name)) facts.shapes.set(name, new Set());
1474
+ facts.shapes.get(name).add(csharpDeclarationShape(child));
1475
+ }
1476
+ } else if (child.type === 'declaration_pattern' || child.type === 'declaration_expression') {
1477
+ declare(child.childForFieldName('name')?.text || child.namedChildren.at(-1)?.text);
1478
+ } else if (child.type === 'assignment_expression') {
1479
+ const left = child.childForFieldName('left');
1480
+ if (left?.type === 'identifier') facts.assigned.add(left.text);
1481
+ } else if (child.type === 'prefix_unary_expression' || child.type === 'postfix_unary_expression') {
1482
+ const operand = child.namedChildren.find(c => c.type === 'identifier');
1483
+ if (operand) facts.assigned.add(operand.text);
1484
+ } else if (child.type === 'argument' &&
1485
+ (child.child(0)?.type === 'ref' || child.child(0)?.type === 'out')) {
1486
+ const operand = child.namedChildren.find(c => c.type === 'identifier');
1487
+ if (operand) facts.assigned.add(operand.text);
1488
+ }
1489
+ walk(child);
1490
+ }
1491
+ };
1492
+ walk(fnNode);
1493
+ byId.set(fnNode.id, facts);
1494
+ return facts;
1495
+ }
1496
+
1497
+ /** `Declared|Constructed` text of a variable declarator, or '?' for any other shape. */
1498
+ function csharpDeclarationShape(declarator) {
1499
+ if (declarator.type !== 'variable_declarator') return '?';
1500
+ const declaration = declarator.parent;
1501
+ const typeText = declaration?.type === 'variable_declaration'
1502
+ ? declaration.childForFieldName('type')?.text : null;
1503
+ const value = csharpDeclaratorValue(declarator);
1504
+ const constructed = value?.type === 'object_creation_expression' ? value.childForFieldName('type')?.text : null;
1505
+ return typeText && constructed ? `${typeText.replace(/\s+/g, '')}|${constructed.replace(/\s+/g, '')}` : '?';
1506
+ }
1507
+
1508
+ function csharpEnclosingCallable(node) {
1509
+ for (let parent = node?.parent; parent; parent = parent.parent) {
1510
+ if (CALLABLE_SCOPE_NODES.has(parent.type) && !isControlFlowLocalArtifact(parent)) return parent;
1511
+ }
1512
+ return null;
1513
+ }
1514
+
1515
+ /** Initializer of a variable declarator (the node after its name). */
1516
+ function csharpDeclaratorValue(declarator) {
1517
+ const named = declarator.namedChildren || [];
1518
+ return declarator.childForFieldName('value') ||
1519
+ (named.length === 2 && named[0].type === 'identifier' ? named[1] : null);
1520
+ }
1521
+
1522
+ /** A `var` local declared once and never assigned in its callable. */
1523
+ function csharpSingleVarLocal(declarator, name) {
1524
+ const declaration = declarator.parent;
1525
+ if (declaration?.type !== 'variable_declaration' ||
1526
+ declaration.childForFieldName('type')?.text !== 'var') return false;
1527
+ const fnNode = csharpEnclosingCallable(declarator);
1528
+ if (!fnNode) return false;
1529
+ const facts = csharpLocalBindingFacts(fnNode);
1530
+ return facts.declared.get(name) === 1 && !facts.assigned.has(name);
1531
+ }
1532
+
1533
+ /**
1534
+ * The member access a one-hop `var` alias stands for (fix #381): `var c =
1535
+ * this.config;` or `var c = config;` (a field or property of the enclosing
1536
+ * type), when `c` is declared once and never assigned. Null otherwise.
1537
+ */
1538
+ function csharpFieldAliasOf(identNode) {
1539
+ const fnNode = csharpEnclosingCallable(identNode);
1540
+ if (!fnNode) return null;
1541
+ const name = identNode.text;
1542
+ const facts = csharpLocalBindingFacts(fnNode);
1543
+ if (facts.declared.get(name) !== 1 || facts.assigned.has(name)) return null;
1544
+ let found = null;
1545
+ const stack = [fnNode];
1546
+ while (stack.length > 0 && !found) {
1547
+ const current = stack.pop();
1548
+ for (const child of current.namedChildren || []) {
1549
+ if (child.type === 'variable_declarator' &&
1550
+ child.childForFieldName('name')?.text === name) {
1551
+ found = child;
1552
+ break;
1553
+ }
1554
+ stack.push(child);
1555
+ }
1556
+ }
1557
+ if (!found || found.endIndex > identNode.startIndex || !csharpSingleVarLocal(found, name)) return null;
1558
+ const value = csharpDeclaratorValue(found);
1559
+ if (!value) return null;
1560
+ if (value.type === 'member_access_expression' && value.namedChildCount === 1 &&
1561
+ ['this', 'this_expression'].includes(value.child(0)?.type)) {
1562
+ return value;
1563
+ }
1564
+ if (value.type === 'identifier' && value.text !== name && !facts.declared.has(value.text) &&
1565
+ enclosingTypeDeclaresMember(identNode, value.text)) {
1566
+ return value;
1567
+ }
1568
+ return null;
1569
+ }
1570
+
654
1571
  function buildVariableTypes(tree, parser) {
655
1572
  const byScope = new Map([['global', new ReceiverTypeMap()]]);
656
1573
  const conflictsByScope = new Map([['global', new Set()]]);
@@ -703,6 +1620,22 @@ function buildVariableTypes(tree, parser) {
703
1620
  });
704
1621
  }
705
1622
  }
1623
+ if (node.type === 'parameter_list') {
1624
+ // `params T[] name` is three siblings of the list (see
1625
+ // structuredParams): its name is a typed parameter too (fix #391).
1626
+ for (let i = 0; i < node.childCount; i++) {
1627
+ if (node.child(i).type !== 'params') continue;
1628
+ const named = [];
1629
+ for (let j = i + 1; j < node.childCount && named.length < 2; j++) {
1630
+ const child = node.child(j);
1631
+ if (child.type === ',' || child.type === ')') break;
1632
+ if (child.isNamed) named.push(child);
1633
+ }
1634
+ if (named.length === 2 && named[1].type === 'identifier') {
1635
+ setType(currentKey, named[1].text, named[0].text, 'annotation', node);
1636
+ }
1637
+ }
1638
+ }
706
1639
  if (node.type === 'parameter') {
707
1640
  let artifactParameter = false;
708
1641
  for (let parent = node.parent; parent; parent = parent.parent) {
@@ -750,8 +1683,47 @@ function buildVariableTypes(tree, parser) {
750
1683
  declarator.namedChildren.find(child => child.type === 'object_creation_expression');
751
1684
  const dynamicType = value?.type === 'object_creation_expression'
752
1685
  ? value.childForFieldName('type')?.text : null;
753
- const type = dynamicType || (typeNode?.text !== 'var' ? typeNode?.text : null);
754
- setType(currentKey, name, type, dynamicType ? 'constructor' : 'annotation', value || typeNode);
1686
+ // `var c = items[i]` takes the declared element (fix #359).
1687
+ let elementType = null;
1688
+ const indexedValue = value ||
1689
+ declarator.namedChildren.find(child => child.type === 'element_access_expression');
1690
+ if (!dynamicType && typeNode?.text === 'var' &&
1691
+ indexedValue?.type === 'element_access_expression') {
1692
+ const indexed = indexedReceiverType(indexedValue, byScope.get(currentKey));
1693
+ if (indexed) {
1694
+ elementType = indexed.namespace
1695
+ ? `${indexed.namespace}.${indexed.name}` : indexed.name;
1696
+ }
1697
+ }
1698
+ // The declared type is the receiver's static type (fix
1699
+ // #394): `IShape s = new Square(); s.Area()` binds
1700
+ // IShape.Area. `var` takes the constructed type; a
1701
+ // declaration of the constructed type keeps the exact
1702
+ // constructor evidence.
1703
+ const declared = typeNode?.text && typeNode.text !== 'var' ? typeNode.text : null;
1704
+ const constructorTyped = !!dynamicType && (!declared ||
1705
+ declared.replace(/\s+/g, '') === dynamicType.replace(/\s+/g, ''));
1706
+ const type = declared || dynamicType || elementType;
1707
+ // A local declared once and never assigned holds exactly the
1708
+ // constructed value: dispatch reaches that type's member,
1709
+ // never an unrelated implementation of the declared type.
1710
+ const fnOfLocal = dynamicType && declared && !constructorTyped
1711
+ ? csharpEnclosingCallable(declarator) : null;
1712
+ const facts = fnOfLocal ? csharpLocalBindingFacts(fnOfLocal) : null;
1713
+ const shapes = facts?.shapes.get(name);
1714
+ const exactOrigin = facts && !facts.assigned.has(name) && shapes?.size === 1 && !shapes.has('?')
1715
+ ? { ...typeOrigin('annotation', typeNode), constructedType: dynamicType } : null;
1716
+ setType(currentKey, name, type, exactOrigin || (constructorTyped ? 'constructor' : 'annotation'),
1717
+ constructorTyped ? value : (declared ? typeNode : (value || typeNode)));
1718
+ // fix #381: `var c = cfg;` carries cfg's type when c is a
1719
+ // single, never-assigned local.
1720
+ const aliasValue = csharpDeclaratorValue(declarator);
1721
+ if (!type && aliasValue?.type === 'identifier' && aliasValue.text !== name &&
1722
+ byScope.get(currentKey)?.has(aliasValue.text) && csharpSingleVarLocal(declarator, name)) {
1723
+ const types = byScope.get(currentKey);
1724
+ setType(currentKey, name, types.get(aliasValue.text),
1725
+ types.origins.get(aliasValue.text) || 'flow');
1726
+ }
755
1727
  }
756
1728
  }
757
1729
  return true;
@@ -770,16 +1742,68 @@ function normalizeReceiverType(raw) {
770
1742
  if (!raw) return null;
771
1743
  let value = String(raw).trim().replace(/\?$/, '');
772
1744
  if (value.endsWith('[]')) return { name: 'Array', namespace: 'System' };
1745
+ // An `extern alias` qualifier names an assembly reference whose types
1746
+ // this index cannot attribute (fix #401): no receiver type.
1747
+ if (/^(?!global::)[A-Za-z_]\w*::/.test(value)) return null;
773
1748
  value = value.replace(/^global::/, '').replace(/::/g, '.');
774
1749
  const match = value.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*(?:<.*>)?$/s);
775
1750
  if (!match) return null;
776
1751
  const parts = match[1].split('.');
1752
+ const typeArgs = genericArityOf(value);
777
1753
  return {
778
1754
  name: parts.pop(),
779
1755
  ...(parts.length > 0 && { namespace: parts.join('.') }),
1756
+ ...(typeArgs > 0 && { typeArgs }),
780
1757
  };
781
1758
  }
782
1759
 
1760
+ // Indexer contracts of BCL collections (fix #359): `Conv[] a` / `List<Conv>`
1761
+ // index to Conv, `Dictionary<K, Conv>` keys to Conv. The declared element slot
1762
+ // is read from the declaration's type text; anything else abstains.
1763
+ const CSHARP_LIST_INDEXERS = new Set(['List', 'IList', 'IReadOnlyList', 'Collection', 'ReadOnlyCollection']);
1764
+ const CSHARP_MAP_INDEXERS = new Set([
1765
+ 'Dictionary', 'IDictionary', 'IReadOnlyDictionary', 'SortedDictionary',
1766
+ 'SortedList', 'ConcurrentDictionary',
1767
+ ]);
1768
+ function csharpIndexerElementText(raw) {
1769
+ if (!raw) return null;
1770
+ const value = String(raw).trim().replace(/\?$/, '');
1771
+ if (value.endsWith('[]')) {
1772
+ const inner = value.slice(0, -2).trim();
1773
+ return inner && !inner.includes('[') ? inner : null;
1774
+ }
1775
+ const open = value.indexOf('<');
1776
+ if (open <= 0 || !value.endsWith('>')) return null;
1777
+ const base = value.slice(0, open).trim().split('.').pop();
1778
+ const args = [];
1779
+ let depth = 0;
1780
+ let start = open + 1;
1781
+ for (let i = open + 1; i < value.length - 1; i++) {
1782
+ const ch = value[i];
1783
+ if (ch === '<' || ch === '[' || ch === '(') depth++;
1784
+ else if (ch === '>' || ch === ']' || ch === ')') depth--;
1785
+ else if (ch === ',' && depth === 0) {
1786
+ args.push(value.slice(start, i).trim());
1787
+ start = i + 1;
1788
+ }
1789
+ }
1790
+ args.push(value.slice(start, value.length - 1).trim());
1791
+ if (CSHARP_LIST_INDEXERS.has(base) && args.length === 1) return args[0];
1792
+ if (CSHARP_MAP_INDEXERS.has(base) && args.length === 2) return args[1];
1793
+ return null;
1794
+ }
1795
+
1796
+ function indexedReceiverType(node, variableTypes) {
1797
+ if (node?.type !== 'element_access_expression') return null;
1798
+ const expression = node.childForFieldName('expression') || node.namedChild(0);
1799
+ if (expression?.type !== 'identifier') return null;
1800
+ const element = csharpIndexerElementText(variableTypes?.get(expression.text));
1801
+ const normalized = normalizeReceiverType(element);
1802
+ if (!normalized || /^[A-Z][A-Z0-9]?$/.test(normalized.name) ||
1803
+ normalized.name === 'Array') return null;
1804
+ return normalized;
1805
+ }
1806
+
783
1807
  function literalReceiverType(node) {
784
1808
  if (!node) return null;
785
1809
  if (node.type === 'string_literal' ||
@@ -822,7 +1846,7 @@ function receiverTypeFromNode(node, variableTypes) {
822
1846
  if (current.type === 'identifier') {
823
1847
  return normalizeReceiverType(variableTypes?.get(current.text));
824
1848
  }
825
- return null;
1849
+ return indexedReceiverType(current, variableTypes);
826
1850
  }
827
1851
 
828
1852
  function receiverCastIsThis(node) {
@@ -856,6 +1880,7 @@ function receiverFieldPath(node, variableTypes, enclosingClass) {
856
1880
  fields: [],
857
1881
  rootType: declared.name,
858
1882
  ...(declared.namespace && { rootNamespace: declared.namespace }),
1883
+ ...(declared.typeArgs && { rootTypeArgs: declared.typeArgs }),
859
1884
  };
860
1885
  }
861
1886
  return {
@@ -982,14 +2007,17 @@ function staticArgKind(node, variableTypes) {
982
2007
  }
983
2008
  case 'array_creation_expression': {
984
2009
  const type = node.childForFieldName('type');
985
- return type ? `type:${type.text}` : 'expr';
2010
+ // `new object[0]` is an object[]: the rank's sizes are not part
2011
+ // of the type (fix #391).
2012
+ return type ? `type:${type.text.replace(/\[([^\]]*)\]/g, (_, inner) =>
2013
+ `[${','.repeat((inner.match(/,/g) || []).length)}]`)}` : 'expr';
986
2014
  }
987
2015
  case 'cast_expression': {
988
2016
  const type = node.childForFieldName('type');
989
2017
  return type ? `cast:${type.text}` : 'expr';
990
2018
  }
991
2019
  case 'identifier': {
992
- const type = variableTypes?.get(node.text);
2020
+ const type = variableTypes?.get(node.text) || enclosingMemberValueType(node);
993
2021
  return type ? `type:${type}` : 'expr';
994
2022
  }
995
2023
  case 'member_access_expression': {
@@ -1078,8 +2106,24 @@ function staticArgKind(node, variableTypes) {
1078
2106
  ? staticArgKind(node.namedChild(0), variableTypes)
1079
2107
  : 'expr';
1080
2108
  case 'lambda_expression':
1081
- case 'anonymous_method_expression':
1082
- return 'lambda';
2109
+ case 'anonymous_method_expression': {
2110
+ // Its parameter count selects among delegate overloads (fix
2111
+ // #391); `delegate { }` without a list converts to any count.
2112
+ const params = node.childForFieldName('parameters');
2113
+ if (!params) return 'lambda';
2114
+ return params.type === 'parameter_list'
2115
+ ? `lambda:${params.namedChildren.filter(child => child.type === 'parameter').length}`
2116
+ : 'lambda:1';
2117
+ }
2118
+ case 'binary_expression': {
2119
+ // String concatenation: `+` with a string operand is a string
2120
+ // (fix #390).
2121
+ if (node.childForFieldName('operator')?.text !== '+') return 'expr';
2122
+ const left = node.childForFieldName('left');
2123
+ const right = node.childForFieldName('right');
2124
+ return staticArgKind(left, variableTypes) === 'string' ||
2125
+ staticArgKind(right, variableTypes) === 'string' ? 'string' : 'expr';
2126
+ }
1083
2127
  default:
1084
2128
  return 'expr';
1085
2129
  }
@@ -1121,13 +2165,122 @@ function assignmentTargetOf(callNode) {
1121
2165
  return null;
1122
2166
  }
1123
2167
 
2168
+ /**
2169
+ * A local variable, parameter or lambda parameter named like a bare
2170
+ * invocation (`Action Run = ..; Run();`) is the invoked delegate, never a
2171
+ * same-named method (fix #369). Walks enclosing blocks (declarations before
2172
+ * the call), foreach/catch/using variables, lambda and callable parameters,
2173
+ * stopping at the enclosing member. Local functions are methods, not values.
2174
+ */
2175
+ const _csharpLocalNamesByTree = new WeakMap();
2176
+ /** Every local/parameter/lambda-parameter name the file declares (fix #369). */
2177
+ function csharpLocalBindingNames(tree) {
2178
+ let names = _csharpLocalNamesByTree.get(tree);
2179
+ if (names) return names;
2180
+ names = new Set();
2181
+ for (const node of tree.rootNode.descendantsOfType([
2182
+ 'variable_declarator', 'parameter', 'implicit_parameter', 'foreach_statement', 'catch_declaration'])) {
2183
+ const nameNode = node.type === 'implicit_parameter' ? node
2184
+ : node.type === 'foreach_statement' ? node.childForFieldName('left')
2185
+ : node.childForFieldName('name') ||
2186
+ (node.type === 'variable_declarator' ? node.namedChild(0) : node.namedChildren.at(-1));
2187
+ if (nameNode?.type === 'identifier' || nameNode?.type === 'implicit_parameter') names.add(nameNode.text);
2188
+ }
2189
+ _csharpLocalNamesByTree.set(tree, names);
2190
+ return names;
2191
+ }
2192
+
2193
+ function csharpBareNameShadowedByLocal(callNode, name) {
2194
+ const declares = declaration => (declaration?.namedChildren || []).some(child =>
2195
+ child.type === 'variable_declarator' &&
2196
+ (child.childForFieldName('name') || child.namedChild(0))?.text === name);
2197
+ const paramsDeclare = list => (list?.namedChildren || []).some(param =>
2198
+ param.type === 'parameter' &&
2199
+ (param.childForFieldName('name') || param.namedChildren.at(-1))?.text === name);
2200
+ let child = callNode;
2201
+ for (let p = callNode.parent; p; child = p, p = p.parent) {
2202
+ switch (p.type) {
2203
+ case 'block':
2204
+ case 'switch_section':
2205
+ for (let i = 0; i < p.namedChildCount; i++) {
2206
+ const statement = p.namedChild(i);
2207
+ if (statement.startIndex >= child.startIndex) break;
2208
+ if (statement.type === 'local_declaration_statement' &&
2209
+ declares(statement.namedChildren.find(c => c.type === 'variable_declaration'))) {
2210
+ return true;
2211
+ }
2212
+ }
2213
+ break;
2214
+ case 'foreach_statement': {
2215
+ const left = p.childForFieldName('left');
2216
+ if (left?.type === 'identifier' && left.text === name) return true;
2217
+ break;
2218
+ }
2219
+ case 'using_statement':
2220
+ case 'for_statement':
2221
+ case 'fixed_statement': {
2222
+ const declaration = p.namedChildren.find(c => c.type === 'variable_declaration');
2223
+ if (declaration && !sameNode(declaration, child) && declares(declaration)) return true;
2224
+ break;
2225
+ }
2226
+ case 'catch_clause': {
2227
+ const declaration = p.namedChildren.find(c => c.type === 'catch_declaration');
2228
+ if ((declaration?.childForFieldName('name') || null)?.text === name) return true;
2229
+ break;
2230
+ }
2231
+ case 'lambda_expression':
2232
+ case 'anonymous_method_expression': {
2233
+ const implicit = p.namedChildren.find(c => c.type === 'implicit_parameter');
2234
+ if (implicit?.text === name) return true;
2235
+ if (paramsDeclare(p.childForFieldName('parameters') ||
2236
+ p.namedChildren.find(c => c.type === 'parameter_list'))) return true;
2237
+ break;
2238
+ }
2239
+ case 'local_function_statement':
2240
+ // A local function sees its own parameters and the
2241
+ // enclosing member's locals declared before it.
2242
+ if (paramsDeclare(p.childForFieldName('parameters') ||
2243
+ p.namedChildren.find(c => c.type === 'parameter_list'))) return true;
2244
+ break;
2245
+ case 'method_declaration':
2246
+ case 'constructor_declaration':
2247
+ case 'operator_declaration':
2248
+ case 'conversion_operator_declaration':
2249
+ case 'destructor_declaration':
2250
+ return paramsDeclare(p.childForFieldName('parameters') ||
2251
+ p.namedChildren.find(c => c.type === 'parameter_list'));
2252
+ case 'accessor_declaration':
2253
+ case 'property_declaration':
2254
+ case 'indexer_declaration':
2255
+ case 'class_declaration':
2256
+ case 'struct_declaration':
2257
+ case 'record_declaration':
2258
+ case 'interface_declaration':
2259
+ return false;
2260
+ default:
2261
+ break;
2262
+ }
2263
+ }
2264
+ return false;
2265
+ }
2266
+
1124
2267
  function findCallsInCode(code, parser) {
1125
- const tree = parseTree(parser, code);
2268
+ const views = csharpViews(parser, code);
2269
+ if (!views) return findCallsInTree(code, parser, safeParse(parser, code, undefined, PARSE_OPTIONS));
2270
+ return csMergeViewItems(views, (tree, reaches) => findCallsInTree(code, parser, tree, reaches), call => call.line)
2271
+ .sort((a, b) => a.line - b.line ||
2272
+ (a.callSite?.column ?? 0) - (b.callSite?.column ?? 0));
2273
+ }
2274
+
2275
+ function findCallsInTree(code, parser, tree, reaches = null) {
1126
2276
  const variableTypesByScope = buildVariableTypes(tree, parser);
1127
2277
  const calls = [];
2278
+ // The variable types in scope at a call (only call nodes ask: the scope
2279
+ // walk climbs parents).
2280
+ const typesAt = node => variableTypesByScope.get(variableScopeKey(node)) ||
2281
+ variableTypesByScope.get('global');
1128
2282
  traverseTree(tree.rootNode, node => {
1129
- const variableTypes = variableTypesByScope.get(variableScopeKey(node)) ||
1130
- variableTypesByScope.get('global');
2283
+ if (reaches && !reaches(node)) return false;
1131
2284
  if (isControlFlowLocalArtifact(node)) {
1132
2285
  const paramsNode = node.childForFieldName('parameters');
1133
2286
  const raw = paramsNode?.text;
@@ -1150,15 +2303,26 @@ function findCallsInCode(code, parser) {
1150
2303
  if (node.type === 'invocation_expression') {
1151
2304
  const identity = invocationIdentity(node.childForFieldName('function'));
1152
2305
  if (!identity.name) return true;
2306
+ const variableTypes = typesAt(node);
1153
2307
  const args = callArgs(node, variableTypes);
1154
2308
  const first = extractStringArg(args.firstArg);
1155
- const receiverRoot = identity.receiver?.split('.')[0];
2309
+ let receiverRoot = identity.receiver?.split('.')[0];
1156
2310
  const functionNode = node.childForFieldName('function');
1157
- const receiverNode = functionNode?.type === 'member_access_expression'
2311
+ let receiverNode = functionNode?.type === 'member_access_expression'
1158
2312
  ? functionNode.childForFieldName('expression') || functionNode.namedChild(0)
1159
2313
  : functionNode?.type === 'conditional_access_expression'
1160
2314
  ? functionNode.childForFieldName('condition') || functionNode.namedChild(0)
1161
2315
  : null;
2316
+ // fix #381: `c.Load()` after `var c = this.config;` receives
2317
+ // exactly like `this.config.Load()`.
2318
+ if (receiverNode?.type === 'identifier' && !variableTypes.has(receiverNode.text)) {
2319
+ const aliased = csharpFieldAliasOf(receiverNode);
2320
+ if (aliased) {
2321
+ receiverNode = aliased;
2322
+ identity.receiver = aliased.text;
2323
+ receiverRoot = aliased.text.split('.')[0];
2324
+ }
2325
+ }
1162
2326
  const unwrappedReceiverNode = unwrapReceiverNode(receiverNode);
1163
2327
  // A root variable's type is the receiver type only for a direct
1164
2328
  // `value.Method()` call. For `value.Property.Method()` the static
@@ -1175,11 +2339,20 @@ function findCallsInCode(code, parser) {
1175
2339
  : null);
1176
2340
  const receiverType = receiverTypeInfo?.name;
1177
2341
  const receiverCastThis = receiverCastIsThis(receiverNode);
2342
+ // `Outcome<int>.Create()`: a generic type name is only ever a
2343
+ // type (fix #380); its arity is part of the type's identity.
2344
+ const genericTypeReceiver = identity.isMethod &&
2345
+ unwrappedReceiverNode?.type === 'generic_name' &&
2346
+ /^[A-Z]/.test(unwrappedReceiverNode.namedChild(0)?.text || '')
2347
+ ? unwrappedReceiverNode : null;
2348
+ if (genericTypeReceiver) identity.receiver = genericTypeReceiver.namedChild(0).text;
1178
2349
  const receiverIsTypeQualified = !!(identity.isMethod &&
1179
- unwrappedReceiverNode?.type === 'identifier' &&
2350
+ (genericTypeReceiver || (unwrappedReceiverNode?.type === 'identifier' &&
1180
2351
  /^[A-Z]/.test(identity.receiver || '') &&
1181
2352
  !variableTypes.has(identity.receiver) &&
1182
- !enclosingTypeDeclaresMember(node, identity.receiver));
2353
+ !enclosingTypeDeclaresMember(node, identity.receiver))));
2354
+ const receiverTypeArgs = genericTypeReceiver
2355
+ ? genericArityOf(genericTypeReceiver.text) : 0;
1183
2356
  const currentNamespace = namespaceOf(node, tree);
1184
2357
  let receiverCall = null;
1185
2358
  let receiverCallIsMethod = false;
@@ -1196,7 +2369,7 @@ function findCallsInCode(code, parser) {
1196
2369
  }
1197
2370
  }
1198
2371
  const assignment = assignmentTargetOf(node);
1199
- let fieldRoot, fieldName, fieldNames, fieldRootType, fieldRootNamespace;
2372
+ let fieldRoot, fieldName, fieldNames, fieldRootType, fieldRootNamespace, fieldRootTypeArgs;
1200
2373
  if (identity.isMethod && !receiverType && identity.receiver &&
1201
2374
  !receiverIsTypeQualified) {
1202
2375
  const fieldPath = receiverFieldPath(
@@ -1207,21 +2380,34 @@ function findCallsInCode(code, parser) {
1207
2380
  fieldName = fieldNames[fieldNames.length - 1];
1208
2381
  fieldRootType = fieldPath.rootType;
1209
2382
  fieldRootNamespace = fieldPath.rootNamespace || currentNamespace;
2383
+ fieldRootTypeArgs = fieldPath.rootTypeArgs;
1210
2384
  }
1211
2385
  }
2386
+ const functionIsBareName = functionNode?.type === 'identifier' &&
2387
+ csharpLocalBindingNames(tree).has(identity.name);
2388
+ // `M<string>(..)`: explicit method type arguments (fix #391).
2389
+ const methodTypeArgs = identity.nameNode?.type === 'generic_name'
2390
+ ? genericArityOf(identity.nameNode.text) : 0;
1212
2391
  calls.push({
1213
2392
  callSite: typeOrigin('call', identity.nameNode || node),
1214
2393
  name: identity.name,
1215
2394
  line: identity.nameNode?.startPosition.row + 1 || node.startPosition.row + 1,
1216
2395
  isMethod: identity.isMethod,
2396
+ ...(methodTypeArgs > 0 && { methodTypeArgs }),
2397
+ ...(functionIsBareName && csharpBareNameShadowedByLocal(node, identity.name) &&
2398
+ { localShadow: true }),
1217
2399
  ...(identity.receiver && { receiver: identity.receiver }),
1218
2400
  ...(receiverIsTypeQualified && { receiverIsTypeQualified: true }),
2401
+ ...(receiverTypeArgs > 0 && { receiverTypeArgs }),
2402
+ ...(receiverType && receiverTypeInfo.typeArgs && { receiverTypeArgs: receiverTypeInfo.typeArgs }),
1219
2403
  ...(receiverType && { receiverType, ...(unwrappedReceiverNode?.type === 'cast_expression' ? {
1220
2404
  receiverTypeSource: 'cast', receiverTypeEvidence: typeOrigin('cast', unwrappedReceiverNode),
1221
2405
  } : unwrappedReceiverNode?.type === 'object_creation_expression' ? {
1222
2406
  receiverTypeSource: 'constructor', receiverTypeEvidence: typeOrigin('constructor', unwrappedReceiverNode),
1223
2407
  } : literalReceiverType(unwrappedReceiverNode) ? {
1224
2408
  receiverTypeSource: 'literal', receiverTypeEvidence: typeOrigin('literal', unwrappedReceiverNode),
2409
+ } : unwrappedReceiverNode?.type === 'element_access_expression' ? {
2410
+ receiverTypeSource: 'annotation', receiverTypeEvidence: typeOrigin('annotation', unwrappedReceiverNode),
1225
2411
  } : variableTypes.fields(identity.receiver)) }),
1226
2412
  ...(receiverCastThis && { receiverCastThis: true }),
1227
2413
  ...(receiverType && receiverTypeInfo.namespace && {
@@ -1233,8 +2419,13 @@ function findCallsInCode(code, parser) {
1233
2419
  receiverFields: fieldNames,
1234
2420
  ...(fieldRootType && { receiverRootType: fieldRootType }),
1235
2421
  ...(fieldRootNamespace && { receiverRootNamespace: fieldRootNamespace }),
2422
+ ...(fieldRootTypeArgs && { receiverRootTypeArgs: fieldRootTypeArgs }),
1236
2423
  }),
1237
2424
  ...(receiverCall && { receiverCall }),
2425
+ // The receiver IS the producer call (fix #395): chained
2426
+ // consumers are typed from their producer, never read as a
2427
+ // type name spelled like the receiver text.
2428
+ ...(receiverCall && { receiverIsChainRoot: true }),
1238
2429
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1239
2430
  ...(receiverCallLine && { receiverCallLine }),
1240
2431
  ...(receiverCallReceiver && { receiverCallReceiver }),
@@ -1280,17 +2471,65 @@ function findCallsInCode(code, parser) {
1280
2471
  }
1281
2472
  if (node.type === 'object_creation_expression' ||
1282
2473
  node.type === 'implicit_object_creation_expression') {
1283
- const typeNode = node.childForFieldName('type');
2474
+ // A target-typed `new(...)` constructs the type its position
2475
+ // declares (fix #393): a typed local or field, a property, or
2476
+ // the return type of the enclosing member.
2477
+ let typeNode = node.childForFieldName('type') ||
2478
+ (node.type === 'implicit_object_creation_expression' ? implicitCreationTarget(node) : null);
2479
+ const slot = !typeNode && node.type === 'implicit_object_creation_expression'
2480
+ ? implicitCreationSlot(node) : null;
2481
+ // A generic receiver's written type (fix #401): `Box<Item> b;
2482
+ // b.Put(new(..))` binds `Put(T)` with the receiver's T.
2483
+ if (slot?.kind === 'argument' && slot.instance && slot.receiverName) {
2484
+ const receiverText = String(typesAt(node)?.get(slot.receiverName) || '');
2485
+ if (/^[A-Za-z_][\w.]*<.+>$/s.test(receiverText)) slot.receiverTypeText = receiverText;
2486
+ }
2487
+ // `x = new(..)` to a local or parameter: its static type (an
2488
+ // untyped local is no member of the class either).
2489
+ const localSlot = slot?.kind === 'member' && !slot.this &&
2490
+ (typesAt(node)?.has(slot.member) || csharpBareNameShadowedByLocal(node, slot.member));
2491
+ if (localSlot) {
2492
+ const local = String(typesAt(node)?.get(slot.member) || '');
2493
+ if (/^[A-Za-z_][\w.]*(<.*>)?\??$/.test(local) && local !== 'dynamic') {
2494
+ typeNode = { text: local.replace(/\?$/, '') };
2495
+ }
2496
+ }
2497
+ if (!typeNode && slot && !localSlot) {
2498
+ // The type is decided at query time from the overload or
2499
+ // member the slot names (fix #400).
2500
+ const args = callArgs(node, typesAt(node));
2501
+ const siteNode = node.child(0) || node;
2502
+ calls.push({
2503
+ callSite: typeOrigin('call', siteNode),
2504
+ name: 'new',
2505
+ line: siteNode.startPosition.row + 1,
2506
+ targetTyped: true,
2507
+ targetSlot: slot,
2508
+ isMethod: false,
2509
+ isConstructor: true,
2510
+ argCount: args.argCount,
2511
+ ...(args.argKinds && { argKinds: args.argKinds }),
2512
+ enclosingFunction: enclosingFunctionOf(node),
2513
+ });
2514
+ return true;
2515
+ }
1284
2516
  if (!typeNode) return true;
1285
- const args = callArgs(node, variableTypes);
2517
+ const args = callArgs(node, typesAt(node));
1286
2518
  const raw = typeNode.text.replace(/<.*>$/, '');
1287
2519
  const name = raw.split('.').pop();
2520
+ const typeArgs = genericArityOf(typeNode.text);
2521
+ // A target-typed site is the `new` token; its type is written
2522
+ // elsewhere.
2523
+ const targetTyped = !node.childForFieldName('type');
2524
+ const siteNode = targetTyped ? (node.child(0) || node) : typeNode;
1288
2525
  calls.push({
1289
- callSite: typeOrigin('call', typeNode),
2526
+ callSite: typeOrigin('call', siteNode),
1290
2527
  name,
1291
- line: typeNode.startPosition.row + 1,
2528
+ line: siteNode.startPosition.row + 1,
2529
+ ...(targetTyped && { targetTyped: true }),
1292
2530
  isMethod: false,
1293
2531
  isConstructor: true,
2532
+ ...(typeArgs > 0 && { typeArgs }),
1294
2533
  argCount: args.argCount,
1295
2534
  ...(args.argKinds && { argKinds: args.argKinds }),
1296
2535
  enclosingFunction: enclosingFunctionOf(node),
@@ -1302,7 +2541,13 @@ function findCallsInCode(code, parser) {
1302
2541
  }
1303
2542
 
1304
2543
  function findImportsInCode(code, parser) {
1305
- const tree = parseTree(parser, code);
2544
+ const views = csharpViews(parser, code);
2545
+ if (!views) return findImportsInTree(safeParse(parser, code, undefined, PARSE_OPTIONS));
2546
+ return csMergeViewItems(views, findImportsInTree, item => item.line)
2547
+ .sort((a, b) => a.line - b.line);
2548
+ }
2549
+
2550
+ function findImportsInTree(tree) {
1306
2551
  const imports = [];
1307
2552
  traverseTreeCached(tree.rootNode, node => {
1308
2553
  if (node.type !== 'using_directive') return true;
@@ -1310,12 +2555,23 @@ function findImportsInCode(code, parser) {
1310
2555
  const named = node.namedChildren || [];
1311
2556
  const moduleNode = named[named.length - 1];
1312
2557
  if (!moduleNode) return false;
2558
+ // `global::N.M` names N.M from the global namespace; any other
2559
+ // spelling is resolved from the namespace the directive sits in
2560
+ // (fix #395), which the directive records.
2561
+ const written = moduleNode.text.replace(/\s+/g, '');
2562
+ const rooted = written.startsWith('global::');
2563
+ const head = node.text.trimStart();
2564
+ const isGlobal = /^global\s+using\b/.test(head);
2565
+ const enclosing = isGlobal ? null : namespaceOf(node, tree);
2566
+ const isStatic = /^(?:global\s+)?using\s+static\b/.test(head);
1313
2567
  imports.push({
1314
- module: moduleNode.text,
2568
+ module: rooted ? written.slice('global::'.length) : moduleNode.text,
1315
2569
  names: nameNode ? [nameNode.text] : ['*'],
1316
2570
  type: 'using',
1317
2571
  line: node.startPosition.row + 1,
1318
- ...(node.text.trimStart().startsWith('global using ') && { global: true }),
2572
+ ...(isGlobal && { global: true }),
2573
+ ...(isStatic && { static: true }),
2574
+ ...(enclosing && !rooted && { namespace: enclosing }),
1319
2575
  });
1320
2576
  return false;
1321
2577
  });
@@ -1323,7 +2579,19 @@ function findImportsInCode(code, parser) {
1323
2579
  }
1324
2580
 
1325
2581
  function findUsagesInCode(code, name, parser, existingTree) {
1326
- const tree = existingTree || parseTree(parser, code);
2582
+ // A query tree of a file with conditional directives is its primary
2583
+ // view; the rows other views own are read from them (fix #391).
2584
+ const known = existingTree && csViewsByTree.get(existingTree);
2585
+ const views = known?.code === code ? known.views
2586
+ : (!existingTree || existingTree.rootNode.hasError ? csharpViews(parser, code) : null);
2587
+ if (views) {
2588
+ return csMergeViewItems(views, tree => usagesInTree(code, name, parser, tree), usage => usage.line)
2589
+ .sort((a, b) => a.line - b.line || (a.column ?? 0) - (b.column ?? 0));
2590
+ }
2591
+ return usagesInTree(code, name, parser, existingTree || safeParse(parser, code, undefined, PARSE_OPTIONS));
2592
+ }
2593
+
2594
+ function usagesInTree(code, name, parser, tree) {
1327
2595
  const usages = [];
1328
2596
  const variableTypesByScope = buildVariableTypes(tree, parser);
1329
2597
  visitNameNodes(tree, code, name, node => {
@@ -1411,22 +2679,108 @@ function isPlatformConcreteCall(receiverType, _methodName) {
1411
2679
  return !!normalized && CSHARP_PLATFORM_RECEIVER_TYPES.has(normalized.name);
1412
2680
  }
1413
2681
 
2682
+ // fix #380: language features whose lowering references a compiler-required
2683
+ // type by its full name (IsExternalInit for `init` accessors and positional
2684
+ // records, RequiredMemberAttribute for `required`, System.Index/Range for
2685
+ // `^i`/`a..b`, ...). A project declaration of such a type is used by the
2686
+ // compiler exactly when the feature appears (trait languageProtocolType).
2687
+ function languageFeatures(tree) {
2688
+ const features = new Set();
2689
+ const root = tree.rootNode;
2690
+ const hasToken = (node, token) => {
2691
+ for (let i = 0; i < node.childCount; i++) {
2692
+ if (node.child(i).type === token || node.child(i).text === token) return true;
2693
+ }
2694
+ return false;
2695
+ };
2696
+ const nodes = root.descendantsOfType([
2697
+ 'accessor_declaration', 'record_declaration', 'modifier', 'prefix_unary_expression',
2698
+ 'range_expression', 'nullable_type', 'nullable_directive', 'struct_declaration',
2699
+ 'type_parameter_constraint', 'tuple_element', 'parameter',
2700
+ ]);
2701
+ for (const node of nodes) {
2702
+ switch (node.type) {
2703
+ case 'accessor_declaration':
2704
+ if (hasToken(node, 'init')) features.add('init');
2705
+ break;
2706
+ case 'record_declaration':
2707
+ if (node.namedChildren.some(child => child.type === 'parameter_list')) features.add('init');
2708
+ break;
2709
+ case 'modifier':
2710
+ if (node.text === 'required') features.add('required');
2711
+ else if (node.text === 'this' && node.parent?.type === 'parameter') features.add('extension-method');
2712
+ else if (node.text === 'in' && node.parent?.type === 'parameter') features.add('readonly');
2713
+ break;
2714
+ case 'prefix_unary_expression':
2715
+ if (node.child(0)?.type === '^') features.add('index-range');
2716
+ break;
2717
+ case 'range_expression':
2718
+ features.add('index-range');
2719
+ break;
2720
+ case 'nullable_type':
2721
+ case 'nullable_directive':
2722
+ features.add('nullable');
2723
+ break;
2724
+ case 'struct_declaration':
2725
+ for (const child of node.namedChildren) {
2726
+ if (child.type !== 'modifier') continue;
2727
+ if (child.text === 'ref') features.add('ref-struct');
2728
+ if (child.text === 'readonly') features.add('readonly');
2729
+ }
2730
+ break;
2731
+ case 'type_parameter_constraint':
2732
+ if (node.text === 'unmanaged') features.add('unmanaged');
2733
+ break;
2734
+ case 'tuple_element':
2735
+ if (node.childForFieldName('name')) features.add('tuple-names');
2736
+ break;
2737
+ default:
2738
+ break;
2739
+ }
2740
+ }
2741
+ return [...features].sort();
2742
+ }
2743
+
1414
2744
  function parse(code, parser) {
1415
- const tree = parseTree(parser, code);
2745
+ const views = csharpViews(parser, code);
2746
+ const tree = views ? views.primary.tree : safeParse(parser, code, undefined, PARSE_OPTIONS);
2747
+ const featureSet = new Set(languageFeatures(tree));
2748
+ let errorRegions = tree.rootNode.hasError ? parseErrorRegions(tree.rootNode) : [];
2749
+ for (const view of views?.secondary || []) {
2750
+ for (const feature of languageFeatures(view.tree)) featureSet.add(feature);
2751
+ if (!view.tree.rootNode.hasError) continue;
2752
+ // A secondary view's damage counts where it supplies the rows.
2753
+ const owned = parseErrorRegions(view.tree.rootNode).filter(([start, end]) => {
2754
+ for (let row = start - 1; row < end; row++) if (view.owns.has(row)) return true;
2755
+ return false;
2756
+ });
2757
+ errorRegions = [...errorRegions, ...owned].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
2758
+ }
2759
+ const features = [...featureSet].sort();
2760
+ const classes = findClasses(code, parser);
1416
2761
  return {
1417
2762
  language: 'csharp',
1418
2763
  totalLines: code.length === 0 ? 0 : code.split('\n').length,
1419
2764
  functions: findFunctions(code, parser),
1420
- classes: findClasses(code, parser),
2765
+ classes,
1421
2766
  stateObjects: findStateObjects(code, parser),
1422
2767
  imports: findImportsInCode(code, parser),
1423
- exports: findExportsInCodeShallow(code, parser),
1424
- ...(tree.rootNode.hasError && { parseRecovery: true }),
2768
+ exports: exportsOfClasses(classes),
2769
+ ...((tree.rootNode.hasError || errorRegions.length > 0) && {
2770
+ parseRecovery: true, parseErrorRegions: errorRegions,
2771
+ }),
2772
+ ...(features.length > 0 && { languageFeatures: features }),
2773
+ ...(views && { conditionalViews: views.groups.map(group => [...group]) }),
1425
2774
  };
1426
2775
  }
1427
2776
 
1428
2777
  function findExportsInCodeShallow(code, parser) {
1429
- const classes = findClasses(code, parser);
2778
+ return exportsOfClasses(findClasses(code, parser));
2779
+ }
2780
+
2781
+ // Public top-level types are the file's exports (read from the classes
2782
+ // parse() already extracted, instead of extracting them again).
2783
+ function exportsOfClasses(classes) {
1430
2784
  const exports = [];
1431
2785
  for (const cls of classes) {
1432
2786
  if (cls.modifiers.includes('public')) {
@@ -1437,6 +2791,9 @@ function findExportsInCodeShallow(code, parser) {
1437
2791
  }
1438
2792
 
1439
2793
  module.exports = {
2794
+ queryTree,
2795
+ queryTreeIsPlain,
2796
+ analysisTree: queryTree,
1440
2797
  findFunctions,
1441
2798
  findClasses,
1442
2799
  findStateObjects,