|
1 | | -import { URI } from 'langium'; |
| 1 | +import { isAstNode, URI, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium'; |
2 | 2 | import { NodeFileSystem } from 'langium/node'; |
3 | 3 | import fs from 'node:fs'; |
4 | 4 | import path from 'node:path'; |
5 | 5 | import { fileURLToPath } from 'node:url'; |
6 | | -import type { Model } from './ast'; |
| 6 | +import { isDataSource, type AstNode, type Model } from './ast'; |
7 | 7 | import { STD_LIB_MODULE_NAME } from './constants'; |
8 | 8 | import { createZModelLanguageServices } from './module'; |
| 9 | +import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; |
9 | 10 |
|
10 | 11 | export function createZModelServices() { |
11 | 12 | return createZModelLanguageServices(NodeFileSystem); |
@@ -60,8 +61,15 @@ export async function loadDocument( |
60 | 61 | const langiumDocuments = services.shared.workspace.LangiumDocuments; |
61 | 62 | const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); |
62 | 63 |
|
| 64 | + // load imports |
| 65 | + const importedURIs = await loadImports(document, langiumDocuments); |
| 66 | + const importedDocuments: LangiumDocument[] = []; |
| 67 | + for (const uri of importedURIs) { |
| 68 | + importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri)); |
| 69 | + } |
| 70 | + |
63 | 71 | // build the document together with standard library, plugin modules, and imported documents |
64 | | - await services.shared.workspace.DocumentBuilder.build([stdLib, ...pluginDocs, document], { |
| 72 | + await services.shared.workspace.DocumentBuilder.build([stdLib, ...pluginDocs, document, ...importedDocuments], { |
65 | 73 | validation: true, |
66 | 74 | }); |
67 | 75 |
|
@@ -95,11 +103,100 @@ export async function loadDocument( |
95 | 103 | }; |
96 | 104 | } |
97 | 105 |
|
| 106 | + const model = document.parseResult.value as Model; |
| 107 | + |
| 108 | + // merge all declarations into the main document |
| 109 | + const imported = mergeImportsDeclarations(langiumDocuments, model); |
| 110 | + |
| 111 | + // remove imported documents |
| 112 | + imported.forEach((model) => { |
| 113 | + langiumDocuments.deleteDocument(model.$document!.uri); |
| 114 | + services.shared.workspace.IndexManager.remove(model.$document!.uri); |
| 115 | + }); |
| 116 | + |
| 117 | + // extra validation after merging imported declarations |
| 118 | + const additionalErrors = validationAfterImportMerge(model); |
| 119 | + if (additionalErrors.length > 0) { |
| 120 | + return { |
| 121 | + success: false, |
| 122 | + errors: additionalErrors, |
| 123 | + warnings, |
| 124 | + }; |
| 125 | + } |
| 126 | + |
98 | 127 | return { |
99 | 128 | success: true, |
100 | 129 | model: document.parseResult.value as Model, |
101 | 130 | warnings, |
102 | 131 | }; |
103 | 132 | } |
104 | 133 |
|
| 134 | +async function loadImports(document: LangiumDocument, documents: LangiumDocuments, uris: Set<string> = new Set()) { |
| 135 | + const uriString = document.uri.toString(); |
| 136 | + if (!uris.has(uriString)) { |
| 137 | + uris.add(uriString); |
| 138 | + const model = document.parseResult.value as Model; |
| 139 | + for (const imp of model.imports) { |
| 140 | + const importedModel = resolveImport(documents, imp); |
| 141 | + if (importedModel) { |
| 142 | + const importedDoc = getDocument(importedModel); |
| 143 | + await loadImports(importedDoc, documents, uris); |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + return Array.from(uris) |
| 148 | + .filter((x) => uriString != x) |
| 149 | + .map((e) => URI.parse(e)); |
| 150 | +} |
| 151 | + |
| 152 | +function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) { |
| 153 | + const importedModels = resolveTransitiveImports(documents, model); |
| 154 | + |
| 155 | + const importedDeclarations = importedModels.flatMap((m) => m.declarations); |
| 156 | + model.declarations.push(...importedDeclarations); |
| 157 | + |
| 158 | + // remove import directives |
| 159 | + model.imports = []; |
| 160 | + |
| 161 | + // fix $container, $containerIndex, and $containerProperty |
| 162 | + linkContentToContainer(model); |
| 163 | + |
| 164 | + return importedModels; |
| 165 | +} |
| 166 | + |
| 167 | +function linkContentToContainer(node: AstNode): void { |
| 168 | + for (const [name, value] of Object.entries(node)) { |
| 169 | + if (!name.startsWith('$')) { |
| 170 | + if (Array.isArray(value)) { |
| 171 | + value.forEach((item, index) => { |
| 172 | + if (isAstNode(item)) { |
| 173 | + (item as Mutable<AstNode>).$container = node; |
| 174 | + (item as Mutable<AstNode>).$containerProperty = name; |
| 175 | + (item as Mutable<AstNode>).$containerIndex = index; |
| 176 | + } |
| 177 | + }); |
| 178 | + } else if (isAstNode(value)) { |
| 179 | + (value as Mutable<AstNode>).$container = node; |
| 180 | + (value as Mutable<AstNode>).$containerProperty = name; |
| 181 | + } |
| 182 | + } |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +function validationAfterImportMerge(model: Model) { |
| 187 | + const errors: string[] = []; |
| 188 | + const dataSources = model.declarations.filter((d) => isDataSource(d)); |
| 189 | + if (dataSources.length > 1) { |
| 190 | + errors.push('Validation error: Multiple datasource declarations are not allowed'); |
| 191 | + } |
| 192 | + |
| 193 | + // at most one `@@auth` model |
| 194 | + const decls = getDataModelAndTypeDefs(model, true); |
| 195 | + const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); |
| 196 | + if (authDecls.length > 1) { |
| 197 | + errors.push('Validation error: Multiple `@@auth` declarations are not allowed'); |
| 198 | + } |
| 199 | + return errors; |
| 200 | +} |
| 201 | + |
105 | 202 | export * from './module'; |
0 commit comments