From 46e00ef30d46024312a5e72813b07157c5c3b9cd Mon Sep 17 00:00:00 2001 From: Christian Schneider Date: Fri, 10 Nov 2023 17:06:41 +0100 Subject: [PATCH] moved generator API to a dedicated package export, solves #1285 * refactored most of our generators to use 'expandToNode' and friends instead of instantiating 'CompositeGeneratorNodes' --- .../domainmodel/src/benchmark/generate.ts | 3 +- examples/domainmodel/src/cli/generator.ts | 5 +- examples/requirements/src/cli/generator.ts | 2 +- examples/statemachine/src/cli/generator.ts | 3 +- examples/statemachine/test/generator.test.ts | 8 +- .../test/yeoman-generator.test.ts | 2 +- .../src/generator/ast-generator.ts | 277 ++++++++---------- .../src/generator/grammar-serializer.ts | 39 ++- .../highlighting/monarch-generator.ts | 88 +++--- .../generator/highlighting/prism-generator.ts | 6 +- .../highlighting/textmate-generator.ts | 6 +- .../src/generator/module-generator.ts | 36 ++- .../src/generator/types-generator.ts | 9 +- packages/langium-cli/src/generator/util.ts | 20 +- .../test/generator/ast-generator.test.ts | 10 +- .../test/generator/types-generator.test.ts | 5 +- .../langium-railroad/src/grammar-railroad.ts | 51 ++-- .../src/language-server/railroad-handler.ts | 9 +- packages/langium/package.json | 4 + .../type-system/type-collector/types.ts | 98 ++++--- packages/langium/src/index.ts | 1 - .../test/generator/generation-tracing.test.ts | 6 +- packages/langium/test/generator/node.test.ts | 2 +- .../test/generator/template-node.test.ts | 3 +- .../test/generator/template-string.test.ts | 2 +- .../grammar/lsp/grammar-formatter.test.ts | 5 +- .../type-system/inferred-types.test.ts | 3 +- .../test/serializer/json-serializer.test.ts | 5 +- packages/langium/test/utils/cst-utils.test.ts | 5 +- 29 files changed, 350 insertions(+), 363 deletions(-) diff --git a/examples/domainmodel/src/benchmark/generate.ts b/examples/domainmodel/src/benchmark/generate.ts index a97c71ac7..215ceede8 100644 --- a/examples/domainmodel/src/benchmark/generate.ts +++ b/examples/domainmodel/src/benchmark/generate.ts @@ -4,7 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { expandToString, URI } from 'langium'; +import { URI } from 'langium'; +import { expandToString } from 'langium/generating'; import type { DomainModelServices } from '../language-server/domain-model-module.js'; export function generateWorkspace(services: DomainModelServices, width: number, size: number): void { diff --git a/examples/domainmodel/src/cli/generator.ts b/examples/domainmodel/src/cli/generator.ts index 8c233c743..21617c51e 100644 --- a/examples/domainmodel/src/cli/generator.ts +++ b/examples/domainmodel/src/cli/generator.ts @@ -4,13 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { IndentNode } from 'langium'; import type { AbstractElement, Domainmodel, Entity, Feature, Type } from '../language-server/generated/ast.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; import chalk from 'chalk'; import _ from 'lodash'; -import { CompositeGeneratorNode, NL, toString } from 'langium'; +import { CompositeGeneratorNode, NL, toString } from 'langium/generating'; import { isEntity, isPackageDeclaration } from '../language-server/generated/ast.js'; import { extractAstNode, extractDestinationAndName, setRootFolder } from './cli-util.js'; import { createDomainModelServices } from '../language-server/domain-model-module.js'; @@ -80,7 +79,7 @@ function generateEntity(entity: Entity, fileNode: CompositeGeneratorNode): void fileNode.append('}', NL); } -function generateFeature(feature: Feature, classBody: IndentNode): [() => void, () => void, () => void] { +function generateFeature(feature: Feature, classBody: CompositeGeneratorNode): [() => void, () => void, () => void] { const name = feature.name; const type = feature.type.$refText + (feature.many ? '[]' : ''); diff --git a/examples/requirements/src/cli/generator.ts b/examples/requirements/src/cli/generator.ts index 973183898..13febe519 100644 --- a/examples/requirements/src/cli/generator.ts +++ b/examples/requirements/src/cli/generator.ts @@ -7,7 +7,7 @@ import type { RequirementModel, TestModel } from '../language-server/generated/ast.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { CompositeGeneratorNode, NL, toString } from 'langium'; +import { CompositeGeneratorNode, NL, toString } from 'langium/generating'; import { extractDestinationAndName } from './cli-util.js'; /** diff --git a/examples/statemachine/src/cli/generator.ts b/examples/statemachine/src/cli/generator.ts index 5517badac..53bf25a8e 100644 --- a/examples/statemachine/src/cli/generator.ts +++ b/examples/statemachine/src/cli/generator.ts @@ -6,8 +6,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { expandToNode as toNode, joinToNode as join, toString } from 'langium'; -import type { Generated } from 'langium'; +import { type Generated, expandToNode as toNode, joinToNode as join, toString } from 'langium/generating'; import type { State, Statemachine } from '../language-server/generated/ast.js'; import { extractDestinationAndName } from './cli-util.js'; diff --git a/examples/statemachine/test/generator.test.ts b/examples/statemachine/test/generator.test.ts index f363cfb4a..4938e8f08 100644 --- a/examples/statemachine/test/generator.test.ts +++ b/examples/statemachine/test/generator.test.ts @@ -4,12 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Generated } from 'langium'; -import type { Statemachine } from '../src/language-server/generated/ast.js'; -import { describe, expect, test } from 'vitest'; -import { EmptyFileSystem, expandToStringWithNL, toString } from 'langium'; +import { EmptyFileSystem } from 'langium'; +import { expandToStringWithNL, toString, type Generated } from 'langium/generating'; import { parseHelper } from 'langium/test'; +import { describe, expect, test } from 'vitest'; import { generateCppContent } from '../src/cli/generator.js'; +import type { Statemachine } from '../src/language-server/generated/ast.js'; import { createStatemachineServices } from '../src/language-server/statemachine-module.js'; describe('Tests the code generator', () => { diff --git a/packages/generator-langium/test/yeoman-generator.test.ts b/packages/generator-langium/test/yeoman-generator.test.ts index 9711ef010..b93fbd4f9 100644 --- a/packages/generator-langium/test/yeoman-generator.test.ts +++ b/packages/generator-langium/test/yeoman-generator.test.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { describe, test } from 'vitest'; -import { normalizeEOL } from 'langium'; +import { normalizeEOL } from 'langium/generating'; import * as path from 'node:path'; import { createHelpers } from 'yeoman-test'; diff --git a/packages/langium-cli/src/generator/ast-generator.ts b/packages/langium-cli/src/generator/ast-generator.ts index c5f638c36..42ba03ab4 100644 --- a/packages/langium-cli/src/generator/ast-generator.ts +++ b/packages/langium-cli/src/generator/ast-generator.ts @@ -3,34 +3,34 @@ * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { GeneratorNode, Grammar, LangiumServices } from 'langium'; +import type { Grammar, LangiumServices } from 'langium'; +import { type Generated, expandToNode, joinToNode, toString } from 'langium/generating'; import type { AstTypes, Property } from 'langium/types'; import type { LangiumConfig } from '../package.js'; -import { IndentNode, CompositeGeneratorNode, NL, toString, streamAllContents, MultiMap, GrammarAST } from 'langium'; +import { streamAllContents, MultiMap, GrammarAST } from 'langium'; import { collectAst, collectTypeHierarchy, findReferenceTypes, hasArrayType, isAstType, hasBooleanType, mergeTypesAndInterfaces } from 'langium/types'; import { collectTerminalRegexps, generatedHeader } from './util.js'; export function generateAst(services: LangiumServices, grammars: Grammar[], config: LangiumConfig): string { const astTypes = collectAst(grammars, services.shared.workspace.LangiumDocuments); - const fileNode = new CompositeGeneratorNode(); - fileNode.append( - generatedHeader, - '/* eslint-disable */', NL, - ); const crossRef = grammars.some(grammar => hasCrossReferences(grammar)); const importFrom = config.langiumInternal ? `../../syntax-tree${config.importExtension}` : 'langium'; - fileNode.append( - `import type { AstNode${crossRef ? ', Reference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}';`, NL, - `import { AbstractAstReflection } from '${importFrom}';`, NL, NL - ); + const fileNode = expandToNode` + ${generatedHeader} - generateTerminalConstants(fileNode, grammars, config); + /* eslint-disable */ + import type { AstNode${crossRef ? ', Reference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}'; + import { AbstractAstReflection } from '${importFrom}'; - astTypes.unions.forEach(union => fileNode.append(union.toAstTypesString(isAstType(union.type)), NL)); - astTypes.interfaces.forEach(iFace => fileNode.append(iFace.toAstTypesString(true), NL)); - astTypes.unions = astTypes.unions.filter(e => isAstType(e.type)); - fileNode.append(generateAstReflection(config, astTypes)); + ${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); } @@ -38,130 +38,111 @@ function hasCrossReferences(grammar: Grammar): boolean { return Boolean(streamAllContents(grammar).find(GrammarAST.isCrossReference)); } -function generateAstReflection(config: LangiumConfig, astTypes: AstTypes): GeneratorNode { +function generateAstReflection(config: LangiumConfig, astTypes: AstTypes): Generated { const typeNames: string[] = astTypes.interfaces.map(t => t.name) .concat(astTypes.unions.map(t => t.name)) .sort(); const crossReferenceTypes = buildCrossReferenceTypes(astTypes); - const reflectionNode = new CompositeGeneratorNode(); - - reflectionNode.append(`export type ${config.projectName}AstType = {`, NL); - reflectionNode.indent(astTypeBody => { - for (const type of typeNames) { - astTypeBody.append(type, ': ', type, NL); + return expandToNode` + export type ${config.projectName}AstType = { + ${joinToNode(typeNames, name => name + ': ' + name, { appendNewLineIfNotEmpty: true })} } - }); - reflectionNode.append('}', NL, NL); - reflectionNode.append( - `export class ${config.projectName}AstReflection extends AbstractAstReflection {`, NL, NL - ); + export class ${config.projectName}AstReflection extends AbstractAstReflection { - reflectionNode.indent(classBody => { - classBody.append('getAllTypes(): string[] {', NL); - classBody.indent(allTypes => { - allTypes.append(`return [${typeNames.map(e => `'${e}'`).join(', ')}];`, NL); - }); - classBody.append( - '}', NL, NL, - 'protected override computeIsSubtype(subtype: string, supertype: string): boolean {', NL, - buildIsSubtypeMethod(astTypes), '}', NL, NL, - 'getReferenceType(refInfo: ReferenceInfo): string {', NL, - buildReferenceTypeMethod(crossReferenceTypes), '}', NL, NL, - 'getTypeMetaData(type: string): TypeMetaData {', NL, - buildTypeMetaDataMethod(astTypes), '}', NL - ); - }); + getAllTypes(): string[] { + return [${typeNames.map(e => `'${e}'`).join(', ')}]; + } - reflectionNode.append( - '}', NL, NL, - `export const reflection = new ${config.projectName}AstReflection();`, NL - ); + protected override computeIsSubtype(subtype: string, supertype: string): boolean { + ${buildIsSubtypeMethod(astTypes)} + } + + getReferenceType(refInfo: ReferenceInfo): string { + ${buildReferenceTypeMethod(crossReferenceTypes)} + } + + getTypeMetaData(type: string): TypeMetaData { + ${buildTypeMetaDataMethod(astTypes)} + } + } - return reflectionNode; + export const reflection = new ${config.projectName}AstReflection(); + `.appendNewLine(); } -function buildTypeMetaDataMethod(astTypes: AstTypes): GeneratorNode { - const typeSwitchNode = new IndentNode(); - typeSwitchNode.append('switch (type) {', NL); - typeSwitchNode.indent(caseNode => { - for (const interfaceType of astTypes.interfaces) { - const props = interfaceType.properties; - const arrayProps = props.filter(e => hasArrayType(e.type)); - const booleanProps = props.filter(e => hasBooleanType(e.type)); - if (arrayProps.length > 0 || booleanProps.length > 0) { - caseNode.append(`case '${interfaceType.name}': {`, NL); - caseNode.indent(caseContent => { - caseContent.append('return {', NL); - caseContent.indent(returnType => { - returnType.append(`name: '${interfaceType.name}',`, NL); - returnType.append( - 'mandatory: [', NL, - buildMandatoryType(arrayProps, booleanProps), - ']', NL); - }); - caseContent.append('};', NL); - }); - caseNode.append('}', NL); +function buildTypeMetaDataMethod(astTypes: AstTypes): Generated { + return expandToNode` + switch (type) { + ${ + joinToNode( + astTypes.interfaces, + interfaceType => { + const props = interfaceType.properties; + const arrayProps = props.filter(e => hasArrayType(e.type)); + const booleanProps = props.filter(e => hasBooleanType(e.type)); + return (arrayProps.length > 0 || booleanProps.length > 0) + ? expandToNode` + case '${interfaceType.name}': { + return { + name: '${interfaceType.name}', + mandatory: [ + ${buildMandatoryType(arrayProps, booleanProps)} + ] + }; + } + ` + : undefined; + }, + { + appendNewLineIfNotEmpty: true + } + ) + } + default: { + return { + name: type, + mandatory: [] + }; } } - caseNode.append('default: {', NL); - caseNode.indent(defaultNode => { - defaultNode.append('return {', NL); - defaultNode.indent(defaultType => { - defaultType.append( - 'name: type,', NL, - 'mandatory: []', NL - ); - }); - defaultNode.append('};', NL); - }); - caseNode.append('}', NL); - }); - typeSwitchNode.append('}', NL); - return typeSwitchNode; + `; } -function buildMandatoryType(arrayProps: Property[], booleanProps: Property[]): GeneratorNode { - const indent = new IndentNode(); +function buildMandatoryType(arrayProps: Property[], booleanProps: Property[]): Generated { const all = arrayProps.concat(booleanProps).sort((a, b) => a.name.localeCompare(b.name)); - for (let i = 0; i < all.length; i++) { - const property = all[i]; - const type = arrayProps.includes(property) ? 'array' : 'boolean'; - indent.append("{ name: '", property.name, "', type: '", type, "' }", i < all.length - 1 ? ',' : '', NL); - } - return indent; + + return joinToNode( + all, + property => { + const type = arrayProps.includes(property) ? 'array' : 'boolean'; + return `{ name: '${property.name}', type: '${type}' }` + }, + { separator: ',', appendNewLineIfNotEmpty: true} + ); } -function buildReferenceTypeMethod(crossReferenceTypes: CrossReferenceType[]): GeneratorNode { - const typeSwitchNode = new IndentNode(); +function buildReferenceTypeMethod(crossReferenceTypes: CrossReferenceType[]): Generated { const buckets = new MultiMap(crossReferenceTypes.map(e => [e.referenceType, `${e.type}:${e.feature}`])); - typeSwitchNode.append('const referenceId = `${refInfo.container.$type}:${refInfo.property}`;', NL); - typeSwitchNode.append('switch (referenceId) {', NL); - typeSwitchNode.indent(caseNode => { - for (const [target, refs] of buckets.entriesGroupedByKey()) { - for (let i = 0; i < refs.length; i++) { - const ref = refs[i]; - caseNode.append(`case '${ref}':`); - if (i === refs.length - 1) { - caseNode.append(' {', NL); - } else { - caseNode.append(NL); - } + return expandToNode` + const referenceId = ${'`${refInfo.container.$type}:${refInfo.property}`'}; + switch (referenceId) { + ${ + joinToNode( + buckets.entriesGroupedByKey(), + ([target, refs]) => expandToNode` + ${joinToNode(refs, ref => `case '${ref}':`, { appendNewLineIfNotEmpty: true, skipNewLineAfterLastItem: true})} { + return ${target}; + } + `, + { appendNewLineIfNotEmpty: true } + ) + } + default: { + throw new Error(${'`${referenceId} is not a valid reference id.`'}); } - caseNode.indent(caseContent => { - caseContent.append(`return ${target};`, NL); - }); - caseNode.append('}', NL); } - caseNode.append('default: {', NL); - caseNode.indent(defaultNode => { - defaultNode.append('throw new Error(`${referenceId} is not a valid reference id.`);', NL); - }); - caseNode.append('}', NL); - }); - typeSwitchNode.append('}', NL); - return typeSwitchNode; + `; } type CrossReferenceType = { @@ -196,34 +177,26 @@ function buildCrossReferenceTypes(astTypes: AstTypes): CrossReferenceType[] { return Array.from(crossReferences.values()).sort((a, b) => a.type.localeCompare(b.type)); } -function buildIsSubtypeMethod(astTypes: AstTypes): GeneratorNode { - const methodNode = new IndentNode(); - methodNode.append( - 'switch (subtype) {', NL - ); - methodNode.indent(switchNode => { - const groups = groupBySupertypes(astTypes); - - for (const [superTypes, typeGroup] of groups.entriesGroupedByKey()) { - for (const typeName of typeGroup) { - switchNode.append(`case ${typeName}:`, NL); +function buildIsSubtypeMethod(astTypes: AstTypes): Generated { + const groups = groupBySupertypes(astTypes); + 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; } - switchNode.contents.pop(); - switchNode.append(' {', NL); - switchNode.indent(caseNode => { - caseNode.append(`return ${superTypes.split(':').sort().map(e => `this.isSubtype(${e}, supertype)`).join(' || ')};`); - }); - switchNode.append(NL, '}', NL); } - - switchNode.append('default: {', NL); - switchNode.indent(defaultNode => { - defaultNode.append('return false;', NL); - }); - switchNode.append('}', NL); - }); - methodNode.append('}', NL); - return methodNode; + `; } function groupBySupertypes(astTypes: AstTypes): MultiMap { @@ -236,19 +209,17 @@ function groupBySupertypes(astTypes: AstTypes): MultiMap { return superToChild; } -function generateTerminalConstants(fileNode: CompositeGeneratorNode, grammars: Grammar[], config: LangiumConfig) { +function generateTerminalConstants(grammars: Grammar[], config: LangiumConfig): Generated { let collection: Record = {}; grammars.forEach(grammar => { const terminalConstants = collectTerminalRegexps(grammar); collection = {...collection, ...terminalConstants}; }); - fileNode.append(`export const ${config.projectName}Terminals = {`, NL); - fileNode.indent(node => { - for (const [name, regexp] of Object.entries(collection)) { - node.append(`${name}: ${regexp.toString()},`, NL); - } - }); - fileNode.append('};', NL, NL); + return expandToNode` + export const ${config.projectName}Terminals = { + ${joinToNode(Object.entries(collection), ([name, regexp]) => `${name}: ${regexp.toString()},`, { appendNewLineIfNotEmpty: true })} + }; + `.appendNewLine(); } diff --git a/packages/langium-cli/src/generator/grammar-serializer.ts b/packages/langium-cli/src/generator/grammar-serializer.ts index 94e622038..ed1f54c8d 100644 --- a/packages/langium-cli/src/generator/grammar-serializer.ts +++ b/packages/langium-cli/src/generator/grammar-serializer.ts @@ -4,26 +4,25 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { URI } from 'vscode-uri'; import type { Grammar, LangiumServices, Reference } from 'langium'; +import { expandToNode, normalizeEOL, toString } from 'langium/generating'; +import type { URI } from 'vscode-uri'; import type { LangiumConfig } from '../package.js'; -import { CompositeGeneratorNode, NL, normalizeEOL, toString } from 'langium'; import { generatedHeader } from './util.js'; export function serializeGrammar(services: LangiumServices, grammars: Grammar[], config: LangiumConfig): string { - const node = new CompositeGeneratorNode(); - node.append(generatedHeader); - - if (config.langiumInternal) { - node.append( - `import type { Grammar } from './ast${config.importExtension}';`, NL, - `import { loadGrammarFromJson } from '../../utils/grammar-util${config.importExtension}';`); - } else { - node.append( - "import type { Grammar } from 'langium';", NL, - "import { loadGrammarFromJson } from 'langium';"); - } - node.append(NL, NL); + const node = expandToNode` + ${generatedHeader} + + + `.appendTemplateIf(!!config.langiumInternal)` + import type { Grammar } from './ast${config.importExtension}'; + import { loadGrammarFromJson } from '../../utils/grammar-util${config.importExtension}'; + `.appendTemplateIf(!config.langiumInternal)` + import type { Grammar } from 'langium'; + import { loadGrammarFromJson } from 'langium'; + `.appendNewLine() + .appendNewLine(); for (let i = 0; i < grammars.length; i++) { const grammar = grammars[i]; @@ -43,12 +42,12 @@ export function serializeGrammar(services: LangiumServices, grammars: Grammar[], const json = normalizeEOL(serializedGrammar .replace(/\\/g, '\\\\') .replace(new RegExp(delimiter, 'g'), '\\' + delimiter)); - node.append( - 'let loaded', grammar.name, 'Grammar: Grammar | undefined;', NL, - 'export const ', grammar.name, 'Grammar = (): Grammar => loaded', grammar.name, 'Grammar ?? (loaded', grammar.name, 'Grammar = loadGrammarFromJson(', delimiter, json, delimiter, '));', NL - ); + node.appendTemplate` + let loaded${grammar.name}Grammar: Grammar | undefined; + export const ${grammar.name}Grammar = (): Grammar => loaded${grammar.name}Grammar ?? (loaded${grammar.name}Grammar = loadGrammarFromJson(${delimiter}${json}${delimiter})); + `.appendNewLine(); if (i < grammars.length - 1) { - node.append(NL); + node.appendNewLine(); } } } diff --git a/packages/langium-cli/src/generator/highlighting/monarch-generator.ts b/packages/langium-cli/src/generator/highlighting/monarch-generator.ts index cd1e9e4b9..896d6c734 100644 --- a/packages/langium-cli/src/generator/highlighting/monarch-generator.ts +++ b/packages/langium-cli/src/generator/highlighting/monarch-generator.ts @@ -4,10 +4,10 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Grammar } from 'langium'; -import type { LangiumLanguageConfig } from '../../package.js'; -import { getTerminalParts, isCommentTerminal, CompositeGeneratorNode, NL, toString, escapeRegExp, GrammarAST, isWhitespaceRegExp } from 'langium'; +import { type Grammar, getTerminalParts, isCommentTerminal, escapeRegExp, GrammarAST, isWhitespaceRegExp } from 'langium'; +import { type Generated, expandToNode, joinToNode, toString } from 'langium/generating'; import { terminalRegex } from 'langium/internal'; +import type { LangiumLanguageConfig } from '../../package.js'; import { collectKeywords } from '../util.js'; /** @@ -190,22 +190,14 @@ function getTokenizerStates(grammar: Grammar): State[] { */ function prettyPrint(monarchGrammar: MonarchGrammar): string { const name = monarchGrammar.languageDefinition.name; - const node = new CompositeGeneratorNode( - `// Monarch syntax highlighting for the ${name} language.`, NL, - 'export default {', NL - ); - - node.indent(grammarDef => { - // add language definitions - prettyPrintLangDef(monarchGrammar.languageDefinition, grammarDef); - grammarDef.append(NL, NL); + const node = expandToNode` + // Monarch syntax highlighting for the ${name} language. + export default { + ${prettyPrintLangDef(monarchGrammar.languageDefinition)} - // add tokenizer parts, simple state machine groupings - prettyPrintTokenizer(monarchGrammar.tokenizer, grammarDef); - grammarDef.append(NL); - - }); - node.append('};', NL); + ${prettyPrintTokenizer(monarchGrammar.tokenizer)} + }; + `.appendNewLine(); return toString(node); } @@ -216,13 +208,12 @@ function prettyPrint(monarchGrammar: MonarchGrammar): string { * @param values Values to add under the given category * @returns GeneratorNode containing this printed language definition entry */ -function genLanguageDefEntry(name: string, values: string[]): CompositeGeneratorNode { - const node = new CompositeGeneratorNode(`${name}: [`, NL); - node.indent(langDefValues => { - langDefValues.append(values.map(v => `'${v}'`).join(',')); - }); - node.append(NL, '],'); - return node; +function genLanguageDefEntry(name: string, values: string[]): Generated { + return expandToNode` + ${name}: [ + ${ values.map(v => `'${v}'`).join(',') } + ], + `; } /** @@ -230,13 +221,13 @@ function genLanguageDefEntry(name: string, values: string[]): CompositeGenerator * @param languageDef LanguageDefinition to pretty print * @param node Existing generator node to append printed language definition to */ -function prettyPrintLangDef(languageDef: LanguageDefinition, node: CompositeGeneratorNode): void { - node.append( - genLanguageDefEntry('keywords', languageDef.keywords), NL, - genLanguageDefEntry('operators', languageDef.operators), NL, - // special case, identify symbols via singular regex - `symbols: ${new RegExp(languageDef.symbols.map(escapeRegExp).join('|')).toString()},` - ); +function prettyPrintLangDef(languageDef: LanguageDefinition): Generated { + return expandToNode` + ${genLanguageDefEntry('keywords', languageDef.keywords)} + ${genLanguageDefEntry('operators', languageDef.operators)} + ${/* special case, identify symbols via singular regex*/ undefined} + symbols: ${new RegExp(languageDef.symbols.map(escapeRegExp).join('|')).toString()}, + `; } /** @@ -244,15 +235,12 @@ function prettyPrintLangDef(languageDef: LanguageDefinition, node: CompositeGene * @param tokenizer Tokenizer portion to print out * @param node Existing generator node to append printed tokenizer to */ -function prettyPrintTokenizer(tokenizer: Tokenizer, node: CompositeGeneratorNode): void { - node.append('tokenizer: {', NL); - node.indent(tokenizerStates => { - for (const state of tokenizer.states) { - prettyPrintState(state, tokenizerStates); - tokenizerStates.append(NL); +function prettyPrintTokenizer(tokenizer: Tokenizer): Generated { + return expandToNode` + tokenizer: { + ${joinToNode(tokenizer.states, prettyPrintState, { appendNewLineIfNotEmpty: true})} } - }); - node.append('}'); + `; } /** @@ -260,14 +248,12 @@ function prettyPrintTokenizer(tokenizer: Tokenizer, node: CompositeGeneratorNode * @param state Tokenizer state to pretty print * @param node Existing enerator node to append printed state to */ -function prettyPrintState(state: State, node: CompositeGeneratorNode): void { - node.append(state.name + ': [', NL); - node.indent(inode => { - for (const rule of state.rules) { - inode.append(prettyPrintRule(rule), NL); - } - }); - node.append('],'); +function prettyPrintState(state: State): Generated { + return expandToNode` + ${state.name}: [ + ${joinToNode(state.rules, prettyPrintRule, { appendNewLineIfNotEmpty: true })} + ], + `; } /** @@ -276,14 +262,14 @@ function prettyPrintState(state: State, node: CompositeGeneratorNode): void { * @param ruleOrState Rule to pretty print. If it's a state, we include that state's contents implicitly within this context. * @returns Generator node containing this printed rule */ -function prettyPrintRule(ruleOrState: Rule | State): CompositeGeneratorNode { +function prettyPrintRule(ruleOrState: Rule | State): Generated { if (isRule(ruleOrState)) { // extract rule pattern, either just a string or a regex w/ parts const rulePatt = ruleOrState.regex instanceof RegExp ? ruleOrState.regex : new RegExp(ruleOrState.regex); - return new CompositeGeneratorNode('{ regex: ' + rulePatt.toString() + ', action: ' + prettyPrintAction(ruleOrState.action) + ' },'); + return expandToNode`{ regex: ${rulePatt.toString()}, action: ${prettyPrintAction(ruleOrState.action)} },`; } else { // include another state by name, implicitly includes all of its contents - return new CompositeGeneratorNode(`{ include: '@${ruleOrState.name}' },`); + return expandToNode`{ include: '@${ruleOrState.name}' },`; } } diff --git a/packages/langium-cli/src/generator/highlighting/prism-generator.ts b/packages/langium-cli/src/generator/highlighting/prism-generator.ts index e01964046..cfc6b9638 100644 --- a/packages/langium-cli/src/generator/highlighting/prism-generator.ts +++ b/packages/langium-cli/src/generator/highlighting/prism-generator.ts @@ -3,11 +3,11 @@ * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Grammar } from 'langium'; -import type { LangiumLanguageConfig } from '../../package.js'; -import { CompositeGeneratorNode, escapeRegExp, GrammarAST, isCommentTerminal, NL, toString } from 'langium'; +import { GrammarAST, escapeRegExp, isCommentTerminal, type Grammar } from 'langium'; +import { CompositeGeneratorNode, NL, toString } from 'langium/generating'; import { terminalRegex } from 'langium/internal'; import _ from 'lodash'; +import type { LangiumLanguageConfig } from '../../package.js'; import { collectKeywords } from '../util.js'; interface HighlightElement { diff --git a/packages/langium-cli/src/generator/highlighting/textmate-generator.ts b/packages/langium-cli/src/generator/highlighting/textmate-generator.ts index 009311fe7..097c1c275 100644 --- a/packages/langium-cli/src/generator/highlighting/textmate-generator.ts +++ b/packages/langium-cli/src/generator/highlighting/textmate-generator.ts @@ -4,9 +4,9 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ import type { Grammar } from 'langium'; -import type { LangiumLanguageConfig } from '../../package.js'; -import { EOL, escapeRegExp, getCaseInsensitivePattern, getTerminalParts, GrammarAST, isCommentTerminal, stream } from 'langium'; +import { escapeRegExp, getCaseInsensitivePattern, getTerminalParts, GrammarAST, isCommentTerminal, stream } from 'langium'; import { terminalRegex } from 'langium/internal'; +import type { LangiumLanguageConfig } from '../../package.js'; import { collectKeywords } from '../util.js'; /* eslint-disable dot-notation */ @@ -57,7 +57,7 @@ export function generateTextMate(grammar: Grammar, config: LangiumLanguageConfig repository: getRepository(grammar, config) }; - return JSON.stringify(json, null, 2) + EOL; + return JSON.stringify(json, null, 2) + '\n'; } function getPatterns(grammar: Grammar, config: LangiumLanguageConfig): Pattern[] { diff --git a/packages/langium-cli/src/generator/module-generator.ts b/packages/langium-cli/src/generator/module-generator.ts index 4e04b944a..ee529f12a 100644 --- a/packages/langium-cli/src/generator/module-generator.ts +++ b/packages/langium-cli/src/generator/module-generator.ts @@ -5,16 +5,18 @@ ******************************************************************************/ import type { Grammar, IParserConfig } from 'langium'; +import { Generated, NL, expandToNode, joinToNode, toString } from 'langium/generating'; import type { LangiumConfig, LangiumLanguageConfig } from '../package.js'; -import { CompositeGeneratorNode, NL, toString } from 'langium'; import { generatedHeader } from './util.js'; export function generateModule(grammars: Grammar[], config: LangiumConfig, grammarConfigMap: Map): string { const parserConfig = config.chevrotainParserConfig; const hasIParserConfigImport = Boolean(parserConfig) || grammars.some(grammar => grammarConfigMap.get(grammar)?.chevrotainParserConfig !== undefined); - const node = new CompositeGeneratorNode(); + const node = expandToNode` + ${generatedHeader} + `.appendNewLine() + .appendNewLine(); - node.append(generatedHeader); if (config.langiumInternal) { node.append(`import type { LanguageMetaData } from '../language-meta-data${config.importExtension}';`, NL); node.append(`import type { Module } from '../../dependency-injection${config.importExtension}';`, NL); @@ -61,14 +63,18 @@ export function generateModule(grammars: Grammar[], config: LangiumConfig, gramm const grammarConfig = grammarConfigMap.get(grammar)!; const grammarParserConfig = grammarConfig.chevrotainParserConfig; if (grammarParserConfig && grammar.name) { - node.append('export const ', grammar.name, 'ParserConfig: IParserConfig = ', generateParserConfig(grammarParserConfig)); + node.append('export const ', grammar.name, 'ParserConfig: IParserConfig = ', generateParserConfig(grammarParserConfig)) + .appendNewLine() + .appendNewLine(); } else { needsGeneralParserConfig = true; } } if (needsGeneralParserConfig && parserConfig) { - node.append('export const parserConfig: IParserConfig = ', generateParserConfig(parserConfig)); + node.append('export const parserConfig: IParserConfig = ', generateParserConfig(parserConfig)) + .appendNewLine() + .appendNewLine(); } node.append('export const ', config.projectName, 'GeneratedSharedModule: Module = {', NL); @@ -111,16 +117,16 @@ export function generateModule(grammars: Grammar[], config: LangiumConfig, gramm return toString(node); } -function generateParserConfig(config: IParserConfig): CompositeGeneratorNode { - const node = new CompositeGeneratorNode(); - node.append('{', NL); - node.indent(configNode => { - for (const [key, value] of Object.entries(config)) { - configNode.append(`${key}: ${typeof value === 'string' ? `'${value}'` : value},`, NL); - } - }); - node.append('};', NL, NL); - return node; +function generateParserConfig(config: IParserConfig): Generated { + return expandToNode` + { + ${joinToNode( + Object.entries(config), + ([key, value]) => `${key}: ${typeof value === 'string' ? `'${value}'` : value},`, + { appendNewLineIfNotEmpty: true } + )} + }; + `; } function appendQuotesAndDot(input: string): string { diff --git a/packages/langium-cli/src/generator/types-generator.ts b/packages/langium-cli/src/generator/types-generator.ts index e0595371c..b82272de8 100644 --- a/packages/langium-cli/src/generator/types-generator.ts +++ b/packages/langium-cli/src/generator/types-generator.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ import type { Grammar, LangiumServices } from 'langium'; -import { CompositeGeneratorNode, NL, toString } from 'langium'; +import { joinToNode, toString } from 'langium/generating'; import { collectAst } from 'langium/types'; import { LangiumGrammarGrammar } from 'langium/internal'; import { collectKeywords } from './util.js'; @@ -12,10 +12,11 @@ import { collectKeywords } from './util.js'; export function generateTypesFile(services: LangiumServices, grammars: Grammar[]): string { const { unions, interfaces } = collectAst(grammars, services.shared.workspace.LangiumDocuments); const reservedWords = new Set(collectKeywords(LangiumGrammarGrammar())); - const fileNode = new CompositeGeneratorNode(); - unions.forEach(union => fileNode.append(union.toDeclaredTypesString(reservedWords)).append(NL)); - interfaces.forEach(iFace => fileNode.append(iFace.toDeclaredTypesString(reservedWords)).append(NL)); + const fileNode = joinToNode([ + joinToNode(unions, union => union.toDeclaredTypesString(reservedWords), { appendNewLineIfNotEmpty: true }), + joinToNode(interfaces, iFace => iFace.toDeclaredTypesString(reservedWords), { appendNewLineIfNotEmpty: true }) + ]); return toString(fileNode); } diff --git a/packages/langium-cli/src/generator/util.ts b/packages/langium-cli/src/generator/util.ts index b6ca80b12..929d027f4 100644 --- a/packages/langium-cli/src/generator/util.ts +++ b/packages/langium-cli/src/generator/util.ts @@ -3,8 +3,8 @@ * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { GeneratorNode, Grammar } from 'langium'; -import { CompositeGeneratorNode, getAllReachableRules, GrammarAST, NL, stream, streamAllContents } from 'langium'; +import { type Grammar, getAllReachableRules, GrammarAST, stream, streamAllContents } from 'langium'; +import { type Generated, expandToNode } from 'langium/generating'; import fs from 'fs-extra'; import * as path from 'node:path'; import * as url from 'node:url'; @@ -49,15 +49,13 @@ function getLangiumCliVersion(): string { return pack.version; } -function getGeneratedHeader(): GeneratorNode { - const node = new CompositeGeneratorNode(); - node.contents.push( - '/******************************************************************************', NL, - ` * This file was generated by langium-cli ${cliVersion}.`, NL, - ' * DO NOT EDIT MANUALLY!', NL, - ' ******************************************************************************/', NL, NL - ); - return node; +function getGeneratedHeader(): Generated { + return expandToNode` + /****************************************************************************** + * This file was generated by langium-cli ${cliVersion}. + * DO NOT EDIT MANUALLY! + ******************************************************************************/ + `; } export function collectKeywords(grammar: Grammar): string[] { diff --git a/packages/langium-cli/test/generator/ast-generator.test.ts b/packages/langium-cli/test/generator/ast-generator.test.ts index 7d08e04f7..e892fd611 100644 --- a/packages/langium-cli/test/generator/ast-generator.test.ts +++ b/packages/langium-cli/test/generator/ast-generator.test.ts @@ -4,13 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Grammar } from 'langium'; -import type { LangiumConfig } from '../../src/package.js'; -import { describe, expect, test } from 'vitest'; -import { createLangiumGrammarServices, EmptyFileSystem, expandToString, normalizeEOL } from 'langium'; +import { EmptyFileSystem, createLangiumGrammarServices, type Grammar } from 'langium'; +import { expandToString, normalizeEOL } from 'langium/generating'; import { parseHelper } from 'langium/test'; -import { RelativePath } from '../../src/package.js'; +import { describe, expect, test } from 'vitest'; import { generateAst } from '../../src/generator/ast-generator.js'; +import type { LangiumConfig } from '../../src/package.js'; +import { RelativePath } from '../../src/package.js'; const services = createLangiumGrammarServices(EmptyFileSystem); const parse = parseHelper(services.grammar); diff --git a/packages/langium-cli/test/generator/types-generator.test.ts b/packages/langium-cli/test/generator/types-generator.test.ts index 29c190b03..0835f3140 100644 --- a/packages/langium-cli/test/generator/types-generator.test.ts +++ b/packages/langium-cli/test/generator/types-generator.test.ts @@ -5,9 +5,10 @@ ******************************************************************************/ import type { Grammar } from 'langium'; -import { describe, expect, test } from 'vitest'; -import { createLangiumGrammarServices, EmptyFileSystem, expandToStringWithNL } from 'langium'; +import { EmptyFileSystem, createLangiumGrammarServices } from 'langium'; +import { expandToStringWithNL } from 'langium/generating'; import { parseHelper } from 'langium/test'; +import { describe, expect, test } from 'vitest'; import { generateTypesFile } from '../../src/generator/types-generator.js'; const { grammar } = createLangiumGrammarServices(EmptyFileSystem); diff --git a/packages/langium-railroad/src/grammar-railroad.ts b/packages/langium-railroad/src/grammar-railroad.ts index 6a7e69e31..1cb3023a6 100644 --- a/packages/langium-railroad/src/grammar-railroad.ts +++ b/packages/langium-railroad/src/grammar-railroad.ts @@ -4,7 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { EOL, GrammarAST, expandToString, findNameAssignment } from 'langium'; +import { GrammarAST, findNameAssignment } from 'langium'; +import { expandToStringLF, expandToStringLFWithNL } from 'langium/generating'; import type { FakeSVG } from 'railroad-diagrams'; import { default as rr } from 'railroad-diagrams'; @@ -40,14 +41,17 @@ export interface GrammarDiagramOptions { } function styling(options?: GrammarDiagramOptions) { - return expandToString` - -${options?.css ? expandToString` -` : ''}`; + return expandToStringLF` + + ${options?.css ? expandToStringLF` + + ` : '' + } + `; } /** @@ -60,19 +64,22 @@ ${options?.css ? expandToString` * @returns A complete HTML document containing all diagrams. */ export function createGrammarDiagramHtml(rules: GrammarAST.ParserRule[], options?: GrammarDiagramOptions): string { - let text = ` - - -${styling(options)}${options?.javascript ? ` -` : ''} - - -`; + let text = expandToStringLFWithNL` + + + + ${styling(options)}${options?.javascript ? ` + ` : ''} + + + `; text += createGrammarDiagram(rules); - text += ` -`; + text += expandToStringLF` + + + `; return text; } @@ -104,7 +111,7 @@ export function createGrammarDiagramSvg(rules: GrammarAST.ParserRule[], options? export function createGrammarDiagram(rules: GrammarAST.ParserRule[]): string { const text: string[] = []; for (const nonTerminal of rules) { - text.push('

