Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/langium/src/grammar/validation/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function registerValidationChecks(services: LangiumGrammarServices): void
],
ParserRule: [
validator.checkParserRuleDataType,
validator.checkFragmentKeywords,
validator.checkRuleParameters,
validator.checkEmptyParserRule,
validator.checkParserRuleReservedName,
Expand Down Expand Up @@ -971,11 +972,23 @@ export class LangiumGrammarValidator {
const dataTypeRule = isDataTypeRule(rule);
if (!hasDatatypeReturnType && dataTypeRule) {
accept('error', 'This parser rule does not create an object. Add a primitive return type or an action to the start of the rule to force object instantiation.', { node: rule, property: 'name' });
} else if (hasDatatypeReturnType && !dataTypeRule) {
} else if (hasDatatypeReturnType && !dataTypeRule && !rule.fragment) { // fragments are validated in the next check below
accept('error', 'Normal parser rules are not allowed to return a primitive value. Use a datatype rule for that.', { node: rule, property: rule.dataType ? 'dataType' : 'returnType' });
}
}

checkFragmentKeywords(rule: ast.ParserRule, accept: ValidationAcceptor): void {
if (rule.fragment) {
if (rule.dataType || rule.returnType || rule.inferredType) {
accept('error',
'Fragment rules cannot specify a return type.',
{ node: rule, property: rule.dataType ? 'dataType' : rule.returnType ? 'returnType' : 'inferredType' }
);
}
// `rule.entry` don't need to be checked, since the grammar allows either `rule.fragment` or `rule.entry`.
}
}

checkInfixRuleDataType(rule: ast.InfixRule, accept: ValidationAcceptor): void {
if (rule.dataType) {
accept('error', 'Infix rules are not allowed to return a primitive value.', { node: rule, property: 'dataType' });
Expand Down
61 changes: 43 additions & 18 deletions packages/langium/test/grammar/grammar-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type { AstNode, Grammar, LangiumDocument, Properties } from 'langium';
import { AstUtils, EmptyFileSystem, GrammarAST, URI } from 'langium';
import { expandToString } from 'langium/generate';
import { IssueCodes, createLangiumGrammarServices } from 'langium/grammar';
import type { ValidationResult } from 'langium/test';
import { clearDocuments, expectError, expectIssue, expectNoIssues, expectWarning, parseHelper, validationHelper } from 'langium/test';
Expand Down Expand Up @@ -322,22 +323,52 @@ describe('checkReferenceToRuleButNotType', () => {
});

describe('Check Rule Fragment Validation', () => {
const grammar = `
grammar g
type Type = Fragment;
fragment Fragment: name=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
`.trim();
test('Fragment used in type definition', async () => {
const grammar = expandToString`
grammar g
type Type = Fragment;
fragment Fragment: name=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
`;
const validationResult = await validate(grammar);
const range = { start: { character: 12, line: 1 }, end: { character: 20, line: 1 } };
expectError(validationResult, 'Cannot use rule fragments in types.', { range });
});

let validationResult: ValidationResult<GrammarAST.Grammar>;
test('Fragment with defined data type', async () => {
const grammar = expandToString`
grammar G
entry R: r1=ID F;
fragment F returns string: r2=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
`;
const validationResult = await validate(grammar);
const range = { start: { character: 19, line: 2 }, end: { character: 25, line: 2 } };
expectError(validationResult, 'Fragment rules cannot specify a return type.', { range });
});

beforeAll(async () => {
validationResult = await validate(grammar);
test('Fragment with defined returnType', async () => {
const grammar = expandToString`
grammar G
entry R: r1=ID F;
fragment F returns R: r2=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
`;
const validationResult = await validate(grammar);
const range = { start: { character: 19, line: 2 }, end: { character: 20, line: 2 } };
expectError(validationResult, 'Fragment rules cannot specify a return type.', { range });
});

test('Rule Fragment Validation', () => {
const range = { start: { character: 16, line: 1 }, end: { character: 24, line: 1 } };
expectError(validationResult, 'Cannot use rule fragments in types.', { range });
test('Fragment with defined inferredType', async () => {
const grammar = expandToString`
grammar G
entry R: r1=ID F;
fragment F infers R: r2=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
`;
const validationResult = await validate(grammar);
const range = { start: { character: 11, line: 2 }, end: { character: 19, line: 2 } };
expectError(validationResult, 'Fragment rules cannot specify a return type.', { range });
});
});

Expand All @@ -354,9 +385,6 @@ describe('Check cross-references to inferred types', () => {
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
`.trim());
expectNoIssues(validationResult);
// expect(validationResult.diagnostics).toHaveLength(1);
// expect(validationResult.diagnostics[0].message).toBe('Cannot infer terminal or data type rule for cross-reference.');
// expectError(validationResult, 'Cannot use rule fragments in types.');
});

test('infer in the parser rules body', async () => {
Expand All @@ -371,9 +399,6 @@ describe('Check cross-references to inferred types', () => {
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
`.trim());
expectNoIssues(validationResult);
// expect(validationResult.diagnostics).toHaveLength(1);
// expect(validationResult.diagnostics[0].message).toBe('Cannot infer terminal or data type rule for cross-reference.');
// expectError(validationResult, 'Cannot use rule fragments in types.');
});
});

Expand Down