@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.
Files changed (43) hide show
  1. package/README.md +6 -0
  2. package/out/generated/ast.d.ts +39 -1
  3. package/out/generated/ast.js +35 -1
  4. package/out/generated/ast.js.map +1 -1
  5. package/out/generated/grammar.js +365 -110
  6. package/out/generated/grammar.js.map +1 -1
  7. package/out/reqlan-definition-provider.js +2 -2
  8. package/out/reqlan-definition-provider.js.map +1 -1
  9. package/out/reqlan-file-link-resolver.js +6 -1
  10. package/out/reqlan-file-link-resolver.js.map +1 -1
  11. package/out/reqlan-import-bindings.d.ts +6 -1
  12. package/out/reqlan-import-bindings.js +18 -1
  13. package/out/reqlan-import-bindings.js.map +1 -1
  14. package/out/reqlan-namespace-import-links.js +4 -3
  15. package/out/reqlan-namespace-import-links.js.map +1 -1
  16. package/out/reqlan-path-resolve.d.ts +1 -0
  17. package/out/reqlan-path-resolve.js +3 -0
  18. package/out/reqlan-path-resolve.js.map +1 -1
  19. package/out/reqlan-references.js +3 -2
  20. package/out/reqlan-references.js.map +1 -1
  21. package/out/reqlan-scope.js +9 -4
  22. package/out/reqlan-scope.js.map +1 -1
  23. package/out/reqlan-semantic-token-provider.js +23 -5
  24. package/out/reqlan-semantic-token-provider.js.map +1 -1
  25. package/out/reqlan-validator.d.ts +7 -0
  26. package/out/reqlan-validator.js +35 -3
  27. package/out/reqlan-validator.js.map +1 -1
  28. package/out/reqlan-workspace-attribute-catalog.js +37 -15
  29. package/out/reqlan-workspace-attribute-catalog.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/generated/ast.ts +53 -2
  32. package/src/generated/grammar.ts +365 -110
  33. package/src/reqlan-definition-provider.ts +2 -1
  34. package/src/reqlan-file-link-resolver.ts +6 -1
  35. package/src/reqlan-import-bindings.ts +26 -1
  36. package/src/reqlan-namespace-import-links.ts +4 -3
  37. package/src/reqlan-path-resolve.ts +4 -0
  38. package/src/reqlan-references.ts +3 -2
  39. package/src/reqlan-scope.ts +9 -5
  40. package/src/reqlan-semantic-token-provider.ts +22 -4
  41. package/src/reqlan-validator.ts +50 -2
  42. package/src/reqlan-workspace-attribute-catalog.ts +50 -15
  43. 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(importDecl.path, document, documents, context);
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
- importDecl.path,
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
  }
@@ -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.path;
44
+ return importPathOf(reference.qualifier.ref);
44
45
  }
45
46
  if (reference.path?.ref) {
46
- return reference.path.ref.path;
47
+ return importPathOf(reference.path.ref);
47
48
  }
48
49
  if (reference.path?.$refText) {
49
50
  return unquoteReqlanString(reference.path.$refText);
@@ -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.map(importDecl =>
206
- this.descriptions.createDescription(importDecl, importDecl.path, document)
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
- acceptor({ node, property: 'path', type: SemanticTokenTypes.string, modifier: SemanticTokenModifiers.declaration });
88
- if (!isFromImport(node) && node.alias) {
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 (!isFromImport(node) && node.alias) {
128
+ if (node.alias) {
111
129
  acceptor({ node, keyword: 'as', type: SemanticTokenTypes.keyword });
112
130
  }
113
131
  }
@@ -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 { importBindings, importedIdeaNames } from './reqlan-import-bindings.js';
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
- const path = unquoteReqlanString(importDecl.path);
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 { isAttribute, isModel, isScalarValue, type Attribute } from './generated/ast.js';
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.parts
45
- .map(part => typeof part === 'string' ? part : '')
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.$type === 'ListValue') {
51
- return attribute.value.items.flatMap(item => {
52
- if (item.$type === 'OneLinerListItem') {
53
- return item.body.content
54
- .map(part => typeof part === 'string' ? part : '')
55
- .join('')
56
- .trim();
57
- }
58
- return [];
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
+ }
@@ -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 'import' specifiers+=FromImportSpecifier (',' specifiers+=FromImportSpecifier)*;
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
  // ---------------------------------------------------------------------------