@reqlan/language 1.6.2 → 1.8.1

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 (109) hide show
  1. package/README.md +18 -0
  2. package/out/generated/ast.d.ts +31 -4
  3. package/out/generated/ast.js +28 -7
  4. package/out/generated/ast.js.map +1 -1
  5. package/out/generated/grammar.js +2 -3318
  6. package/out/generated/grammar.js.map +1 -1
  7. package/out/generated/module.d.ts +2 -2
  8. package/out/generated/module.js +2 -2
  9. package/out/generated/module.js.map +1 -1
  10. package/out/index.d.ts +4 -0
  11. package/out/index.js +4 -0
  12. package/out/index.js.map +1 -1
  13. package/out/reqlan-async-parser.d.ts +37 -0
  14. package/out/reqlan-async-parser.js +117 -0
  15. package/out/reqlan-async-parser.js.map +1 -0
  16. package/out/reqlan-code-action-provider.d.ts +15 -12
  17. package/out/reqlan-code-action-provider.js +55 -20
  18. package/out/reqlan-code-action-provider.js.map +1 -1
  19. package/out/reqlan-comment-resolver.js +5 -0
  20. package/out/reqlan-comment-resolver.js.map +1 -1
  21. package/out/reqlan-completion-provider.d.ts +10 -0
  22. package/out/reqlan-completion-provider.js +64 -30
  23. package/out/reqlan-completion-provider.js.map +1 -1
  24. package/out/reqlan-definition-provider.d.ts +7 -0
  25. package/out/reqlan-definition-provider.js +86 -19
  26. package/out/reqlan-definition-provider.js.map +1 -1
  27. package/out/reqlan-document-factory.d.ts +16 -0
  28. package/out/reqlan-document-factory.js +50 -0
  29. package/out/reqlan-document-factory.js.map +1 -0
  30. package/out/reqlan-document-link-provider.d.ts +2 -1
  31. package/out/reqlan-document-link-provider.js +4 -0
  32. package/out/reqlan-document-link-provider.js.map +1 -1
  33. package/out/reqlan-file-link-resolver.d.ts +17 -4
  34. package/out/reqlan-file-link-resolver.js +89 -14
  35. package/out/reqlan-file-link-resolver.js.map +1 -1
  36. package/out/reqlan-import-edits.d.ts +3 -1
  37. package/out/reqlan-import-edits.js.map +1 -1
  38. package/out/reqlan-inbound-reference-inlay-label.d.ts +11 -0
  39. package/out/reqlan-inbound-reference-inlay-label.js +60 -35
  40. package/out/reqlan-inbound-reference-inlay-label.js.map +1 -1
  41. package/out/reqlan-inlay-hint-provider.d.ts +6 -0
  42. package/out/reqlan-inlay-hint-provider.js +34 -6
  43. package/out/reqlan-inlay-hint-provider.js.map +1 -1
  44. package/out/reqlan-linker.js +2 -2
  45. package/out/reqlan-linker.js.map +1 -1
  46. package/out/reqlan-markdown-links.js +4 -0
  47. package/out/reqlan-markdown-links.js.map +1 -1
  48. package/out/reqlan-module.d.ts +2 -1
  49. package/out/reqlan-module.js +12 -2
  50. package/out/reqlan-module.js.map +1 -1
  51. package/out/reqlan-parse-budget.d.ts +50 -0
  52. package/out/reqlan-parse-budget.js +89 -0
  53. package/out/reqlan-parse-budget.js.map +1 -0
  54. package/out/reqlan-parse-worker.d.ts +1 -0
  55. package/out/reqlan-parse-worker.js +34 -0
  56. package/out/reqlan-parse-worker.js.map +1 -0
  57. package/out/reqlan-path-resolve.d.ts +8 -0
  58. package/out/reqlan-path-resolve.js +18 -4
  59. package/out/reqlan-path-resolve.js.map +1 -1
  60. package/out/reqlan-reference-search-site.d.ts +11 -0
  61. package/out/reqlan-reference-search-site.js +11 -0
  62. package/out/reqlan-reference-search-site.js.map +1 -1
  63. package/out/reqlan-scope.d.ts +11 -0
  64. package/out/reqlan-scope.js +28 -1
  65. package/out/reqlan-scope.js.map +1 -1
  66. package/out/reqlan-semantic-token-provider.js +6 -1
  67. package/out/reqlan-semantic-token-provider.js.map +1 -1
  68. package/out/reqlan-token-builder.d.ts +3 -2
  69. package/out/reqlan-token-builder.js +150 -26
  70. package/out/reqlan-token-builder.js.map +1 -1
  71. package/out/reqlan-validator.d.ts +10 -1
  72. package/out/reqlan-validator.js +36 -2
  73. package/out/reqlan-validator.js.map +1 -1
  74. package/out/reqlan-wildcard-resolve.d.ts +45 -0
  75. package/out/reqlan-wildcard-resolve.js +203 -0
  76. package/out/reqlan-wildcard-resolve.js.map +1 -0
  77. package/out/reqlan-workspace-manager.d.ts +18 -0
  78. package/out/reqlan-workspace-manager.js +31 -0
  79. package/out/reqlan-workspace-manager.js.map +1 -0
  80. package/package.json +1 -1
  81. package/src/generated/ast.ts +41 -10
  82. package/src/generated/grammar.ts +2 -3318
  83. package/src/generated/module.ts +2 -2
  84. package/src/index.ts +4 -0
  85. package/src/reqlan-async-parser.ts +178 -0
  86. package/src/reqlan-code-action-provider.ts +67 -32
  87. package/src/reqlan-comment-resolver.ts +5 -0
  88. package/src/reqlan-completion-provider.ts +82 -28
  89. package/src/reqlan-definition-provider.ts +91 -18
  90. package/src/reqlan-document-factory.ts +75 -0
  91. package/src/reqlan-document-link-provider.ts +9 -1
  92. package/src/reqlan-file-link-resolver.ts +112 -17
  93. package/src/reqlan-import-edits.ts +3 -1
  94. package/src/reqlan-inbound-reference-inlay-label.ts +93 -38
  95. package/src/reqlan-inlay-hint-provider.ts +36 -6
  96. package/src/reqlan-linker.ts +4 -2
  97. package/src/reqlan-markdown-links.ts +4 -0
  98. package/src/reqlan-module.ts +15 -3
  99. package/src/reqlan-parse-budget.ts +155 -0
  100. package/src/reqlan-parse-worker.ts +35 -0
  101. package/src/reqlan-path-resolve.ts +28 -5
  102. package/src/reqlan-reference-search-site.ts +25 -0
  103. package/src/reqlan-scope.ts +34 -1
  104. package/src/reqlan-semantic-token-provider.ts +6 -0
  105. package/src/reqlan-token-builder.ts +174 -28
  106. package/src/reqlan-validator.ts +56 -1
  107. package/src/reqlan-wildcard-resolve.ts +281 -0
  108. package/src/reqlan-workspace-manager.ts +47 -0
  109. package/src/reqlan.langium +17 -8
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Custom token builder so line comments do not match `//` in URLs or inside string literals,
3
- * and so `@` only introduces attributes at the start of a line.
2
+ * Custom token builder so line/block comments do not match inside string literals,
3
+ * empty slash-star-star-slash glob segments do not steal path text, and `@` only
4
+ * introduces attributes at the start of a line.
4
5
  */
