Skip to content

Commit

Permalink
Execute langium-cli using a command to generate types definition
Browse files Browse the repository at this point in the history
  • Loading branch information
dhuebner committed Nov 4, 2022
1 parent 4fd8469 commit cef63c4
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 29 deletions.
31 changes: 8 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/langium-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^7.2.0",
"commander": "^8.0.0",
"fs-extra": "^9.1.0",
"jsonschema": "^1.4.0",
"langium": "~0.5.0",
Expand Down
25 changes: 24 additions & 1 deletion packages/langium-cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ import { getUserChoice, log } from './generator/util';
import { getFilePath, LangiumConfig, LangiumLanguageConfig, RelativePath } from './package';
import { validateParser } from './parser-validation';
import chalk from 'chalk';
import { generateTypesFile } from './generator/types-generator';

export type GenerateOptions = {
file?: string;
watch: boolean
}

export type ExtractTypesOptions = {
grammar: string;
output?: string;
}

export type GeneratorResult = 'success' | 'failure';

type GrammarElement = GrammarAST.AbstractRule | GrammarAST.Type | GrammarAST.Interface;
Expand Down Expand Up @@ -221,6 +227,23 @@ export async function generate(config: LangiumConfig, options: GenerateOptions):
return 'success';
}

export async function generateTypes(options: ExtractTypesOptions): Promise<void> {
const absGrammarPath = URI.file(options.grammar);
const document = documents.getOrCreateDocument(absGrammarPath);
const allUris = eagerLoad(document);
await sharedServices.workspace.DocumentBuilder.update(allUris, []);
let grammarDoc;
for (const doc of documents.all) {
await sharedServices.workspace.DocumentBuilder.build([doc]);
grammarDoc = doc;
}
if (grammarDoc) {
const genTypes = generateTypesFile(grammarServices, [grammarDoc.parseResult.value as Grammar]);
await writeWithFail(path.join(path.resolve(options.grammar, '..'), options.output ?? 'types.langium'), genTypes, { watch: false });
}
return;
}

