@reqlan/language 1.4.1 → 1.5.0
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 +6 -0
- package/out/generated/ast.d.ts +39 -1
- package/out/generated/ast.js +35 -1
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +365 -110
- package/out/generated/grammar.js.map +1 -1
- package/out/reqlan-definition-provider.js +2 -2
- package/out/reqlan-definition-provider.js.map +1 -1
- package/out/reqlan-file-link-resolver.js +6 -1
- package/out/reqlan-file-link-resolver.js.map +1 -1
- package/out/reqlan-import-bindings.d.ts +6 -1
- package/out/reqlan-import-bindings.js +18 -1
- package/out/reqlan-import-bindings.js.map +1 -1
- package/out/reqlan-namespace-import-links.js +4 -3
- package/out/reqlan-namespace-import-links.js.map +1 -1
- package/out/reqlan-path-resolve.d.ts +1 -0
- package/out/reqlan-path-resolve.js +3 -0
- package/out/reqlan-path-resolve.js.map +1 -1
- package/out/reqlan-references.js +3 -2
- package/out/reqlan-references.js.map +1 -1
- package/out/reqlan-scope.js +9 -4
- package/out/reqlan-scope.js.map +1 -1
- package/out/reqlan-semantic-token-provider.js +23 -5
- package/out/reqlan-semantic-token-provider.js.map +1 -1
- package/out/reqlan-validator.d.ts +7 -0
- package/out/reqlan-validator.js +35 -3
- package/out/reqlan-validator.js.map +1 -1
- package/out/reqlan-workspace-attribute-catalog.js +37 -15
- package/out/reqlan-workspace-attribute-catalog.js.map +1 -1
- package/package.json +1 -1
- package/src/generated/ast.ts +53 -2
- package/src/generated/grammar.ts +365 -110
- package/src/reqlan-definition-provider.ts +2 -1
- package/src/reqlan-file-link-resolver.ts +6 -1
- package/src/reqlan-import-bindings.ts +26 -1
- package/src/reqlan-namespace-import-links.ts +4 -3
- package/src/reqlan-path-resolve.ts +4 -0
- package/src/reqlan-references.ts +3 -2
- package/src/reqlan-scope.ts +9 -5
- package/src/reqlan-semantic-token-provider.ts +22 -4
- package/src/reqlan-validator.ts +50 -2
- package/src/reqlan-workspace-attribute-catalog.ts +50 -15
- package/src/reqlan.langium +18 -2
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
isFileSymbolReference,
|
|
31
31
|
isFromImport,
|
|
32
32
|
isImport,
|
|
33
|
+
isInvalidFromImport,
|
|
33
34
|
isLocalReference,
|
|
34
35
|
isQualifiedReference,
|
|
35
36
|
type FileReference,
|
|
@@ -308,7 +309,7 @@ export class ReqlanDefinitionProvider extends DefaultDefinitionProvider {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
function isImportPathTarget(node: unknown, targetCst: CstNode): node is Import {
|
|
311
|
-
if (!isImport(node)) {
|
|
312
|
+
if (!isImport(node) || isInvalidFromImport(node)) {
|
|
312
313
|
return false;
|
|
313
314
|
}
|
|
314
315
|
if (isFromImport(node) || !node.alias) {
|
|
@@ -7,6 +7,7 @@ import type { Range } from 'vscode-languageserver';
|
|
|
7
7
|
import { resolveFileUri } from './reqlan-comment-resolver.js';
|
|
8
8
|
import type { EmbeddedFileReference } from './reqlan-embedded-file-references.js';
|
|
9
9
|
import { findEmbeddedFileReferencesInText } from './reqlan-embedded-file-references.js';
|
|
10
|
+
import { importPathOf } from './reqlan-import-bindings.js';
|
|
10
11
|
import { findImportedDocument, resolveExistingImportUri } from './reqlan-imports.js';
|
|
11
12
|
import type { PathResolveContext } from './reqlan-path-resolve.js';
|
|
12
13
|
import { qualifiedReferenceImportPath } from './reqlan-references.js';
|
|
@@ -100,12 +101,16 @@ export function resolveImportPathLink(
|
|
|
100
101
|
documents: LangiumDocuments,
|
|
101
102
|
context?: PathResolveContext
|
|
102
103
|
): ResolvedFileLink | undefined {
|
|
104
|
+
const path = importPathOf(importDecl);
|
|
105
|
+
if (!path) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
103
108
|
const document = AstUtils.getDocument(importDecl);
|
|
104
109
|
const pathNode = GrammarUtils.findNodeForProperty(importDecl.$cstNode, 'path');
|
|
105
110
|
if (!pathNode) {
|
|
106
111
|
return undefined;
|
|
107
112
|
}
|
|
108
|
-
const imported = findImportedDocument(
|
|
113
|
+
const imported = findImportedDocument(path, document, documents, context);
|
|
109
114
|
const target = imported?.parseResult.value.$cstNode;
|
|
110
115
|
if (!imported || !target) {
|
|
111
116
|
return undefined;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AstNode } from 'langium';
|
|
2
2
|
import {
|
|
3
3
|
isFromImport,
|
|
4
|
+
isInvalidFromImport,
|
|
4
5
|
isNamespaceImport,
|
|
5
6
|
isQualifiedImport,
|
|
7
|
+
type FromImport,
|
|
6
8
|
type FromImportSpecifier,
|
|
7
|
-
type Import
|
|
9
|
+
type Import,
|
|
10
|
+
type InvalidFromImport
|
|
8
11
|
} from './generated/ast.js';
|
|
9
12
|
|
|
10
13
|
export interface ImportBinding {
|
|
@@ -13,12 +16,31 @@ export interface ImportBinding {
|
|
|
13
16
|
property: 'alias' | 'idea';
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
export type PathBearingImport = Exclude<Import, InvalidFromImport>;
|
|
20
|
+
|
|
16
21
|
export function specifierBindingName(specifier: FromImportSpecifier): string | undefined {
|
|
17
22
|
return specifier.alias ?? specifier.idea.$refText;
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
/** True when a from-import has the required `import <idea>` clause. */
|
|
26
|
+
export function isWellFormedFromImport(importDecl: FromImport): boolean {
|
|
27
|
+
return importDecl.specifiers.length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isPathBearingImport(importDecl: Import): importDecl is PathBearingImport {
|
|
31
|
+
return !isInvalidFromImport(importDecl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function importPathOf(importDecl: Import): string | undefined {
|
|
35
|
+
return isPathBearingImport(importDecl) ? importDecl.path : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export function importBindings(importDecl: Import): ImportBinding[] {
|
|
21
39
|
if (isFromImport(importDecl)) {
|
|
40
|
+
// Mistaken `from "path" as alias` (no idea specifiers) must not bind a name.
|
|
41
|
+
if (!isWellFormedFromImport(importDecl)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
22
44
|
return importDecl.specifiers.flatMap(specifier => {
|
|
23
45
|
const name = specifierBindingName(specifier);
|
|
24
46
|
if (!name) {
|
|
@@ -31,6 +53,9 @@ export function importBindings(importDecl: Import): ImportBinding[] {
|
|
|
31
53
|
}];
|
|
32
54
|
});
|
|
33
55
|
}
|
|
56
|
+
if (isInvalidFromImport(importDecl)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
34
59
|
if (isQualifiedImport(importDecl)) {
|
|
35
60
|
const name = importDecl.alias ?? importDecl.idea.$refText;
|
|
36
61
|
if (!name) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { CstNode, FileSystemProvider, LangiumDocuments } from 'langium';
|
|
5
5
|
import { AstUtils, GrammarUtils } from 'langium';
|
|
6
6
|
import { isIdea, isModel, isOneLinerIdea, type Import, type LocalReference, type QualifiedReference } from './generated/ast.js';
|
|
7
|
-
import { findNamespaceImportByAlias } from './reqlan-import-bindings.js';
|
|
7
|
+
import { findNamespaceImportByAlias, importPathOf } from './reqlan-import-bindings.js';
|
|
8
8
|
import { bindingNameSourceRange, resolveImportedFileLink, type ResolvedFileLink } from './reqlan-file-link-resolver.js';
|
|
9
9
|
import type { PathResolveContext } from './reqlan-path-resolve.js';
|
|
10
10
|
|
|
@@ -62,15 +62,16 @@ export function resolveNamespaceImportReferenceLink(
|
|
|
62
62
|
if (!importDecl || !isNamespaceImportBinding(importDecl, bindingName)) {
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
+
const path = importPathOf(importDecl);
|
|
65
66
|
const pathNode = referencePathNode(reference, importDecl);
|
|
66
|
-
if (!pathNode) {
|
|
67
|
+
if (!path || !pathNode) {
|
|
67
68
|
return undefined;
|
|
68
69
|
}
|
|
69
70
|
return resolveImportedFileLink(
|
|
70
71
|
document,
|
|
71
72
|
documents,
|
|
72
73
|
fileSystem,
|
|
73
|
-
|
|
74
|
+
path,
|
|
74
75
|
bindingNameSourceRange(pathNode, bindingName),
|
|
75
76
|
context
|
|
76
77
|
);
|
|
@@ -30,6 +30,7 @@ export interface RqHtmlExportConfig {
|
|
|
30
30
|
includeFilePages?: boolean;
|
|
31
31
|
includeCodeFilePages?: boolean;
|
|
32
32
|
includeClusterPages?: boolean;
|
|
33
|
+
includeAttributePages?: boolean;
|
|
33
34
|
includePrintPages?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -253,6 +254,9 @@ function parseHtmlExportConfig(raw: unknown): RqHtmlExportConfig | undefined {
|
|
|
253
254
|
if (typeof record.includeClusterPages === 'boolean') {
|
|
254
255
|
config.includeClusterPages = record.includeClusterPages;
|
|
255
256
|
}
|
|
257
|
+
if (typeof record.includeAttributePages === 'boolean') {
|
|
258
|
+
config.includeAttributePages = record.includeAttributePages;
|
|
259
|
+
}
|
|
256
260
|
if (typeof record.includePrintPages === 'boolean') {
|
|
257
261
|
config.includePrintPages = record.includePrintPages;
|
|
258
262
|
}
|
package/src/reqlan-references.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type QualifiedReference,
|
|
13
13
|
type ReferenceTarget
|
|
14
14
|
} from './generated/ast.js';
|
|
15
|
+
import { importPathOf } from './reqlan-import-bindings.js';
|
|
15
16
|
import { unquoteReqlanString } from './reqlan-quoted-strings.js';
|
|
16
17
|
|
|
17
18
|
export function referenceImport(target: ReferenceTarget): Reference<Import> | undefined {
|
|
@@ -40,10 +41,10 @@ export function referenceFilePath(target: ReferenceTarget): string | undefined {
|
|
|
40
41
|
|
|
41
42
|
export function qualifiedReferenceImportPath(reference: QualifiedReference): string | undefined {
|
|
42
43
|
if (reference.qualifier?.ref) {
|
|
43
|
-
return reference.qualifier.ref
|
|
44
|
+
return importPathOf(reference.qualifier.ref);
|
|
44
45
|
}
|
|
45
46
|
if (reference.path?.ref) {
|
|
46
|
-
return reference.path.ref
|
|
47
|
+
return importPathOf(reference.path.ref);
|
|
47
48
|
}
|
|
48
49
|
if (reference.path?.$refText) {
|
|
49
50
|
return unquoteReqlanString(reference.path.$refText);
|
package/src/reqlan-scope.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isFromImportSpecifier,
|
|
9
9
|
isIdea,
|
|
10
10
|
isIdeaSet,
|
|
11
|
+
isInvalidFromImport,
|
|
11
12
|
isLocalReference,
|
|
12
13
|
isModel,
|
|
13
14
|
isNamespaceImport,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
type Model,
|
|
21
22
|
type QualifiedReference
|
|
22
23
|
} from './generated/ast.js';
|
|
23
|
-
import { findFromImportSpecifierByBinding, specifierBindingName } from './reqlan-import-bindings.js';
|
|
24
|
+
import { findFromImportSpecifierByBinding, importPathOf, specifierBindingName } from './reqlan-import-bindings.js';
|
|
24
25
|
import { findImportedDocument } from './reqlan-imports.js';
|
|
25
26
|
import type { ReqlanServices } from './reqlan-module.js';
|
|
26
27
|
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
@@ -202,9 +203,12 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
|
202
203
|
if (!isModel(model)) {
|
|
203
204
|
return new StreamScope(stream([]));
|
|
204
205
|
}
|
|
205
|
-
const descriptions = model.imports.
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
const descriptions = model.imports.flatMap(importDecl => {
|
|
207
|
+
const path = importPathOf(importDecl);
|
|
208
|
+
return path
|
|
209
|
+
? [this.descriptions.createDescription(importDecl, path, document)]
|
|
210
|
+
: [];
|
|
211
|
+
});
|
|
208
212
|
return new StreamScope(stream(descriptions));
|
|
209
213
|
}
|
|
210
214
|
|
|
@@ -305,7 +309,7 @@ export class ReqlanScopeProvider extends DefaultScopeProvider {
|
|
|
305
309
|
}
|
|
306
310
|
|
|
307
311
|
function importAlias(entry: Import): string | undefined {
|
|
308
|
-
if (isFromImport(entry)) {
|
|
312
|
+
if (isFromImport(entry) || isInvalidFromImport(entry)) {
|
|
309
313
|
return undefined;
|
|
310
314
|
}
|
|
311
315
|
return entry.alias;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isIdea,
|
|
15
15
|
isIdeaSet,
|
|
16
16
|
isImport,
|
|
17
|
+
isInvalidFromImport,
|
|
17
18
|
isLocalReference,
|
|
18
19
|
isMarkdownLink,
|
|
19
20
|
isQualifiedImport,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
isOneLinerIdea,
|
|
22
23
|
type Import
|
|
23
24
|
} from './generated/ast.js';
|
|
25
|
+
import { isWellFormedFromImport } from './reqlan-import-bindings.js';
|
|
24
26
|
|
|
25
27
|
export class ReqlanSemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
26
28
|
|
|
@@ -84,8 +86,12 @@ export class ReqlanSemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
84
86
|
}
|
|
85
87
|
if (isImport(node)) {
|
|
86
88
|
highlightImportKeywords(node, acceptor);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
if (!isInvalidFromImport(node)) {
|
|
90
|
+
acceptor({ node, property: 'path', type: SemanticTokenTypes.string, modifier: SemanticTokenModifiers.declaration });
|
|
91
|
+
}
|
|
92
|
+
if (isFromImport(node) && node.alias) {
|
|
93
|
+
acceptor({ node, property: 'alias', type: SemanticTokenTypes.namespace });
|
|
94
|
+
} else if (!isFromImport(node) && !isInvalidFromImport(node) && node.alias) {
|
|
89
95
|
acceptor({ node, property: 'alias', type: SemanticTokenTypes.namespace, modifier: SemanticTokenModifiers.declaration });
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -103,11 +109,23 @@ export class ReqlanSemanticTokenProvider extends AbstractSemanticTokenProvider {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
function highlightImportKeywords(node: Import, acceptor: SemanticTokenAcceptor): void {
|
|
106
|
-
if (isFromImport(node)) {
|
|
112
|
+
if (isFromImport(node) || isInvalidFromImport(node)) {
|
|
107
113
|
acceptor({ node, keyword: 'from', type: SemanticTokenTypes.keyword });
|
|
108
114
|
}
|
|
115
|
+
if (isFromImport(node)) {
|
|
116
|
+
if (isWellFormedFromImport(node)) {
|
|
117
|
+
acceptor({ node, keyword: 'import', type: SemanticTokenTypes.keyword });
|
|
118
|
+
}
|
|
119
|
+
if (node.alias) {
|
|
120
|
+
acceptor({ node, keyword: 'as', type: SemanticTokenTypes.keyword });
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isInvalidFromImport(node)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
109
127
|
acceptor({ node, keyword: 'import', type: SemanticTokenTypes.keyword });
|
|
110
|
-
if (
|
|
128
|
+
if (node.alias) {
|
|
111
129
|
acceptor({ node, keyword: 'as', type: SemanticTokenTypes.keyword });
|
|
112
130
|
}
|
|
113
131
|
}
|
package/src/reqlan-validator.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { AstNode, ValidationAcceptor, ValidationChecks } from 'langium';
|
|
|
2
2
|
import { AstUtils } from 'langium';
|
|
3
3
|
import type { ReqlanAstType } from './generated/ast.js';
|
|
4
4
|
import {
|
|
5
|
+
isFromImport,
|
|
5
6
|
isIdea,
|
|
7
|
+
isInvalidFromImport,
|
|
6
8
|
isOneLinerIdea,
|
|
7
9
|
type Model
|
|
8
10
|
} from './generated/ast.js';
|
|
@@ -10,7 +12,12 @@ import {
|
|
|
10
12
|
collectFileLinks,
|
|
11
13
|
fileLinkTargetIssueMessage
|
|
12
14
|
} from './reqlan-file-link-resolver.js';
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
importBindings,
|
|
17
|
+
importPathOf,
|
|
18
|
+
importedIdeaNames,
|
|
19
|
+
isWellFormedFromImport
|
|
20
|
+
} from './reqlan-import-bindings.js';
|
|
14
21
|
import { isResolvableImportPath } from './reqlan-imports.js';
|
|
15
22
|
import type { ReqlanServices } from './reqlan-module.js';
|
|
16
23
|
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
@@ -19,6 +26,7 @@ import { unquoteReqlanString } from './reqlan-quoted-strings.js';
|
|
|
19
26
|
/**
|
|
20
27
|
* Registers validation hooks for the requirement graph AST.
|
|
21
28
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
29
|
+
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
22
30
|
*/
|
|
23
31
|
export function registerValidationChecks(services: ReqlanServices) {
|
|
24
32
|
const registry = services.validation.ValidationRegistry;
|
|
@@ -32,6 +40,7 @@ export function registerValidationChecks(services: ReqlanServices) {
|
|
|
32
40
|
/**
|
|
33
41
|
* Custom validations for Reqlan documents.
|
|
34
42
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
43
|
+
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
35
44
|
*/
|
|
36
45
|
export class ReqlanValidator {
|
|
37
46
|
|
|
@@ -40,10 +49,42 @@ export class ReqlanValidator {
|
|
|
40
49
|
checkModelDuplicates(model: Model, accept: ValidationAcceptor): void {
|
|
41
50
|
this.checkDuplicateImportBindings(model, accept);
|
|
42
51
|
this.checkDuplicateIdeaNames(model, accept);
|
|
52
|
+
this.checkImportSyntax(model, accept);
|
|
43
53
|
this.checkImportTargets(model, accept);
|
|
44
54
|
this.checkFileReferenceTargets(model, accept);
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Local diagnostics for recoverable but invalid import shapes.
|
|
59
|
+
* Keeps the rest of the file parseable — see import_error_recovery.
|
|
60
|
+
*/
|
|
61
|
+
checkImportSyntax(model: Model, accept: ValidationAcceptor): void {
|
|
62
|
+
for (const importDecl of model.imports) {
|
|
63
|
+
if (isInvalidFromImport(importDecl)) {
|
|
64
|
+
accept('error', 'Invalid from-import: expected a quoted path, then `import <idea>`.', {
|
|
65
|
+
node: importDecl
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!isFromImport(importDecl) || isWellFormedFromImport(importDecl)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (importDecl.alias) {
|
|
73
|
+
accept(
|
|
74
|
+
'error',
|
|
75
|
+
`Invalid syntax: use \`import "${unquoteReqlanString(importDecl.path)}" as ${importDecl.alias}\` for a namespace import, or \`from "${unquoteReqlanString(importDecl.path)}" import <idea>\`.`,
|
|
76
|
+
{ node: importDecl }
|
|
77
|
+
);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
accept(
|
|
81
|
+
'error',
|
|
82
|
+
`Invalid from-import: expected \`from "${unquoteReqlanString(importDecl.path)}" import <idea>\`.`,
|
|
83
|
+
{ node: importDecl }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
checkDuplicateImportBindings(model: Model, accept: ValidationAcceptor): void {
|
|
48
89
|
const seen = new Map<string, ImportBindingSource>();
|
|
49
90
|
for (const importDecl of model.imports) {
|
|
@@ -102,7 +143,14 @@ export class ReqlanValidator {
|
|
|
102
143
|
const { shared } = this.services;
|
|
103
144
|
const pathContext = pathResolveContextFromServices(this.services);
|
|
104
145
|
for (const importDecl of model.imports) {
|
|
105
|
-
|
|
146
|
+
if (isInvalidFromImport(importDecl)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (isFromImport(importDecl) && !isWellFormedFromImport(importDecl)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const rawPath = importPathOf(importDecl);
|
|
153
|
+
const path = rawPath ? unquoteReqlanString(rawPath) : undefined;
|
|
106
154
|
if (!path || isRemoteImportPath(path)) {
|
|
107
155
|
continue;
|
|
108
156
|
}
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { LangiumDocuments } from 'langium';
|
|
5
5
|
import { AstUtils } from 'langium';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isAttribute,
|
|
8
|
+
isBlockValue,
|
|
9
|
+
isListValue,
|
|
10
|
+
isModel,
|
|
11
|
+
isScalarValue,
|
|
12
|
+
type Attribute
|
|
13
|
+
} from './generated/ast.js';
|
|
7
14
|
import type { AttributeCatalog } from './reqlan-attribute-catalog.js';
|
|
8
15
|
|
|
9
16
|
export function collectWorkspaceAttributeCatalog(documents: LangiumDocuments): AttributeCatalog {
|
|
@@ -41,22 +48,50 @@ function valuesForAttribute(attribute: Attribute): string[] {
|
|
|
41
48
|
return [];
|
|
42
49
|
}
|
|
43
50
|
if (isScalarValue(attribute.value)) {
|
|
44
|
-
const text = attribute.value
|
|
45
|
-
.
|
|
46
|
-
.join('')
|
|
47
|
-
.trim();
|
|
51
|
+
const text = normalizeCatalogText(attribute.value.$cstNode?.text)
|
|
52
|
+
|| inlineCatalogParts(attribute.value.parts);
|
|
48
53
|
return text ? [text] : [];
|
|
49
54
|
}
|
|
50
|
-
if (attribute.value
|
|
51
|
-
return attribute.value.items
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}).filter(Boolean);
|
|
55
|
+
if (isListValue(attribute.value)) {
|
|
56
|
+
return attribute.value.items
|
|
57
|
+
.map(item => normalizeCatalogText(item.$cstNode?.text))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
if (isBlockValue(attribute.value)) {
|
|
61
|
+
const raw = attribute.value.$cstNode?.text ?? '';
|
|
62
|
+
const inner = raw.replace(/^\{\s*/, '').replace(/\s*\}$/, '').trim();
|
|
63
|
+
return inner ? [inner.replace(/\s+/g, ' ')] : [];
|
|
60
64
|
}
|
|
61
65
|
return [];
|
|
62
66
|
}
|
|
67
|
+
|
|
68
|
+
function normalizeCatalogText(text: string | undefined): string {
|
|
69
|
+
return text?.replace(/\s+/g, ' ').trim() ?? '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inlineCatalogParts(parts: ReadonlyArray<unknown>): string {
|
|
73
|
+
return parts
|
|
74
|
+
.map(part => {
|
|
75
|
+
if (typeof part === 'string') {
|
|
76
|
+
return part;
|
|
77
|
+
}
|
|
78
|
+
if (!part || typeof part !== 'object') {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
const node = part as {
|
|
82
|
+
text?: string;
|
|
83
|
+
inlineCode?: string;
|
|
84
|
+
$cstNode?: { text?: string };
|
|
85
|
+
};
|
|
86
|
+
if (typeof node.text === 'string' && node.text.length > 0) {
|
|
87
|
+
return node.text;
|
|
88
|
+
}
|
|
89
|
+
if (typeof node.inlineCode === 'string' && node.inlineCode.length > 0) {
|
|
90
|
+
return node.inlineCode;
|
|
91
|
+
}
|
|
92
|
+
return node.$cstNode?.text ?? '';
|
|
93
|
+
})
|
|
94
|
+
.join('')
|
|
95
|
+
.replace(/\s+/g, ' ')
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
package/src/reqlan.langium
CHANGED
|
@@ -200,13 +200,19 @@ LocalReference:
|
|
|
200
200
|
|
|
201
201
|
// ---------------------------------------------------------------------------
|
|
202
202
|
// import_from, import_namespace, import_qualified — see imports.rq
|
|
203
|
+
// InvalidFromImport recovers unquoted/malformed `from` lines so later ideas still parse.
|
|
204
|
+
// FromImport allows a mistaken `from "path" as alias` shape for a local diagnostic
|
|
205
|
+
// (valid form still requires `import` + idea specifiers).
|
|
203
206
|
// ---------------------------------------------------------------------------
|
|
204
207
|
|
|
205
208
|
Import:
|
|
206
|
-
FromImport | NamespaceImport | QualifiedImport;
|
|
209
|
+
FromImport | NamespaceImport | QualifiedImport | InvalidFromImport;
|
|
207
210
|
|
|
208
211
|
FromImport:
|
|
209
|
-
'from' path=STRING
|
|
212
|
+
'from' path=STRING (
|
|
213
|
+
'import' specifiers+=FromImportSpecifier (',' specifiers+=FromImportSpecifier)*
|
|
214
|
+
| 'as' alias=ID
|
|
215
|
+
)?;
|
|
210
216
|
|
|
211
217
|
FromImportSpecifier:
|
|
212
218
|
idea=[IdeaDeclaration:ID] ('as' alias=ID)?;
|
|
@@ -217,6 +223,16 @@ NamespaceImport:
|
|
|
217
223
|
QualifiedImport:
|
|
218
224
|
'import' path=STRING '.' ideaset=ID '.' idea=[IdeaDeclaration:ID] ('as' alias=ID)?;
|
|
219
225
|
|
|
226
|
+
// `from` not followed by a quoted path — consume the rest of the line.
|
|
227
|
+
InvalidFromImport:
|
|
228
|
+
'from' head=InvalidImportHeadToken tokens+=InvalidImportToken*;
|
|
229
|
+
|
|
230
|
+
InvalidImportHeadToken returns string:
|
|
231
|
+
ID | WORD | NUMBER | OTHER | PlainPunctuation | '(' | ')' | '|' | 'as' | 'import' | 'from' | '`' | '[' | ']';
|
|
232
|
+
|
|
233
|
+
InvalidImportToken returns string:
|
|
234
|
+
ID | STRING | WORD | NUMBER | OTHER | PlainPunctuation | '(' | ')' | '|' | 'as' | 'import' | 'from' | '`' | '[' | ']';
|
|
235
|
+
|
|
220
236
|
// ---------------------------------------------------------------------------
|
|
221
237
|
// comments, string_literals
|
|
222
238
|
// ---------------------------------------------------------------------------
|