Skip to content

Commit 4bbc180

Browse files
Respect the returned type of data type rules (#1500)
1 parent a19e6e1 commit 4bbc180

File tree

5 files changed

+123
-6
lines changed

5 files changed

+123
-6
lines changed

packages/langium-cli/test/generator/ast-generator.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,60 @@ describe('Ast generator', () => {
201201
}
202202
`);
203203

204+
testGeneratedAst('check generated property with datatype rule of type number: single-value', `
205+
grammar TestGrammar
206+
207+
Node: num=A;
208+
A returns number: '1';
209+
210+
hidden terminal WS: /\\s+/;
211+
terminal ID: /[_a-zA-Z][\\w_]*/;
212+
`, expandToString`
213+
export type A = number;
214+
215+
export function isA(item: unknown): item is A {
216+
return typeof item === 'number';
217+
}
218+
219+
export interface Node extends AstNode {
220+
readonly $type: 'Node';
221+
num: A;
222+
}
223+
224+
export const Node = 'Node';
225+
226+
export function isNode(item: unknown): item is Node {
227+
return reflection.isInstance(item, Node);
228+
}
229+
`);
230+
231+
testGeneratedAst('check generated property with datatype rule of type number: multi-value', `
232+
grammar TestGrammar
233+
234+
Node: num+=A*;
235+
A returns number: '1';
236+
237+
hidden terminal WS: /\\s+/;
238+
terminal ID: /[_a-zA-Z][\\w_]*/;
239+
`, expandToString`
240+
export type A = number;
241+
242+
export function isA(item: unknown): item is A {
243+
return typeof item === 'number';
244+
}
245+
246+
export interface Node extends AstNode {
247+
readonly $type: 'Node';
248+
num: Array<A>;
249+
}
250+
251+
export const Node = 'Node';
252+
253+
export function isNode(item: unknown): item is Node {
254+
return reflection.isInstance(item, Node);
255+
}
256+
`);
257+
204258
testGeneratedAst('should generate checker functions for datatype rules of type boolean', `
205259
grammar TestGrammar
206260

packages/langium/src/grammar/type-system/type-collector/inferred-types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MultiMap } from '../../../utils/collections.js';
1111
import { isAlternatives, isKeyword, isParserRule, isAction, isGroup, isUnorderedGroup, isAssignment, isRuleCall, isCrossReference, isTerminalRule } from '../../../languages/generated/ast.js';
1212
import { getTypeNameWithoutError, isPrimitiveGrammarType } from '../../internal-grammar-util.js';
1313
import { mergePropertyTypes } from './plain-types.js';
14-
import { isOptionalCardinality, terminalRegex, getRuleType } from '../../../utils/grammar-utils.js';
14+
import { isOptionalCardinality, terminalRegex, getRuleTypeName } from '../../../utils/grammar-utils.js';
1515

1616
interface TypePart {
1717
name?: string
@@ -470,7 +470,7 @@ function findTypes(terminal: AbstractElement, types: TypeCollection): void {
470470
} else if (isKeyword(terminal)) {
471471
types.types.add(`'${terminal.value}'`);
472472
} else if (isRuleCall(terminal) && terminal.rule.ref) {
473-
types.types.add(getRuleType(terminal.rule.ref));
473+
types.types.add(getRuleTypeName(terminal.rule.ref));
474474
} else if (isCrossReference(terminal) && terminal.type.ref) {
475475
const refTypeName = getTypeNameWithoutError(terminal.type.ref);
476476
if (refTypeName) {
@@ -494,7 +494,7 @@ function addRuleCall(graph: TypeGraph, current: TypePart, ruleCall: RuleCall): v
494494
current.properties.push(...properties);
495495
}
496496
} else if (isParserRule(rule)) {
497-
current.ruleCalls.push(getRuleType(rule));
497+
current.ruleCalls.push(getRuleTypeName(rule));
498498
}
499499
}
500500

packages/langium/src/grammar/validation/validation-resources-collector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { stream } from '../../utils/stream.js';
1515
import { isAction, isAlternatives, isGroup, isUnorderedGroup } from '../../languages/generated/ast.js';
1616
import { mergeInterfaces, mergeTypesAndInterfaces } from '../type-system/types-util.js';
1717
import { collectValidationAst } from '../type-system/ast-collector.js';
18-
import { getActionType, getRuleType } from '../../utils/grammar-utils.js';
18+
import { getActionType, getRuleTypeName } from '../../utils/grammar-utils.js';
1919

2020
export class LangiumGrammarValidationResourcesCollector {
2121
private readonly documents: LangiumDocuments;
@@ -94,7 +94,7 @@ function collectNameToRulesActions({ parserRules, datatypeRules }: AstResources)
9494
// collect rules
9595
stream(parserRules)
9696
.concat(datatypeRules)
97-
.forEach(rule => acc.add(getRuleType(rule), rule));
97+
.forEach(rule => acc.add(getRuleTypeName(rule), rule));
9898

9999
// collect actions
100100
function collectActions(element: AbstractElement) {

packages/langium/src/utils/grammar-utils.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,36 @@ export function getActionType(action: ast.Action): string | undefined {
433433
return undefined; // not inferring and not referencing a valid type
434434
}
435435

436-
export function getRuleType(rule: ast.AbstractRule): string {
436+
/**
437+
* This function is used at development time (for code generation and the internal type system) to get the type of the AST node produced by the given rule.
438+
* For data type rules, the name of the rule is returned,
439+
* e.g. "INT_value returns number: MY_INT;" returns "INT_value".
440+
* @param rule the given rule
441+
* @returns the name of the AST node type of the rule
442+
*/
443+
export function getRuleTypeName(rule: ast.AbstractRule): string {
437444
if (ast.isTerminalRule(rule)) {
438445
return rule.type?.name ?? 'string';
439446
} else {
440447
return isDataTypeRule(rule) ? rule.name : getExplicitRuleType(rule) ?? rule.name;
441448
}
442449
}
443450

451+
/**
452+
* This function is used at runtime to get the actual type of the values produced by the given rule at runtime.
453+
* For data type rules, the name of the declared return type of the rule is returned (if any),
454+
* e.g. "INT_value returns number: MY_INT;" returns "number".
455+
* @param rule the given rule
456+
* @returns the name of the type of the produced values of the rule at runtime
457+
*/
458+
export function getRuleType(rule: ast.AbstractRule): string {
459+
if (ast.isTerminalRule(rule)) {
460+
return rule.type?.name ?? 'string';
461+
} else {
462+
return getExplicitRuleType(rule) ?? rule.name;
463+
}
464+
}
465+
444466
export function terminalRegex(terminalRule: ast.TerminalRule): RegExp {
445467
const flags: Flags = {
446468
s: false,

packages/langium/test/parser/langium-parser-builder.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,47 @@ describe('One name for terminal and non-terminal rules', () => {
286286

287287
});
288288

289+
describe('check the default value converter for data type rules using terminal rules', () => {
290+
const grammar = `
291+
grammar Test
292+
293+
entry Main:
294+
propInteger=INT_value
295+
propBoolean=BOOLEAN_value
296+
propString=STRING_value;
297+
298+
INT_value returns number: MY_INT;
299+
BOOLEAN_value returns boolean: MY_BOOLEAN;
300+
STRING_value returns string: MY_ID;
301+
302+
terminal MY_INT returns number: /((-|\\+)?[0-9]+)/;
303+
terminal MY_BOOLEAN returns string: /(true)|(false)/;
304+
terminal MY_ID returns string: /[a-zA-Z]+/;
305+
306+
hidden terminal WS: /\\s+/;
307+
`;
308+
309+
let parser: LangiumParser;
310+
beforeAll(async () => {
311+
parser = await parserFromGrammar(grammar);
312+
});
313+
314+
test('Should have no definition errors', () => {
315+
expect(parser.definitionErrors).toHaveLength(0);
316+
});
317+
318+
test('string vs number', async () => {
319+
const result = parser.parse('123 true abc');
320+
expect(result.lexerErrors.length).toBe(0);
321+
expect(result.parserErrors.length).toBe(0);
322+
const value = result.value as unknown as { propInteger: number, propBoolean: boolean, propString: string };
323+
expect(value.propInteger).not.toBe('123');
324+
expect(value.propInteger).toBe(123);
325+
expect(value.propBoolean).toBe(true);
326+
expect(value.propString).toBe('abc');
327+
});
328+
});
329+
289330
describe('Boolean value converter', () => {
290331
const content = `
291332
grammar G

0 commit comments

Comments
 (0)