langium-cli
Version:
CLI for Langium - the language engineering tool
213 lines (192 loc) • 10.7 kB
JavaScript
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import {} from 'langium';
import { expandToNode, joinToNode, toString } from 'langium/generate';
import { collectAst, collectTypeHierarchy, escapeQuotes, findReferenceTypes, isAstType, mergeTypesAndInterfaces } from 'langium/grammar';
import { collectKeywords, collectTerminalRegexps } from './langium-util.js';
import { generatedHeader } from './node-util.js';
function generateAstHeader(langiumConfig) {
const importFrom = langiumConfig.langiumInternal ? `../../syntax-tree${langiumConfig.importExtension}` : 'langium';
return expandToNode `
${generatedHeader}
/* eslint-disable */
import * as langium from '${importFrom}';
`;
}
export function generateAstSingleLanguageProject(services, embeddedGrammar, config) {
const astTypes = collectAst(embeddedGrammar, { services });
const astTypesFiltered = {
interfaces: [...astTypes.interfaces],
unions: astTypes.unions.filter(e => isAstType(e.type)),
};
const fileNode = expandToNode `
${generateAstHeader(config)}
${generateTerminalsAndKeywords([embeddedGrammar], config.projectName)}
${joinToNode(generateTypeDefinitions(astTypes), t => t.generatedCode, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true })}
${generateAstType(config.projectName, astTypesFiltered)}
${generateAstReflection(config.projectName, astTypesFiltered)}
`.appendNewLine();
return toString(fileNode);
}
export function generateAstMultiLanguageProject(services, languages, config) {
const astTypes = collectAst(languages.map(l => l.embeddedGrammar), { services });
const fileNode = expandToNode `
${generateAstHeader(config)}
${joinToNode(// a namespace for each each language with its language-specific elements: reachable terminals & keywords, complete AstType list
languages, language => expandToNode `
/** Contains the reachable terminals & keywords and all available types of the '${language.identifier}' language. */
export namespace ${language.identifier} {
${generateTerminalsAndKeywords([language.embeddedGrammar], '')}
${generateAstType('', collectAst([language.embeddedGrammar], { services, filterNonAstTypeUnions: true }))}
}
`.appendNewLine().appendNewLine())}
// the terminals, keywords and types of the whole '${config.projectName}' project
${ // reachable terminals & keywords for the whole project
generateTerminalsAndKeywordsComposed(languages.map(l => l.identifier), config.projectName)}
${ // AstType list for the whole project
generateAstTypeComposed(languages.map(l => l.identifier), config.projectName)}
// all type definitions of the the whole '${config.projectName}' project
${joinToNode(generateTypeDefinitions(astTypes), t => t.generatedCode, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true })}
${ // reflection for the whole project
astTypes.unions = astTypes.unions.filter(e => isAstType(e.type)), // Note that the `unions` are changed in-place here!
generateAstReflection(config.projectName, astTypes) // Note that here are some more in-place changes!
}
`.appendNewLine();
return toString(fileNode);
}
function generateTypeDefinitions(astTypes) {
return [
...astTypes.unions.map(union => ({ typeName: union.name, generatedCode: union.toAstTypesString(isAstType(union.type)) })),
...astTypes.interfaces.map(iFace => ({ typeName: iFace.name, generatedCode: iFace.toAstTypesString(true) })),
].sort((l, r) => l.typeName.localeCompare(r.typeName));
}
export function getLanguageIdentifier(_config, grammar) {
return grammar.name; // there is a check in the CLI, that the top-level grammar of a language always has a 'name' value!
}
function generateAstType(name, astTypes) {
const typeNames = astTypes.interfaces.map(t => t.name)
.concat(astTypes.unions.map(t => t.name))
.sort();
return expandToNode `
export type ${name}AstType = {
${joinToNode(typeNames, name => name + ': ' + name, { appendNewLineIfNotEmpty: true })}
}
`;
}
function generateAstTypeComposed(identifiers, name) {
identifiers.sort(); // in-place, for a stable order
return expandToNode `
export type ${name}AstType = ${joinToNode(identifiers, identifier => `${identifier}.AstType`, { separator: ' & ' })}
`;
}
function generateAstReflection(name, astTypes) {
const typeNames = astTypes.interfaces.map(t => t.name)
.concat(astTypes.unions.map(t => t.name))
.sort();
const typeHierarchy = collectTypeHierarchy(mergeTypesAndInterfaces(astTypes));
return expandToNode `
export class ${name}AstReflection extends langium.AbstractAstReflection {
override readonly types = {
${joinToNode(typeNames, typeName => {
const interfaceType = astTypes.interfaces.find(t => t.name === typeName);
const unionType = astTypes.unions.find(t => t.name === typeName);
if (interfaceType || unionType) {
const props = interfaceType?.superProperties ?? [];
const superTypes = typeHierarchy.superTypes.get(typeName) || [];
return expandToNode `
${typeName}: {
name: ${typeName}.$type,
properties: {
${buildPropertyMetaData(props, typeName)}
},
superTypes: [${superTypes.map(t => `${t}.$type`).join(', ')}]
}
`;
}
return undefined;
}, { separator: ',', appendNewLineIfNotEmpty: true })}
} as const satisfies langium.AstMetaData
}
export const reflection = new ${name}AstReflection();
`;
}
function buildPropertyMetaData(props, ownerTypeName) {
const all = props.sort((a, b) => a.name.localeCompare(b.name));
return joinToNode(all, property => {
const defaultValue = stringifyDefaultValue(property.defaultValue);
const refTypes = findReferenceTypes(property.type);
const refType = refTypes.length > 0 ? refTypes[0] : undefined;
const attributes = [`name: ${ownerTypeName}.${property.name}`];
if (defaultValue) {
attributes.push(`defaultValue: ${defaultValue}`);
}
if (refType) {
attributes.push(`referenceType: ${refType}.$type`);
}
return expandToNode `
${property.name}: {
${joinToNode(attributes, attr => attr, { separator: ',', appendNewLineIfNotEmpty: true })}
}
`;
}, { 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 generateTerminalsAndKeywords(grammars, name) {
// Collects only reached/used terminals and keywords, i.e. elements which are not reachable, when transitively following the entry rule of the grammars.
// For grammars without entry rule, all elements are collected.
// Called terminal fragments are ignored "because referenced terminals are expanded/inlined
// before registering the relevant terminals in the lexer/generating the regexes to ast.ts.
// You might argue that generating sub terminals might still be useful.
// However, the value converter is always asked for converting terminals being accepted by a expanded terminal regex.
// Thus, using the expanded terminal definition and doing finer evaluations by checking for matched groups according to the actual terminal regex is the natural way to go"
// (https://github.com/eclipse-langium/langium/pull/1979#issuecomment-3089241029).
// Terminals are not alphabetically sorted, since the current order reflects the order of terminals during parsing.
let collection = {};
const keywordTokens = new Set();
grammars.forEach(grammar => {
const terminalConstants = collectTerminalRegexps(grammar); // collects only reachable terminals, ignore called terminal fragments, ignores imported grammars
collection = { ...collection, ...terminalConstants };
for (const keyword of collectKeywords(grammar)) { // collects only reachable keywords, ignores imported grammars
keywordTokens.add(keyword);
}
});
const keywordStrings = Array.from(keywordTokens).sort().map((keyword) => JSON.stringify(keyword));
return expandToNode `
export const ${name}Terminals = {
${joinToNode(Object.entries(collection), ([name, regexp]) => `${name}: ${regexp.toString()},`, { appendNewLineIfNotEmpty: true })}
};
export type ${name}TerminalNames = keyof typeof ${name}Terminals;
export type ${name}KeywordNames =${keywordStrings.length > 0 ? undefined : ' never;'}
${joinToNode(keywordStrings, keyword => `| ${keyword}`, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true })?.append(';')}
export type ${name}TokenNames = ${name}TerminalNames | ${name}KeywordNames;
`;
}
function generateTerminalsAndKeywordsComposed(identifiers, name) {
identifiers.sort(); // in-place, for a stable order
return expandToNode `
export const ${name}Terminals = {
${joinToNode(identifiers, identifier => `...${identifier}.Terminals,`, { appendNewLineIfNotEmpty: true })}
};
export type ${name}TerminalNames = keyof typeof ${name}Terminals;
export type ${name}KeywordNames = ${joinToNode(identifiers, identifier => `${identifier}.KeywordNames`, { separator: ' | ' })};
export type ${name}TokenNames = ${name}TerminalNames | ${name}KeywordNames;
`;
}
//# sourceMappingURL=ast-generator.js.map