gitnexus 1.6.11-rc.2 → 1.6.11-rc.21

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 (158) hide show
  1. package/README.md +43 -1
  2. package/dist/_shared/impact-risk.d.ts +37 -0
  3. package/dist/_shared/impact-risk.d.ts.map +1 -0
  4. package/dist/_shared/impact-risk.js +92 -0
  5. package/dist/_shared/impact-risk.js.map +1 -0
  6. package/dist/_shared/index.d.ts +2 -0
  7. package/dist/_shared/index.d.ts.map +1 -1
  8. package/dist/_shared/index.js +2 -0
  9. package/dist/_shared/index.js.map +1 -1
  10. package/dist/cli/ai-context.js +4 -4
  11. package/dist/cli/analyze-config.d.ts +2 -0
  12. package/dist/cli/analyze-config.js +16 -0
  13. package/dist/cli/analyze-options.d.ts +4 -0
  14. package/dist/cli/analyze.d.ts +17 -0
  15. package/dist/cli/analyze.js +43 -7
  16. package/dist/cli/eval-server.js +22 -5
  17. package/dist/cli/group.js +7 -1
  18. package/dist/cli/help-i18n.js +2 -0
  19. package/dist/cli/i18n/en.d.ts +12 -2
  20. package/dist/cli/i18n/en.js +12 -2
  21. package/dist/cli/i18n/resources.d.ts +22 -2
  22. package/dist/cli/i18n/zh-CN.d.ts +10 -0
  23. package/dist/cli/i18n/zh-CN.js +12 -2
  24. package/dist/cli/index.js +11 -4
  25. package/dist/cli/status.js +91 -6
  26. package/dist/cli/watch-queue.d.ts +41 -0
  27. package/dist/cli/watch-queue.js +184 -0
  28. package/dist/cli/watch.d.ts +20 -0
  29. package/dist/cli/watch.js +372 -0
  30. package/dist/cli/wiki.js +15 -2
  31. package/dist/config/ignore-service.d.ts +11 -0
  32. package/dist/config/ignore-service.js +41 -4
  33. package/dist/config/repo-control-file.d.ts +3 -0
  34. package/dist/config/repo-control-file.js +115 -0
  35. package/dist/core/group/config-parser.js +20 -2
  36. package/dist/core/group/cross-impact.d.ts +2 -1
  37. package/dist/core/group/cross-impact.js +33 -2
  38. package/dist/core/group/extractors/fs-utils.d.ts +2 -0
  39. package/dist/core/group/extractors/fs-utils.js +86 -0
  40. package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
  41. package/dist/core/group/extractors/graphql-extractor.js +652 -0
  42. package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
  43. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  44. package/dist/core/group/extractors/manifest-extractor.js +1 -1
  45. package/dist/core/group/matching.js +8 -1
  46. package/dist/core/group/service.js +5 -1
  47. package/dist/core/group/storage.js +1 -0
  48. package/dist/core/group/sync.d.ts +3 -1
  49. package/dist/core/group/sync.js +43 -5
  50. package/dist/core/group/types.d.ts +19 -5
  51. package/dist/core/incremental/derived-writeback.d.ts +36 -0
  52. package/dist/core/incremental/derived-writeback.js +68 -0
  53. package/dist/core/incremental/subgraph-extract.d.ts +6 -4
  54. package/dist/core/incremental/subgraph-extract.js +7 -5
  55. package/dist/core/index-content-drift.d.ts +54 -0
  56. package/dist/core/index-content-drift.js +127 -0
  57. package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
  58. package/dist/core/ingestion/filesystem-walker.js +20 -3
  59. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
  60. package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
  61. package/dist/core/ingestion/language-provider.d.ts +43 -0
  62. package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
  63. package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
  64. package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
  65. package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
  66. package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
  67. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  68. package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
  69. package/dist/core/ingestion/languages/java/captures.js +14 -1
  70. package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
  71. package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
  72. package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
  73. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
  74. package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
  75. package/dist/core/ingestion/languages/java.js +2 -0
  76. package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
  77. package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
  78. package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
  79. package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
  80. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
  81. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
  82. package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
  83. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
  84. package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
  85. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
  86. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
  87. package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
  88. package/dist/core/ingestion/languages/kotlin.js +2 -0
  89. package/dist/core/ingestion/pipeline-phases/di.js +47 -14
  90. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
  91. package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
  92. package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
  93. package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
  94. package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
  95. package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
  96. package/dist/core/ingestion/pipeline.d.ts +6 -0
  97. package/dist/core/ingestion/pipeline.js +44 -12
  98. package/dist/core/ingestion/scope-extractor.js +1 -0
  99. package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
  100. package/dist/core/ingestion/utils/symbol-labels.js +2 -2
  101. package/dist/core/ingestion/workers/parse-worker.js +28 -2
  102. package/dist/core/lbug/lbug-adapter.d.ts +59 -0
  103. package/dist/core/lbug/lbug-adapter.js +154 -1
  104. package/dist/core/run-analyze.d.ts +17 -0
  105. package/dist/core/run-analyze.js +231 -23
  106. package/dist/core/search/fts-indexes.d.ts +19 -1
  107. package/dist/core/search/fts-indexes.js +28 -1
  108. package/dist/core/wiki/generator.js +8 -0
  109. package/dist/core/wiki/grok-client.d.ts +21 -0
  110. package/dist/core/wiki/grok-client.js +287 -0
  111. package/dist/core/wiki/llm-client.d.ts +1 -1
  112. package/dist/core/wiki/llm-client.js +5 -2
  113. package/dist/core/wiki/local-cli-client.d.ts +11 -0
  114. package/dist/core/wiki/local-cli-client.js +22 -9
  115. package/dist/mcp/local/local-backend.d.ts +24 -7
  116. package/dist/mcp/local/local-backend.js +190 -81
  117. package/dist/mcp/local/pdg-impact.d.ts +8 -4
  118. package/dist/mcp/local/pdg-impact.js +7 -2
  119. package/dist/mcp/repository-policy.d.ts +5 -1
  120. package/dist/mcp/repository-policy.js +48 -4
  121. package/dist/mcp/resources.js +2 -1
  122. package/dist/mcp/server.js +6 -5
  123. package/dist/mcp/tools.js +31 -19
  124. package/dist/server/api.js +23 -64
  125. package/dist/server/grep-params.d.ts +18 -0
  126. package/dist/server/grep-params.js +83 -0
  127. package/dist/server/grep-scan.d.ts +23 -0
  128. package/dist/server/grep-scan.js +107 -0
  129. package/dist/server/grep-worker.d.ts +1 -0
  130. package/dist/server/grep-worker.js +12 -0
  131. package/dist/server/mcp-http.d.ts +8 -0
  132. package/dist/server/mcp-http.js +16 -1
  133. package/dist/storage/file-hash.d.ts +5 -0
  134. package/dist/storage/file-hash.js +16 -6
  135. package/dist/storage/fs-atomic.d.ts +24 -0
  136. package/dist/storage/fs-atomic.js +86 -2
  137. package/dist/storage/git.d.ts +19 -8
  138. package/dist/storage/git.js +83 -24
  139. package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
  140. package/dist/storage/gitnexus-managed-paths.js +46 -0
  141. package/dist/storage/parse-cache.d.ts +22 -4
  142. package/dist/storage/parse-cache.js +106 -29
  143. package/dist/storage/parsedfile-store.d.ts +34 -60
  144. package/dist/storage/parsedfile-store.js +177 -171
  145. package/dist/storage/repo-manager.d.ts +11 -1
  146. package/dist/storage/repo-manager.js +23 -2
  147. package/dist/storage/repo-meta.d.ts +12 -0
  148. package/dist/storage/v8-sidecar.d.ts +48 -0
  149. package/dist/storage/v8-sidecar.js +347 -0
  150. package/dist/types/pipeline.d.ts +14 -0
  151. package/package.json +4 -1
  152. package/scripts/cross-platform-shard.ts +4 -2
  153. package/scripts/cross-platform-tests.ts +7 -1
  154. package/skills/gitnexus-cli.md +11 -3
  155. package/skills/gitnexus-impact-analysis.md +9 -0
  156. package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
  157. package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
  158. package/web/index.html +1 -1
