From 7a0216638ba9d0869739b3370d50b718eaa036ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Spo=CC=88nemann?= Date: Fri, 8 Mar 2024 08:50:33 +0100 Subject: [PATCH] Added ParserOptions to parseHelper --- packages/langium/src/parser/langium-parser.ts | 6 +++- packages/langium/src/test/langium-test.ts | 11 ++++-- packages/langium/src/workspace/documents.ts | 36 +++++++++---------- .../test/parser/langium-parser.test.ts | 31 ++++++++++------ 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/langium/src/parser/langium-parser.ts b/packages/langium/src/parser/langium-parser.ts index 7d02ba038..a073a26bd 100644 --- a/packages/langium/src/parser/langium-parser.ts +++ b/packages/langium/src/parser/langium-parser.ts @@ -131,6 +131,10 @@ export abstract class AbstractLangiumParser implements BaseParser { } } +export interface ParserOptions { + rule?: string +} + export class LangiumParser extends AbstractLangiumParser { private readonly linker: Linker; private readonly converter: ValueConverter; @@ -160,7 +164,7 @@ export class LangiumParser extends AbstractLangiumParser { return ruleMethod; } - parse(input: string, options: { rule?: string } = {}): ParseResult { + parse(input: string, options: ParserOptions = {}): ParseResult { this.nodeBuilder.buildRootNode(input); const lexerResult = this.lexer.tokenize(input); this.wrapper.input = lexerResult.tokens; diff --git a/packages/langium/src/test/langium-test.ts b/packages/langium/src/test/langium-test.ts index b523c3f8e..098ab8399 100644 --- a/packages/langium/src/test/langium-test.ts +++ b/packages/langium/src/test/langium-test.ts @@ -9,6 +9,9 @@ import type { LangiumCoreServices, LangiumSharedCoreServices } from '../services import type { AstNode, CstNode, Properties } from '../syntax-tree.js'; import { type LangiumDocument, TextDocument } from '../workspace/documents.js'; import type { BuildOptions } from '../workspace/document-builder.js'; +import type { AsyncDisposable } from '../utils/disposable.js'; +import type { LangiumServices, LangiumSharedLSPServices } from '../lsp/lsp-services.js'; +import type { ParserOptions } from '../parser/langium-parser.js'; import { DiagnosticSeverity, MarkupContent } from 'vscode-languageserver-types'; import { escapeRegExp } from '../utils/regexp-utils.js'; import { URI } from '../utils/uri-utils.js'; @@ -16,16 +19,18 @@ import { findNodeForProperty } from '../utils/grammar-utils.js'; import { SemanticTokensDecoder } from '../lsp/semantic-token-provider.js'; import * as assert from 'node:assert'; import { stream } from '../utils/stream.js'; -import type { AsyncDisposable } from '../utils/disposable.js'; import { Disposable } from '../utils/disposable.js'; import { normalizeEOL } from '../generate/template-string.js'; -import type { LangiumServices, LangiumSharedLSPServices } from '../lsp/lsp-services.js'; export interface ParseHelperOptions extends BuildOptions { /** * Specifies the URI of the generated document. Will use a counter variable if not specified. */ documentUri?: string; + /** + * Options passed to the LangiumParser. + */ + parserOptions?: ParserOptions } let nextDocumentId = 1; @@ -35,7 +40,7 @@ export function parseHelper(services: LangiumCoreSe const documentBuilder = services.shared.workspace.DocumentBuilder; return async (input, options) => { const uri = URI.parse(options?.documentUri ?? `file:///${nextDocumentId++}${metaData.fileExtensions[0] ?? ''}`); - const document = services.shared.workspace.LangiumDocumentFactory.fromString(input, uri); + const document = services.shared.workspace.LangiumDocumentFactory.fromString(input, uri, options?.parserOptions); services.shared.workspace.LangiumDocuments.addDocument(document); await documentBuilder.build([document], options); return document; diff --git a/packages/langium/src/workspace/documents.ts b/packages/langium/src/workspace/documents.ts index dd8203535..51b79d7d4 100644 --- a/packages/langium/src/workspace/documents.ts +++ b/packages/langium/src/workspace/documents.ts @@ -15,7 +15,7 @@ export { TextDocument } from 'vscode-languageserver-textdocument'; import type { Diagnostic, Range } from 'vscode-languageserver-types'; import type { FileSystemProvider } from './file-system-provider.js'; -import type { ParseResult } from '../parser/langium-parser.js'; +import type { ParseResult, ParserOptions } from '../parser/langium-parser.js'; import type { ServiceRegistry } from '../service-registry.js'; import type { LangiumSharedCoreServices } from '../services.js'; import type { AstNode, AstNodeDescription, Mutable, Reference } from '../syntax-tree.js'; @@ -126,7 +126,7 @@ export interface LangiumDocumentFactory { /** * Create a Langium document from a `TextDocument` (usually associated with a file). */ - fromTextDocument(textDocument: TextDocument, uri?: URI): LangiumDocument; + fromTextDocument(textDocument: TextDocument, uri?: URI, options?: ParserOptions): LangiumDocument; /** * Create a Langium document from a `TextDocument` asynchronously. This action can be cancelled if a cancellable parser implementation has been provided. */ @@ -135,7 +135,7 @@ export interface LangiumDocumentFactory { /** * Create an Langium document from an in-memory string. */ - fromString(text: string, uri: URI): LangiumDocument; + fromString(text: string, uri: URI, options?: ParserOptions): LangiumDocument; /** * Create a Langium document from an in-memory string asynchronously. This action can be cancelled if a cancellable parser implementation has been provided. */ @@ -178,24 +178,24 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory { return this.createAsync(uri, content, cancellationToken); } - fromTextDocument(textDocument: TextDocument, uri?: URI): LangiumDocument; + fromTextDocument(textDocument: TextDocument, uri?: URI, options?: ParserOptions): LangiumDocument; fromTextDocument(textDocument: TextDocument, uri: URI | undefined, cancellationToken: CancellationToken): Promise>; - fromTextDocument(textDocument: TextDocument, uri?: URI, cancellationToken?: CancellationToken): LangiumDocument | Promise> { + fromTextDocument(textDocument: TextDocument, uri?: URI, token?: CancellationToken | ParserOptions): LangiumDocument | Promise> { uri = uri ?? URI.parse(textDocument.uri); - if (cancellationToken) { - return this.createAsync(uri, textDocument, cancellationToken); + if (CancellationToken.is(token)) { + return this.createAsync(uri, textDocument, token); } else { - return this.create(uri, textDocument); + return this.create(uri, textDocument, token); } } - fromString(text: string, uri: URI): LangiumDocument; + fromString(text: string, uri: URI, options?: ParserOptions): LangiumDocument; fromString(text: string, uri: URI, cancellationToken: CancellationToken): Promise>; - fromString(text: string, uri: URI, cancellationToken?: CancellationToken): LangiumDocument | Promise> { - if (cancellationToken) { - return this.createAsync(uri, text, cancellationToken); + fromString(text: string, uri: URI, token?: CancellationToken | ParserOptions): LangiumDocument | Promise> { + if (CancellationToken.is(token)) { + return this.createAsync(uri, text, token); } else { - return this.create(uri, text); + return this.create(uri, text, token); } } @@ -203,9 +203,9 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory { return this.create(uri, { $model: model }); } - protected create(uri: URI, content: string | TextDocument | { $model: T }): LangiumDocument { + protected create(uri: URI, content: string | TextDocument | { $model: T }, options?: ParserOptions): LangiumDocument { if (typeof content === 'string') { - const parseResult = this.parse(uri, content); + const parseResult = this.parse(uri, content, options); return this.createLangiumDocument(parseResult, uri, undefined, content); } else if ('$model' in content) { @@ -213,7 +213,7 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory { return this.createLangiumDocument(parseResult, uri); } else { - const parseResult = this.parse(uri, content.getText()); + const parseResult = this.parse(uri, content.getText(), options); return this.createLangiumDocument(parseResult, uri, content); } } @@ -300,9 +300,9 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory { return document; } - protected parse(uri: URI, text: string): ParseResult { + protected parse(uri: URI, text: string, options?: ParserOptions): ParseResult { const services = this.serviceRegistry.getServices(uri); - return services.parser.LangiumParser.parse(text); + return services.parser.LangiumParser.parse(text, options); } protected parseAsync(uri: URI, text: string, cancellationToken: CancellationToken): Promise> { diff --git a/packages/langium/test/parser/langium-parser.test.ts b/packages/langium/test/parser/langium-parser.test.ts index 9288fdfca..7da85a974 100644 --- a/packages/langium/test/parser/langium-parser.test.ts +++ b/packages/langium/test/parser/langium-parser.test.ts @@ -4,40 +4,41 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { LangiumParser } from 'langium'; +import type { AstNode, LangiumCoreServices } from 'langium'; import { describe, expect, test, beforeEach } from 'vitest'; -import { createServicesForGrammar } from 'langium/grammar'; +import { createServicesForGrammar } from 'langium/grammar'; +import { parseHelper } from 'langium/test'; describe('Partial parsing', () => { const content = ` grammar Test - entry Model: (a+=A | b+=B)*; + entry Model: 'model' (a+=A | b+=B)*; A: 'a' name=ID; B: 'b' name=ID; terminal ID: /[_a-zA-Z][\\w_]*/; hidden terminal WS: /\\s+/; `; - let parser: LangiumParser; + let services: LangiumCoreServices; beforeEach(async () => { - parser = await parserFromGrammar(content); + services = await createServicesForGrammar({ grammar: content }); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any function expectCorrectParse(text: string, rule?: string): any { - const result = parser.parse(text, { rule }); + const result = services.parser.LangiumParser.parse(text, { rule }); expect(result.parserErrors.length).toBe(0); return result.value; } function expectErrorneousParse(text: string, rule?: string): void { - const result = parser.parse(text, { rule }); + const result = services.parser.LangiumParser.parse(text, { rule }); expect(result.parserErrors.length).toBeGreaterThan(0); } test('Should parse correctly with normal entry rule', () => { - const result = expectCorrectParse('a Foo b Bar'); + const result = expectCorrectParse('model a Foo b Bar'); expect(result.a[0].name).toEqual('Foo'); expect(result.b[0].name).toEqual('Bar'); }); @@ -45,16 +46,26 @@ describe('Partial parsing', () => { test('Should parse correctly with alternative entry rule A', () => { const result = expectCorrectParse('a Foo', 'A'); expect(result.name).toEqual('Foo'); + expectErrorneousParse('model a Foo', 'A'); expectErrorneousParse('b Bar', 'A'); }); test('Should parse correctly with alternative entry rule B', () => { const result = expectCorrectParse('b Foo', 'B'); expect(result.name).toEqual('Foo'); + expectErrorneousParse('model b Foo', 'B'); expectErrorneousParse('a Foo', 'B'); }); + + test('Parse helper supports using alternative entry rule A', async () => { + const parse = parseHelper(services); + const document = await parse('a Foo', { parserOptions: { rule: 'A' } }); + expect(document.parseResult.parserErrors.length).toBe(0); + expect(document.parseResult.value.name).toEqual('Foo'); + }); + }); -async function parserFromGrammar(grammar: string): Promise { - return (await createServicesForGrammar({ grammar })).parser.LangiumParser; +interface A extends AstNode { + name: string }