UNPKG

langium-cli

Version:

CLI for Langium - the language engineering tool

213 lines (192 loc) 10.7 kB
/****************************************************************************** * 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