@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.
- package/README.md +18 -0
- package/out/generated/ast.d.ts +31 -4
- package/out/generated/ast.js +28 -7
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +2 -3318
- package/out/generated/grammar.js.map +1 -1
- package/out/generated/module.d.ts +2 -2
- package/out/generated/module.js +2 -2
- package/out/generated/module.js.map +1 -1
- package/out/index.d.ts +4 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -1
- package/out/reqlan-async-parser.d.ts +37 -0
- package/out/reqlan-async-parser.js +117 -0
- package/out/reqlan-async-parser.js.map +1 -0
- package/out/reqlan-code-action-provider.d.ts +15 -12
- package/out/reqlan-code-action-provider.js +55 -20
- package/out/reqlan-code-action-provider.js.map +1 -1
- package/out/reqlan-comment-resolver.js +5 -0
- package/out/reqlan-comment-resolver.js.map +1 -1
- package/out/reqlan-completion-provider.d.ts +10 -0
- package/out/reqlan-completion-provider.js +64 -30
- package/out/reqlan-completion-provider.js.map +1 -1
- package/out/reqlan-definition-provider.d.ts +7 -0
- package/out/reqlan-definition-provider.js +86 -19
- package/out/reqlan-definition-provider.js.map +1 -1
- package/out/reqlan-document-factory.d.ts +16 -0
- package/out/reqlan-document-factory.js +50 -0
- package/out/reqlan-document-factory.js.map +1 -0
- package/out/reqlan-document-link-provider.d.ts +2 -1
- package/out/reqlan-document-link-provider.js +4 -0
- package/out/reqlan-document-link-provider.js.map +1 -1
- package/out/reqlan-file-link-resolver.d.ts +17 -4
- package/out/reqlan-file-link-resolver.js +89 -14
- package/out/reqlan-file-link-resolver.js.map +1 -1
- package/out/reqlan-import-edits.d.ts +3 -1
- package/out/reqlan-import-edits.js.map +1 -1
- package/out/reqlan-inbound-reference-inlay-label.d.ts +11 -0
- package/out/reqlan-inbound-reference-inlay-label.js +60 -35
- package/out/reqlan-inbound-reference-inlay-label.js.map +1 -1
- package/out/reqlan-inlay-hint-provider.d.ts +6 -0
- package/out/reqlan-inlay-hint-provider.js +34 -6
- package/out/reqlan-inlay-hint-provider.js.map +1 -1
- package/out/reqlan-linker.js +2 -2
- package/out/reqlan-linker.js.map +1 -1
- package/out/reqlan-markdown-links.js +4 -0
- package/out/reqlan-markdown-links.js.map +1 -1
- package/out/reqlan-module.d.ts +2 -1
- package/out/reqlan-module.js +12 -2
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-parse-budget.d.ts +50 -0
- package/out/reqlan-parse-budget.js +89 -0
- package/out/reqlan-parse-budget.js.map +1 -0
- package/out/reqlan-parse-worker.d.ts +1 -0
- package/out/reqlan-parse-worker.js +34 -0
- package/out/reqlan-parse-worker.js.map +1 -0
- package/out/reqlan-path-resolve.d.ts +8 -0
- package/out/reqlan-path-resolve.js +18 -4
- package/out/reqlan-path-resolve.js.map +1 -1
- package/out/reqlan-reference-search-site.d.ts +11 -0
- package/out/reqlan-reference-search-site.js +11 -0
- package/out/reqlan-reference-search-site.js.map +1 -1
- package/out/reqlan-scope.d.ts +11 -0
- package/out/reqlan-scope.js +28 -1
- package/out/reqlan-scope.js.map +1 -1
- package/out/reqlan-semantic-token-provider.js +6 -1
- package/out/reqlan-semantic-token-provider.js.map +1 -1
- package/out/reqlan-token-builder.d.ts +3 -2
- package/out/reqlan-token-builder.js +150 -26
- package/out/reqlan-token-builder.js.map +1 -1
- package/out/reqlan-validator.d.ts +10 -1
- package/out/reqlan-validator.js +36 -2
- package/out/reqlan-validator.js.map +1 -1
- package/out/reqlan-wildcard-resolve.d.ts +45 -0
- package/out/reqlan-wildcard-resolve.js +203 -0
- package/out/reqlan-wildcard-resolve.js.map +1 -0
- package/out/reqlan-workspace-manager.d.ts +18 -0
- package/out/reqlan-workspace-manager.js +31 -0
- package/out/reqlan-workspace-manager.js.map +1 -0
- package/package.json +1 -1
- package/src/generated/ast.ts +41 -10
- package/src/generated/grammar.ts +2 -3318
- package/src/generated/module.ts +2 -2
- package/src/index.ts +4 -0
- package/src/reqlan-async-parser.ts +178 -0
- package/src/reqlan-code-action-provider.ts +67 -32
- package/src/reqlan-comment-resolver.ts +5 -0
- package/src/reqlan-completion-provider.ts +82 -28
- package/src/reqlan-definition-provider.ts +91 -18
- package/src/reqlan-document-factory.ts +75 -0
- package/src/reqlan-document-link-provider.ts +9 -1
- package/src/reqlan-file-link-resolver.ts +112 -17
- package/src/reqlan-import-edits.ts +3 -1
- package/src/reqlan-inbound-reference-inlay-label.ts +93 -38
- package/src/reqlan-inlay-hint-provider.ts +36 -6
- package/src/reqlan-linker.ts +4 -2
- package/src/reqlan-markdown-links.ts +4 -0
- package/src/reqlan-module.ts +15 -3
- package/src/reqlan-parse-budget.ts +155 -0
- package/src/reqlan-parse-worker.ts +35 -0
- package/src/reqlan-path-resolve.ts +28 -5
- package/src/reqlan-reference-search-site.ts +25 -0
- package/src/reqlan-scope.ts +34 -1
- package/src/reqlan-semantic-token-provider.ts +6 -0
- package/src/reqlan-token-builder.ts +174 -28
- package/src/reqlan-validator.ts +56 -1
- package/src/reqlan-wildcard-resolve.ts +281 -0
- package/src/reqlan-workspace-manager.ts +47 -0
- package/src/reqlan.langium +17 -8
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom token builder so line comments do not match
|
|
3
|
-
*
|
|
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
|
|
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
|
|
160
|
+
const before = textBeforeOnLine(text, offset);
|
|
129
161
|
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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 <
|
|
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
|
|
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:
|
|
353
|
+
structuralDepth: structuralDepth[index] ?? 0,
|
|
354
|
+
proseDepth: proseDepth[index] ?? 0
|
|
237
355
|
};
|
|
238
356
|
}
|
|
239
357
|
|
|
240
|
-
function
|
|
241
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
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',
|
package/src/reqlan-validator.ts
CHANGED
|
@@ -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];
|