@@ -0,0 +1,954 @@
1
+ /**
2
+ * ASP.NET Core ViewComponent convention support.
3
+ *
4
+ * Same bound as Spring Boot DI in Java/Kotlin: do not resolve into the SDK
5
+ * (`Microsoft.AspNetCore.Mvc.ViewComponent`, `IViewComponentHelper`,
6
+ * `Component.InvokeAsync` itself). Those types live outside the workspace.
7
+ * The only hop worth taking is the framework convention that lands on an
8
+ * **in-repo** class — `InvokeAsync("Foo")` → workspace `FooViewComponent`,
9
+ * just as a Spring `@Autowired IFoo` fans out to an in-repo `@Service`,
10
+ * not to `ApplicationContext`.
11
+ *
12
+ * Razor templates are not parsed as C# (markup + code would poison
13
+ * tree-sitter-c-sharp). A small Razor state machine extracts C# islands and
14
+ * markup tag helpers; C# files use a string/comment-aware lexer so attributes
15
+ * and literals are not mistaken for helper calls. Literal names are enough
16
+ * because the target catalog is already built from parsed `.cs` classes.
17
+ */
18
+ import fs from 'node:fs/promises';
19
+ import path from 'node:path';
20
+ import { glob } from 'glob';
21
+ import { createIgnoreFilter } from '../../../../config/ignore-service.js';
22
+ import { generateId } from '../../../../lib/utils.js';
23
+ import { getMaxFileSizeBytes } from '../../utils/max-file-size.js';
24
+ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
25
+ import { definitionIdPosition } from '../../scope-resolution/utils/definition-id.js';
26
+ const VIEW_COMPONENT_SUFFIX = 'ViewComponent';
27
+ const VIEW_COMPONENT_TAG_RE = /<\s*vc:([a-z][a-z0-9-]*)\b/gi;
28
+ const COMPONENT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
29
+ const TYPE_MODIFIERS = new Set([
30
+ 'public',
31
+ 'internal',
32
+ 'protected',
33
+ 'private',
34
+ 'abstract',
35
+ 'sealed',
36
+ 'partial',
37
+ 'static',
38
+ 'new',
39
+ 'file',
40
+ 'required',
41
+ 'unsafe',
42
+ 'readonly',
43
+ ]);
44
+ const RAZOR_BLOCK_KEYWORDS = new Set([
45
+ 'if',
46
+ 'for',
47
+ 'foreach',
48
+ 'while',
49
+ 'using',
50
+ 'switch',
51
+ 'try',
52
+ 'lock',
53
+ 'functions',
54
+ 'helper',
55
+ 'code',
56
+ 'section',
57
+ 'do',
58
+ ]);
59
+ class SourceCursor {
60
+ source;
61
+ i = 0;
62
+ line = 1;
63
+ col = 0;
64
+ constructor(source) {
65
+ this.source = source;
66
+ }
67
+ get length() {
68
+ return this.source.length;
69
+ }
70
+ get done() {
71
+ return this.i >= this.source.length;
72
+ }
73
+ peek(n = 0) {
74
+ return this.source[this.i + n] ?? '';
75
+ }
76
+ startsWith(value) {
77
+ return this.source.startsWith(value, this.i);
78
+ }
79
+ snapshot() {
80
+ return { i: this.i, line: this.line, col: this.col };
81
+ }
82
+ restore(pos) {
83
+ this.i = pos.i;
84
+ this.line = pos.line;
85
+ this.col = pos.col;
86
+ }
87
+ advance(count = 1) {
88
+ const end = Math.min(this.i + count, this.source.length);
89
+ while (this.i < end) {
90
+ const ch = this.source[this.i];
91
+ this.i += 1;
92
+ if (ch === '\n') {
93
+ this.line += 1;
94
+ this.col = 0;
95
+ }
96
+ else {
97
+ this.col += 1;
98
+ }
99
+ }
100
+ }
101
+ }
102
+ function isIdentStart(ch) {
103
+ return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '@';
104
+ }
105
+ function isIdentPart(ch) {
106
+ return isIdentStart(ch) || (ch >= '0' && ch <= '9');
107
+ }
108
+ function skipWhitespace(cur) {
109
+ while (!cur.done) {
110
+ const ch = cur.peek();
111
+ if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r' && ch !== '\f' && ch !== '\v')
112
+ break;
113
+ cur.advance();
114
+ }
115
+ }
116
+ /** Skip line comments and block comments. Returns true if a comment was consumed. */
117
+ function skipCsharpComment(cur) {
118
+ if (cur.startsWith('//')) {
119
+ while (!cur.done && cur.peek() !== '\n')
120
+ cur.advance();
121
+ return true;
122
+ }
123
+ if (cur.startsWith('/*')) {
124
+ cur.advance(2);
125
+ while (!cur.done && !cur.startsWith('*/'))
126
+ cur.advance();
127
+ if (cur.startsWith('*/'))
128
+ cur.advance(2);
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ function skipCsharpTrivia(cur) {
134
+ for (;;) {
135
+ skipWhitespace(cur);
136
+ if (!skipCsharpComment(cur))
137
+ return;
138
+ }
139
+ }
140
+ function skipRegularString(cur, interpolated) {
141
+ cur.advance(); // opening "
142
+ while (!cur.done) {
143
+ const ch = cur.peek();
144
+ if (ch === '\\') {
145
+ cur.advance(2);
146
+ continue;
147
+ }
148
+ if (interpolated && ch === '{') {
149
+ if (cur.peek(1) === '{') {
150
+ cur.advance(2);
151
+ continue;
152
+ }
153
+ skipInterpolation(cur);
154
+ continue;
155
+ }
156
+ cur.advance();
157
+ if (ch === '"')
158
+ return;
159
+ }
160
+ }
161
+ function skipVerbatimString(cur, interpolated) {
162
+ cur.advance(2); // @"
163
+ while (!cur.done) {
164
+ const ch = cur.peek();
165
+ if (ch === '"') {
166
+ if (cur.peek(1) === '"') {
167
+ cur.advance(2);
168
+ continue;
169
+ }
170
+ cur.advance();
171
+ return;
172
+ }
173
+ if (interpolated && ch === '{') {
174
+ if (cur.peek(1) === '{') {
175
+ cur.advance(2);
176
+ continue;
177
+ }
178
+ skipInterpolation(cur);
179
+ continue;
180
+ }
181
+ cur.advance();
182
+ }
183
+ }
184
+ function skipRawString(cur) {
185
+ let quoteCount = 0;
186
+ while (cur.peek() === '"') {
187
+ quoteCount += 1;
188
+ cur.advance();
189
+ }
190
+ while (!cur.done) {
191
+ if (cur.peek() !== '"') {
192
+ cur.advance();
193
+ continue;
194
+ }
195
+ let seen = 0;
196
+ while (cur.peek() === '"') {
197
+ seen += 1;
198
+ cur.advance();
199
+ }
200
+ if (seen >= quoteCount)
201
+ return;
202
+ }
203
+ }
204
+ function skipInterpolation(cur) {
205
+ cur.advance(); // {
206
+ let depth = 1;
207
+ while (!cur.done && depth > 0) {
208
+ skipCsharpTrivia(cur);
209
+ if (cur.done)
210
+ return;
211
+ if (skipCsharpString(cur))
212
+ continue;
213
+ const ch = cur.peek();
214
+ if (ch === '{')
215
+ depth += 1;
216
+ else if (ch === '}')
217
+ depth -= 1;
218
+ cur.advance();
219
+ }
220
+ }
221
+ function skipCsharpString(cur) {
222
+ const ch = cur.peek();
223
+ if (ch === "'") {
224
+ cur.advance();
225
+ if (cur.peek() === '\\')
226
+ cur.advance(2);
227
+ else
228
+ cur.advance();
229
+ if (cur.peek() === "'")
230
+ cur.advance();
231
+ return true;
232
+ }
233
+ if (ch === '"') {
234
+ if (cur.peek(1) === '"' && cur.peek(2) === '"')
235
+ skipRawString(cur);
236
+ else
237
+ skipRegularString(cur, false);
238
+ return true;
239
+ }
240
+ if (ch === '$' && cur.peek(1) === '@' && cur.peek(2) === '"') {
241
+ cur.advance();
242
+ skipVerbatimString(cur, true);
243
+ return true;
244
+ }
245
+ if (ch === '@' && cur.peek(1) === '$' && cur.peek(2) === '"') {
246
+ cur.advance(2);
247
+ skipVerbatimString(cur, true);
248
+ return true;
249
+ }
250
+ if (ch === '@' && cur.peek(1) === '"') {
251
+ skipVerbatimString(cur, false);
252
+ return true;
253
+ }
254
+ if (ch === '$' && cur.peek(1) === '"') {
255
+ if (cur.peek(2) === '"' && cur.peek(3) === '"') {
256
+ cur.advance();
257
+ skipRawString(cur);
258
+ }
259
+ else {
260
+ cur.advance();
261
+ skipRegularString(cur, true);
262
+ }
263
+ return true;
264
+ }
265
+ return false;
266
+ }
267
+ function readIdent(cur) {
268
+ if (!isIdentStart(cur.peek()))
269
+ return undefined;
270
+ const start = cur.i;
271
+ if (cur.peek() === '@')
272
+ cur.advance();
273
+ if (!isIdentStart(cur.peek()) && !(cur.peek() >= 'A' && cur.peek() <= 'z')) {
274
+ cur.i = start;
275
+ return undefined;
276
+ }
277
+ while (isIdentPart(cur.peek()) && cur.peek() !== '@')
278
+ cur.advance();
279
+ const raw = cur.source.slice(start, cur.i);
280
+ return raw.startsWith('@') ? raw.slice(1) : raw;
281
+ }
282
+ function tryReadIdent(cur) {
283
+ skipCsharpTrivia(cur);
284
+ return readIdent(cur);
285
+ }
286
+ function decodeCsharpString(cur) {
287
+ skipCsharpTrivia(cur);
288
+ const start = cur.snapshot();
289
+ const ch = cur.peek();
290
+ if (ch === '$')
291
+ return undefined;
292
+ if (ch === '@' && cur.peek(1) === '"') {
293
+ cur.advance(2);
294
+ let value = '';
295
+ while (!cur.done) {
296
+ if (cur.peek() === '"') {
297
+ if (cur.peek(1) === '"') {
298
+ value += '"';
299
+ cur.advance(2);
300
+ continue;
301
+ }
302
+ cur.advance();
303
+ return value;
304
+ }
305
+ value += cur.peek();
306
+ cur.advance();
307
+ }
308
+ cur.restore(start);
309
+ return undefined;
310
+ }
311
+ if (ch === '"' && cur.peek(1) === '"' && cur.peek(2) === '"') {
312
+ let quoteCount = 0;
313
+ while (cur.peek() === '"') {
314
+ quoteCount += 1;
315
+ cur.advance();
316
+ }
317
+ const bodyStart = cur.i;
318
+ while (!cur.done) {
319
+ if (cur.peek() !== '"') {
320
+ cur.advance();
321
+ continue;
322
+ }
323
+ const closeStart = cur.i;
324
+ let seen = 0;
325
+ while (cur.peek() === '"') {
326
+ seen += 1;
327
+ cur.advance();
328
+ }
329
+ if (seen >= quoteCount) {
330
+ return cur.source.slice(bodyStart, closeStart);
331
+ }
332
+ }
333
+ cur.restore(start);
334
+ return undefined;
335
+ }
336
+ if (ch === '"') {
337
+ cur.advance();
338
+ let value = '';
339
+ while (!cur.done) {
340
+ const next = cur.peek();
341
+ if (next === '\\') {
342
+ cur.advance();
343
+ const esc = cur.peek();
344
+ cur.advance();
345
+ const map = {
346
+ n: '\n',
347
+ r: '\r',
348
+ t: '\t',
349
+ '"': '"',
350
+ '\\': '\\',
351
+ '0': '\0',
352
+ };
353
+ value += map[esc] ?? esc;
354
+ continue;
355
+ }
356
+ if (next === '"') {
357
+ cur.advance();
358
+ return value;
359
+ }
360
+ value += next;
361
+ cur.advance();
362
+ }
363
+ cur.restore(start);
364
+ return undefined;
365
+ }
366
+ return undefined;
367
+ }
368
+ function skipBalanced(cur, open, close) {
369
+ skipCsharpTrivia(cur);
370
+ if (cur.peek() !== open)
371
+ return false;
372
+ let depth = 0;
373
+ while (!cur.done) {
374
+ skipCsharpTrivia(cur);
375
+ if (cur.done)
376
+ return false;
377
+ if (skipCsharpString(cur))
378
+ continue;
379
+ const ch = cur.peek();
380
+ if (ch === open)
381
+ depth += 1;
382
+ else if (ch === close) {
383
+ depth -= 1;
384
+ cur.advance();
385
+ if (depth === 0)
386
+ return true;
387
+ continue;
388
+ }
389
+ cur.advance();
390
+ }
391
+ return false;
392
+ }
393
+ function componentNameFromLiteral(value) {
394
+ if (value === undefined || !COMPONENT_NAME_RE.test(value))
395
+ return undefined;
396
+ return value;
397
+ }
398
+ function isViewComponentAttributeName(name) {
399
+ return name === 'ViewComponent' || name === 'ViewComponentAttribute';
400
+ }
401
+ function readQualifiedTail(cur) {
402
+ skipCsharpTrivia(cur);
403
+ let name = readIdent(cur);
404
+ if (name === undefined)
405
+ return undefined;
406
+ for (;;) {
407
+ skipCsharpTrivia(cur);
408
+ if (cur.peek() === '.' || (cur.peek() === ':' && cur.peek(1) === ':')) {
409
+ cur.advance(cur.peek() === ':' ? 2 : 1);
410
+ skipCsharpTrivia(cur);
411
+ const next = readIdent(cur);
412
+ if (next === undefined)
413
+ return name;
414
+ name = next;
415
+ continue;
416
+ }
417
+ return name;
418
+ }
419
+ }
420
+ function readViewComponentNameArgument(cur) {
421
+ skipCsharpTrivia(cur);
422
+ if (cur.peek() !== '(')
423
+ return undefined;
424
+ cur.advance();
425
+ let alias;
426
+ while (!cur.done && cur.peek() !== ')') {
427
+ skipCsharpTrivia(cur);
428
+ if (cur.peek() === ')')
429
+ break;
430
+ const beforeArg = cur.snapshot();
431
+ const ident = readIdent(cur);
432
+ skipCsharpTrivia(cur);
433
+ if (ident === 'Name' && cur.peek() === '=') {
434
+ cur.advance();
435
+ alias = componentNameFromLiteral(decodeCsharpString(cur));
436
+ }
437
+ else {
438
+ cur.restore(beforeArg);
439
+ skipCsharpTrivia(cur);
440
+ if (cur.peek() === '"' || cur.peek() === '@') {
441
+ // Positional string arguments are not ViewComponentAttribute.Name.
442
+ skipCsharpString(cur);
443
+ }
444
+ else if (cur.peek() === '(' || cur.peek() === '[' || cur.peek() === '{') {
445
+ const open = cur.peek();
446
+ const close = open === '(' ? ')' : open === '[' ? ']' : '}';
447
+ skipBalanced(cur, open, close);
448
+ }
449
+ else {
450
+ while (!cur.done && cur.peek() !== ',' && cur.peek() !== ')') {
451
+ if (skipCsharpString(cur))
452
+ continue;
453
+ if (skipCsharpComment(cur))
454
+ continue;
455
+ cur.advance();
456
+ }
457
+ }
458
+ }
459
+ skipCsharpTrivia(cur);
460
+ if (cur.peek() === ',')
461
+ cur.advance();
462
+ }
463
+ if (cur.peek() === ')')
464
+ cur.advance();
465
+ return alias;
466
+ }
467
+ function collectInvokeAfterIdent(ident, cur, previous, memberReceiver, names) {
468
+ skipCsharpTrivia(cur);
469
+ const hasMvcReceiver = previous !== '.' || memberReceiver === 'this' || memberReceiver === 'base';
470
+ if (ident === 'ViewComponent' && cur.peek() === '(') {
471
+ if (previous === '[' || previous === ',' || !hasMvcReceiver)
472
+ return;
473
+ cur.advance();
474
+ const name = componentNameFromLiteral(decodeCsharpString(cur));
475
+ if (name !== undefined)
476
+ names.add(name);
477
+ return;
478
+ }
479
+ if (ident !== 'Component' || cur.peek() !== '.' || !hasMvcReceiver)
480
+ return;
481
+ const afterDot = cur.snapshot();
482
+ cur.advance();
483
+ skipCsharpTrivia(cur);
484
+ if (readIdent(cur) !== 'InvokeAsync') {
485
+ cur.restore(afterDot);
486
+ return;
487
+ }
488
+ skipCsharpTrivia(cur);
489
+ if (cur.peek() !== '(')
490
+ return;
491
+ cur.advance();
492
+ const name = componentNameFromLiteral(decodeCsharpString(cur));
493
+ if (name !== undefined)
494
+ names.add(name);
495
+ }
496
+ /** In-repo C# `Component.InvokeAsync("X")` / `ViewComponent("X")` literals. */
497
+ export function extractCsharpViewComponentInvocations(source) {
498
+ if (!source.includes('ViewComponent') && !source.includes('InvokeAsync'))
499
+ return [];
500
+ const names = new Set();
501
+ const cur = new SourceCursor(source);
502
+ let previous;
503
+ let memberReceiver;
504
+ let squareDepth = 0;
505
+ while (!cur.done) {
506
+ skipCsharpTrivia(cur);
507
+ if (cur.done)
508
+ break;
509
+ if (skipCsharpString(cur)) {
510
+ previous = 'string';
511
+ continue;
512
+ }
513
+ const ident = readIdent(cur);
514
+ if (ident !== undefined) {
515
+ const inAttribute = squareDepth > 0;
516
+ collectInvokeAfterIdent(ident, cur, inAttribute ? '[' : previous, memberReceiver, names);
517
+ previous = ident;
518
+ memberReceiver = undefined;
519
+ continue;
520
+ }
521
+ const ch = cur.peek();
522
+ if (ch === '[')
523
+ squareDepth += 1;
524
+ else if (ch === ']' && squareDepth > 0)
525
+ squareDepth -= 1;
526
+ memberReceiver = ch === '.' ? previous : undefined;
527
+ previous = ch;
528
+ cur.advance();
529
+ }
530
+ return [...names];
531
+ }
532
+ function parseAttributeListBody(cur) {
533
+ const aliases = [];
534
+ skipCsharpTrivia(cur);
535
+ const specifier = cur.snapshot();
536
+ const specifierName = readIdent(cur);
537
+ skipCsharpTrivia(cur);
538
+ if (specifierName !== undefined && cur.peek() === ':' && cur.peek(1) !== ':') {
539
+ cur.advance();
540
+ }
541
+ else {
542
+ cur.restore(specifier);
543
+ }
544
+ while (!cur.done && cur.peek() !== ']') {
545
+ skipCsharpTrivia(cur);
546
+ if (cur.peek() === ']')
547
+ break;
548
+ const tail = readQualifiedTail(cur);
549
+ skipCsharpTrivia(cur);
550
+ if (tail !== undefined && isViewComponentAttributeName(tail) && cur.peek() === '(') {
551
+ const alias = readViewComponentNameArgument(cur);
552
+ if (alias !== undefined)
553
+ aliases.push(alias);
554
+ }
555
+ else if (cur.peek() === '(') {
556
+ skipBalanced(cur, '(', ')');
557
+ }
558
+ skipCsharpTrivia(cur);
559
+ if (cur.peek() === ',')
560
+ cur.advance();
561
+ else
562
+ break;
563
+ }
564
+ if (cur.peek() === ']')
565
+ cur.advance();
566
+ return aliases;
567
+ }
568
+ /**
569
+ * Explicit `[ViewComponent(Name = "...")]` aliases keyed to the following
570
+ * class declaration. Positional constructor arguments are ignored: the MVC
571
+ * attribute only exposes `Name` as a property.
572
+ */
573
+ export function extractViewComponentAliasBinds(source) {
574
+ if (!source.includes('ViewComponent'))
575
+ return [];
576
+ const binds = [];
577
+ const cur = new SourceCursor(source);
578
+ const pending = [];
579
+ const flushPending = (className, startLine, startCol) => {
580
+ const aliases = pending.flatMap((entry) => entry.aliases);
581
+ const start = pending[0];
582
+ binds.push({
583
+ className,
584
+ startLine: start?.startLine ?? startLine,
585
+ startCol: start?.startCol ?? startCol,
586
+ aliases: [...new Set(aliases)],
587
+ });
588
+ pending.length = 0;
589
+ };
590
+ while (!cur.done) {
591
+ skipCsharpTrivia(cur);
592
+ if (cur.done)
593
+ break;
594
+ if (skipCsharpString(cur))
595
+ continue;
596
+ const startLine = cur.line;
597
+ const startCol = cur.col;
598
+ if (cur.peek() === '[') {
599
+ cur.advance();
600
+ const aliases = parseAttributeListBody(cur);
601
+ pending.push({ startLine, startCol, aliases });
602
+ continue;
603
+ }
604
+ const ident = readIdent(cur);
605
+ if (ident === undefined) {
606
+ pending.length = 0;
607
+ cur.advance();
608
+ continue;
609
+ }
610
+ if (TYPE_MODIFIERS.has(ident))
611
+ continue;
612
+ if (ident === 'class' || ident === 'record') {
613
+ let className = tryReadIdent(cur);
614
+ if (ident === 'record' && (className === 'class' || className === 'struct')) {
615
+ className = tryReadIdent(cur);
616
+ }
617
+ if (className !== undefined && pending.some((entry) => entry.aliases.length > 0)) {
618
+ flushPending(className, startLine, startCol);
619
+ }
620
+ else {
621
+ pending.length = 0;
622
+ }
623
+ continue;
624
+ }
625
+ pending.length = 0;
626
+ }
627
+ return binds;
628
+ }
629
+ /** Extract explicit `[ViewComponent(Name = "...")]` aliases by class name. */
630
+ export function extractViewComponentAliases(source) {
631
+ const aliases = new Map();
632
+ for (const bind of extractViewComponentAliasBinds(source)) {
633
+ if (bind.aliases.length === 0)
634
+ continue;
635
+ const existing = aliases.get(bind.className);
636
+ if (existing) {
637
+ for (const alias of bind.aliases) {
638
+ if (!existing.includes(alias))
639
+ existing.push(alias);
640
+ }
641
+ }
642
+ else {
643
+ aliases.set(bind.className, [...bind.aliases]);
644
+ }
645
+ }
646
+ return aliases;
647
+ }
648
+ function tagNameToComponentName(tagName) {
649
+ return tagName
650
+ .split('-')
651
+ .filter(Boolean)
652
+ .map((part) => part[0].toUpperCase() + part.slice(1))
653
+ .join('');
654
+ }
655
+ function collectVcTags(span, names) {
656
+ VIEW_COMPONENT_TAG_RE.lastIndex = 0;
657
+ for (const match of span.matchAll(VIEW_COMPONENT_TAG_RE)) {
658
+ names.add(tagNameToComponentName(match[1]));
659
+ }
660
+ }
661
+ function skipRazorComment(cur) {
662
+ if (!cur.startsWith('@*'))
663
+ return false;
664
+ cur.advance(2);
665
+ while (!cur.done && !cur.startsWith('*@'))
666
+ cur.advance();
667
+ if (cur.startsWith('*@'))
668
+ cur.advance(2);
669
+ return true;
670
+ }
671
+ function countAtRun(cur) {
672
+ let count = 0;
673
+ while (cur.peek() === '@') {
674
+ count += 1;
675
+ cur.advance();
676
+ }
677
+ return count;
678
+ }
679
+ function scanCsharpSpan(span, names) {
680
+ for (const name of extractCsharpViewComponentInvocations(span))
681
+ names.add(name);
682
+ }
683
+ function skipOptionalParens(cur) {
684
+ skipWhitespace(cur);
685
+ if (cur.peek() === '(')
686
+ skipBalanced(cur, '(', ')');
687
+ }
688
+ function consumeRazorCodeBlock(cur, names) {
689
+ skipCsharpTrivia(cur);
690
+ skipOptionalParens(cur);
691
+ skipCsharpTrivia(cur);
692
+ if (cur.peek() !== '{') {
693
+ const start = cur.i;
694
+ while (!cur.done && cur.peek() !== '\n' && cur.peek() !== '{') {
695
+ if (skipCsharpString(cur) || skipCsharpComment(cur))
696
+ continue;
697
+ cur.advance();
698
+ }
699
+ scanCsharpSpan(cur.source.slice(start, cur.i), names);
700
+ if (cur.peek() === '{')
701
+ consumeRazorCodeBlock(cur, names);
702
+ return;
703
+ }
704
+ const bodyStart = cur.i + 1;
705
+ if (!skipBalanced(cur, '{', '}'))
706
+ return;
707
+ scanCsharpSpan(cur.source.slice(bodyStart, cur.i - 1), names);
708
+ }
709
+ function consumeImplicitExpression(cur, names) {
710
+ const start = cur.i;
711
+ skipCsharpTrivia(cur);
712
+ if (cur.peek() === '(') {
713
+ const innerStart = cur.i + 1;
714
+ if (skipBalanced(cur, '(', ')')) {
715
+ scanCsharpSpan(cur.source.slice(innerStart, cur.i - 1), names);
716
+ }
717
+ return;
718
+ }
719
+ // Implicit expressions: `@await Component.InvokeAsync("X")` / `@Component.InvokeAsync(...)`.
720
+ while (!cur.done) {
721
+ skipCsharpTrivia(cur);
722
+ if (cur.done)
723
+ break;
724
+ if (skipCsharpString(cur))
725
+ continue;
726
+ if (cur.peek() === '(') {
727
+ skipBalanced(cur, '(', ')');
728
+ continue;
729
+ }
730
+ if (cur.peek() === '{') {
731
+ skipBalanced(cur, '{', '}');
732
+ continue;
733
+ }
734
+ const ch = cur.peek();
735
+ if (ch === '<' || ch === '\n')
736
+ break;
737
+ if (ch === '@')
738
+ break;
739
+ if (!isIdentPart(ch) && ch !== '.' && ch !== '?') {
740
+ if (ch === ';')
741
+ cur.advance();
742
+ break;
743
+ }
744
+ cur.advance();
745
+ }
746
+ scanCsharpSpan(cur.source.slice(start, cur.i), names);
747
+ }
748
+ function consumeRazorTransition(cur, names) {
749
+ skipWhitespace(cur);
750
+ if (cur.peek() === '{') {
751
+ consumeRazorCodeBlock(cur, names);
752
+ return;
753
+ }
754
+ if (cur.peek() === '(') {
755
+ consumeImplicitExpression(cur, names);
756
+ return;
757
+ }
758
+ const identStart = cur.snapshot();
759
+ const ident = readIdent(cur);
760
+ if (ident === undefined) {
761
+ consumeImplicitExpression(cur, names);
762
+ return;
763
+ }
764
+ if (ident === 'await' || ident === 'Component') {
765
+ cur.restore(identStart);
766
+ consumeImplicitExpression(cur, names);
767
+ return;
768
+ }
769
+ if (RAZOR_BLOCK_KEYWORDS.has(ident)) {
770
+ if (ident === 'section' || ident === 'helper')
771
+ tryReadIdent(cur);
772
+ consumeRazorCodeBlock(cur, names);
773
+ return;
774
+ }
775
+ cur.restore(identStart);
776
+ consumeImplicitExpression(cur, names);
777
+ }
778
+ /** Extract statically resolvable ViewComponent names from one Razor template. */
779
+ export function extractRazorViewComponentInvocations(source) {
780
+ // Most views do not invoke a ViewComponent. Avoid the character-by-character
781
+ // Razor scan unless one of the two supported invocation spellings is present.
782
+ // This is only a coarse gate; the state machine below still decides whether a
783
+ // token is executable markup/C# or a comment/string/escaped transition.
784
+ if (!source.includes('InvokeAsync') && !/<\s*vc:/i.test(source))
785
+ return [];
786
+ const names = new Set();
787
+ const cur = new SourceCursor(source);
788
+ let markupStart = 0;
789
+ const flushMarkup = () => {
790
+ if (cur.i > markupStart)
791
+ collectVcTags(source.slice(markupStart, cur.i), names);
792
+ };
793
+ while (!cur.done) {
794
+ if (cur.peek() !== '@') {
795
+ cur.advance();
796
+ continue;
797
+ }
798
+ flushMarkup();
799
+ if (skipRazorComment(cur)) {
800
+ markupStart = cur.i;
801
+ continue;
802
+ }
803
+ const atCount = countAtRun(cur);
804
+ const leftover = atCount % 2;
805
+ if (leftover === 0) {
806
+ markupStart = cur.i;
807
+ continue;
808
+ }
809
+ consumeRazorTransition(cur, names);
810
+ markupStart = cur.i;
811
+ }
812
+ flushMarkup();
813
+ return [...names];
814
+ }
815
+ /**
816
+ * Read Razor views once per C# resolution pass. The same ignore rules and file
817
+ * size ceiling as repository scanning are applied, and edge emission later
818
+ * additionally requires a live File node. This prevents ignored, oversized,
819
+ * or concurrently removed templates from entering the graph.
820
+ */
821
+ export async function loadRazorViewComponentConfig(repoRoot) {
822
+ const ignore = await createIgnoreFilter(repoRoot);
823
+ const paths = await glob('**/*.cshtml', {
824
+ cwd: repoRoot,
825
+ nodir: true,
826
+ dot: false,
827
+ ignore,
828
+ });
829
+ paths.sort();
830
+ const maxBytes = getMaxFileSizeBytes();
831
+ const views = new Map();
832
+ for (const rawPath of paths) {
833
+ const filePath = rawPath.replace(/\\/g, '/');
834
+ // The size gate and the read go through one handle so both observe the same
835
+ // inode. Re-resolving the path for the read would let a template swapped in
836
+ // between them be read unchecked (CodeQL js/file-system-race).
837
+ let handle;
838
+ try {
839
+ handle = await fs.open(path.join(repoRoot, filePath), 'r');
840
+ const stat = await handle.stat();
841
+ if (!stat.isFile() || stat.size > maxBytes)
842
+ continue;
843
+ const source = await handle.readFile('utf8');
844
+ views.set(filePath, extractRazorViewComponentInvocations(source));
845
+ }
846
+ catch {
847
+ // A view may disappear between glob/open/read during watch mode.
848
+ }
849
+ finally {
850
+ await handle?.close().catch(() => { });
851
+ }
852
+ }
853
+ return { views };
854
+ }
855
+ function addCandidate(candidates, invocationName, targetId) {
856
+ const key = invocationName.toLocaleLowerCase('en-US');
857
+ const existing = candidates.get(key);
858
+ if (existing) {
859
+ existing.add(targetId);
860
+ }
861
+ else {
862
+ candidates.set(key, new Set([targetId]));
863
+ }
864
+ }
865
+ function bindAliasesForClass(binds, className, nodeId, filePath) {
866
+ const matches = binds.filter((bind) => bind.className === className);
867
+ if (matches.length === 0)
868
+ return undefined;
869
+ if (matches.length === 1)
870
+ return matches[0].aliases;
871
+ const pos = definitionIdPosition(nodeId, filePath);
872
+ if (pos === undefined)
873
+ return undefined;
874
+ const atPosition = matches.filter((bind) => bind.startLine === pos.line && bind.startCol === pos.column);
875
+ if (atPosition.length === 1)
876
+ return atPosition[0].aliases;
877
+ return undefined;
878
+ }
879
+ /**
880
+ * Emit workspace File → in-repo ViewComponent Class CALLS edges.
881
+ *
882
+ * Targets are only Class nodes produced from this repo's `.cs` files. There is
883
+ * no lookup of ASP.NET SDK types; `: ViewComponent` in source is a naming
884
+ * hint, not a resolved EXTENDS edge to `Microsoft.AspNetCore.Mvc.ViewComponent`.
885
+ *
886
+ * Ambiguous component names fail closed: two in-repo classes claiming the
887
+ * same name is not evidence for picking either one.
888
+ */
889
+ export function emitRazorViewComponentEdges(graph, parsedFiles, nodeLookup, config, csharpSources) {
890
+ if (!config)
891
+ return;
892
+ const candidates = new Map();
893
+ for (const parsed of parsedFiles) {
894
+ if (!parsed.filePath.endsWith('.cs'))
895
+ continue;
896
+ const source = csharpSources.get(parsed.filePath) ?? '';
897
+ const binds = source.includes('ViewComponent') ? extractViewComponentAliasBinds(source) : [];
898
+ for (const def of parsed.localDefs) {
899
+ if (def.type !== 'Class')
900
+ continue;
901
+ const className = def.qualifiedName?.split('.').pop() ?? def.nodeId.split(':').pop() ?? '';
902
+ const conventionalName = className.endsWith(VIEW_COMPONENT_SUFFIX)
903
+ ? className.slice(0, -VIEW_COMPONENT_SUFFIX.length)
904
+ : undefined;
905
+ const explicitAliases = bindAliasesForClass(binds, className, def.nodeId, parsed.filePath);
906
+ if (!conventionalName && (explicitAliases === undefined || explicitAliases.length === 0)) {
907
+ continue;
908
+ }
909
+ const targetId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
910
+ if (!targetId || !graph.getNode(targetId))
911
+ continue;
912
+ // An explicit [ViewComponent(Name = "...")] replaces the suffix name,
913
+ // matching ASP.NET. Never register the SDK base type as a candidate.
914
+ if (explicitAliases !== undefined && explicitAliases.length > 0) {
915
+ for (const alias of explicitAliases)
916
+ addCandidate(candidates, alias, targetId);
917
+ }
918
+ else if (conventionalName) {
919
+ addCandidate(candidates, conventionalName, targetId);
920
+ }
921
+ }
922
+ }
923
+ const emitFromFile = (filePath, invocationNames) => {
924
+ const sourceId = generateId('File', filePath);
925
+ if (!graph.getNode(sourceId))
926
+ return;
927
+ for (const invocationName of invocationNames) {
928
+ const matches = candidates.get(invocationName.toLocaleLowerCase('en-US'));
929
+ if (!matches || matches.size !== 1)
930
+ continue;
931
+ const targetId = matches.values().next().value;
932
+ if (typeof targetId !== 'string' || !graph.getNode(targetId))
933
+ continue;
934
+ graph.addRelationship({
935
+ id: generateId('CALLS', `${sourceId}:razor-view-component:${targetId}`),
936
+ sourceId,
937
+ targetId,
938
+ type: 'CALLS',
939
+ confidence: 0.9,
940
+ reason: 'aspnet-razor-view-component',
941
+ });
942
+ }
943
+ };
944
+ for (const [viewPath, invocationNames] of config.views) {
945
+ emitFromFile(viewPath, invocationNames);
946
+ }
947
+ for (const [filePath, source] of csharpSources) {
948
+ if (!filePath.endsWith('.cs'))
949
+ continue;
950
+ if (!source.includes('ViewComponent') && !source.includes('InvokeAsync'))
951
+ continue;
952
+ emitFromFile(filePath, extractCsharpViewComponentInvocations(source));
953
+ }
954
+ }