@reqlan/language 1.9.7 → 1.10.2
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 +41 -0
- package/out/file-path-rewrite.d.ts +1 -1
- package/out/generated/ast.d.ts +2 -2
- package/out/generated/ast.js +1 -1
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +1 -1
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/reqlan-comment-resolver.d.ts +1 -1
- package/out/reqlan-completion-provider.d.ts +2 -2
- package/out/reqlan-definition-provider.js +1 -1
- package/out/reqlan-definition-provider.js.map +1 -1
- package/out/reqlan-document-link-provider.d.ts +2 -0
- package/out/reqlan-document-link-provider.js +3 -0
- package/out/reqlan-document-link-provider.js.map +1 -1
- package/out/reqlan-embedded-file-references.d.ts +3 -0
- package/out/reqlan-embedded-file-references.js +47 -5
- package/out/reqlan-embedded-file-references.js.map +1 -1
- package/out/reqlan-file-link-resolver.d.ts +8 -0
- package/out/reqlan-file-link-resolver.js +46 -11
- package/out/reqlan-file-link-resolver.js.map +1 -1
- package/out/reqlan-file-references.d.ts +1 -0
- package/out/reqlan-file-references.js +24 -16
- package/out/reqlan-file-references.js.map +1 -1
- package/out/reqlan-ignore-error.d.ts +2 -1
- package/out/reqlan-ignore-error.js +4 -17
- package/out/reqlan-ignore-error.js.map +1 -1
- package/out/reqlan-imports.d.ts +3 -1
- package/out/reqlan-imports.js +2 -5
- package/out/reqlan-imports.js.map +1 -1
- package/out/reqlan-module.js +1 -0
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-reference-at-position.d.ts +3 -0
- package/out/reqlan-reference-at-position.js +8 -2
- package/out/reqlan-reference-at-position.js.map +1 -1
- package/out/reqlan-scope.d.ts +14 -0
- package/out/reqlan-scope.js +58 -15
- package/out/reqlan-scope.js.map +1 -1
- package/out/reqlan-validator.d.ts +7 -1
- package/out/reqlan-validator.js +21 -4
- package/out/reqlan-validator.js.map +1 -1
- package/package.json +3 -2
- package/src/file-path-rewrite.ts +1 -1
- package/src/generated/ast.ts +2 -2
- package/src/generated/grammar.ts +1 -1
- package/src/index.ts +5 -0
- package/src/reqlan-comment-resolver.ts +1 -1
- package/src/reqlan-completion-provider.ts +2 -2
- package/src/reqlan-definition-provider.ts +1 -1
- package/src/reqlan-document-link-provider.ts +5 -0
- package/src/reqlan-embedded-file-references.ts +54 -5
- package/src/reqlan-file-link-resolver.ts +56 -11
- package/src/reqlan-file-references.ts +26 -16
- package/src/reqlan-ignore-error.ts +4 -18
- package/src/reqlan-imports.ts +4 -5
- package/src/reqlan-module.ts +1 -0
- package/src/reqlan-reference-at-position.ts +16 -2
- package/src/reqlan-scope.ts +73 -16
- package/src/reqlan-validator.ts +25 -4
- package/src/reqlan.langium +4 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves file references, idea references, and import paths to target URIs and editor ranges.
|
|
3
3
|
* Document links use these so same-file and cross-file refs share the same underline/click affordance.
|
|
4
|
+
* Missing file refs stay in this result so the validator can underline them without creating a link.
|
|
5
|
+
* rq:["../../../reqlan rq/extension/language-support/language-server-errors.rq".file_reference_errors]
|
|
6
|
+
* rq:["../../../reqlan rq/extension/syntax/features-syntax.rq".file_references]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_folder_targets]
|
|
4
8
|
*/
|
|
5
9
|
import type { AstNode, CstNode, FileSystemProvider, LangiumDocument, LangiumDocuments, Reference, URI } from 'langium';
|
|
6
10
|
import { AstUtils, CstUtils, GrammarUtils } from 'langium';
|
|
@@ -50,6 +54,8 @@ export type ReferenceResolution = 'file' | 'folder' | 'missing' | 'wildcard';
|
|
|
50
54
|
|
|
51
55
|
export type FileLinkTargetIssue = 'empty' | 'parse-error';
|
|
52
56
|
|
|
57
|
+
export const FILE_REFERENCE_MISSING = 'file-reference-missing';
|
|
58
|
+
|
|
53
59
|
export interface ResolvedFileLink {
|
|
54
60
|
sourceRange: Range;
|
|
55
61
|
targetUri: string;
|
|
@@ -57,6 +63,8 @@ export interface ResolvedFileLink {
|
|
|
57
63
|
resolution?: ReferenceResolution;
|
|
58
64
|
folderFiles?: string[];
|
|
59
65
|
targetIssue?: FileLinkTargetIssue;
|
|
66
|
+
/** Authored path (no test/line suffix) for missing-file diagnostics. */
|
|
67
|
+
authoredPath?: string;
|
|
60
68
|
/** Present when resolution is `wildcard`. */
|
|
61
69
|
wildcardArgs?: WildcardReferenceArgs;
|
|
62
70
|
wildcardMatches?: WildcardMatch[];
|
|
@@ -126,14 +134,18 @@ export function resolveImportPathLink(
|
|
|
126
134
|
}
|
|
127
135
|
const imported = findImportedDocument(path, document, documents, context);
|
|
128
136
|
const target = imported?.parseResult.value.$cstNode;
|
|
129
|
-
if (
|
|
137
|
+
if (imported && target) {
|
|
138
|
+
return {
|
|
139
|
+
sourceRange,
|
|
140
|
+
targetUri: imported.textDocument.uri,
|
|
141
|
+
targetRange: target.range
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const fileSystem = context?.fileSystem;
|
|
145
|
+
if (!fileSystem) {
|
|
130
146
|
return undefined;
|
|
131
147
|
}
|
|
132
|
-
return
|
|
133
|
-
sourceRange,
|
|
134
|
-
targetUri: imported.textDocument.uri,
|
|
135
|
-
targetRange: target.range
|
|
136
|
-
};
|
|
148
|
+
return resolveImportedFileLink(document, documents, fileSystem, path, sourceRange, context);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
export function resolveQualifiedReferencePathLink(
|
|
@@ -238,21 +250,28 @@ function resolvePathStringLink(
|
|
|
238
250
|
targetUri: URI,
|
|
239
251
|
sourceRange: Range
|
|
240
252
|
): ResolvedFileLink | undefined {
|
|
253
|
+
if (!parsed.filePath.trim()) {
|
|
254
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
255
|
+
}
|
|
256
|
+
if (isRemoteFileReferencePath(parsed.filePath)) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
241
259
|
const resolution = classifyReferenceUri(targetUri, documents, fileSystem);
|
|
242
260
|
if (resolution === 'missing') {
|
|
243
|
-
return
|
|
261
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
244
262
|
}
|
|
245
263
|
if (resolution === 'folder') {
|
|
246
264
|
return {
|
|
247
265
|
sourceRange,
|
|
248
266
|
targetUri: targetUri.toString(),
|
|
249
267
|
resolution,
|
|
250
|
-
folderFiles: listFolderFileNames(fileSystem, targetUri)
|
|
268
|
+
folderFiles: listFolderFileNames(fileSystem, targetUri),
|
|
269
|
+
authoredPath: parsed.filePath
|
|
251
270
|
};
|
|
252
271
|
}
|
|
253
272
|
const text = readTargetText(targetUri, documents, fileSystem);
|
|
254
273
|
if (text === undefined) {
|
|
255
|
-
return
|
|
274
|
+
return missingFileLink(sourceRange, targetUri, parsed.filePath);
|
|
256
275
|
}
|
|
257
276
|
const targetIssue = detectFileLinkTargetIssue(text, findTargetDocument(targetUri, documents));
|
|
258
277
|
let targetRange: Range | undefined;
|
|
@@ -271,10 +290,24 @@ function resolvePathStringLink(
|
|
|
271
290
|
targetUri: targetUri.toString(),
|
|
272
291
|
targetRange,
|
|
273
292
|
resolution: 'file',
|
|
274
|
-
targetIssue
|
|
293
|
+
targetIssue,
|
|
294
|
+
authoredPath: parsed.filePath
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function missingFileLink(sourceRange: Range, targetUri: URI, authoredPath: string): ResolvedFileLink {
|
|
299
|
+
return {
|
|
300
|
+
sourceRange,
|
|
301
|
+
targetUri: targetUri.toString(),
|
|
302
|
+
resolution: 'missing',
|
|
303
|
+
authoredPath
|
|
275
304
|
};
|
|
276
305
|
}
|
|
277
306
|
|
|
307
|
+
function isRemoteFileReferencePath(path: string): boolean {
|
|
308
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(path);
|
|
309
|
+
}
|
|
310
|
+
|
|
278
311
|
export function detectFileLinkTargetIssue(
|
|
279
312
|
text: string,
|
|
280
313
|
targetDocument: LangiumDocument | undefined
|
|
@@ -297,6 +330,14 @@ export function fileLinkTargetIssueMessage(issue: FileLinkTargetIssue): string {
|
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
332
|
|
|
333
|
+
export function fileLinkMissingMessage(path: string): string {
|
|
334
|
+
const trimmed = path.trim();
|
|
335
|
+
if (!trimmed) {
|
|
336
|
+
return 'Could not resolve file reference.';
|
|
337
|
+
}
|
|
338
|
+
return `Could not resolve file reference '${trimmed}'.`;
|
|
339
|
+
}
|
|
340
|
+
|
|
300
341
|
function findTargetDocument(targetUri: URI, documents: LangiumDocuments): LangiumDocument | undefined {
|
|
301
342
|
const direct = documents.getDocument(targetUri);
|
|
302
343
|
if (direct) {
|
|
@@ -347,7 +388,11 @@ export function resolveImportedFileLink(
|
|
|
347
388
|
context?: PathResolveContext
|
|
348
389
|
): ResolvedFileLink | undefined {
|
|
349
390
|
const targetUri = resolveExistingImportUri(filePath, document, documents, fileSystem, context);
|
|
350
|
-
|
|
391
|
+
const link = resolvePathStringLink(document, documents, fileSystem, { filePath }, targetUri, sourceRange);
|
|
392
|
+
if (!link || link.resolution === 'missing') {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return link;
|
|
351
396
|
}
|
|
352
397
|
|
|
353
398
|
export function filePathRangeInStringNode(pathNode: CstNode, parsed: ParsedFileReference): Range {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses file path strings from bracket references, including L# line suffixes and :test name suffixes.
|
|
3
|
+
* rq:["../../../reqlan rq/language/syntax.rq".reference_file]
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
export interface ParsedFileReference {
|
|
@@ -30,28 +31,37 @@ export function parseFileReferenceString(file: string): ParsedFileReference {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function parseTestNameSuffix(file: string): { filePath: string; testName?: string } {
|
|
33
|
-
let
|
|
34
|
-
for (let index = file.length - 1; index >= 0; index--) {
|
|
34
|
+
for (let index = 0; index < file.length; index++) {
|
|
35
35
|
if (file[index] !== ':') {
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
38
|
+
if (isUriSchemeColon(file, index) || isWindowsDriveColon(file, index)) {
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return { filePath: file };
|
|
41
|
+
const testName = file.slice(index + 1);
|
|
42
|
+
if (!testName) {
|
|
43
|
+
return { filePath: file };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
filePath: file.slice(0, index),
|
|
47
|
+
testName
|
|
48
|
+
};
|
|
50
49
|
}
|
|
51
|
-
return {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
return { filePath: file };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `https://` / `file://` — the colon that starts `://`. */
|
|
54
|
+
function isUriSchemeColon(file: string, index: number): boolean {
|
|
55
|
+
return file[index + 1] === '/' && file[index + 2] === '/';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Drive letter in `C:\…` or `file://C:/…`. */
|
|
59
|
+
function isWindowsDriveColon(file: string, index: number): boolean {
|
|
60
|
+
const previous = file[index - 1];
|
|
61
|
+
const next = file[index + 1];
|
|
62
|
+
return previous !== undefined
|
|
63
|
+
&& /[A-Za-z]/.test(previous)
|
|
64
|
+
&& (next === '\\' || next === '/');
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
const NON_RQ_FILE_EXTENSION = /\.[^./\\]+$/;
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Langium glue: drop diagnostics on lines that `//rq-ignore-error` suppresses.
|
|
3
|
+
* Line detection is the core Rust scanner (`find_rq_ignore_error_target_lines`).
|
|
3
4
|
* rq:["../../../reqlan rq/language/syntax.rq".comment_reference_ignore]
|
|
4
5
|
*/
|
|
5
6
|
import { DocumentState, type LangiumDocument, type LangiumSharedCoreServices } from 'langium';
|
|
6
7
|
import type { Diagnostic } from 'vscode-languageserver';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const RQ_IGNORE_ERROR = /\/\/\s*rq-ignore-error\b/;
|
|
8
|
+
import { findRqIgnoreErrorTargetLines as nativeFindRqIgnoreErrorTargetLines } from '@reqlan/analytical/core';
|
|
10
9
|
|
|
11
10
|
export function findRqIgnoreErrorTargetLines(text: string): Set<number> {
|
|
12
|
-
|
|
13
|
-
const targetLines = new Set<number>();
|
|
14
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
15
|
-
const line = lines[lineIndex]!;
|
|
16
|
-
const commentStart = findLineCommentStart(line);
|
|
17
|
-
if (commentStart < 0) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const commentText = line.slice(commentStart);
|
|
21
|
-
if (RQ_IGNORE_ERROR.test(commentText) && lineIndex + 1 < lines.length) {
|
|
22
|
-
targetLines.add(lineIndex + 1);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return targetLines;
|
|
11
|
+
return new Set(nativeFindRqIgnoreErrorTargetLines(text));
|
|
26
12
|
}
|
|
27
13
|
|
|
28
14
|
export function filterRqIgnoredDiagnostics(text: string, diagnostics: Diagnostic[]): Diagnostic[] {
|
package/src/reqlan-imports.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves imported requirement documents from relative and import-root-aliased paths.
|
|
3
3
|
* Interacts with scope linking and go-to-definition for import paths.
|
|
4
|
+
* A path that exists as a folder is a valid import target (same as a file).
|
|
4
5
|
* rq:["../../../reqlan rq/ontology.rq".import_statement]
|
|
5
6
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".implicit_file_extension]
|
|
7
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_folder_targets]
|
|
6
8
|
*/
|
|
7
9
|
import type { FileSystemProvider, LangiumDocument, LangiumDocuments, URI } from 'langium';
|
|
8
10
|
import {
|
|
@@ -16,7 +18,7 @@ export const IMPLICIT_IMPORT_EXTENSION = '.rq';
|
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Import paths written without an extension mean `.rq`; the literal path stays a fallback so
|
|
19
|
-
* extensionless files on disk keep resolving.
|
|
21
|
+
* extensionless files and folders on disk keep resolving.
|
|
20
22
|
*/
|
|
21
23
|
export function importPathCandidates(path: string): string[] {
|
|
22
24
|
const implicit = importPathWithImplicitExtension(path);
|
|
@@ -99,10 +101,7 @@ function isImportTarget(
|
|
|
99
101
|
if (documents.getDocument(uri)) {
|
|
100
102
|
return true;
|
|
101
103
|
}
|
|
102
|
-
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
return !fileSystem.statSync(uri).isDirectory;
|
|
104
|
+
return fileSystem?.existsSync(uri) === true;
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
function withFileSystem(
|
package/src/reqlan-module.ts
CHANGED
|
@@ -110,6 +110,7 @@ export function createReqlanServices(context: DefaultSharedModuleContext): {
|
|
|
110
110
|
);
|
|
111
111
|
shared.ServiceRegistry.register(Reqlan);
|
|
112
112
|
registerValidationChecks(Reqlan);
|
|
113
|
+
// rq:["../../../reqlan rq/language/syntax.rq".comment_reference_ignore]
|
|
113
114
|
registerRqIgnoreErrorFiltering(shared);
|
|
114
115
|
if (!context.connection) {
|
|
115
116
|
// We don't run inside a language server
|
|
@@ -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
|
|
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
|
|
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,
|
package/src/reqlan-scope.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
166
|
+
return [];
|
|
152
167
|
}
|
|
153
|
-
|
|
154
|
-
.filter(
|
|
155
|
-
.map(
|
|
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 ||
|
|
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 ||
|
|
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
|
|
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 && (
|
|
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(
|
|
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 ||
|
|
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 && (
|
|
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 ||
|
|
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(
|
|
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
|
+
}
|
package/src/reqlan-validator.ts
CHANGED
|
@@ -3,10 +3,9 @@ import { AstUtils } from 'langium';
|
|
|
3
3
|
import type { reqlanAstType } from './generated/ast.js';
|
|
4
4
|
import {
|
|
5
5
|
isFromImport,
|
|
6
|
-
|
|
6
|
+
isIdeaDeclaration,
|
|
7
7
|
isInvalidFromImport,
|
|
8
8
|
isModel,
|
|
9
|
-
isOneLinerIdea,
|
|
10
9
|
isWildcardReference,
|
|
11
10
|
type AnonymousBlock,
|
|
12
11
|
type Model
|
|
@@ -14,6 +13,8 @@ import {
|
|
|
14
13
|
import { collectCommentReferenceIssues } from './reqlan-comment-diagnostics.js';
|
|
15
14
|
import {
|
|
16
15
|
collectFileLinks,
|
|
16
|
+
FILE_REFERENCE_MISSING,
|
|
17
|
+
fileLinkMissingMessage,
|
|
17
18
|
fileLinkTargetIssueMessage
|
|
18
19
|
} from './reqlan-file-link-resolver.js';
|
|
19
20
|
import {
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
/**
|
|
35
36
|
* Registers validation hooks for the requirement graph AST.
|
|
36
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]
|
|
37
39
|
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
38
40
|
* rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
|
|
39
41
|
* rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
|
|
@@ -53,10 +55,13 @@ export function registerValidationChecks(services: ReqlanServices) {
|
|
|
53
55
|
/**
|
|
54
56
|
* Custom validations for Reqlan documents.
|
|
55
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]
|
|
56
60
|
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
57
61
|
* rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
|
|
58
62
|
* rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
|
|
59
63
|
* rq:["../../../reqlan rq/language/syntax.rq".comment_reference_resolution_error]
|
|
64
|
+
* rq:["../../../reqlan rq/core_analysis/check.rq".check_wildcard_sparse]
|
|
60
65
|
* rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
|
|
61
66
|
*/
|
|
62
67
|
export class ReqlanValidator {
|
|
@@ -139,6 +144,8 @@ export class ReqlanValidator {
|
|
|
139
144
|
/**
|
|
140
145
|
* Duplicate check uses import *bindings* only (alias when present, otherwise the idea name).
|
|
141
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]
|
|
142
149
|
*/
|
|
143
150
|
checkDuplicateIdeaNames(model: Model, accept: ValidationAcceptor): void {
|
|
144
151
|
const seen = new Map<string, AstNode>();
|
|
@@ -148,7 +155,7 @@ export class ReqlanValidator {
|
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
for (const element of model.elements) {
|
|
151
|
-
if (!
|
|
158
|
+
if (!isIdeaDeclaration(element)) {
|
|
152
159
|
continue;
|
|
153
160
|
}
|
|
154
161
|
const name = element.name;
|
|
@@ -164,7 +171,7 @@ export class ReqlanValidator {
|
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
/**
|
|
167
|
-
* 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.
|
|
168
175
|
* Missing imported ideas are reported by the linker as unresolved references.
|
|
169
176
|
*/
|
|
170
177
|
checkImportTargets(model: Model, accept: ValidationAcceptor): void {
|
|
@@ -208,6 +215,14 @@ export class ReqlanValidator {
|
|
|
208
215
|
shared.workspace.FileSystemProvider,
|
|
209
216
|
pathResolveContextFromServices(this.services)
|
|
210
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
|
+
}
|
|
211
226
|
if (!link.targetIssue) {
|
|
212
227
|
continue;
|
|
213
228
|
}
|
|
@@ -258,6 +273,12 @@ export class ReqlanValidator {
|
|
|
258
273
|
`No ideas match wildcard reference [${wildcardReferenceLabel(path, node.ideaPattern)}].`,
|
|
259
274
|
{ node }
|
|
260
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
|
+
);
|
|
261
282
|
}
|
|
262
283
|
}
|
|
263
284
|
}
|
package/src/reqlan.langium
CHANGED
|
@@ -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 '{'
|