@reqlan/language 1.4.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 +24 -0
- package/out/file-path-rewrite.d.ts +25 -0
- package/out/file-path-rewrite.js +56 -0
- package/out/file-path-rewrite.js.map +1 -0
- package/out/generated/ast.d.ts +1157 -0
- package/out/generated/ast.js +980 -0
- package/out/generated/ast.js.map +1 -0
- package/out/generated/grammar.d.ts +7 -0
- package/out/generated/grammar.js +3071 -0
- package/out/generated/grammar.js.map +1 -0
- package/out/generated/module.d.ts +20 -0
- package/out/generated/module.js +32 -0
- package/out/generated/module.js.map +1 -0
- package/out/index.d.ts +34 -0
- package/out/index.js +35 -0
- package/out/index.js.map +1 -0
- package/out/reqlan-attribute-catalog.d.ts +19 -0
- package/out/reqlan-attribute-catalog.js +87 -0
- package/out/reqlan-attribute-catalog.js.map +1 -0
- package/out/reqlan-code-action-provider.d.ts +55 -0
- package/out/reqlan-code-action-provider.js +269 -0
- package/out/reqlan-code-action-provider.js.map +1 -0
- package/out/reqlan-code-lens-provider.d.ts +13 -0
- package/out/reqlan-code-lens-provider.js +27 -0
- package/out/reqlan-code-lens-provider.js.map +1 -0
- package/out/reqlan-code-lens-settings.d.ts +34 -0
- package/out/reqlan-code-lens-settings.js +12 -0
- package/out/reqlan-code-lens-settings.js.map +1 -0
- package/out/reqlan-comment-resolver.d.ts +35 -0
- package/out/reqlan-comment-resolver.js +308 -0
- package/out/reqlan-comment-resolver.js.map +1 -0
- package/out/reqlan-completion-context.d.ts +46 -0
- package/out/reqlan-completion-context.js +244 -0
- package/out/reqlan-completion-context.js.map +1 -0
- package/out/reqlan-completion-provider.d.ts +41 -0
- package/out/reqlan-completion-provider.js +317 -0
- package/out/reqlan-completion-provider.js.map +1 -0
- package/out/reqlan-definition-provider.d.ts +31 -0
- package/out/reqlan-definition-provider.js +240 -0
- package/out/reqlan-definition-provider.js.map +1 -0
- package/out/reqlan-document-highlight-provider.d.ts +10 -0
- package/out/reqlan-document-highlight-provider.js +11 -0
- package/out/reqlan-document-highlight-provider.js.map +1 -0
- package/out/reqlan-document-link-provider.d.ts +14 -0
- package/out/reqlan-document-link-provider.js +24 -0
- package/out/reqlan-document-link-provider.js.map +1 -0
- package/out/reqlan-embedded-file-references.d.ts +11 -0
- package/out/reqlan-embedded-file-references.js +50 -0
- package/out/reqlan-embedded-file-references.js.map +1 -0
- package/out/reqlan-file-link-resolver.d.ts +44 -0
- package/out/reqlan-file-link-resolver.js +347 -0
- package/out/reqlan-file-link-resolver.js.map +1 -0
- package/out/reqlan-file-references.d.ts +15 -0
- package/out/reqlan-file-references.js +72 -0
- package/out/reqlan-file-references.js.map +1 -0
- package/out/reqlan-grammar-config.d.ts +5 -0
- package/out/reqlan-grammar-config.js +9 -0
- package/out/reqlan-grammar-config.js.map +1 -0
- package/out/reqlan-idea-reference-site.d.ts +14 -0
- package/out/reqlan-idea-reference-site.js +47 -0
- package/out/reqlan-idea-reference-site.js.map +1 -0
- package/out/reqlan-idea-summary.d.ts +6 -0
- package/out/reqlan-idea-summary.js +134 -0
- package/out/reqlan-idea-summary.js.map +1 -0
- package/out/reqlan-ignore-error.d.ts +10 -0
- package/out/reqlan-ignore-error.js +42 -0
- package/out/reqlan-ignore-error.js.map +1 -0
- package/out/reqlan-import-bindings.d.ts +12 -0
- package/out/reqlan-import-bindings.js +65 -0
- package/out/reqlan-import-bindings.js.map +1 -0
- package/out/reqlan-import-edits.d.ts +17 -0
- package/out/reqlan-import-edits.js +96 -0
- package/out/reqlan-import-edits.js.map +1 -0
- package/out/reqlan-imports.d.ts +22 -0
- package/out/reqlan-imports.js +61 -0
- package/out/reqlan-imports.js.map +1 -0
- package/out/reqlan-inbound-reference-inlay-label.d.ts +27 -0
- package/out/reqlan-inbound-reference-inlay-label.js +142 -0
- package/out/reqlan-inbound-reference-inlay-label.js.map +1 -0
- package/out/reqlan-inlay-hint-provider.d.ts +15 -0
- package/out/reqlan-inlay-hint-provider.js +54 -0
- package/out/reqlan-inlay-hint-provider.js.map +1 -0
- package/out/reqlan-inlay-hint-settings.d.ts +10 -0
- package/out/reqlan-inlay-hint-settings.js +10 -0
- package/out/reqlan-inlay-hint-settings.js.map +1 -0
- package/out/reqlan-linker.d.ts +26 -0
- package/out/reqlan-linker.js +83 -0
- package/out/reqlan-linker.js.map +1 -0
- package/out/reqlan-markdown-links.d.ts +28 -0
- package/out/reqlan-markdown-links.js +113 -0
- package/out/reqlan-markdown-links.js.map +1 -0
- package/out/reqlan-module.d.ts +41 -0
- package/out/reqlan-module.js +78 -0
- package/out/reqlan-module.js.map +1 -0
- package/out/reqlan-name-catalog.d.ts +23 -0
- package/out/reqlan-name-catalog.js +63 -0
- package/out/reqlan-name-catalog.js.map +1 -0
- package/out/reqlan-name-provider.d.ts +10 -0
- package/out/reqlan-name-provider.js +38 -0
- package/out/reqlan-name-provider.js.map +1 -0
- package/out/reqlan-namespace-import-links.d.ts +10 -0
- package/out/reqlan-namespace-import-links.js +69 -0
- package/out/reqlan-namespace-import-links.js.map +1 -0
- package/out/reqlan-path-completion.d.ts +50 -0
- package/out/reqlan-path-completion.js +304 -0
- package/out/reqlan-path-completion.js.map +1 -0
- package/out/reqlan-path-references.d.ts +5 -0
- package/out/reqlan-path-references.js +124 -0
- package/out/reqlan-path-references.js.map +1 -0
- package/out/reqlan-path-resolve.d.ts +81 -0
- package/out/reqlan-path-resolve.js +245 -0
- package/out/reqlan-path-resolve.js.map +1 -0
- package/out/reqlan-quoted-strings.d.ts +9 -0
- package/out/reqlan-quoted-strings.js +26 -0
- package/out/reqlan-quoted-strings.js.map +1 -0
- package/out/reqlan-reference-at-position.d.ts +20 -0
- package/out/reqlan-reference-at-position.js +89 -0
- package/out/reqlan-reference-at-position.js.map +1 -0
- package/out/reqlan-reference-code-lens.d.ts +32 -0
- package/out/reqlan-reference-code-lens.js +240 -0
- package/out/reqlan-reference-code-lens.js.map +1 -0
- package/out/reqlan-reference-search-site.d.ts +34 -0
- package/out/reqlan-reference-search-site.js +136 -0
- package/out/reqlan-reference-search-site.js.map +1 -0
- package/out/reqlan-references.d.ts +16 -0
- package/out/reqlan-references.js +51 -0
- package/out/reqlan-references.js.map +1 -0
- package/out/reqlan-scope.d.ts +25 -0
- package/out/reqlan-scope.js +259 -0
- package/out/reqlan-scope.js.map +1 -0
- package/out/reqlan-semantic-token-provider.d.ts +8 -0
- package/out/reqlan-semantic-token-provider.js +91 -0
- package/out/reqlan-semantic-token-provider.js.map +1 -0
- package/out/reqlan-token-builder.d.ts +12 -0
- package/out/reqlan-token-builder.js +459 -0
- package/out/reqlan-token-builder.js.map +1 -0
- package/out/reqlan-validator.d.ts +25 -0
- package/out/reqlan-validator.js +119 -0
- package/out/reqlan-validator.js.map +1 -0
- package/out/reqlan-workspace-attribute-catalog.d.ts +6 -0
- package/out/reqlan-workspace-attribute-catalog.js +54 -0
- package/out/reqlan-workspace-attribute-catalog.js.map +1 -0
- package/out/src/file-path-rewrite.d.ts +25 -0
- package/out/src/file-path-rewrite.js +56 -0
- package/out/src/file-path-rewrite.js.map +1 -0
- package/out/src/generated/ast.d.ts +1173 -0
- package/out/src/generated/ast.js +994 -0
- package/out/src/generated/ast.js.map +1 -0
- package/out/src/generated/grammar.d.ts +7 -0
- package/out/src/generated/grammar.js +3045 -0
- package/out/src/generated/grammar.js.map +1 -0
- package/out/src/generated/module.d.ts +20 -0
- package/out/src/generated/module.js +32 -0
- package/out/src/generated/module.js.map +1 -0
- package/out/src/index.d.ts +29 -0
- package/out/src/index.js +30 -0
- package/out/src/index.js.map +1 -0
- package/out/src/reqlan-attribute-catalog.d.ts +19 -0
- package/out/src/reqlan-attribute-catalog.js +87 -0
- package/out/src/reqlan-attribute-catalog.js.map +1 -0
- package/out/src/reqlan-code-action-provider.d.ts +34 -0
- package/out/src/reqlan-code-action-provider.js +206 -0
- package/out/src/reqlan-code-action-provider.js.map +1 -0
- package/out/src/reqlan-comment-resolver.d.ts +35 -0
- package/out/src/reqlan-comment-resolver.js +308 -0
- package/out/src/reqlan-comment-resolver.js.map +1 -0
- package/out/src/reqlan-completion-context.d.ts +46 -0
- package/out/src/reqlan-completion-context.js +244 -0
- package/out/src/reqlan-completion-context.js.map +1 -0
- package/out/src/reqlan-completion-provider.d.ts +41 -0
- package/out/src/reqlan-completion-provider.js +320 -0
- package/out/src/reqlan-completion-provider.js.map +1 -0
- package/out/src/reqlan-definition-provider.d.ts +31 -0
- package/out/src/reqlan-definition-provider.js +240 -0
- package/out/src/reqlan-definition-provider.js.map +1 -0
- package/out/src/reqlan-document-highlight-provider.d.ts +10 -0
- package/out/src/reqlan-document-highlight-provider.js +11 -0
- package/out/src/reqlan-document-highlight-provider.js.map +1 -0
- package/out/src/reqlan-document-link-provider.d.ts +14 -0
- package/out/src/reqlan-document-link-provider.js +24 -0
- package/out/src/reqlan-document-link-provider.js.map +1 -0
- package/out/src/reqlan-embedded-file-references.d.ts +11 -0
- package/out/src/reqlan-embedded-file-references.js +50 -0
- package/out/src/reqlan-embedded-file-references.js.map +1 -0
- package/out/src/reqlan-file-link-resolver.d.ts +44 -0
- package/out/src/reqlan-file-link-resolver.js +347 -0
- package/out/src/reqlan-file-link-resolver.js.map +1 -0
- package/out/src/reqlan-file-references.d.ts +15 -0
- package/out/src/reqlan-file-references.js +72 -0
- package/out/src/reqlan-file-references.js.map +1 -0
- package/out/src/reqlan-grammar-config.d.ts +5 -0
- package/out/src/reqlan-grammar-config.js +9 -0
- package/out/src/reqlan-grammar-config.js.map +1 -0
- package/out/src/reqlan-idea-summary.d.ts +6 -0
- package/out/src/reqlan-idea-summary.js +78 -0
- package/out/src/reqlan-idea-summary.js.map +1 -0
- package/out/src/reqlan-ignore-error.d.ts +10 -0
- package/out/src/reqlan-ignore-error.js +42 -0
- package/out/src/reqlan-ignore-error.js.map +1 -0
- package/out/src/reqlan-import-bindings.d.ts +12 -0
- package/out/src/reqlan-import-bindings.js +65 -0
- package/out/src/reqlan-import-bindings.js.map +1 -0
- package/out/src/reqlan-import-edits.d.ts +17 -0
- package/out/src/reqlan-import-edits.js +96 -0
- package/out/src/reqlan-import-edits.js.map +1 -0
- package/out/src/reqlan-imports.d.ts +22 -0
- package/out/src/reqlan-imports.js +61 -0
- package/out/src/reqlan-imports.js.map +1 -0
- package/out/src/reqlan-inbound-reference-inlay-label.d.ts +23 -0
- package/out/src/reqlan-inbound-reference-inlay-label.js +128 -0
- package/out/src/reqlan-inbound-reference-inlay-label.js.map +1 -0
- package/out/src/reqlan-inlay-hint-provider.d.ts +15 -0
- package/out/src/reqlan-inlay-hint-provider.js +54 -0
- package/out/src/reqlan-inlay-hint-provider.js.map +1 -0
- package/out/src/reqlan-inlay-hint-settings.d.ts +10 -0
- package/out/src/reqlan-inlay-hint-settings.js +10 -0
- package/out/src/reqlan-inlay-hint-settings.js.map +1 -0
- package/out/src/reqlan-linker.d.ts +26 -0
- package/out/src/reqlan-linker.js +86 -0
- package/out/src/reqlan-linker.js.map +1 -0
- package/out/src/reqlan-markdown-links.d.ts +28 -0
- package/out/src/reqlan-markdown-links.js +113 -0
- package/out/src/reqlan-markdown-links.js.map +1 -0
- package/out/src/reqlan-module.d.ts +41 -0
- package/out/src/reqlan-module.js +76 -0
- package/out/src/reqlan-module.js.map +1 -0
- package/out/src/reqlan-name-catalog.d.ts +23 -0
- package/out/src/reqlan-name-catalog.js +63 -0
- package/out/src/reqlan-name-catalog.js.map +1 -0
- package/out/src/reqlan-name-provider.d.ts +10 -0
- package/out/src/reqlan-name-provider.js +38 -0
- package/out/src/reqlan-name-provider.js.map +1 -0
- package/out/src/reqlan-namespace-import-links.d.ts +10 -0
- package/out/src/reqlan-namespace-import-links.js +81 -0
- package/out/src/reqlan-namespace-import-links.js.map +1 -0
- package/out/src/reqlan-path-completion.d.ts +50 -0
- package/out/src/reqlan-path-completion.js +304 -0
- package/out/src/reqlan-path-completion.js.map +1 -0
- package/out/src/reqlan-path-references.d.ts +5 -0
- package/out/src/reqlan-path-references.js +124 -0
- package/out/src/reqlan-path-references.js.map +1 -0
- package/out/src/reqlan-path-resolve.d.ts +59 -0
- package/out/src/reqlan-path-resolve.js +173 -0
- package/out/src/reqlan-path-resolve.js.map +1 -0
- package/out/src/reqlan-quoted-strings.d.ts +9 -0
- package/out/src/reqlan-quoted-strings.js +26 -0
- package/out/src/reqlan-quoted-strings.js.map +1 -0
- package/out/src/reqlan-reference-at-position.d.ts +20 -0
- package/out/src/reqlan-reference-at-position.js +89 -0
- package/out/src/reqlan-reference-at-position.js.map +1 -0
- package/out/src/reqlan-references.d.ts +16 -0
- package/out/src/reqlan-references.js +51 -0
- package/out/src/reqlan-references.js.map +1 -0
- package/out/src/reqlan-scope.d.ts +25 -0
- package/out/src/reqlan-scope.js +266 -0
- package/out/src/reqlan-scope.js.map +1 -0
- package/out/src/reqlan-semantic-token-provider.d.ts +8 -0
- package/out/src/reqlan-semantic-token-provider.js +97 -0
- package/out/src/reqlan-semantic-token-provider.js.map +1 -0
- package/out/src/reqlan-token-builder.d.ts +12 -0
- package/out/src/reqlan-token-builder.js +459 -0
- package/out/src/reqlan-token-builder.js.map +1 -0
- package/out/src/reqlan-validator.d.ts +25 -0
- package/out/src/reqlan-validator.js +119 -0
- package/out/src/reqlan-validator.js.map +1 -0
- package/out/src/reqlan-workspace-attribute-catalog.d.ts +6 -0
- package/out/src/reqlan-workspace-attribute-catalog.js +54 -0
- package/out/src/reqlan-workspace-attribute-catalog.js.map +1 -0
- package/out/test/code-actions.test.d.ts +1 -0
- package/out/test/code-actions.test.js +126 -0
- package/out/test/code-actions.test.js.map +1 -0
- package/out/test/comment-in-string.test.d.ts +1 -0
- package/out/test/comment-in-string.test.js +100 -0
- package/out/test/comment-in-string.test.js.map +1 -0
- package/out/test/completion.test.d.ts +1 -0
- package/out/test/completion.test.js +467 -0
- package/out/test/completion.test.js.map +1 -0
- package/out/test/file-path-rewrite.test.d.ts +1 -0
- package/out/test/file-path-rewrite.test.js +64 -0
- package/out/test/file-path-rewrite.test.js.map +1 -0
- package/out/test/ignore-error.test.d.ts +1 -0
- package/out/test/ignore-error.test.js +87 -0
- package/out/test/ignore-error.test.js.map +1 -0
- package/out/test/implicit-import-extension.test.d.ts +1 -0
- package/out/test/implicit-import-extension.test.js +203 -0
- package/out/test/implicit-import-extension.test.js.map +1 -0
- package/out/test/import-edits.test.d.ts +1 -0
- package/out/test/import-edits.test.js +20 -0
- package/out/test/import-edits.test.js.map +1 -0
- package/out/test/inlay-hints.test.d.ts +1 -0
- package/out/test/inlay-hints.test.js +238 -0
- package/out/test/inlay-hints.test.js.map +1 -0
- package/out/test/linking.test.d.ts +1 -0
- package/out/test/linking.test.js +754 -0
- package/out/test/linking.test.js.map +1 -0
- package/out/test/naked-strings.test.d.ts +1 -0
- package/out/test/naked-strings.test.js +50 -0
- package/out/test/naked-strings.test.js.map +1 -0
- package/out/test/parsing.test.d.ts +1 -0
- package/out/test/parsing.test.js +773 -0
- package/out/test/parsing.test.js.map +1 -0
- package/out/test/path-resolve.test.d.ts +1 -0
- package/out/test/path-resolve.test.js +88 -0
- package/out/test/path-resolve.test.js.map +1 -0
- package/out/test/references.test.d.ts +1 -0
- package/out/test/references.test.js +168 -0
- package/out/test/references.test.js.map +1 -0
- package/out/test/rqconfig.test.d.ts +1 -0
- package/out/test/rqconfig.test.js +275 -0
- package/out/test/rqconfig.test.js.map +1 -0
- package/out/test/test-requirement-refs.d.ts +8 -0
- package/out/test/test-requirement-refs.js +35 -0
- package/out/test/test-requirement-refs.js.map +1 -0
- package/out/test/testing.test.d.ts +1 -0
- package/out/test/testing.test.js +23 -0
- package/out/test/testing.test.js.map +1 -0
- package/out/test/validating.test.d.ts +1 -0
- package/out/test/validating.test.js +117 -0
- package/out/test/validating.test.js.map +1 -0
- package/out/tsconfig.tsbuildinfo +1 -0
- package/package.json +54 -0
- package/src/chevrotain.d.ts +8 -0
- package/src/file-path-rewrite.ts +96 -0
- package/src/generated/ast.ts +1481 -0
- package/src/generated/grammar.ts +3074 -0
- package/src/generated/module.ts +38 -0
- package/src/index.ts +34 -0
- package/src/reqlan-attribute-catalog.ts +100 -0
- package/src/reqlan-code-action-provider.ts +385 -0
- package/src/reqlan-code-lens-provider.ts +54 -0
- package/src/reqlan-code-lens-settings.ts +42 -0
- package/src/reqlan-comment-resolver.ts +372 -0
- package/src/reqlan-comment.langium +23 -0
- package/src/reqlan-completion-context.ts +330 -0
- package/src/reqlan-completion-provider.ts +400 -0
- package/src/reqlan-definition-provider.ts +319 -0
- package/src/reqlan-document-highlight-provider.ts +21 -0
- package/src/reqlan-document-link-provider.ts +45 -0
- package/src/reqlan-embedded-file-references.ts +72 -0
- package/src/reqlan-file-link-resolver.ts +485 -0
- package/src/reqlan-file-references.ts +84 -0
- package/src/reqlan-grammar-config.ts +14 -0
- package/src/reqlan-idea-reference-site.ts +74 -0
- package/src/reqlan-idea-summary.ts +162 -0
- package/src/reqlan-ignore-error.ts +50 -0
- package/src/reqlan-import-bindings.ts +87 -0
- package/src/reqlan-import-edits.ts +124 -0
- package/src/reqlan-imports.ts +115 -0
- package/src/reqlan-inbound-reference-inlay-label.ts +194 -0
- package/src/reqlan-inlay-hint-provider.ts +88 -0
- package/src/reqlan-inlay-hint-settings.ts +15 -0
- package/src/reqlan-linker.ts +108 -0
- package/src/reqlan-markdown-links.ts +136 -0
- package/src/reqlan-module.ts +104 -0
- package/src/reqlan-name-catalog.ts +81 -0
- package/src/reqlan-name-provider.ts +56 -0
- package/src/reqlan-namespace-import-links.ts +92 -0
- package/src/reqlan-path-completion.ts +420 -0
- package/src/reqlan-path-references.ts +140 -0
- package/src/reqlan-path-resolve.ts +346 -0
- package/src/reqlan-quoted-strings.ts +30 -0
- package/src/reqlan-reference-at-position.ts +154 -0
- package/src/reqlan-reference-code-lens.ts +323 -0
- package/src/reqlan-reference-search-site.ts +202 -0
- package/src/reqlan-references.ts +76 -0
- package/src/reqlan-scope.ts +312 -0
- package/src/reqlan-semantic-token-provider.ts +113 -0
- package/src/reqlan-token-builder.ts +492 -0
- package/src/reqlan-validator.ts +149 -0
- package/src/reqlan-workspace-attribute-catalog.ts +62 -0
- package/src/reqlan.langium +236 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom token builder so line comments do not match `//` in URLs or inside string literals,
|
|
3
|
+
* and so `@` only introduces attributes at the start of a line.
|
|
4
|
+
*/
|
|
5
|
+
import type { Grammar } from 'langium';
|
|
6
|
+
import { DefaultTokenBuilder, type TokenBuilderOptions } from 'langium';
|
|
7
|
+
|
|
8
|
+
const SL_COMMENT_PATTERN = /\/\/[^\n\r]*/y;
|
|
9
|
+
|
|
10
|
+
function isInsideNakedQuote(text: string, offset: number): boolean {
|
|
11
|
+
let inDouble = false;
|
|
12
|
+
let inSingle = false;
|
|
13
|
+
const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
|
|
14
|
+
for (let index = lineStart; index < offset; index++) {
|
|
15
|
+
const char = text[index]!;
|
|
16
|
+
if (inDouble || inSingle) {
|
|
17
|
+
if (char === '\\') {
|
|
18
|
+
index++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inDouble && char === '"') {
|
|
22
|
+
inDouble = false;
|
|
23
|
+
} else if (inSingle && char === "'") {
|
|
24
|
+
inSingle = false;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (char === '"') {
|
|
29
|
+
inDouble = true;
|
|
30
|
+
} else if (char === "'") {
|
|
31
|
+
inSingle = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return inDouble || inSingle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const slCommentPattern = (text: string, offset: number): RegExpExecArray | null => {
|
|
38
|
+
if (text.charCodeAt(offset) !== 47 || text.charCodeAt(offset + 1) !== 47) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (isInsideNakedQuote(text, offset)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let previousIndex = offset - 1;
|
|
45
|
+
while (previousIndex >= 0 && /[ \t]/.test(text[previousIndex]!)) {
|
|
46
|
+
previousIndex--;
|
|
47
|
+
}
|
|
48
|
+
const previous = previousIndex >= 0 ? text[previousIndex]! : '';
|
|
49
|
+
if (previous === ':' || previous === '/') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
SL_COMMENT_PATTERN.lastIndex = offset;
|
|
53
|
+
return SL_COMMENT_PATTERN.exec(text);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function makeMatch(text: string, offset: number, length: number): RegExpExecArray {
|
|
57
|
+
const image = text.slice(offset, offset + length);
|
|
58
|
+
const match = [image] as unknown as RegExpExecArray;
|
|
59
|
+
match.index = offset;
|
|
60
|
+
match.input = text;
|
|
61
|
+
return match;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `@` marks an attribute only when it is the first non-whitespace character on a line.
|
|
66
|
+
* Mid-line `@` (and `\@` at line start) fall through to OTHER as body text.
|
|
67
|
+
*/
|
|
68
|
+
const atSignAttributeMarker = (text: string, offset: number): RegExpExecArray | null => {
|
|
69
|
+
if (text[offset] !== '@') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let previousIndex = offset - 1;
|
|
73
|
+
while (previousIndex >= 0 && (text[previousIndex] === ' ' || text[previousIndex] === '\t')) {
|
|
74
|
+
previousIndex--;
|
|
75
|
+
}
|
|
76
|
+
if (previousIndex >= 0 && text[previousIndex] !== '\n' && text[previousIndex] !== '\r') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return makeMatch(text, offset, 1);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const markdownLink = (text: string, offset: number): RegExpExecArray | null => {
|
|
83
|
+
if (text[offset] !== '[' || text[offset + 1] === '[') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
let index = offset + 1;
|
|
87
|
+
while (index < text.length) {
|
|
88
|
+
if (text[index] === ']' && text[index + 1] === '(') {
|
|
89
|
+
const label = text.slice(offset + 1, index);
|
|
90
|
+
const targetStart = index + 2;
|
|
91
|
+
let targetEnd = targetStart;
|
|
92
|
+
while (targetEnd < text.length && text[targetEnd] !== ')') {
|
|
93
|
+
if (text[targetEnd] === '\n' || text[targetEnd] === '\r') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
targetEnd++;
|
|
97
|
+
}
|
|
98
|
+
if (targetEnd >= text.length || label.length === 0 || targetEnd === targetStart) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return makeMatch(text, offset, targetEnd + 1 - offset);
|
|
102
|
+
}
|
|
103
|
+
if (text[index] === '\n' || text[index] === '\r') {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
index++;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const topLevelBlockOpener = /(?:^|\n)[ \t]*(?:[A-Za-z_.][\w.-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/;
|
|
112
|
+
|
|
113
|
+
function isEscapedAt(text: string, offset: number): boolean {
|
|
114
|
+
let backslashes = 0;
|
|
115
|
+
for (let index = offset - 1; index >= 0 && text[index] === '\\'; index--) {
|
|
116
|
+
backslashes++;
|
|
117
|
+
}
|
|
118
|
+
return backslashes % 2 === 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isStructuralOpenBraceAtDepth(text: string, offset: number, depth: number): boolean {
|
|
122
|
+
if (text[offset] !== '{') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (isEscapedAt(text, offset)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const before = text.slice(0, offset).replace(/[ \t]+$/, '');
|
|
129
|
+
|
|
130
|
+
if (depth === 0 && topLevelBlockOpener.test(before)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (/@[A-Za-z_][\w-]*(?::)?\s*$/.test(before)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (depth >= 1 && isLineStartAt(text, offset)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (depth >= 1 && /[A-Za-z_][\w-]*\s*$/.test(before) && !isLineStartAt(text, offset)) {
|
|
140
|
+
const after = text.slice(offset + 1);
|
|
141
|
+
return /^[ \t]*(?:\r?\n|$)/.test(after);
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A `}` closes a structural block only when it is the last non-whitespace on its line
|
|
148
|
+
* and it is not pairing with an unmatched prose `{` earlier on that line / in the body.
|
|
149
|
+
* Own-line closers (`}` after only indentation) always close structurally so an
|
|
150
|
+
* unbalanced prose `{` does not swallow the idea's closing brace.
|
|
151
|
+
*/
|
|
152
|
+
function isStructuralCloseBraceAt(text: string, offset: number): boolean {
|
|
153
|
+
if (text[offset] !== '}') {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (isEscapedAt(text, offset)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const after = text.slice(offset + 1);
|
|
160
|
+
const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
|
|
161
|
+
if (restOfLine.trim().length !== 0) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const { structuralDepth, proseDepth } = scanBraceState(text, offset);
|
|
165
|
+
if (structuralDepth <= 0) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (isLineStartAt(text, offset)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return proseDepth === 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface BraceScanState {
|
|
175
|
+
structuralDepth: number;
|
|
176
|
+
proseDepth: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function scanBraceState(text: string, offset: number): BraceScanState {
|
|
180
|
+
let structuralDepth = 0;
|
|
181
|
+
const proseDepthByStructural: number[] = [0];
|
|
182
|
+
|
|
183
|
+
const proseDepthAt = (): number => proseDepthByStructural[structuralDepth] ?? 0;
|
|
184
|
+
const setProseDepth = (value: number): void => {
|
|
185
|
+
proseDepthByStructural[structuralDepth] = value;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (let index = 0; index < offset; index++) {
|
|
189
|
+
const char = text[index];
|
|
190
|
+
if (char === '{') {
|
|
191
|
+
if (isEscapedAt(text, index)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (isStructuralOpenBraceAtDepth(text, index, structuralDepth)) {
|
|
195
|
+
structuralDepth++;
|
|
196
|
+
proseDepthByStructural[structuralDepth] = 0;
|
|
197
|
+
} else {
|
|
198
|
+
setProseDepth(proseDepthAt() + 1);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (char !== '}') {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (isEscapedAt(text, index)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const after = text.slice(index + 1);
|
|
209
|
+
const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
|
|
210
|
+
const atEndOfLine = restOfLine.trim().length === 0;
|
|
211
|
+
if (!atEndOfLine) {
|
|
212
|
+
if (proseDepthAt() > 0) {
|
|
213
|
+
setProseDepth(proseDepthAt() - 1);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (structuralDepth <= 0) {
|
|
218
|
+
if (proseDepthAt() > 0) {
|
|
219
|
+
setProseDepth(proseDepthAt() - 1);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (isLineStartAt(text, index)) {
|
|
224
|
+
structuralDepth--;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (proseDepthAt() > 0) {
|
|
228
|
+
setProseDepth(proseDepthAt() - 1);
|
|
229
|
+
} else {
|
|
230
|
+
structuralDepth--;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
structuralDepth,
|
|
236
|
+
proseDepth: proseDepthAt()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function scanStructuralBraceDepth(text: string, offset: number): number {
|
|
241
|
+
return scanBraceState(text, offset).structuralDepth;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isStructuralOpenBraceAt(text: string, offset: number): boolean {
|
|
245
|
+
return isStructuralOpenBraceAtDepth(text, offset, scanStructuralBraceDepth(text, offset));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const structuralOpenBrace = (text: string, offset: number): RegExpExecArray | null => {
|
|
249
|
+
if (text[offset] !== '{') {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return isStructuralOpenBraceAt(text, offset) ? makeMatch(text, offset, 1) : null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const structuralCloseBrace = (text: string, offset: number): RegExpExecArray | null => {
|
|
256
|
+
if (text[offset] !== '}') {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return isStructuralCloseBraceAt(text, offset) ? makeMatch(text, offset, 1) : null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const proseOpenBrace = (text: string, offset: number): RegExpExecArray | null => {
|
|
263
|
+
if (text[offset] !== '{') {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return isStructuralOpenBraceAt(text, offset) ? null : makeMatch(text, offset, 1);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const proseCloseBrace = (text: string, offset: number): RegExpExecArray | null => {
|
|
270
|
+
if (text[offset] !== '}') {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return isStructuralCloseBraceAt(text, offset) ? null : makeMatch(text, offset, 1);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
function braceDepthBefore(text: string, offset: number): number {
|
|
277
|
+
return scanStructuralBraceDepth(text, offset);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isLineStartAt(text: string, offset: number): boolean {
|
|
281
|
+
let previousIndex = offset - 1;
|
|
282
|
+
while (previousIndex >= 0 && (text[previousIndex] === ' ' || text[previousIndex] === '\t')) {
|
|
283
|
+
previousIndex--;
|
|
284
|
+
}
|
|
285
|
+
return previousIndex < 0 || text[previousIndex] === '\n' || text[previousIndex] === '\r';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isStringLiteralContext(text: string, offset: number): boolean {
|
|
289
|
+
const depth = braceDepthBefore(text, offset);
|
|
290
|
+
const trimmed = text.slice(0, offset).replace(/[ \t]+$/, '');
|
|
291
|
+
if (depth === 0 && /\b(?:from|import)\s*$/.test(trimmed)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (/\[\s*$/.test(trimmed)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
const lineStart = trimmed.lastIndexOf('\n') + 1;
|
|
298
|
+
if (trimmed.slice(lineStart).length === 0 && depth === 0) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Import keywords are reserved only at top level; in bodies they lex as ordinary words. */
|
|
305
|
+
function topLevelFromKeyword(text: string, offset: number): RegExpExecArray | null {
|
|
306
|
+
if (!text.startsWith('from', offset)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const next = text[offset + 4];
|
|
310
|
+
if (next !== undefined && /[\w-]/.test(next)) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (braceDepthBefore(text, offset) !== 0 || !isLineStartAt(text, offset)) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return makeMatch(text, offset, 4);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function topLevelImportKeyword(text: string, offset: number): RegExpExecArray | null {
|
|
320
|
+
if (!text.startsWith('import', offset)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const next = text[offset + 6];
|
|
324
|
+
if (next !== undefined && /[\w-]/.test(next)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (braceDepthBefore(text, offset) !== 0) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
if (isLineStartAt(text, offset)) {
|
|
331
|
+
return makeMatch(text, offset, 6);
|
|
332
|
+
}
|
|
333
|
+
const before = text.slice(0, offset).replace(/[ \t]+$/, '');
|
|
334
|
+
if (/(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
|
|
335
|
+
return makeMatch(text, offset, 6);
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function topLevelAsKeyword(text: string, offset: number): RegExpExecArray | null {
|
|
341
|
+
if (!text.startsWith('as', offset)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const next = text[offset + 2];
|
|
345
|
+
if (next !== undefined && /[\w-]/.test(next)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if (braceDepthBefore(text, offset) !== 0) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const before = text.slice(0, offset).replace(/[ \t]+$/, '');
|
|
352
|
+
if (/(?:[_a-zA-Z][\w-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
|
|
353
|
+
return makeMatch(text, offset, 2);
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const reqlanString = (text: string, offset: number): RegExpExecArray | null => {
|
|
359
|
+
const quote = text[offset];
|
|
360
|
+
if (quote !== '"' && quote !== "'") {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
if (!isStringLiteralContext(text, offset)) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
let index = offset + 1;
|
|
367
|
+
while (index < text.length) {
|
|
368
|
+
const char = text[index]!;
|
|
369
|
+
if (char === '\n' || char === '\r') {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (char === '\\') {
|
|
373
|
+
index += 2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (char === quote) {
|
|
377
|
+
return makeMatch(text, offset, index + 1 - offset);
|
|
378
|
+
}
|
|
379
|
+
index++;
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/** Body words with interior apostrophes (e.g. user's); plain identifiers stay ID. */
|
|
385
|
+
const reqlanWord = (text: string, offset: number): RegExpExecArray | null => {
|
|
386
|
+
const match = /^[A-Za-z_][\w']*/.exec(text.slice(offset));
|
|
387
|
+
if (!match || !match[0].includes("'")) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
return makeMatch(text, offset, match[0].length);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export class ReqlanTokenBuilder extends DefaultTokenBuilder {
|
|
394
|
+
override buildTokens(grammar: Grammar, options?: TokenBuilderOptions) {
|
|
395
|
+
const tokens = super.buildTokens(grammar, options);
|
|
396
|
+
if (!Array.isArray(tokens)) {
|
|
397
|
+
return tokens;
|
|
398
|
+
}
|
|
399
|
+
const atToken = tokens.find(token => token.name === '@');
|
|
400
|
+
if (atToken) {
|
|
401
|
+
atToken.PATTERN = atSignAttributeMarker;
|
|
402
|
+
atToken.LINE_BREAKS = false;
|
|
403
|
+
}
|
|
404
|
+
const markdownIndex = tokens.findIndex(token => token.name === 'MARKDOWN_LINK');
|
|
405
|
+
if (markdownIndex >= 0) {
|
|
406
|
+
const [markdown] = tokens.splice(markdownIndex, 1);
|
|
407
|
+
tokens.unshift(markdown);
|
|
408
|
+
}
|
|
409
|
+
const wordIndex = tokens.findIndex(token => token.name === 'WORD');
|
|
410
|
+
const idIndex = tokens.findIndex(token => token.name === 'ID');
|
|
411
|
+
if (wordIndex >= 0 && idIndex >= 0 && idIndex < wordIndex) {
|
|
412
|
+
const [word] = tokens.splice(wordIndex, 1);
|
|
413
|
+
tokens.splice(idIndex, 0, word);
|
|
414
|
+
}
|
|
415
|
+
for (const [name, pattern] of [
|
|
416
|
+
['from', topLevelFromKeyword],
|
|
417
|
+
['import', topLevelImportKeyword],
|
|
418
|
+
['as', topLevelAsKeyword]
|
|
419
|
+
] as const) {
|
|
420
|
+
const token = tokens.find(entry => entry.name === name);
|
|
421
|
+
if (token) {
|
|
422
|
+
token.PATTERN = pattern;
|
|
423
|
+
token.LINE_BREAKS = false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const openBrace = tokens.find(entry => entry.name === '{');
|
|
427
|
+
if (openBrace) {
|
|
428
|
+
openBrace.PATTERN = structuralOpenBrace;
|
|
429
|
+
openBrace.LINE_BREAKS = false;
|
|
430
|
+
}
|
|
431
|
+
const closeBrace = tokens.find(entry => entry.name === '}');
|
|
432
|
+
if (closeBrace) {
|
|
433
|
+
closeBrace.PATTERN = structuralCloseBrace;
|
|
434
|
+
closeBrace.LINE_BREAKS = false;
|
|
435
|
+
}
|
|
436
|
+
const otherIndex = tokens.findIndex(entry => entry.name === 'OTHER');
|
|
437
|
+
if (otherIndex >= 0) {
|
|
438
|
+
const other = tokens[otherIndex]!;
|
|
439
|
+
const basePattern = other.PATTERN;
|
|
440
|
+
other.PATTERN = (text: string, offset: number) => {
|
|
441
|
+
const proseBrace = proseOpenBrace(text, offset) ?? proseCloseBrace(text, offset);
|
|
442
|
+
if (proseBrace) {
|
|
443
|
+
return proseBrace;
|
|
444
|
+
}
|
|
445
|
+
if (typeof basePattern === 'function') {
|
|
446
|
+
return basePattern(text, offset);
|
|
447
|
+
}
|
|
448
|
+
if (basePattern instanceof RegExp) {
|
|
449
|
+
const slice = text.slice(offset);
|
|
450
|
+
const match = basePattern.exec(slice);
|
|
451
|
+
return match?.index === 0 ? makeMatch(text, offset, match[0].length) : null;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
};
|
|
455
|
+
other.LINE_BREAKS = false;
|
|
456
|
+
}
|
|
457
|
+
return tokens;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
protected override buildTerminalToken(terminal: { name: string }): import('chevrotain').TokenType {
|
|
461
|
+
if (terminal.name === 'SL_COMMENT') {
|
|
462
|
+
return {
|
|
463
|
+
name: 'SL_COMMENT',
|
|
464
|
+
GROUP: 'hidden',
|
|
465
|
+
LINE_BREAKS: true,
|
|
466
|
+
PATTERN: slCommentPattern
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (terminal.name === 'MARKDOWN_LINK') {
|
|
470
|
+
return {
|
|
471
|
+
name: 'MARKDOWN_LINK',
|
|
472
|
+
LINE_BREAKS: false,
|
|
473
|
+
PATTERN: markdownLink
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (terminal.name === 'STRING') {
|
|
477
|
+
return {
|
|
478
|
+
name: 'STRING',
|
|
479
|
+
LINE_BREAKS: false,
|
|
480
|
+
PATTERN: reqlanString
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (terminal.name === 'WORD') {
|
|
484
|
+
return {
|
|
485
|
+
name: 'WORD',
|
|
486
|
+
LINE_BREAKS: false,
|
|
487
|
+
PATTERN: reqlanWord
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return super.buildTerminalToken(terminal as Parameters<DefaultTokenBuilder['buildTerminalToken']>[0]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { AstNode, ValidationAcceptor, ValidationChecks } from 'langium';
|
|
2
|
+
import { AstUtils } from 'langium';
|
|
3
|
+
import type { ReqlanAstType } from './generated/ast.js';
|
|
4
|
+
import {
|
|
5
|
+
isIdea,
|
|
6
|
+
isOneLinerIdea,
|
|
7
|
+
type Model
|
|
8
|
+
} from './generated/ast.js';
|
|
9
|
+
import {
|
|
10
|
+
collectFileLinks,
|
|
11
|
+
fileLinkTargetIssueMessage
|
|
12
|
+
} from './reqlan-file-link-resolver.js';
|
|
13
|
+
import { importBindings, importedIdeaNames } from './reqlan-import-bindings.js';
|
|
14
|
+
import { isResolvableImportPath } from './reqlan-imports.js';
|
|
15
|
+
import type { ReqlanServices } from './reqlan-module.js';
|
|
16
|
+
import { pathResolveContextFromServices } from './reqlan-path-resolve.js';
|
|
17
|
+
import { unquoteReqlanString } from './reqlan-quoted-strings.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers validation hooks for the requirement graph AST.
|
|
21
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
22
|
+
*/
|
|
23
|
+
export function registerValidationChecks(services: ReqlanServices) {
|
|
24
|
+
const registry = services.validation.ValidationRegistry;
|
|
25
|
+
const validator = services.validation.ReqlanValidator;
|
|
26
|
+
const checks: ValidationChecks<ReqlanAstType> = {
|
|
27
|
+
Model: validator.checkModelDuplicates
|
|
28
|
+
};
|
|
29
|
+
registry.register(checks, validator);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Custom validations for Reqlan documents.
|
|
34
|
+
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
35
|
+
*/
|
|
36
|
+
export class ReqlanValidator {
|
|
37
|
+
|
|
38
|
+
constructor(private readonly services: ReqlanServices) {}
|
|
39
|
+
|
|
40
|
+
checkModelDuplicates(model: Model, accept: ValidationAcceptor): void {
|
|
41
|
+
this.checkDuplicateImportBindings(model, accept);
|
|
42
|
+
this.checkDuplicateIdeaNames(model, accept);
|
|
43
|
+
this.checkImportTargets(model, accept);
|
|
44
|
+
this.checkFileReferenceTargets(model, accept);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
checkDuplicateImportBindings(model: Model, accept: ValidationAcceptor): void {
|
|
48
|
+
const seen = new Map<string, ImportBindingSource>();
|
|
49
|
+
for (const importDecl of model.imports) {
|
|
50
|
+
for (const binding of importBindings(importDecl)) {
|
|
51
|
+
if (seen.has(binding.name)) {
|
|
52
|
+
accept('error', `'${binding.name}' is already defined in this file.`, {
|
|
53
|
+
node: binding.node,
|
|
54
|
+
property: binding.property
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
seen.set(binding.name, binding);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
checkDuplicateIdeaNames(model: Model, accept: ValidationAcceptor): void {
|
|
64
|
+
const seen = new Map<string, AstNode>();
|
|
65
|
+
for (const importDecl of model.imports) {
|
|
66
|
+
for (const binding of importBindings(importDecl)) {
|
|
67
|
+
seen.set(binding.name, importDecl);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const element of model.elements) {
|
|
71
|
+
if (!isIdea(element) && !isOneLinerIdea(element)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const name = element.name;
|
|
75
|
+
if (seen.has(name)) {
|
|
76
|
+
accept('error', `'${name}' is already defined in this file.`, {
|
|
77
|
+
node: element,
|
|
78
|
+
property: 'name'
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const importedNameConflict = model.imports.some(importDecl =>
|
|
83
|
+
importedIdeaNames(importDecl).includes(name)
|
|
84
|
+
);
|
|
85
|
+
if (importedNameConflict) {
|
|
86
|
+
accept('error', `'${name}' is already defined in this file.`, {
|
|
87
|
+
node: element,
|
|
88
|
+
property: 'name'
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
seen.set(name, element);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Error underline when an `import` / `from` path does not resolve to a file.
|
|
98
|
+
* Missing imported ideas are reported by the linker as unresolved references.
|
|
99
|
+
*/
|
|
100
|
+
checkImportTargets(model: Model, accept: ValidationAcceptor): void {
|
|
101
|
+
const document = AstUtils.getDocument(model);
|
|
102
|
+
const { shared } = this.services;
|
|
103
|
+
const pathContext = pathResolveContextFromServices(this.services);
|
|
104
|
+
for (const importDecl of model.imports) {
|
|
105
|
+
const path = unquoteReqlanString(importDecl.path);
|
|
106
|
+
if (!path || isRemoteImportPath(path)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (isResolvableImportPath(
|
|
110
|
+
path,
|
|
111
|
+
document,
|
|
112
|
+
shared.workspace.LangiumDocuments,
|
|
113
|
+
shared.workspace.FileSystemProvider,
|
|
114
|
+
pathContext
|
|
115
|
+
)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
accept('error', `Could not resolve import '${path}'.`, {
|
|
119
|
+
node: importDecl,
|
|
120
|
+
property: 'path'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
checkFileReferenceTargets(model: Model, accept: ValidationAcceptor): void {
|
|
126
|
+
const document = AstUtils.getDocument(model);
|
|
127
|
+
const { shared } = this.services;
|
|
128
|
+
for (const link of collectFileLinks(
|
|
129
|
+
document,
|
|
130
|
+
shared.workspace.LangiumDocuments,
|
|
131
|
+
shared.workspace.FileSystemProvider,
|
|
132
|
+
pathResolveContextFromServices(this.services)
|
|
133
|
+
)) {
|
|
134
|
+
if (!link.targetIssue) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
accept('warning', fileLinkTargetIssueMessage(link.targetIssue), {
|
|
138
|
+
node: model,
|
|
139
|
+
range: link.sourceRange
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type ImportBindingSource = ReturnType<typeof importBindings>[number];
|
|
146
|
+
|
|
147
|
+
function isRemoteImportPath(path: string): boolean {
|
|
148
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(path);
|
|
149
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects attribute keys and values from parsed workspace documents.
|
|
3
|
+
*/
|
|
4
|
+
import type { LangiumDocuments } from 'langium';
|
|
5
|
+
import { AstUtils } from 'langium';
|
|
6
|
+
import { isAttribute, isModel, isScalarValue, type Attribute } from './generated/ast.js';
|
|
7
|
+
import type { AttributeCatalog } from './reqlan-attribute-catalog.js';
|
|
8
|
+
|
|
9
|
+
export function collectWorkspaceAttributeCatalog(documents: LangiumDocuments): AttributeCatalog {
|
|
10
|
+
const keys = new Set<string>();
|
|
11
|
+
const valuesByKey = new Map<string, Set<string>>();
|
|
12
|
+
for (const document of documents.all.toArray()) {
|
|
13
|
+
const model = document.parseResult.value;
|
|
14
|
+
if (!isModel(model)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
for (const attribute of AstUtils.streamAst(model).filter(isAttribute)) {
|
|
18
|
+
keys.add(attribute.name);
|
|
19
|
+
const values = valuesForAttribute(attribute);
|
|
20
|
+
if (values.length === 0) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!valuesByKey.has(attribute.name)) {
|
|
24
|
+
valuesByKey.set(attribute.name, new Set());
|
|
25
|
+
}
|
|
26
|
+
for (const value of values) {
|
|
27
|
+
valuesByKey.get(attribute.name)!.add(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
keys: [...keys].sort((left, right) => left.localeCompare(right)),
|
|
33
|
+
valuesByKey: Object.fromEntries(
|
|
34
|
+
[...valuesByKey.entries()].map(([key, values]) => [key, [...values].sort()])
|
|
35
|
+
)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function valuesForAttribute(attribute: Attribute): string[] {
|
|
40
|
+
if (!attribute.value) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
if (isScalarValue(attribute.value)) {
|
|
44
|
+
const text = attribute.value.parts
|
|
45
|
+
.map(part => typeof part === 'string' ? part : '')
|
|
46
|
+
.join('')
|
|
47
|
+
.trim();
|
|
48
|
+
return text ? [text] : [];
|
|
49
|
+
}
|
|
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);
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|