|
| 1 | +import { isAstNode, URI, type AstNode, type LangiumDocument, type LangiumDocuments, type Mutable } from 'langium'; |
| 2 | +import fs from 'node:fs'; |
| 3 | +import path from 'node:path'; |
| 4 | +import { fileURLToPath } from 'node:url'; |
| 5 | +import { isDataSource, type Model } from './ast'; |
| 6 | +import { STD_LIB_MODULE_NAME } from './constants'; |
| 7 | +import { createZModelServices } from './module'; |
| 8 | +import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Loads ZModel document from the given file name. Include the additional document |
| 12 | + * files if given. |
| 13 | + */ |
| 14 | +export async function loadDocument( |
| 15 | + fileName: string, |
| 16 | + additionalModelFiles: string[] = [], |
| 17 | +): Promise< |
| 18 | + { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } |
| 19 | +> { |
| 20 | + const { ZModelLanguage: services } = createZModelServices(false); |
| 21 | + const extensions = services.LanguageMetaData.fileExtensions; |
| 22 | + if (!extensions.includes(path.extname(fileName))) { |
| 23 | + return { |
| 24 | + success: false, |
| 25 | + errors: ['invalid schema file extension'], |
| 26 | + warnings: [], |
| 27 | + }; |
| 28 | + } |
| 29 | + |
| 30 | + if (!fs.existsSync(fileName)) { |
| 31 | + return { |
| 32 | + success: false, |
| 33 | + errors: ['schema file does not exist'], |
| 34 | + warnings: [], |
| 35 | + }; |
| 36 | + } |
| 37 | + |
| 38 | + // load standard library |
| 39 | + |
| 40 | + // isomorphic __dirname |
| 41 | + const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); |
| 42 | + const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( |
| 43 | + URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))), |
| 44 | + ); |
| 45 | + |
| 46 | + // load the document |
| 47 | + const langiumDocuments = services.shared.workspace.LangiumDocuments; |
| 48 | + const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); |
| 49 | + |
| 50 | + // load imports |
| 51 | + const importedURIs = await loadImports(document, langiumDocuments); |
| 52 | + const importedDocuments: LangiumDocument[] = []; |
| 53 | + for (const uri of importedURIs) { |
| 54 | + importedDocuments.push(await langiumDocuments.getOrCreateDocument(uri)); |
| 55 | + } |
| 56 | + |
| 57 | + // build the document together with standard library, additional modules, and imported documents |
| 58 | + |
| 59 | + // load additional model files |
| 60 | + const additionalDocs = await Promise.all( |
| 61 | + additionalModelFiles.map((file) => |
| 62 | + services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))), |
| 63 | + ), |
| 64 | + ); |
| 65 | + |
| 66 | + await services.shared.workspace.DocumentBuilder.build([stdLib, ...additionalDocs, document, ...importedDocuments], { |
| 67 | + validation: { |
| 68 | + stopAfterLexingErrors: true, |
| 69 | + stopAfterParsingErrors: true, |
| 70 | + stopAfterLinkingErrors: true, |
| 71 | + }, |
| 72 | + }); |
| 73 | + |
| 74 | + const diagnostics = langiumDocuments.all |
| 75 | + .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) |
| 76 | + .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) |
| 77 | + .toArray(); |
| 78 | + |
| 79 | + const errors: string[] = []; |
| 80 | + const warnings: string[] = []; |
| 81 | + |
| 82 | + if (diagnostics.length > 0) { |
| 83 | + for (const { doc, diag } of diagnostics) { |
| 84 | + const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${ |
| 85 | + diag.range.start.line + 1 |
| 86 | + }:${diag.range.start.character + 1} - ${diag.message}`; |
| 87 | + |
| 88 | + if (diag.severity === 1) { |
| 89 | + errors.push(message); |
| 90 | + } else { |
| 91 | + warnings.push(message); |
| 92 | + } |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + if (errors.length > 0) { |
| 97 | + return { |
| 98 | + success: false, |
| 99 | + errors, |
| 100 | + warnings, |
| 101 | + }; |
| 102 | + } |
| 103 | + |
| 104 | + const model = document.parseResult.value as Model; |
| 105 | + |
| 106 | + // merge all declarations into the main document |
| 107 | + const imported = mergeImportsDeclarations(langiumDocuments, model); |
| 108 | + |
| 109 | + // remove imported documents |
| 110 | + imported.forEach((model) => { |
| 111 | + langiumDocuments.deleteDocument(model.$document!.uri); |
| 112 | + services.shared.workspace.IndexManager.remove(model.$document!.uri); |
| 113 | + }); |
| 114 | + |
| 115 | + // extra validation after merging imported declarations |
| 116 | + const additionalErrors = validationAfterImportMerge(model); |
| 117 | + if (additionalErrors.length > 0) { |
| 118 | + return { |
| 119 | + success: false, |
| 120 | + errors: additionalErrors, |
| 121 | + warnings, |
| 122 | + }; |
| 123 | + } |
| 124 | + |
| 125 | + return { |
| 126 | + success: true, |
| 127 | + model: document.parseResult.value as Model, |
| 128 | + warnings, |
| 129 | + }; |
| 130 | +} |
| 131 | + |
| 132 | +async function loadImports(document: LangiumDocument, documents: LangiumDocuments, uris: Set<string> = new Set()) { |
| 133 | + const uriString = document.uri.toString(); |
| 134 | + if (!uris.has(uriString)) { |
| 135 | + uris.add(uriString); |
| 136 | + const model = document.parseResult.value as Model; |
| 137 | + for (const imp of model.imports) { |
| 138 | + const importedModel = resolveImport(documents, imp); |
| 139 | + if (importedModel) { |
| 140 | + const importedDoc = getDocument(importedModel); |
| 141 | + await loadImports(importedDoc, documents, uris); |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + return Array.from(uris) |
| 146 | + .filter((x) => uriString != x) |
| 147 | + .map((e) => URI.parse(e)); |
| 148 | +} |
| 149 | + |
| 150 | +function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) { |
| 151 | + const importedModels = resolveTransitiveImports(documents, model); |
| 152 | + |
| 153 | + const importedDeclarations = importedModels.flatMap((m) => m.declarations); |
| 154 | + model.declarations.push(...importedDeclarations); |
| 155 | + |
| 156 | + // remove import directives |
| 157 | + model.imports = []; |
| 158 | + |
| 159 | + // fix $container, $containerIndex, and $containerProperty |
| 160 | + linkContentToContainer(model); |
| 161 | + |
| 162 | + return importedModels; |
| 163 | +} |
| 164 | + |
| 165 | +function linkContentToContainer(node: AstNode): void { |
| 166 | + for (const [name, value] of Object.entries(node)) { |
| 167 | + if (!name.startsWith('$')) { |
| 168 | + if (Array.isArray(value)) { |
| 169 | + value.forEach((item, index) => { |
| 170 | + if (isAstNode(item)) { |
| 171 | + (item as Mutable<AstNode>).$container = node; |
| 172 | + (item as Mutable<AstNode>).$containerProperty = name; |
| 173 | + (item as Mutable<AstNode>).$containerIndex = index; |
| 174 | + } |
| 175 | + }); |
| 176 | + } else if (isAstNode(value)) { |
| 177 | + (value as Mutable<AstNode>).$container = node; |
| 178 | + (value as Mutable<AstNode>).$containerProperty = name; |
| 179 | + } |
| 180 | + } |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +function validationAfterImportMerge(model: Model) { |
| 185 | + const errors: string[] = []; |
| 186 | + const dataSources = model.declarations.filter((d) => isDataSource(d)); |
| 187 | + if (dataSources.length === 0) { |
| 188 | + errors.push('Validation error: schema must have a datasource declaration'); |
| 189 | + } else { |
| 190 | + if (dataSources.length > 1) { |
| 191 | + errors.push('Validation error: multiple datasource declarations are not allowed'); |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + // at most one `@@auth` model |
| 196 | + const decls = getDataModelAndTypeDefs(model, true); |
| 197 | + const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); |
| 198 | + if (authDecls.length > 1) { |
| 199 | + errors.push('Validation error: Multiple `@@auth` declarations are not allowed'); |
| 200 | + } |
| 201 | + return errors; |
| 202 | +} |
0 commit comments