langium-cli
Version:
CLI for Langium - the language engineering tool
204 lines (192 loc) • 9.25 kB
JavaScript
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