diff --git a/packages/langium/src/grammar/langium-grammar-validator.ts b/packages/langium/src/grammar/langium-grammar-validator.ts index 3633305c8..57dd11888 100644 --- a/packages/langium/src/grammar/langium-grammar-validator.ts +++ b/packages/langium/src/grammar/langium-grammar-validator.ts @@ -21,6 +21,7 @@ import * as ast from './generated/ast'; import { isParserRule, isRuleCall } from './generated/ast'; import { findKeywordNode, findNameAssignment, getEntryRule, getTypeName, isDataTypeRule, isOptional, resolveImport, resolveTransitiveImports, terminalRegex } from './grammar-util'; import type { LangiumGrammarServices } from './langium-grammar-module'; +import { collectInferredTypes } from './type-system/inferred-types'; import { applyErrorToAssignment, collectAllInterfaces, InterfaceInfo, validateTypesConsistency } from './type-system/type-validator'; export class LangiumGrammarValidationRegistry extends ValidationRegistry { @@ -415,10 +416,22 @@ export class LangiumGrammarValidator { accept('error', 'Rules are not allowed to return union types.', { node: rule, property: 'returnType' }); } } + for (const interfaceType of grammar.interfaces) { interfaceType.superTypes.forEach((superType, i) => { if (superType.ref && ast.isType(superType.ref)) { accept('error', 'Interfaces cannot extend union types.', { node: interfaceType, property: 'superTypes', index: i }); + } else if(superType.ref && ast.isParserRule(superType.ref)) { + // collect just the beginning of whatever inferred types this standalone rule produces + // looking to exclude anything that would be a union down the line + const inferred = collectInferredTypes([superType.ref as ast.ParserRule], []); + if(inferred.unions.length > 0) { + // inferred union type also cannot be extended + accept('error', `An interface cannot extend a union type, which was inferred from parser rule ${superType.ref.name}.`, { node: interfaceType, property: 'superTypes', index: i }); + } else { + // otherwise we'll allow it, but issue a warning against basing declared off of inferred types + accept('warning', 'Extending an interface by a parser rule gives an ambiguous type, instead of the expected declared type.', { node: interfaceType, property: 'superTypes', index: i }); + } } }); } diff --git a/packages/langium/src/test/langium-test.ts b/packages/langium/src/test/langium-test.ts index daa217e88..957981370 100644 --- a/packages/langium/src/test/langium-test.ts +++ b/packages/langium/src/test/langium-test.ts @@ -244,7 +244,7 @@ function filterByOptions> = []; if ('node' in options) { const cstNode = options.property - ? findNodeForFeature(options.node.$cstNode, options.property.name, options.property.index) + ? findNodeForFeature(options.node?.$cstNode, options.property.name, options.property.index) : options.node.$cstNode; if (!cstNode) { throw new Error('Cannot find the node!'); diff --git a/packages/langium/test/grammar/langium-grammar-validator.test.ts b/packages/langium/test/grammar/langium-grammar-validator.test.ts index 3f8ae631b..c2969d537 100644 --- a/packages/langium/test/grammar/langium-grammar-validator.test.ts +++ b/packages/langium/test/grammar/langium-grammar-validator.test.ts @@ -6,12 +6,26 @@ import { createLangiumGrammarServices } from '../../src'; import { Assignment, Grammar, ParserRule } from '../../src/grammar/generated/ast'; -import { expectError, validationHelper } from '../../src/test'; +import { expectError, expectWarning, validationHelper } from '../../src/test'; const services = createLangiumGrammarServices(); const validate = validationHelper(services.grammar); describe('Langium grammar validation', () => { + + test('Declared interfaces warn when extending inferred interfaces', async () => { + const validationResult = await validate(` + InferredT: prop=ID; + + interface DeclaredExtendsInferred extends InferredT {}`); + + // should get a warning when basing declared types on inferred types + expectWarning(validationResult, /Extending an interface by a parser rule gives an ambiguous type, instead of the expected declared type./, { + node: validationResult.document.parseResult.value.interfaces[0], + property: {name: 'superTypes'} + }); + }); + test('Parser rule should not assign fragments', async () => { // arrange const grammarText = ` @@ -30,4 +44,41 @@ describe('Langium grammar validation', () => { property: {name: 'terminal'} }); }); + + test('Declared interfaces cannot extend inferred unions directly', async () => { + const validationResult = await validate(` + InferredUnion: InferredI1 | InferredI2; + + InferredI1: prop1=ID; + InferredI2: prop2=ID; + + interface DeclaredExtendsUnion extends InferredUnion {} + `); + + // should get an error on DeclaredExtendsUnion, since it cannot extend an inferred union + expectError(validationResult, /An interface cannot extend a union type, which was inferred from parser rule InferredUnion./, { + node: validationResult.document.parseResult.value.interfaces[0], + property: {name: 'superTypes'} + }); + }); + + test('Declared interfaces cannot extend inferred unions via indirect inheritance', async () => { + + const validationResult = await validate(` + InferredUnion: InferredI1 | InferredI2; + + InferredI1: prop1=ID; + InferredI2: prop2=ID; + + Intermediary: InferredUnion; + + interface DeclaredExtendsInferred extends Intermediary {} + `); + + // same error, but being sure that this holds when an inferred type extends another inferred type + expectError(validationResult, /An interface cannot extend a union type, which was inferred from parser rule Intermediary./, { + node: validationResult.document.parseResult.value.interfaces[0], + property: {name: 'superTypes'} + }); + }); }); \ No newline at end of file