UNPKG

langium-cli

Version:

CLI for Langium - the language engineering tool

204 lines (192 loc) 9.25 kB
import { EOL, expandToNode, joinToNode, toString } from 'langium/generate'; import { AstUtils, MultiMap, GrammarAST } from 'langium'; import { collectAst, collectTypeHierarchy, findReferenceTypes, isAstType, mergeTypesAndInterfaces, escapeQuotes } from 'langium/grammar'; import { generatedHeader } from './node-util.js'; import { collectKeywords, collectTerminalRegexps } from './langium-util.js'; export function generateAst(services, grammars, config) { const astTypes = collectAst(grammars, services.shared.workspace.LangiumDocuments); const crossRef = grammars.some(grammar => hasCrossReferences(grammar)); const importFrom = config.langiumInternal ? `../../syntax-tree${config.importExtension}` : 'langium'; /* eslint-disable @typescript-eslint/indent */ const fileNode = expandToNode ` ${generatedHeader} /* eslint-disable */ import type { AstNode${crossRef ? ', Reference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}'; import { AbstractAstReflection } from '${importFrom}'; ${generateTerminalConstants(grammars, config)} ${joinToNode(astTypes.unions, union => union.toAstTypesString(isAstType(union.type)), { appendNewLineIfNotEmpty: true })} ${joinToNode(astTypes.interfaces, iFace => iFace.toAstTypesString(true), { appendNewLineIfNotEmpty: true })} ${astTypes.unions = astTypes.unions.filter(e => isAstType(e.type)), generateAstReflection(config, astTypes)} `; return toString(fileNode); /* eslint-enable @typescript-eslint/indent */ } function hasCrossReferences(grammar) { return Boolean(AstUtils.streamAllContents(grammar).find(GrammarAST.isCrossReference)); } function generateAstReflection(config, astTypes) { const typeNames = astTypes.interfaces.map(t => t.name) .concat(astTypes.unions.map(t => t.name)) .sort(); const crossReferenceTypes = buildCrossReferenceTypes(astTypes); return expandToNode ` export type ${config.projectName}AstType = { ${joinToNode(typeNames, name => name + ': ' + name, { appendNewLineIfNotEmpty: true })} } export class ${config.projectName}AstReflection extends AbstractAstReflection { getAllTypes(): string[] { return [${typeNames.join(', ')}]; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { ${buildIsSubtypeMethod(astTypes)} } getReferenceType(refInfo: ReferenceInfo): string { ${buildReferenceTypeMethod(crossReferenceTypes)} } getTypeMetaData(type: string): TypeMetaData { ${buildTypeMetaDataMethod(astTypes)} } } export const reflection = new ${config.projectName}AstReflection(); `.appendNewLine(); } function buildTypeMetaDataMethod(astTypes) { /* eslint-disable @typescript-eslint/indent */ return expandToNode ` switch (type) { ${joinToNode(astTypes.interfaces, interfaceType => { const props = interfaceType.superProperties; return (props.length > 0) ? expandToNode ` case ${interfaceType.name}: { return { name: ${interfaceType.name}, properties: [ ${buildPropertyType(props)} ] }; } ` : undefined; }, { appendNewLineIfNotEmpty: true })} default: { return { name: type, properties: [] }; } } `; /* eslint-enable @typescript-eslint/indent */ } function buildPropertyType(props) { const all = props.sort((a, b) => a.name.localeCompare(b.name)); return joinToNode(all, property => { const defaultValue = stringifyDefaultValue(property.defaultValue); return `{ name: '${escapeQuotes(property.name, "'")}'${defaultValue ? `, defaultValue: ${defaultValue}` : ''} }`; }, { separator: ',', appendNewLineIfNotEmpty: true }); } function stringifyDefaultValue(value) { if (typeof value === 'string') { // Escape all double quotes return `'${escapeQuotes(value, "'")}'`; } else if (Array.isArray(value)) { return `[${value.map(e => stringifyDefaultValue(e)).join(', ')}]`; } else if (value !== undefined) { return value.toString(); } else { return undefined; } } function buildReferenceTypeMethod(crossReferenceTypes) { const buckets = new MultiMap(crossReferenceTypes.map(e => [e.referenceType, `${e.type}:${e.feature}`])); /* eslint-disable @typescript-eslint/indent */ return expandToNode ` const referenceId = ${'`${refInfo.container.$type}:${refInfo.property}`'}; switch (referenceId) { ${joinToNode(buckets.entriesGroupedByKey(), ([target, refs]) => expandToNode ` ${joinToNode(refs, ref => `case '${escapeQuotes(ref, "'")}':`, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true })} { return ${target}; } `, { appendNewLineIfNotEmpty: true })} default: { throw new Error(${'`${referenceId} is not a valid reference id.`'}); } } `; /* eslint-enable @typescript-eslint/indent */ } function buildCrossReferenceTypes(astTypes) { const crossReferences = new MultiMap(); for (const typeInterface of astTypes.interfaces) { for (const property of typeInterface.properties.sort((a, b) => a.name.localeCompare(b.name))) { const refTypes = findReferenceTypes(property.type); for (const refType of refTypes) { crossReferences.add(typeInterface.name, { type: typeInterface.name, feature: property.name, referenceType: refType }); } } // Since the types are topologically sorted we can assume // that all super type properties have already been processed for (const superType of typeInterface.interfaceSuperTypes) { const superTypeCrossReferences = crossReferences.get(superType.name).map(e => (Object.assign(Object.assign({}, e), { type: typeInterface.name }))); crossReferences.addAll(typeInterface.name, superTypeCrossReferences); } } return Array.from(crossReferences.values()).sort((a, b) => a.type.localeCompare(b.type)); } function buildIsSubtypeMethod(astTypes) { const groups = groupBySupertypes(astTypes); /* eslint-disable @typescript-eslint/indent */ return expandToNode ` switch (subtype) { ${joinToNode(groups.entriesGroupedByKey(), ([superTypes, typeGroup]) => expandToNode ` ${joinToNode(typeGroup, typeName => `case ${typeName}:`, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true })} { return ${superTypes.split(':').sort().map(e => `this.isSubtype(${e}, supertype)`).join(' || ')}; } `, { appendNewLineIfNotEmpty: true })} default: { return false; } } `; /* eslint-enable @typescript-eslint/indent */ } function groupBySupertypes(astTypes) { const hierarchy = collectTypeHierarchy(mergeTypesAndInterfaces(astTypes)); const superToChild = new MultiMap(); for (const [name, superTypes] of hierarchy.superTypes.entriesGroupedByKey()) { superToChild.add(superTypes.join(':'), name); } return superToChild; } function generateTerminalConstants(grammars, config) { let collection = {}; const keywordTokens = new Set(); grammars.forEach(grammar => { const terminalConstants = collectTerminalRegexps(grammar); collection = Object.assign(Object.assign({}, collection), terminalConstants); for (const keyword of collectKeywords(grammar)) { keywordTokens.add(keyword); } }); const keywordStrings = Array.from(keywordTokens).sort().map((keyword) => JSON.stringify(keyword)); return expandToNode ` export const ${config.projectName}Terminals = { ${joinToNode(Object.entries(collection), ([name, regexp]) => `${name}: ${regexp.toString()},`, { appendNewLineIfNotEmpty: true })} }; export type ${config.projectName}TerminalNames = keyof typeof ${config.projectName}Terminals; export type ${config.projectName}KeywordNames = ${keywordStrings.length > 0 ? keywordStrings.map(keyword => `${EOL} | ${keyword}`).join('') : 'never'}; export type ${config.projectName}TokenNames = ${config.projectName}TerminalNames | ${config.projectName}KeywordNames; `.appendNewLine(); } //# sourceMappingURL=ast-generator.js.map