5
6
  import type { Grammar } from 'langium';
6
7
  import { DefaultTokenBuilder, type TokenBuilderOptions } from 'langium';
7
8
 
8
9
  const SL_COMMENT_PATTERN = /\/\/[^\n\r]*/y;
10
+ const ML_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//y;
9
11
 
10
12
  function isInsideNakedQuote(text: string, offset: number): boolean {
11
13
  let inDouble = false;
@@ -53,6 +55,28 @@ const slCommentPattern = (text: string, offset: number): RegExpExecArray | null
53
55
  return SL_COMMENT_PATTERN.exec(text);
54
56
  };
55
57
 
58
+ // Block comments must not open inside quotes. Reject the empty four-character
59
+ // slash-star-star-slash form so recursive globs like ../mod/**/*.rq stay path text
60
+ // in prose and naked quotes (reference STRING paths already consume the whole literal).
61
+ const mlCommentPattern = (text: string, offset: number): RegExpExecArray | null => {
62
+ if (text.charCodeAt(offset) !== 47 || text.charCodeAt(offset + 1) !== 42) {
63
+ return null;
64
+ }
65
+ if (isInsideNakedQuote(text, offset)) {
66
+ return null;
67
+ }
68
+ ML_COMMENT_PATTERN.lastIndex = offset;
69
+ const match = ML_COMMENT_PATTERN.exec(text);
70
+ if (!match) {
71
+ return null;
72
+ }
73
+ // /* immediately followed by */ — recursive glob segment, not a comment.
74
+ if (match[0].length === 4) {
75
+ return null;
76
+ }
77
+ return match;
78
+ };
79
+
56
80
  function makeMatch(text: string, offset: number, length: number): RegExpExecArray {
57
81
  const image = text.slice(offset, offset + length);
58
82
  const match = [image] as unknown as RegExpExecArray;
@@ -108,7 +132,15 @@ const markdownLink = (text: string, offset: number): RegExpExecArray | null => {
108
132
  return null;
109
133
  };
110
134
 
111
- const topLevelBlockOpener = /(?:^|\n)[ \t]*(?:[A-Za-z_.][\w.-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/;
135
+ const blockOpenerLine = /^[ \t]*(?:[A-Za-z_.][\w.-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/;
136
+
137
+ function lineStartOffset(text: string, offset: number): number {
138
+ return text.lastIndexOf('\n', offset - 1) + 1;
139
+ }
140
+
141
+ function textBeforeOnLine(text: string, offset: number): string {
142
+ return text.slice(lineStartOffset(text, offset), offset).replace(/[ \t]+$/, '');
143
+ }
112
144
 
113
145
  function isEscapedAt(text: string, offset: number): boolean {
114
146
  let backslashes = 0;
@@ -125,9 +157,11 @@ function isStructuralOpenBraceAtDepth(text: string, offset: number, depth: numbe
125
157
  if (isEscapedAt(text, offset)) {
126
158
  return false;
127
159
  }
128
- const before = text.slice(0, offset).replace(/[ \t]+$/, '');
160
+ const before = textBeforeOnLine(text, offset);
129
161
 
130
- if (depth === 0 && topLevelBlockOpener.test(before)) {
162
+ // Top-level named blocks (`name {`) and nameless recoverable blocks (`{` alone).
163
+ // Leading indentation is allowed — top-level ideas are often indented in fixtures.
164
+ if (depth === 0 && (blockOpenerLine.test(before) || (isLineStartAt(text, offset) && before.trim().length === 0))) {
131
165
  return true;
132
166
  }
133
167
  if (/@[A-Za-z_][\w-]*(?::)?\s*$/.test(before)) {
@@ -137,8 +171,7 @@ function isStructuralOpenBraceAtDepth(text: string, offset: number, depth: numbe
137
171
  return true;
138
172
  }
139
173
  if (depth >= 1 && /[A-Za-z_][\w-]*\s*$/.test(before) && !isLineStartAt(text, offset)) {
140
- const after = text.slice(offset + 1);
141
- return /^[ \t]*(?:\r?\n|$)/.test(after);
174
+ return restOfLineIsBlank(text, offset);
142
175
  }
143
176
  return false;
144
177
  }
@@ -156,12 +189,10 @@ function isStructuralCloseBraceAt(text: string, offset: number): boolean {
156
189
  if (isEscapedAt(text, offset)) {
157
190
  return false;
158
191
  }
159
- const after = text.slice(offset + 1);
160
- const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
161
- if (restOfLine.trim().length !== 0) {
192
+ if (!restOfLineIsBlank(text, offset)) {
162
193
  return false;
163
194
  }
164
- const { structuralDepth, proseDepth } = scanBraceState(text, offset);
195
+ const { structuralDepth, proseDepth } = braceStateBefore(text, offset);
165
196
  if (structuralDepth <= 0) {
166
197
  return false;
167
198
  }
@@ -176,16 +207,84 @@ interface BraceScanState {
176
207
  proseDepth: number;
177
208
  }
178
209
 
179
- function scanBraceState(text: string, offset: number): BraceScanState {
210
+ /**
211
+ * Sparse depth timeline: event `i` applies for offsets in
212
+ * [offsets[i], offsets[i + 1]). Avoids O(n) Int32Arrays on every re-lex.
213
+ */
214
+ interface BraceScanCache {
215
+ text: string;
216
+ offsets: number[];
217
+ structuralDepth: number[];
218
+ proseDepth: number[];
219
+ }
220
+
221
+ let braceScanCache: BraceScanCache | undefined;
222
+
223
+ function fenceEndAfter(text: string, openOffset: number): number {
224
+ // Match CODE_FENCE: ```…``` including optional body; return index after closing fence.
225
+ if (
226
+ text.charCodeAt(openOffset) !== 96
227
+ || text.charCodeAt(openOffset + 1) !== 96
228
+ || text.charCodeAt(openOffset + 2) !== 96
229
+ ) {
230
+ return openOffset;
231
+ }
232
+ const afterOpen = openOffset + 3;
233
+ const firstNewline = text.indexOf('\n', afterOpen);
234
+ if (firstNewline < 0) {
235
+ const sameLineClose = text.indexOf('```', afterOpen);
236
+ return sameLineClose < 0 ? text.length : sameLineClose + 3;
237
+ }
238
+ const close = text.indexOf('```', firstNewline + 1);
239
+ return close < 0 ? text.length : close + 3;
240
+ }
241
+
242
+ function restOfLineIsBlank(text: string, offset: number): boolean {
243
+ for (let index = offset + 1; index < text.length; index++) {
244
+ const char = text[index];
245
+ if (char === '\n' || char === '\r') {
246
+ return true;
247
+ }
248
+ if (char !== ' ' && char !== '\t') {
249
+ return false;
250
+ }
251
+ }
252
+ return true;
253
+ }
254
+
255
+ function buildBraceScanCache(text: string): BraceScanCache {
180
256
  let structuralDepth = 0;
181
257
  const proseDepthByStructural: number[] = [0];
258
+ const offsets = [0];
259
+ const structuralDepthAt = [0];
260
+ const proseDepthAtOffsets = [0];
182
261
 
183
262
  const proseDepthAt = (): number => proseDepthByStructural[structuralDepth] ?? 0;
184
263
  const setProseDepth = (value: number): void => {
185
264
  proseDepthByStructural[structuralDepth] = value;
186
265
  };
266
+ const recordStateFrom = (offset: number): void => {
267
+ const last = offsets.length - 1;
268
+ if (offsets[last] === offset) {
269
+ structuralDepthAt[last] = structuralDepth;
270
+ proseDepthAtOffsets[last] = proseDepthAt();
271
+ return;
272
+ }
273
+ offsets.push(offset);
274
+ structuralDepthAt.push(structuralDepth);
275
+ proseDepthAtOffsets.push(proseDepthAt());
276
+ };
187
277
 
188
- for (let index = 0; index < offset; index++) {
278
+ for (let index = 0; index < text.length; index++) {
279
+ // Fenced snippets are opaque to the parser; braces inside must not change depth.
280
+ if (
281
+ text.charCodeAt(index) === 96
282
+ && text.charCodeAt(index + 1) === 96
283
+ && text.charCodeAt(index + 2) === 96
284
+ ) {
285
+ index = fenceEndAfter(text, index) - 1;
286
+ continue;
287
+ }
189
288
  const char = text[index];
190
289
  if (char === '{') {
191
290
  if (isEscapedAt(text, index)) {
@@ -197,6 +296,7 @@ function scanBraceState(text: string, offset: number): BraceScanState {
197
296
  } else {
198
297
  setProseDepth(proseDepthAt() + 1);
199
298
  }
299
+ recordStateFrom(index + 1);
200
300
  continue;
201
301
  }
202
302
  if (char !== '}') {
@@ -205,23 +305,24 @@ function scanBraceState(text: string, offset: number): BraceScanState {
205
305
  if (isEscapedAt(text, index)) {
206
306
  continue;
207
307
  }
208
- const after = text.slice(index + 1);
209
- const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
210
- const atEndOfLine = restOfLine.trim().length === 0;
308
+ const atEndOfLine = restOfLineIsBlank(text, index);
211
309
  if (!atEndOfLine) {
212
310
  if (proseDepthAt() > 0) {
213
311
  setProseDepth(proseDepthAt() - 1);
312
+ recordStateFrom(index + 1);
214
313
  }
215
314
  continue;
216
315
  }
217
316
  if (structuralDepth <= 0) {
218
317
  if (proseDepthAt() > 0) {
219
318
  setProseDepth(proseDepthAt() - 1);
319
+ recordStateFrom(index + 1);
220
320
  }
221
321
  continue;
222
322
  }
223
323
  if (isLineStartAt(text, index)) {
224
324
  structuralDepth--;
325
+ recordStateFrom(index + 1);
225
326
  continue;
226
327
  }
227
328
  if (proseDepthAt() > 0) {
@@ -229,20 +330,41 @@ function scanBraceState(text: string, offset: number): BraceScanState {
229
330
  } else {
230
331
  structuralDepth--;
231
332
  }
333
+ recordStateFrom(index + 1);
232
334
  }
233
335
 
336
+ return { text, offsets, structuralDepth: structuralDepthAt, proseDepth: proseDepthAtOffsets };
337
+ }
338
+
339
+ function braceStateAtOffset(cache: BraceScanCache, offset: number): BraceScanState {
340
+ const { offsets, structuralDepth, proseDepth } = cache;
341
+ let low = 0;
342
+ let high = offsets.length - 1;
343
+ while (low <= high) {
344
+ const mid = (low + high) >> 1;
345
+ if (offsets[mid]! <= offset) {
346
+ low = mid + 1;
347
+ } else {
348
+ high = mid - 1;
349
+ }
350
+ }
351
+ const index = Math.max(0, high);
234
352
  return {
235
- structuralDepth,
236
- proseDepth: proseDepthAt()
353
+ structuralDepth: structuralDepth[index] ?? 0,
354
+ proseDepth: proseDepth[index] ?? 0
237
355
  };
238
356
  }
239
357
 
240
- function scanStructuralBraceDepth(text: string, offset: number): number {
241
- return scanBraceState(text, offset).structuralDepth;
358
+ function braceStateBefore(text: string, offset: number): BraceScanState {
359
+ if (braceScanCache?.text !== text) {
360
+ braceScanCache = buildBraceScanCache(text);
361
+ }
362
+ const boundedOffset = Math.max(0, Math.min(offset, text.length));
363
+ return braceStateAtOffset(braceScanCache, boundedOffset);
242
364
  }
243
365
 
244
366
  function isStructuralOpenBraceAt(text: string, offset: number): boolean {
245
- return isStructuralOpenBraceAtDepth(text, offset, scanStructuralBraceDepth(text, offset));
367
+ return isStructuralOpenBraceAtDepth(text, offset, braceStateBefore(text, offset).structuralDepth);
246
368
  }
247
369
 
248
370
  const structuralOpenBrace = (text: string, offset: number): RegExpExecArray | null => {
@@ -274,7 +396,7 @@ const proseCloseBrace = (text: string, offset: number): RegExpExecArray | null =
274
396
  };
275
397
 
276
398
  function braceDepthBefore(text: string, offset: number): number {
277
- return scanStructuralBraceDepth(text, offset);
399
+ return braceStateBefore(text, offset).structuralDepth;
278
400
  }
279
401
 
280
402
  function isLineStartAt(text: string, offset: number): boolean {
@@ -287,15 +409,14 @@ function isLineStartAt(text: string, offset: number): boolean {
287
409
 
288
410
  function isStringLiteralContext(text: string, offset: number): boolean {
289
411
  const depth = braceDepthBefore(text, offset);
290
- const trimmed = text.slice(0, offset).replace(/[ \t]+$/, '');
412
+ const trimmed = textBeforeOnLine(text, offset);
291
413
  if (depth === 0 && /\b(?:from|import)\s*$/.test(trimmed)) {
292
414
  return true;
293
415
  }
294
416
  if (/\[\s*$/.test(trimmed)) {
295
417
  return true;
296
418
  }
297
- const lineStart = trimmed.lastIndexOf('\n') + 1;
298
- if (trimmed.slice(lineStart).length === 0 && depth === 0) {
419
+ if (trimmed.length === 0 && depth === 0) {
299
420
  return true;
300
421
  }
301
422
  return false;
@@ -330,7 +451,7 @@ function topLevelImportKeyword(text: string, offset: number): RegExpExecArray |
330
451
  if (isLineStartAt(text, offset)) {
331
452
  return makeMatch(text, offset, 6);
332
453
  }
333
- const before = text.slice(0, offset).replace(/[ \t]+$/, '');
454
+ const before = textBeforeOnLine(text, offset);
334
455
  if (/(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
335
456
  return makeMatch(text, offset, 6);
336
457
  }
@@ -348,8 +469,15 @@ function topLevelAsKeyword(text: string, offset: number): RegExpExecArray | null
348
469
  if (braceDepthBefore(text, offset) !== 0) {
349
470
  return null;
350
471
  }
351
- const before = text.slice(0, offset).replace(/[ \t]+$/, '');
352
- if (/(?:[_a-zA-Z][\w-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
472
+ // `as` is reserved only on from/import lines — not after arbitrary top-level identifiers
473
+ // (e.g. one-liner prose "such as this").
474
+ const linePrefix = text.slice(lineStartOffset(text, offset), offset);
475
+ if (!/^[ \t]*(?:from|import)\b/.test(linePrefix)) {
476
+ return null;
477
+ }
478
+ const before = textBeforeOnLine(text, offset);
479
+ // After import path (optionally qualified) or after a from-import specifier name.
480
+ if (/(?:(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')(?:\.[A-Za-z_][\w-]*)*|[A-Za-z_][\w-]*)$/.test(before)) {
353
481
  return makeMatch(text, offset, 2);
354
482
  }
355
483
  return null;
@@ -454,6 +582,16 @@ export class ReqlanTokenBuilder extends DefaultTokenBuilder {
454
582
  };
455
583
  other.LINE_BREAKS = false;
456
584
  }
585
+ // ID is a prefix of WILDCARD_NAME (e.g. import_ vs import_*) — require longer-alt check.
586
+ const idToken = tokens.find(entry => entry.name === 'ID');
587
+ const wildcardToken = tokens.find(entry => entry.name === 'WILDCARD_NAME');
588
+ if (idToken && wildcardToken) {
589
+ const existing = idToken.LONGER_ALT;
590
+ const alts = Array.isArray(existing) ? existing : existing ? [existing] : [];
591
+ if (!alts.includes(wildcardToken)) {
592
+ idToken.LONGER_ALT = [...alts, wildcardToken];
593
+ }
594
+ }
457
595
  return tokens;
458
596
  }
459
597
 
@@ -466,6 +604,14 @@ export class ReqlanTokenBuilder extends DefaultTokenBuilder {
466
604
  PATTERN: slCommentPattern
467
605
  };
468
606
  }
607
+ if (terminal.name === 'ML_COMMENT') {
608
+ return {
609
+ name: 'ML_COMMENT',
610
+ GROUP: 'hidden',
611
+ LINE_BREAKS: true,
612
+ PATTERN: mlCommentPattern
613
+ };
614
+ }
469
615
  if (terminal.name === 'MARKDOWN_LINK') {
470
616
  return {
471
617
  name: 'MARKDOWN_LINK',
@@ -5,7 +5,10 @@ import {
5
5
  isFromImport,
6
6
  isIdea,
7
7
  isInvalidFromImport,
8
+ isModel,
8
9
  isOneLinerIdea,
10
+ isWildcardReference,
11
+ type AnonymousBlock,
9
12
  type Model
10
13
  } from './generated/ast.js';
11
14
  import {
@@ -21,18 +24,25 @@ import { isResolvableImportPath } from './reqlan-imports.js';
21
24
  import type { ReqlanServices } from './reqlan-module.js';
22
25
  import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
23
26
  import { unquoteReqlanString } from './reqlan-quoted-strings.js';
27
+ import {
28
+ resolveWildcardReferenceMatches,
29
+ parseWildcardPathPattern,
30
+ wildcardReferenceLabel
31
+ } from './reqlan-wildcard-resolve.js';
24
32
 
25
33
  /**
26
34
  * Registers validation hooks for the requirement graph AST.
27
35
  * rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
28
36
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
29
37
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
38
+ * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
30
39
  */
31
40
  export function registerValidationChecks(services: ReqlanServices) {
32
41
  const registry = services.validation.ValidationRegistry;
33
42
  const validator = services.validation.ReqlanValidator;
34
43
  const checks: ValidationChecks<ReqlanAstType> = {
35
- Model: validator.checkModelDuplicates
44
+ Model: validator.checkModelDuplicates,
45
+ AnonymousBlock: validator.checkNamelessIdeaBlock
36
46
  };
37
47
  registry.register(checks, validator);
38
48
  }
@@ -42,6 +52,7 @@ export function registerValidationChecks(services: ReqlanServices) {
42
52
  * rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
43
53
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
44
54
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
55
+ * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
45
56
  */
46
57
  export class ReqlanValidator {
47
58
 
@@ -53,6 +64,23 @@ export class ReqlanValidator {
53
64
  this.checkImportSyntax(model, accept);
54
65
  this.checkImportTargets(model, accept);
55
66
  this.checkFileReferenceTargets(model, accept);
67
+ this.checkWildcardReferences(model, accept);
68
+ }
69
+
70
+ /**
71
+ * Top-level nameless `{ ... }` blocks parse so the rest of the file stays alive,
72
+ * but they need a name to be addressable ideas.
73
+ * List-item anonymous blocks remain valid (see lists).
74
+ */
75
+ checkNamelessIdeaBlock(block: AnonymousBlock, accept: ValidationAcceptor): void {
76
+ if (!isModel(block.$container)) {
77
+ return;
78
+ }
79
+ accept(
80
+ 'warning',
81
+ 'Nameless idea block: add a name before `{` (for example `my_idea { ... }`). The block was kept so the rest of the file still parses.',
82
+ { node: block }
83
+ );
56
84
  }
57
85
 
58
86
  /**
@@ -183,6 +211,33 @@ export class ReqlanValidator {
183
211
  });
184
212
  }
185
213
  }
214
+
215
+ checkWildcardReferences(model: Model, accept: ValidationAcceptor): void {
216
+ const { shared } = this.services;
217
+ const context = pathResolveContextFromServices(this.services);
218
+ for (const node of AstUtils.streamAst(model)) {
219
+ if (!isWildcardReference(node)) {
220
+ continue;
221
+ }
222
+ const path = parseWildcardPathPattern(node.pathPattern);
223
+ if (!path.trim()) {
224
+ accept('warning', 'Wildcard path pattern is empty.', { node, property: 'pathPattern' });
225
+ continue;
226
+ }
227
+ const matches = resolveWildcardReferenceMatches(
228
+ node,
229
+ shared.workspace.LangiumDocuments,
230
+ context
231
+ );
232
+ if (matches.length === 0) {
233
+ accept(
234
+ 'warning',
235
+ `No ideas match wildcard reference [${wildcardReferenceLabel(path, node.ideaPattern)}].`,
236
+ { node }
237
+ );
238
+ }
239
+ }
240
+ }
186
241
  }
187
242
 
188
243
  type ImportBindingSource = ReturnType<typeof importBindings>[number];