diff --git a/packages/langium/src/parser/langium-parser.ts b/packages/langium/src/parser/langium-parser.ts index 6702270f1..7d02ba038 100644 --- a/packages/langium/src/parser/langium-parser.ts +++ b/packages/langium/src/parser/langium-parser.ts @@ -53,6 +53,7 @@ interface AssignmentElement { export interface BaseParser { rule(rule: ParserRule, impl: RuleImpl): RuleResult; + getRule(name: string): RuleResult | undefined; alternatives(idx: number, choices: Array>): void; optional(idx: number, callback: DSLMethodOpts): void; many(idx: number, callback: DSLMethodOpts): void; @@ -75,6 +76,9 @@ export abstract class AbstractLangiumParser implements BaseParser { protected readonly wrapper: ChevrotainWrapper; protected _unorderedGroups: Map = new Map(); + protected allRules = new Map(); + protected mainRule!: RuleResult; + constructor(services: LangiumCoreServices) { this.lexer = services.parser.Lexer; const tokens = this.lexer.definition; @@ -106,6 +110,10 @@ export abstract class AbstractLangiumParser implements BaseParser { abstract action($type: string, action: Action): void; abstract construct(): unknown; + getRule(name: string): RuleResult | undefined { + return this.allRules.get(name); + } + isRecording(): boolean { return this.wrapper.IS_RECORDING; } @@ -129,7 +137,6 @@ export class LangiumParser extends AbstractLangiumParser { private readonly astReflection: AstReflection; private readonly nodeBuilder = new CstNodeBuilder(); private stack: any[] = []; - private mainRule!: RuleResult; private assignmentMap = new Map(); private get current(): any { @@ -146,17 +153,22 @@ export class LangiumParser extends AbstractLangiumParser { rule(rule: ParserRule, impl: RuleImpl): RuleResult { const type = rule.fragment ? undefined : isDataTypeRule(rule) ? DatatypeSymbol : getTypeName(rule); const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(type, impl).bind(this)); + this.allRules.set(rule.name, ruleMethod); if (rule.entry) { this.mainRule = ruleMethod; } return ruleMethod; } - parse(input: string): ParseResult { + parse(input: string, options: { rule?: string } = {}): ParseResult { this.nodeBuilder.buildRootNode(input); const lexerResult = this.lexer.tokenize(input); this.wrapper.input = lexerResult.tokens; - const result = this.mainRule.call(this.wrapper, {}); + const ruleMethod = options.rule ? this.allRules.get(options.rule) : this.mainRule; + if (!ruleMethod) { + throw new Error(options.rule ? `No rule found with name '${options.rule}'` : 'No main rule available.'); + } + const result = ruleMethod.call(this.wrapper, {}); this.nodeBuilder.addHiddenTokens(lexerResult.hidden); this.unorderedGroups.clear(); return { @@ -424,7 +436,6 @@ export interface CompletionParserResult { } export class LangiumCompletionParser extends AbstractLangiumParser { - private mainRule!: RuleResult; private tokens: IToken[] = []; private elementStack: AbstractElement[] = []; @@ -457,6 +468,7 @@ export class LangiumCompletionParser extends AbstractLangiumParser { rule(rule: ParserRule, impl: RuleImpl): RuleResult { const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(impl).bind(this)); + this.allRules.set(rule.name, ruleMethod); if (rule.entry) { this.mainRule = ruleMethod; } diff --git a/packages/langium/src/parser/parser-builder-base.ts b/packages/langium/src/parser/parser-builder-base.ts index ae667364b..9c34d8081 100644 --- a/packages/langium/src/parser/parser-builder-base.ts +++ b/packages/langium/src/parser/parser-builder-base.ts @@ -26,7 +26,6 @@ type RuleContext = { type ParserContext = { parser: BaseParser tokens: TokenTypeDictionary - rules: Map ruleNames: Map } @@ -39,11 +38,9 @@ type Predicate = (args: Args) => boolean; type Method = (args: Args) => void; export function createParser(grammar: Grammar, parser: T, tokens: TokenTypeDictionary): T { - const rules = new Map(); const parserContext: ParserContext = { parser, tokens, - rules, ruleNames: new Map() }; buildRules(parserContext, grammar); @@ -62,10 +59,7 @@ function buildRules(parserContext: ParserContext, grammar: Grammar): void { many: 1, or: 1 }; - ctx.rules.set( - rule.name, - parserContext.parser.rule(rule, buildElement(ctx, rule.definition)) - ); + parserContext.parser.rule(rule, buildElement(ctx, rule.definition)); } } @@ -369,7 +363,7 @@ function wrap(ctx: RuleContext, guard: Condition | undefined, method: Method, ca function getRule(ctx: ParserContext, element: ParserRule | AbstractElement): Rule { const name = getRuleName(ctx, element); - const rule = ctx.rules.get(name); + const rule = ctx.parser.getRule(name); if (!rule) throw new Error(`Rule "${name}" not found."`); return rule; } diff --git a/packages/langium/test/parser/langium-parser.test.ts b/packages/langium/test/parser/langium-parser.test.ts new file mode 100644 index 000000000..9288fdfca --- /dev/null +++ b/packages/langium/test/parser/langium-parser.test.ts @@ -0,0 +1,60 @@ +/****************************************************************************** + * Copyright 2024 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 type { LangiumParser } from 'langium'; +import { describe, expect, test, beforeEach } from 'vitest'; +import { createServicesForGrammar } from 'langium/grammar'; + +describe('Partial parsing', () => { + const content = ` + grammar Test + entry 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; + + beforeEach(async () => { + parser = await parserFromGrammar(content); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function expectCorrectParse(text: string, rule?: string): any { + const result = parser.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 }); + expect(result.parserErrors.length).toBeGreaterThan(0); + } + + test('Should parse correctly with normal entry rule', () => { + const result = expectCorrectParse('a Foo b Bar'); + expect(result.a[0].name).toEqual('Foo'); + expect(result.b[0].name).toEqual('Bar'); + }); + + test('Should parse correctly with alternative entry rule A', () => { + const result = expectCorrectParse('a Foo', 'A'); + expect(result.name).toEqual('Foo'); + 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('a Foo', 'B'); + }); +}); + +async function parserFromGrammar(grammar: string): Promise { + return (await createServicesForGrammar({ grammar })).parser.LangiumParser; +}