', nonTerminal.name, '

', EOL, createRuleDiagram(nonTerminal)); + text.push('

', nonTerminal.name, '

', '\n', createRuleDiagram(nonTerminal)); } return text.join(''); } diff --git a/packages/langium-vscode/src/language-server/railroad-handler.ts b/packages/langium-vscode/src/language-server/railroad-handler.ts index b520ff076..559184d78 100644 --- a/packages/langium-vscode/src/language-server/railroad-handler.ts +++ b/packages/langium-vscode/src/language-server/railroad-handler.ts @@ -5,12 +5,13 @@ ******************************************************************************/ import type { Grammar, LangiumServices } from 'langium'; -import { DocumentState, GrammarAST, URI, expandToString } from 'langium'; -import type { Connection} from 'vscode-languageserver'; -import { DiagnosticSeverity } from 'vscode-languageserver'; -import { DOCUMENTS_VALIDATED_NOTIFICATION, RAILROAD_DIAGRAM_REQUEST } from './messages.js'; +import { DocumentState, GrammarAST, URI } from 'langium'; import { createGrammarDiagramHtml } from 'langium-railroad'; +import { expandToString } from 'langium/generating'; import { resolveTransitiveImports } from 'langium/internal'; +import type { Connection } from 'vscode-languageserver'; +import { DiagnosticSeverity } from 'vscode-languageserver'; +import { DOCUMENTS_VALIDATED_NOTIFICATION, RAILROAD_DIAGRAM_REQUEST } from './messages.js'; export function registerRailroadConnectionHandler(connection: Connection, services: LangiumServices): void { const documentBuilder = services.shared.workspace.DocumentBuilder; diff --git a/packages/langium/package.json b/packages/langium/package.json index 8eaf6b6c6..59d06a93a 100644 --- a/packages/langium/package.json +++ b/packages/langium/package.json @@ -30,6 +30,10 @@ "types": "./lib/index.d.ts", "import": "./lib/index.js" }, + "./generating": { + "types": "./lib/generator/index.d.ts", + "import": "./lib/generator/index.js" + }, "./test": { "types": "./lib/test/index.d.ts", "import": "./lib/test/index.js" diff --git a/packages/langium/src/grammar/type-system/type-collector/types.ts b/packages/langium/src/grammar/type-system/type-collector/types.ts index 520cfe6bf..100b25d21 100644 --- a/packages/langium/src/grammar/type-system/type-collector/types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/types.ts @@ -4,9 +4,9 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { CompositeGeneratorNode, expandToNode, expandToStringWithNL, joinToNode, toString, type Generated } from '../../../generator/index.js'; import type { CstNode } from '../../../syntax-tree.js'; -import type { Assignment, Action, TypeAttribute } from '../../generated/ast.js'; -import { CompositeGeneratorNode, NL, toString } from '../../../generator/generator-node.js'; +import type { Action, Assignment, TypeAttribute } from '../../generated/ast.js'; import { distinctAndSorted } from '../types-util.js'; export interface Property { @@ -119,25 +119,27 @@ export class UnionType { } toAstTypesString(reflectionInfo: boolean): string { - const unionNode = new CompositeGeneratorNode(); - unionNode.append(`export type ${this.name} = ${propertyTypeToString(this.type, 'AstType')};`, NL); + const unionNode = expandToNode` + export type ${this.name} = ${propertyTypeToString(this.type, 'AstType')}; + `.appendNewLine(); if (reflectionInfo) { - unionNode.append(NL); - pushReflectionInfo(unionNode, this.name); + unionNode.appendNewLine() + .append(addReflectionInfo(this.name)); } if (this.dataType) { - pushDataTypeReflectionInfo(unionNode, this); + unionNode.appendNewLine() + .append(addDataTypeReflectionInfo(this)); } return toString(unionNode); } toDeclaredTypesString(reservedWords: Set): string { - const unionNode = new CompositeGeneratorNode(); - unionNode.append(`type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypeToString(this.type, 'DeclaredType')};`, NL); - return toString(unionNode); + return expandToStringWithNL` + type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypeToString(this.type, 'DeclaredType')}; + ` } } @@ -214,26 +216,29 @@ export class InterfaceType { } toAstTypesString(reflectionInfo: boolean): string { - const interfaceNode = new CompositeGeneratorNode(); - const interfaceSuperTypes = this.interfaceSuperTypes.map(e => e.name); const superTypes = interfaceSuperTypes.length > 0 ? distinctAndSorted([...interfaceSuperTypes]) : ['AstNode']; - interfaceNode.append(`export interface ${this.name} extends ${superTypes.join(', ')} {`, NL); + const interfaceNode = expandToNode` + export interface ${this.name} extends ${superTypes.join(', ')} { + `.appendNewLine(); interfaceNode.indent(body => { if (this.containerTypes.size > 0) { - body.append(`readonly $container: ${distinctAndSorted([...this.containerTypes].map(e => e.name)).join(' | ')};`, NL); + body.append(`readonly $container: ${distinctAndSorted([...this.containerTypes].map(e => e.name)).join(' | ')};`).appendNewLine(); } if (this.typeNames.size > 0) { - body.append(`readonly $type: ${distinctAndSorted([...this.typeNames]).map(e => `'${e}'`).join(' | ')};`, NL); + body.append(`readonly $type: ${distinctAndSorted([...this.typeNames]).map(e => `'${e}'`).join(' | ')};`).appendNewLine(); } - pushProperties(body, this.properties, 'AstType'); + body.append( + pushProperties(this.properties, 'AstType') + ) }); - interfaceNode.append('}', NL); + interfaceNode.append('}').appendNewLine(); if (reflectionInfo) { - interfaceNode.append(NL); - pushReflectionInfo(interfaceNode, this.name); + interfaceNode + .appendNewLine() + .append(addReflectionInfo(this.name)); } return toString(interfaceNode); @@ -244,11 +249,13 @@ export class InterfaceType { const name = escapeReservedWords(this.name, reservedWords); const superTypes = distinctAndSorted(this.interfaceSuperTypes.map(e => e.name)).join(', '); - interfaceNode.append(`interface ${name}${superTypes.length > 0 ? ` extends ${superTypes}` : ''} {`, NL); + interfaceNode.appendTemplate` + interface ${name}${superTypes.length > 0 ? ` extends ${superTypes}` : ''} { + `.appendNewLine(); - interfaceNode.indent(body => pushProperties(body, this.properties, 'DeclaredType', reservedWords)); + interfaceNode.indent([ pushProperties(this.properties, 'DeclaredType', reservedWords) ]); - interfaceNode.append('}', NL); + interfaceNode.append('}').appendNewLine(); return toString(interfaceNode); } } @@ -380,11 +387,10 @@ function typeParenthesis(type: PropertyType, name: string): string { } function pushProperties( - node: CompositeGeneratorNode, properties: Property[], mode: 'AstType' | 'DeclaredType', reserved = new Set() -) { +): Generated { function propertyToString(property: Property): string { const name = mode === 'AstType' ? property.name : escapeReservedWords(property.name, reserved); @@ -393,8 +399,11 @@ function pushProperties( return `${name}${optional ? '?' : ''}: ${propType}`; } - distinctAndSorted(properties, (a, b) => a.name.localeCompare(b.name)) - .forEach(property => node.append(propertyToString(property), NL)); + return joinToNode( + distinctAndSorted(properties, (a, b) => a.name.localeCompare(b.name)), + propertyToString, + { appendNewLineIfNotEmpty: true } + ); } export function isMandatoryPropertyType(propertyType: PropertyType): boolean { @@ -412,16 +421,17 @@ export function isMandatoryPropertyType(propertyType: PropertyType): boolean { } } -function pushReflectionInfo(node: CompositeGeneratorNode, name: string) { - node.append(`export const ${name} = '${name}';`, NL); - node.append(NL); +function addReflectionInfo(name: string): Generated { + return expandToNode` + export const ${name} = '${name}'; - node.append(`export function is${name}(item: unknown): item is ${name} {`, NL); - node.indent(body => body.append(`return reflection.isInstance(item, ${name});`, NL)); - node.append('}', NL); + export function is${name}(item: unknown): item is ${name} { + return reflection.isInstance(item, ${name}); + } + `.appendNewLine(); } -function pushDataTypeReflectionInfo(node: CompositeGeneratorNode, union: UnionType) { +function addDataTypeReflectionInfo(union: UnionType): Generated { switch (union.dataType) { case 'string': if (containsOnlyStringTypes(union.type)) { @@ -429,21 +439,19 @@ function pushDataTypeReflectionInfo(node: CompositeGeneratorNode, union: UnionTy const strings = collectStringValuesFromDataType(union.type); const regexes = collectRegexesFromDataType(union.type); if (subTypes.length === 0 && strings.length === 0 && regexes.length === 0) { - generateIsDataTypeFunction(node, union.name, `typeof item === '${union.dataType}'`); + return generateIsDataTypeFunction(union.name, `typeof item === '${union.dataType}'`); } else { const returnString = createDataTypeCheckerFunctionReturnString(subTypes, strings, regexes); - generateIsDataTypeFunction(node, union.name, returnString); + return generateIsDataTypeFunction(union.name, returnString); } } - break; + return; case 'number': case 'boolean': case 'bigint': - generateIsDataTypeFunction(node, union.name, `typeof item === '${union.dataType}'`); - break; + return generateIsDataTypeFunction(union.name, `typeof item === '${union.dataType}'`); case 'Date': - generateIsDataTypeFunction(node, union.name, 'item instanceof Date'); - break; + return generateIsDataTypeFunction(union.name, 'item instanceof Date'); default: return; } @@ -538,8 +546,10 @@ function collectRegexesFromDataType(propertyType: PropertyType): string[] { return regexes; } -function generateIsDataTypeFunction(node: CompositeGeneratorNode, unionName: string, returnString: string) { - node.append(NL, `export function is${unionName}(item: unknown): item is ${unionName} {`, NL); - node.indent(body => body.append(`return ${returnString};`, NL)); - node.append('}', NL); +function generateIsDataTypeFunction(unionName: string, returnString: string): Generated { + return expandToNode` + export function is${unionName}(item: unknown): item is ${unionName} { + return ${returnString}; + } + `.appendNewLine(); } diff --git a/packages/langium/src/index.ts b/packages/langium/src/index.ts index ba312e8ef..8141e4dac 100644 --- a/packages/langium/src/index.ts +++ b/packages/langium/src/index.ts @@ -10,7 +10,6 @@ export * from './service-registry.js'; export * from './services.js'; export * from './syntax-tree.js'; export * from './documentation/index.js'; -export * from './generator/index.js'; export * from './grammar/index.js'; export * from './lsp/index.js'; export * from './parser/index.js'; diff --git a/packages/langium/test/generator/generation-tracing.test.ts b/packages/langium/test/generator/generation-tracing.test.ts index 570f91662..5cacbea69 100644 --- a/packages/langium/test/generator/generation-tracing.test.ts +++ b/packages/langium/test/generator/generation-tracing.test.ts @@ -4,10 +4,10 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { beforeAll, describe, expect, test } from 'vitest'; -import { createServicesForGrammar, expandToNode, expandTracedToNode, expandToString, findNodeForKeyword, findNodesForProperty, joinTracedToNode, joinTracedToNodeIf, toStringAndTrace, traceToNode, TreeStreamImpl } from 'langium'; -import type { SourceRegion, TraceRegion, AstNodeWithTextRegion, AstNode } from 'langium'; +import { TreeStreamImpl, createServicesForGrammar, findNodeForKeyword, findNodesForProperty, type AstNode, type AstNodeWithTextRegion } from 'langium'; +import { expandToNode, expandToString, expandTracedToNode, joinTracedToNode, joinTracedToNodeIf, toStringAndTrace, traceToNode, type SourceRegion, type TraceRegion } from 'langium/generating'; import { parseHelper } from 'langium/test'; +import { beforeAll, describe, expect, test } from 'vitest'; // don't bather because of unexpected indentations, e.g. within template substitutions /* eslint-disable @typescript-eslint/indent */ diff --git a/packages/langium/test/generator/node.test.ts b/packages/langium/test/generator/node.test.ts index 5d16fd0dd..12c629294 100644 --- a/packages/langium/test/generator/node.test.ts +++ b/packages/langium/test/generator/node.test.ts @@ -6,7 +6,7 @@ import { EOL } from 'os'; import { describe, expect, test } from 'vitest'; -import { CompositeGeneratorNode, IndentNode, NewLineNode, NL, NLEmpty, toString as process } from 'langium'; +import { CompositeGeneratorNode, IndentNode, NewLineNode, NL, NLEmpty, toString as process } from 'langium/generating'; describe('new lines', () => { diff --git a/packages/langium/test/generator/template-node.test.ts b/packages/langium/test/generator/template-node.test.ts index 67176557c..f4bdf12b8 100644 --- a/packages/langium/test/generator/template-node.test.ts +++ b/packages/langium/test/generator/template-node.test.ts @@ -4,8 +4,9 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { stream } from 'langium'; +import { CompositeGeneratorNode, EOL, NL, joinToNode, expandToNode as n, expandToString as s, toString } from 'langium/generating'; import { describe, expect, test } from 'vitest'; -import { joinToNode, expandToNode as n, expandToString as s, stream, CompositeGeneratorNode, EOL, toString, NL } from 'langium'; // deactivate the eslint check 'no-unexpected-multiline' with the message // 'Unexpected newline between template tag and template literal', as that's done on purposes in tests below! diff --git a/packages/langium/test/generator/template-string.test.ts b/packages/langium/test/generator/template-string.test.ts index 0679acc08..869604ea8 100644 --- a/packages/langium/test/generator/template-string.test.ts +++ b/packages/langium/test/generator/template-string.test.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { expect, test } from 'vitest'; -import { expandToStringLF as s } from 'langium'; +import { expandToStringLF as s } from 'langium/generating'; test('Should not throw when substituting null', () => { expect(s`${null}`).toBe('null'); diff --git a/packages/langium/test/grammar/lsp/grammar-formatter.test.ts b/packages/langium/test/grammar/lsp/grammar-formatter.test.ts index ca735739e..14ab845c1 100644 --- a/packages/langium/test/grammar/lsp/grammar-formatter.test.ts +++ b/packages/langium/test/grammar/lsp/grammar-formatter.test.ts @@ -4,9 +4,10 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { describe, test } from 'vitest'; -import { EmptyFileSystem, createLangiumGrammarServices, expandToString } from 'langium'; +import { EmptyFileSystem, createLangiumGrammarServices } from 'langium'; +import { expandToString } from 'langium/generating'; import { expectFormatting } from 'langium/test'; +import { describe, test } from 'vitest'; const services = createLangiumGrammarServices(EmptyFileSystem); const formatting = expectFormatting(services.grammar); diff --git a/packages/langium/test/grammar/type-system/inferred-types.test.ts b/packages/langium/test/grammar/type-system/inferred-types.test.ts index 16fb8f221..e3ab2b948 100644 --- a/packages/langium/test/grammar/type-system/inferred-types.test.ts +++ b/packages/langium/test/grammar/type-system/inferred-types.test.ts @@ -7,7 +7,8 @@ import type { Grammar } from 'langium'; import type { AstTypes } from 'langium/types'; import { describe, expect, test } from 'vitest'; -import { createLangiumGrammarServices, EmptyFileSystem, expandToString, EOL } from 'langium'; +import { createLangiumGrammarServices, EmptyFileSystem } from 'langium'; +import { expandToString, EOL } from 'langium/generating'; import { collectAst, mergeTypesAndInterfaces } from 'langium/types'; import { clearDocuments, parseHelper } from 'langium/test'; diff --git a/packages/langium/test/serializer/json-serializer.test.ts b/packages/langium/test/serializer/json-serializer.test.ts index e9def8d19..51fa1b6b2 100644 --- a/packages/langium/test/serializer/json-serializer.test.ts +++ b/packages/langium/test/serializer/json-serializer.test.ts @@ -5,10 +5,11 @@ ******************************************************************************/ import type { AstNode, Reference } from 'langium'; +import { createServicesForGrammar } from 'langium'; +import { clearDocuments, parseHelper } from 'langium/test'; import { beforeEach, describe, expect, test } from 'vitest'; import { URI } from 'vscode-uri'; -import { createServicesForGrammar, expandToStringLF } from 'langium'; -import { clearDocuments, parseHelper } from 'langium/test'; +import { expandToStringLF } from '../../src/generator/template-string.js'; describe('JsonSerializer', async () => { diff --git a/packages/langium/test/utils/cst-utils.test.ts b/packages/langium/test/utils/cst-utils.test.ts index 5d8794e55..9812b1447 100644 --- a/packages/langium/test/utils/cst-utils.test.ts +++ b/packages/langium/test/utils/cst-utils.test.ts @@ -5,9 +5,10 @@ ******************************************************************************/ import type { Grammar, LeafCstNode } from 'langium'; -import { describe, expect, test } from 'vitest'; -import { createLangiumGrammarServices, findLeafNodeAtOffset, EmptyFileSystem, findLeafNodeBeforeOffset, expandToString } from 'langium'; +import { createLangiumGrammarServices, findLeafNodeAtOffset, EmptyFileSystem, findLeafNodeBeforeOffset } from 'langium'; import { parseHelper } from 'langium/test'; +import { describe, expect, test } from 'vitest'; +import { expandToString } from '../../src/generator/template-string.js'; const services = createLangiumGrammarServices(EmptyFileSystem); const parser = parseHelper(services.grammar);