@reqlan/language 1.9.6 → 1.10.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 (87) hide show
  1. package/README.md +42 -0
  2. package/README.template.md +2 -0
  3. package/out/file-path-rewrite.d.ts +1 -1
  4. package/out/generated/ast.d.ts +2 -2
  5. package/out/generated/ast.js +1 -1
  6. package/out/generated/ast.js.map +1 -1
  7. package/out/generated/grammar.js +1 -1
  8. package/out/generated/grammar.js.map +1 -1
  9. package/out/index.d.ts +3 -1
  10. package/out/index.js +3 -1
  11. package/out/index.js.map +1 -1
  12. package/out/reqlan-code-action-provider.d.ts +1 -0
  13. package/out/reqlan-code-action-provider.js +12 -21
  14. package/out/reqlan-code-action-provider.js.map +1 -1
  15. package/out/reqlan-comment-diagnostics.d.ts +11 -0
  16. package/out/reqlan-comment-diagnostics.js +81 -11
  17. package/out/reqlan-comment-diagnostics.js.map +1 -1
  18. package/out/reqlan-comment-resolver.d.ts +1 -1
  19. package/out/reqlan-completion-provider.d.ts +2 -2
  20. package/out/reqlan-definition-provider.js +1 -1
  21. package/out/reqlan-definition-provider.js.map +1 -1
  22. package/out/reqlan-document-builder.d.ts +9 -0
  23. package/out/reqlan-document-builder.js +26 -0
  24. package/out/reqlan-document-builder.js.map +1 -0
  25. package/out/reqlan-document-link-provider.d.ts +5 -1
  26. package/out/reqlan-document-link-provider.js +9 -1
  27. package/out/reqlan-document-link-provider.js.map +1 -1
  28. package/out/reqlan-embedded-file-references.d.ts +3 -0
  29. package/out/reqlan-embedded-file-references.js +47 -5
  30. package/out/reqlan-embedded-file-references.js.map +1 -1
  31. package/out/reqlan-file-link-resolver.d.ts +8 -0
  32. package/out/reqlan-file-link-resolver.js +46 -11
  33. package/out/reqlan-file-link-resolver.js.map +1 -1
  34. package/out/reqlan-file-references.d.ts +1 -0
  35. package/out/reqlan-file-references.js +24 -16
  36. package/out/reqlan-file-references.js.map +1 -1
  37. package/out/reqlan-idea-move-imports.d.ts +24 -0
  38. package/out/reqlan-idea-move-imports.js +309 -0
  39. package/out/reqlan-idea-move-imports.js.map +1 -0
  40. package/out/reqlan-idea-refactor.d.ts +12 -4
  41. package/out/reqlan-idea-refactor.js +85 -13
  42. package/out/reqlan-idea-refactor.js.map +1 -1
  43. package/out/reqlan-ignore-error.d.ts +2 -1
  44. package/out/reqlan-ignore-error.js +4 -17
  45. package/out/reqlan-ignore-error.js.map +1 -1
  46. package/out/reqlan-import-edits.d.ts +1 -0
  47. package/out/reqlan-import-edits.js +8 -0
  48. package/out/reqlan-import-edits.js.map +1 -1
  49. package/out/reqlan-imports.d.ts +3 -1
  50. package/out/reqlan-imports.js +2 -5
  51. package/out/reqlan-imports.js.map +1 -1
  52. package/out/reqlan-module.js +4 -1
  53. package/out/reqlan-module.js.map +1 -1
  54. package/out/reqlan-reference-at-position.d.ts +3 -0
  55. package/out/reqlan-reference-at-position.js +8 -2
  56. package/out/reqlan-reference-at-position.js.map +1 -1
  57. package/out/reqlan-scope.d.ts +14 -0
  58. package/out/reqlan-scope.js +58 -15
  59. package/out/reqlan-scope.js.map +1 -1
  60. package/out/reqlan-validator.d.ts +12 -1
  61. package/out/reqlan-validator.js +38 -4
  62. package/out/reqlan-validator.js.map +1 -1
  63. package/package.json +3 -2
  64. package/src/file-path-rewrite.ts +1 -1
  65. package/src/generated/ast.ts +2 -2
  66. package/src/generated/grammar.ts +1 -1
  67. package/src/index.ts +9 -0
  68. package/src/reqlan-code-action-provider.ts +12 -24
  69. package/src/reqlan-comment-diagnostics.ts +108 -11
  70. package/src/reqlan-comment-resolver.ts +1 -1
  71. package/src/reqlan-completion-provider.ts +2 -2
  72. package/src/reqlan-definition-provider.ts +1 -1
  73. package/src/reqlan-document-builder.ts +35 -0
  74. package/src/reqlan-document-link-provider.ts +23 -3
  75. package/src/reqlan-embedded-file-references.ts +54 -5
  76. package/src/reqlan-file-link-resolver.ts +56 -11
  77. package/src/reqlan-file-references.ts +26 -16
  78. package/src/reqlan-idea-move-imports.ts +398 -0
  79. package/src/reqlan-idea-refactor.ts +116 -14
  80. package/src/reqlan-ignore-error.ts +4 -18
  81. package/src/reqlan-import-edits.ts +9 -0
  82. package/src/reqlan-imports.ts +4 -5
  83. package/src/reqlan-module.ts +4 -1
  84. package/src/reqlan-reference-at-position.ts +16 -2
  85. package/src/reqlan-scope.ts +73 -16
  86. package/src/reqlan-validator.ts +48 -4
  87. package/src/reqlan.langium +4 -1
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Locates file path references at a cursor position and resolves them to files, folders, or missing paths.
3
+ * Missing targets are omitted here so go-to-definition can still try import-path resolution
4
+ * (for example `from "./target"` → `./target.rq`). Diagnostics still underline those refs.
5
+ * rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
3
6
  */