/**
* Writes contents of a grammar for syntax highlighting to a file, logging any errors and continuing without throwing
* @param grammar Grammar contents to write
Expand Down Expand Up @@ -265,7 +288,7 @@ async function mkdirWithFail(path: string, options: GenerateOptions): Promise<bo
}
}

async function writeWithFail(path: string, content: string, options: GenerateOptions): Promise<void> {
async function writeWithFail(path: string, content: string, options: { watch: boolean }): Promise<void> {
try {
await fs.writeFile(path, content);
} catch (e) {
Expand Down
49 changes: 49 additions & 0 deletions packages/langium-cli/src/generator/types-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/******************************************************************************
* Copyright 2022 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 { CompositeGeneratorNode, Grammar, LangiumServices, NL, processGeneratorNode } from 'langium';
import { collectAst, distinctAndSorted, PropertyType } from 'langium/lib/grammar/type-system';
import { LangiumGrammarGrammar } from 'langium/lib/grammar/generated/grammar';
import { collectKeywords } from './util';

export function generateTypesFile(services: LangiumServices, grammars: Grammar[]): string {
const astTypes = collectAst(services.shared.workspace.LangiumDocuments, grammars);
const fileNode = new CompositeGeneratorNode();
const reservedWords = new Set(collectKeywords(LangiumGrammarGrammar()));
astTypes.unions.filter((union) => !astTypes.interfaces.find((iface) =>
iface.name === union.name
)).forEach((union) => {
fileNode.append(`type ${escapeReservedWords(union.name, reservedWords)} = ${propertyTypesToString(union.union)};`);
fileNode.append(NL).append(NL);
});
astTypes.interfaces.forEach((iFace) => {
fileNode.append(`interface ${escapeReservedWords(iFace.name, reservedWords)}${iFace.interfaceSuperTypes.length > 0 ? (' extends ' + Array.from(iFace.interfaceSuperTypes).join(', ')) : ''} {`);
fileNode.append(NL);
fileNode.indent((body) => {
iFace.properties.forEach(property =>
body.append(`${escapeReservedWords(property.name, reservedWords)}${property.optional ? '?' : ''}: ${propertyTypesToString(property.typeAlternatives)}`).append(NL)
);
});
fileNode.append('}').append(NL).append(NL);
}
);
return processGeneratorNode(fileNode);
}

function propertyTypesToString(alternatives: PropertyType[]): string {
return distinctAndSorted(alternatives.map(typePropertyToString)).join(' | ');
}

function typePropertyToString(propertyType: PropertyType): string {
let res = distinctAndSorted(propertyType.types).join(' | ');
res = propertyType.reference ? `@${res}` : res;
res = propertyType.array ? `${res}[]` : res;
return res;
}

function escapeReservedWords(name: string, reserved: Set<string>): string {
return reserved.has(name) ? `^${name}` : name;
}
3 changes: 1 addition & 2 deletions packages/langium-cli/src/generator/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import { CompositeGeneratorNode, GeneratorNode, getAllReachableRules, Grammar, G
import fs from 'fs-extra';
import path from 'path';
import * as readline from 'readline';
import type { GenerateOptions } from '../generate';
import chalk from 'chalk';

//eslint-disable-next-line @typescript-eslint/no-explicit-any
export function log(level: 'log' | 'warn' | 'error', options: GenerateOptions, message: string, ...args: any[]): void {
export function log(level: 'log' | 'warn' | 'error', options: { watch: boolean }, message: string, ...args: any[]): void {
if (options.watch) {
console[level](getTime() + message, ...args);
} else {
Expand Down
13 changes: 12 additions & 1 deletion packages/langium-cli/src/langium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import { Command } from 'commander';
import { validate } from 'jsonschema';
import { generate, GenerateOptions, GeneratorResult } from './generate';
import { ExtractTypesOptions, generate, GenerateOptions, generateTypes, GeneratorResult } from './generate';
import { cliVersion, elapsedTime, getTime, log, schema } from './generator/util';
import { LangiumConfig, loadConfigs, RelativePath } from './package';
import path from 'path';
Expand All @@ -27,6 +27,17 @@ program
process.exit(1);
});
});
program.command('extract-types')
.argument('<file>', 'the langium grammar file to generate types for')
.option('-o, --output <file>', 'output file name', 'types.langium')
.action((file, options: ExtractTypesOptions) => {
options.grammar = file;
generateTypes(options).catch(err => {
console.error(err);
process.exit(1);
});
})
.action;

program.parse(process.argv);

Expand Down
109 changes: 109 additions & 0 deletions packages/langium-cli/test/generator/types-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/******************************************************************************
* Copyright 2022 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 { createLangiumGrammarServices, EmptyFileSystem, Grammar } from 'langium';
import { parseHelper } from 'langium/test';
import { generateTypesFile } from '../../src/generator/types-generator';

const { grammar } = createLangiumGrammarServices(EmptyFileSystem);

describe('Types generator', () => {

test('should generate types file', async () => {
const result = (await parseHelper<Grammar>(grammar)(TEST_GRAMMAR)).parseResult;
const typesFileContent = generateTypesFile(grammar, [result.value]);
expect(typesFileContent).toMatch(EXPECTED_TYPES);
});

});

const EXPECTED_TYPES =
`type AbstractDefinition = DeclaredParameter | Definition;
type Expression = BinaryExpression | FunctionCall | NumberLiteral;
type Statement = Definition | Evaluation;
interface BinaryExpression {
left: Expression
operator: '*' | '+' | '-' | '/'
right: Expression
}
interface DeclaredParameter {
name: string
}
interface Definition {
name: string
args?: DeclaredParameter[]
expr: Expression
}
interface Evaluation {
expression: Expression
}
interface FunctionCall {
func: @AbstractDefinition
args?: Expression[]
}
interface Module {
name: string
statements?: Statement[]
}
interface NumberLiteral {
value: number
}
`;

const TEST_GRAMMAR =
`
grammar Arithmetics
entry Module:
'module' name=ID
(statements+=Statement)*;
Statement:
Definition | Evaluation;
Definition:
'def' name=ID ('(' args+=DeclaredParameter (',' args+=DeclaredParameter)* ')')?
':' expr=Expression ';';
DeclaredParameter:
name=ID;
type AbstractDefinition = Definition | DeclaredParameter;
Evaluation:
expression=Expression ';';
Expression:
Addition;
Addition infers Expression:
Multiplication ({infer BinaryExpression.left=current} operator=('+' | '-') right=Multiplication)*;
Multiplication infers Expression:
PrimaryExpression ({infer BinaryExpression.left=current} operator=('*' | '/') right=PrimaryExpression)*;
PrimaryExpression infers Expression:
'(' Expression ')' |
{infer NumberLiteral} value=NUMBER |
{infer FunctionCall} func=[AbstractDefinition] ('(' args+=Expression (',' args+=Expression)* ')')?;
hidden terminal WS: /\\s+/;
terminal ID: /[_a-zA-Z][\\w_]*/;
terminal NUMBER returns number: /[0-9]+(\\.[0-9]*)?/;
hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
`;
20 changes: 19 additions & 1 deletion packages/langium-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,28 @@
"description": "Specifies the exclusion patterns during initial workspace indexing. You will need to reload your extension afterwards."
}
}
},
"commands": [
{
"category": "Developer",
"command": "langium-vscode.generate-types",
"title": "Generate Langium Types Definition",
"enablement": "editorLangId == langium"
}
],
"menus": {
"editor/context": [
{
"command": "langium-vscode.generate-types",
"when": "editorLangId == langium",
"group": "z_commands"
}
]
}
},
"activationEvents": [
"onLanguage:langium"
"onLanguage:langium",
"langium-vscode.generate-types"
],
"main": "out/extension.js",
"scripts": {
Expand Down
Loading

0 comments on commit cef63c4

Please sign in to comment.