4
7
  import type { CstNode, FileSystemProvider, LangiumDocument, LangiumDocuments } from 'langium';
5
8
  import { CstUtils, GrammarUtils } from 'langium';
@@ -55,15 +58,26 @@ export function findFileReferenceAtPosition(
55
58
  }
56
59
  const embeddedReference = findEmbeddedFileReferenceAt(document, position);
57
60
  if (embeddedReference) {
58
- return resolveEmbeddedFileReferenceLink(embeddedReference, document, documents, fileSystem, pathContext);
61
+ return navigableFileLink(
62
+ resolveEmbeddedFileReferenceLink(embeddedReference, document, documents, fileSystem, pathContext)
63
+ );
59
64
  }
60
65
  const fileReference = findFileReferenceNodeAtPosition(document, position);
61
66
  if (fileReference) {
62
- return resolveFileReferenceLink(fileReference.node, documents, fileSystem, pathContext);
67
+ return navigableFileLink(
68
+ resolveFileReferenceLink(fileReference.node, documents, fileSystem, pathContext)
69
+ );
63
70
  }
64
71
  return undefined;
65
72
  }
66
73
 
74
+ function navigableFileLink(link: ResolvedFileLink | undefined): ResolvedFileLink | undefined {
75
+ if (!link || link.resolution === 'missing') {
76
+ return undefined;
77
+ }
78
+ return link;
79
+ }
80
+
67
81
  function resolvePathReference(
68
82
  document: LangiumDocument,
69
83
  path: string,
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * Publishes idea and ideaset declarations for file-local and cross-file reference resolution.
3
+ * Local declarations are collected first; imports are consulted only when a name is not local.
4
+ * rq:["../../../reqlan rq/language/syntax.rq".same_file_reference]
5
+ * rq:["../../../reqlan rq/language/syntax.rq".reference_resolution_order]
3
6
  */
4
7
  import type { AstNodeDescription, LangiumDocument, ReferenceInfo } from 'langium';
5
8
  import { AstUtils, DefaultScopeComputation, DefaultScopeProvider, StreamScope, stream, type Scope } from 'langium';
6
9
  import {
7
10
  isFromImport,
8
11
  isFromImportSpecifier,
9
- isIdea,
12
+ isIdeaDeclaration,
10
13
  isIdeaSet,
11
14
  isInvalidFromImport,
12
15
  isLocalReference,
13
16
  isModel,
14
17
  isNamespaceImport,
15
- isOneLinerIdea,
16
18
  isQualifiedImport,
17
19
  isQualifiedReference,
18
20
  type FromImportSpecifier,
@@ -34,7 +36,7 @@ export class ReqlanScopeComputation extends DefaultScopeComputation {
34
36
  const model = document.parseResult.value;
35
37
  if (isModel(model)) {
36
38
  for (const element of model.elements) {
37
- if (isIdea(element) || isOneLinerIdea(element) || isIdeaSet(element)) {
39
+ if (isIdeaDeclaration(element)) {
38
40
  this.addExportedSymbol(element, exports, document);
39
41
  }
40
42
  }
@@ -146,20 +148,49 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
146
148
  }
147
149
 
148
150
  private buildFileIdeasScope(document: LangiumDocument): Scope {
151
+ const locals = this.localDeclarationDescriptions(document);
152
+ return new StreamScope(
153
+ stream(locals),
154
+ new LazyScope(() => this.importedIdeaScope(document)),
155
+ { concatOuterScope: false }
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Every named top-level declaration in the file, regardless of source order.
161
+ * Bare `[name]` and ideaset members resolve against this set first.
162
+ */
163
+ private localDeclarationDescriptions(document: LangiumDocument): AstNodeDescription[] {
149
164
  const model = document.parseResult.value as Model;
150
165
  if (!isModel(model)) {
151
- return new StreamScope(stream([]));
166
+ return [];
152
167
  }
153
- const descriptions = model.elements
154
- .filter(element => isIdea(element) || isOneLinerIdea(element))
155
- .map(idea => this.descriptions.createDescription(idea, idea.name, document));
168
+ return model.elements
169
+ .filter(isIdeaDeclaration)
170
+ .map(element => this.descriptions.createDescription(element, element.name, document));
171
+ }
172
+
173
+ /**
174
+ * Imported bindings only. Built lazily so a fully-local file never walks import `.ref`s
175
+ * while resolving same-file names.
176
+ */
177
+ private importedIdeaScope(document: LangiumDocument): Scope {
178
+ return new StreamScope(stream(this.importedIdeaDescriptions(document)));
179
+ }
180
+
181
+ private importedIdeaDescriptions(document: LangiumDocument): AstNodeDescription[] {
182
+ const model = document.parseResult.value as Model;
183
+ if (!isModel(model)) {
184
+ return [];
185
+ }
186
+ const descriptions: AstNodeDescription[] = [];
156
187
  // Imported ideas enter scope under their binding name only (alias when present).
157
188
  // See import_tokenisation: aliased base names are not reserved locally.
158
189
  for (const importDecl of model.imports) {
159
190
  if (isFromImport(importDecl)) {
160
191
  for (const specifier of importDecl.specifiers) {
161
192
  const target = specifier.idea.ref;
162
- if (!target || (!isIdea(target) && !isOneLinerIdea(target))) {
193
+ if (!target || !isIdeaDeclaration(target)) {
163
194
  continue;
164
195
  }
165
196
  const name = specifierBindingName(specifier) ?? target.name;
@@ -169,21 +200,21 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
169
200
  }
170
201
  if (isQualifiedImport(importDecl)) {
171
202
  const target = importDecl.idea.ref;
172
- if (!target || (!isIdea(target) && !isOneLinerIdea(target))) {
203
+ if (!target || !isIdeaDeclaration(target)) {
173
204
  continue;
174
205
  }
175
206
  const name = importDecl.alias ?? target.name;
176
207
  descriptions.push(this.descriptions.createDescription(target, name, AstUtils.getDocument(target)));
177
208
  }
178
209
  }
179
- return new StreamScope(stream(descriptions));
210
+ return descriptions;
180
211
  }
181
212
 
182
213
  private scopeForIdeasetMembers(ideaset: IdeaSet, document: LangiumDocument): Scope {
183
214
  const descriptions = ideaset.members
184
215
  .map(member => member.ref)
185
216
  .filter((target): target is NonNullable<typeof target> =>
186
- target !== undefined && (isIdea(target) || isOneLinerIdea(target))
217
+ target !== undefined && isIdeaDeclaration(target)
187
218
  )
188
219
  .map(target => this.descriptions.createDescription(target, target.name, AstUtils.getDocument(target)));
189
220
  if (descriptions.length > 0) {
@@ -209,7 +240,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
209
240
  return new StreamScope(stream([]));
210
241
  }
211
242
  const locals = model.elements
212
- .filter(element => isIdea(element) || isOneLinerIdea(element) || isIdeaSet(element))
243
+ .filter(isIdeaDeclaration)
213
244
  .map(element => this.descriptions.createDescription(element, element.name, document));
214
245
  const namedImports = model.imports.flatMap(importDecl => {
215
246
  if (isFromImport(importDecl)) {
@@ -247,7 +278,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
247
278
  return undefined;
248
279
  }
249
280
  const target = importDecl.idea.ref;
250
- if (!target || (!isIdea(target) && !isOneLinerIdea(target))) {
281
+ if (!target || !isIdeaDeclaration(target)) {
251
282
  return undefined;
252
283
  }
253
284
  const description = this.descriptions.createDescription(target, target.name, imported);
@@ -261,7 +292,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
261
292
  const descriptions = importDecl.specifiers
262
293
  .map(specifier => specifier.idea.ref)
263
294
  .filter((target): target is NonNullable<typeof target> =>
264
- target !== undefined && (isIdea(target) || isOneLinerIdea(target))
295
+ target !== undefined && isIdeaDeclaration(target)
265
296
  )
266
297
  .map(target => this.descriptions.createDescription(target, target.name, imported));
267
298
  return new StreamScope(stream(descriptions));
@@ -272,7 +303,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
272
303
  return undefined;
273
304
  }
274
305
  const target = importDecl.idea.ref;
275
- if (!target || (!isIdea(target) && !isOneLinerIdea(target))) {
306
+ if (!target || !isIdeaDeclaration(target)) {
276
307
  return undefined;
277
308
  }
278
309
  const description = this.descriptions.createDescription(target, target.name, imported);
@@ -315,7 +346,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
315
346
  return undefined;
316
347
  }
317
348
  const descriptions = model.elements
318
- .filter(element => isIdea(element) || isOneLinerIdea(element))
349
+ .filter(isIdeaDeclaration)
319
350
  .map(idea => this.descriptions.createDescription(idea, idea.name, imported));
320
351
  const scope = new StreamScope(stream(descriptions));
321
352
  this.importPathCache.set(key, { version: imported.textDocument.version, scope });
@@ -349,3 +380,29 @@ function importAlias(entry: Import): string | undefined {
349
380
  }
350
381
  return entry.alias;
351
382
  }
383
+
384
+ /**
385
+ * Defers outer-scope construction until a local lookup misses.
386
+ * StreamScope `getElement` does not call the outer scope when a local name matches.
387
+ */
388
+ class LazyScope implements Scope {
389
+ private resolved: Scope | undefined;
390
+
391
+ constructor(private readonly load: () => Scope) {}
392
+
393
+ getElement(name: string): AstNodeDescription | undefined {
394
+ return this.inner().getElement(name);
395
+ }
396
+
397
+ getElements(name: string): ReturnType<Scope['getElements']> {
398
+ return this.inner().getElements(name);
399
+ }
400
+
401
+ getAllElements(): ReturnType<Scope['getAllElements']> {
402
+ return this.inner().getAllElements();
403
+ }
404
+
405
+ private inner(): Scope {
406
+ return this.resolved ??= this.load();
407
+ }
408
+ }
@@ -3,16 +3,18 @@ import { AstUtils } from 'langium';
3
3
  import type { reqlanAstType } from './generated/ast.js';
4
4
  import {
5
5
  isFromImport,
6
- isIdea,
6
+ isIdeaDeclaration,
7
7
  isInvalidFromImport,
8
8
  isModel,
9
- isOneLinerIdea,
10
9
  isWildcardReference,
11
10
  type AnonymousBlock,
12
11
  type Model
13
12
  } from './generated/ast.js';
13
+ import { collectCommentReferenceIssues } from './reqlan-comment-diagnostics.js';
14
14
  import {
15
15
  collectFileLinks,
16
+ FILE_REFERENCE_MISSING,
17
+ fileLinkMissingMessage,
16
18
  fileLinkTargetIssueMessage
17
19
  } from './reqlan-file-link-resolver.js';
18
20
  import {
@@ -33,9 +35,12 @@ import {
33
35
  /**
34
36
  * Registers validation hooks for the requirement graph AST.
35
37
  * rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
38
+ * rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
36
39
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
37
40
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
38
41
  * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
42
+ * rq:["../../../reqlan rq/language/syntax.rq".comment_reference_resolution_error]
43
+ * rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
39
44
  */
40
45
  export function registerValidationChecks(services: ReqlanServices) {
41
46
  const registry = services.validation.ValidationRegistry;
@@ -50,9 +55,14 @@ export function registerValidationChecks(services: ReqlanServices) {
50
55
  /**
51
56
  * Custom validations for Reqlan documents.
52
57
  * rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
58
+ * rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_folder_targets]
59
+ * rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
53
60
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
54
61
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
55
62
  * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
63
+ * rq:["../../../reqlan rq/language/syntax.rq".comment_reference_resolution_error]
64
+ * rq:["../../../reqlan rq/core_analysis/check.rq".check_wildcard_sparse]
65
+ * rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
56
66
  */
57
67
  export class ReqlanValidator {
58
68
 
@@ -64,6 +74,7 @@ export class ReqlanValidator {
64
74
  this.checkImportSyntax(model, accept);
65
75
  this.checkImportTargets(model, accept);
66
76
  this.checkFileReferenceTargets(model, accept);
77
+ this.checkCommentReferences(model, accept);
67
78
  this.checkWildcardReferences(model, accept);
68
79
  }
69
80
 
@@ -133,6 +144,8 @@ export class ReqlanValidator {
133
144
  /**
134
145
  * Duplicate check uses import *bindings* only (alias when present, otherwise the idea name).
135
146
  * An aliased import does not reserve the imported base name for local ideas.
147
+ * Ideasets share the same name space as ideas in the file.
148
+ * rq:["../../../reqlan rq/language/syntax.rq".idea_name]
136
149
  */
137
150
  checkDuplicateIdeaNames(model: Model, accept: ValidationAcceptor): void {
138
151
  const seen = new Map<string, AstNode>();
@@ -142,7 +155,7 @@ export class ReqlanValidator {
142
155
  }
143
156
  }
144
157
  for (const element of model.elements) {
145
- if (!isIdea(element) && !isOneLinerIdea(element)) {
158
+ if (!isIdeaDeclaration(element)) {
146
159
  continue;
147
160
  }
148
161
  const name = element.name;
@@ -158,7 +171,7 @@ export class ReqlanValidator {
158
171
  }
159
172
 
160
173
  /**
161
- * Error underline when an `import` / `from` path does not resolve to a file.
174
+ * Error underline when an `import` / `from` path does not resolve to a file or folder.
162
175
  * Missing imported ideas are reported by the linker as unresolved references.
163
176
  */
164
177
  checkImportTargets(model: Model, accept: ValidationAcceptor): void {
@@ -202,6 +215,14 @@ export class ReqlanValidator {
202
215
  shared.workspace.FileSystemProvider,
203
216
  pathResolveContextFromServices(this.services)
204
217
  )) {
218
+ if (link.resolution === 'missing') {
219
+ accept('error', fileLinkMissingMessage(link.authoredPath ?? ''), {
220
+ node: model,
221
+ range: link.sourceRange,
222
+ code: FILE_REFERENCE_MISSING
223
+ });
224
+ continue;
225
+ }
205
226
  if (!link.targetIssue) {
206
227
  continue;
207
228
  }
@@ -212,6 +233,23 @@ export class ReqlanValidator {
212
233
  }
213
234
  }
214
235
 
236
+ checkCommentReferences(model: Model, accept: ValidationAcceptor): void {
237
+ const document = AstUtils.getDocument(model);
238
+ const { shared } = this.services;
239
+ for (const issue of collectCommentReferenceIssues(
240
+ document,
241
+ shared.workspace.LangiumDocuments,
242
+ shared.workspace.FileSystemProvider,
243
+ pathResolveContextFromServices(this.services)
244
+ )) {
245
+ accept('error', issue.message, {
246
+ node: model,
247
+ range: issue.range,
248
+ code: issue.code
249
+ });
250
+ }
251
+ }
252
+
215
253
  checkWildcardReferences(model: Model, accept: ValidationAcceptor): void {
216
254
  const { shared } = this.services;
217
255
  const context = pathResolveContextFromServices(this.services);
@@ -235,6 +273,12 @@ export class ReqlanValidator {
235
273
  `No ideas match wildcard reference [${wildcardReferenceLabel(path, node.ideaPattern)}].`,
236
274
  { node }
237
275
  );
276
+ } else if (matches.length === 1) {
277
+ accept(
278
+ 'warning',
279
+ `Wildcard reference [${wildcardReferenceLabel(path, node.ideaPattern)}] matches only 1 idea.`,
280
+ { node }
281
+ );
238
282
  }
239
283
  }
240
284
  }
@@ -38,8 +38,11 @@ OneLinerIdea:
38
38
  OneLinerBody:
39
39
  content+=(MarkdownLink | BracketReference | WikiLink | OneLinerText)+;
40
40
 
41
+ // Bare [name] refs resolve to any of these. Collect all declarations before linking
42
+ // so a reference may target a name declared later in the same file.
43
+ // rq:["../../../reqlan rq/language/syntax.rq".reference_resolution_order]
41
44
  IdeaDeclaration:
42
- Idea | OneLinerIdea;
45
+ Idea | OneLinerIdea | IdeaSet;
43
46
 
44
47
  Idea:
45
48
  name=IdeaName '{'