Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for deep and declared hierarchies #973

Merged
21 changes: 10 additions & 11 deletions packages/langium/src/grammar/type-system/ast-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,22 @@ export function specifyAstNodeProperties(astTypes: AstTypes) {
const array = Array.from(nameToType.values());
addSubTypes(array);
buildContainerTypes(array);
buildTypeNames(nameToType);
buildTypeNames(array);
}

function buildTypeNames(nameToType: Map<string, TypeOption>) {
const queue = Array.from(nameToType.values()).filter(e => e.subTypes.size === 0);
function buildTypeNames(types: TypeOption[]) {
// Recursively collect all subtype names
const visited = new Set<TypeOption>();
for (const type of queue) {
const collect = (type: TypeOption): void => {
if (visited.has(type)) return;
visited.add(type);
type.typeNames.add(type.name);
const superTypes = Array.from(type.superTypes)
.map(superType => nameToType.get(superType.name))
.filter(e => e !== undefined) as TypeOption[];
for (const superType of superTypes) {
type.typeNames.forEach(e => superType.typeNames.add(e));
for (const subtype of type.subTypes) {
collect(subtype);
subtype.typeNames.forEach(n => type.typeNames.add(n));
}
queue.push(...superTypes.filter(e => !visited.has(e)));
}
};
types.forEach(collect);
}

/**
Expand Down
27 changes: 21 additions & 6 deletions packages/langium/src/grammar/validation/types-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { MultiMap } from '../../utils/collections';
import { DiagnosticInfo, ValidationAcceptor, ValidationChecks } from '../../validation/validation-registry';
import { extractAssignments } from '../internal-grammar-util';
import { LangiumGrammarServices } from '../langium-grammar-module';
import { flattenPropertyUnion, InterfaceType, isInterfaceType, isMandatoryPropertyType, isReferenceType, isTypeAssignable, isUnionType, Property, PropertyType, propertyTypeToString } from '../type-system/type-collector/types';
import { DeclaredInfo, InferredInfo, isDeclared, isInferred, isInferredAndDeclared, LangiumGrammarDocument } from '../workspace/documents';
import { flattenPropertyUnion, InterfaceType, isArrayType, isInterfaceType, isMandatoryPropertyType, isPropertyUnion, isReferenceType, isTypeAssignable, isUnionType, isValueType, Property, PropertyType, propertyTypeToString } from '../type-system/type-collector/types';
import { DeclaredInfo, InferredInfo, isDeclared, isInferred, isInferredAndDeclared, LangiumGrammarDocument, ValidationResources } from '../workspace/documents';

export function registerTypeValidationChecks(services: LangiumGrammarServices): void {
const registry = services.validation.ValidationRegistry;
Expand Down Expand Up @@ -50,7 +50,7 @@ export class LangiumGrammarTypesValidator {
validateInferredInterface(typeInfo.inferred as InterfaceType, accept);
}
if (isInferredAndDeclared(typeInfo)) {
validateDeclaredAndInferredConsistency(typeInfo, accept);
validateDeclaredAndInferredConsistency(typeInfo, validationResources, accept);
}
}
}
Expand Down Expand Up @@ -170,7 +170,7 @@ function arePropTypesIdentical(a: Property, b: Property): boolean {

///////////////////////////////////////////////////////////////////////////////

function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & DeclaredInfo, accept: ValidationAcceptor) {
function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & DeclaredInfo, resources: ValidationResources, accept: ValidationAcceptor) {
const { inferred, declared, declaredNode, inferredNodes } = typeInfo;
const typeName = declared.name;

Expand Down Expand Up @@ -211,7 +211,7 @@ function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & Declare
applyErrorToRulesAndActions(`in a rule that returns type '${typeName}'`),
);
} else if (isInterfaceType(inferred) && isInterfaceType(declared)) {
validatePropertiesConsistency(inferred, declared,
validatePropertiesConsistency(inferred, declared, resources,
applyErrorToRulesAndActions(`in a rule that returns type '${typeName}'`),
applyErrorToProperties,
applyMissingPropErrorToRules
Expand Down Expand Up @@ -241,6 +241,7 @@ function isOptionalProperty(prop: Property): boolean {
function validatePropertiesConsistency(
inferred: InterfaceType,
declared: InterfaceType,
resources: ValidationResources,
applyErrorToType: (errorMessage: string) => void,
applyErrorToProperties: (nodes: Set<ast.Assignment | ast.Action | ast.TypeAttribute>, errorMessage: string) => void,
applyMissingPropErrorToRules: (missingProp: string) => void
Expand All @@ -251,13 +252,27 @@ function validatePropertiesConsistency(
// This field only contains properties of itself or super types
const declaredProps = new Map(declared.superProperties.map(e => [e.name, e]));

// The inferred props may not have full hierarchy information so try finding
// a corresponding declared type
const matchingProp = (type: PropertyType): PropertyType => {
if (isPropertyUnion(type)) return { types: type.types.map(t => matchingProp(t)) };
if (isReferenceType(type)) return { referenceType: matchingProp(type.referenceType) };
if (isArrayType(type)) return { elementType: matchingProp(type.elementType) };
if (isValueType(type)) {
const resource = resources.typeToValidationInfo.get(type.value.name);
if (!resource) return type;
return { value: 'declared' in resource ? resource.declared : resource.inferred };
}
return type;
};

// detects extra properties & validates matched ones on consistency by the 'optional' property
for (const [name, foundProp] of allInferredProps.entries()) {
const expectedProp = declaredProps.get(name);
if (expectedProp) {
const foundTypeAsStr = propertyTypeToString(foundProp.type, 'DeclaredType');
const expectedTypeAsStr = propertyTypeToString(expectedProp.type, 'DeclaredType');
const typeAlternativesErrors = isTypeAssignable(foundProp.type, expectedProp.type);
const typeAlternativesErrors = isTypeAssignable(matchingProp(foundProp.type), expectedProp.type);
if (!typeAlternativesErrors) {
const errorMsgPrefix = `The assigned type '${foundTypeAsStr}' is not compatible with the declared property '${name}' of type '${expectedTypeAsStr}'.`;
applyErrorToProperties(foundProp.astNodes, errorMsgPrefix);
Expand Down
96 changes: 96 additions & 0 deletions packages/langium/test/grammar/type-system/inferred-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,102 @@ describe('types of `$container` and `$type` are correct', () => {
}
`);
});

test('types of `$type` for declared linear hierarchies', async () => {
await expectTypes(`
interface A {}
interface B extends A {}
`, expandToString`
export interface A extends AstNode {
readonly $type: 'A' | 'B';
}
export interface B extends A {
readonly $type: 'B';
}
`);
});

test('types of `$type` for declared tree hierarchies', async () => {
await expectTypes(`
interface A {}
interface B extends A {}
interface C extends A {}
interface X extends B {}
interface Y extends B {}
`, expandToString`
export interface A extends AstNode {
readonly $type: 'A' | 'B' | 'C' | 'X' | 'Y';
}
export interface B extends A {
readonly $type: 'B' | 'X' | 'Y';
}
export interface C extends A {
readonly $type: 'C';
}
export interface X extends B {
readonly $type: 'X';
}
export interface Y extends B {
readonly $type: 'Y';
}
`);
});

test('types of `$type` for declared multiple-inheritance hierarchies', async () => {
await expectTypes(`
interface A {}
interface B {}
interface C extends B, A {}
`, expandToString`
export interface A extends AstNode {
readonly $type: 'A' | 'C';
}
export interface B extends AstNode {
readonly $type: 'B' | 'C';
}
export interface C extends A, B {
readonly $type: 'C';
}
`);
});

test('types of `$type` for declared complex hierarchies', async () => {
await expectTypes(`
interface A {}
interface B extends A {}
interface C extends B {}
interface D extends C {}
interface E extends C {}
interface F extends D {}
interface G extends D {}
interface H extends A {}
`, expandToString`
export interface A extends AstNode {
readonly $type: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H';
}
export interface B extends A {
readonly $type: 'B' | 'C' | 'D' | 'E' | 'F' | 'G';
}
export interface H extends A {
readonly $type: 'H';
}
export interface C extends B {
readonly $type: 'C' | 'D' | 'E' | 'F' | 'G';
}
export interface D extends C {
readonly $type: 'D' | 'F' | 'G';
}
export interface E extends C {
readonly $type: 'E';
}
export interface F extends D {
readonly $type: 'F';
}
export interface G extends D {
readonly $type: 'G';
}
`);
});
});

// https://github.com/langium/langium/issues/744
Expand Down
17 changes: 17 additions & 0 deletions packages/langium/test/grammar/type-system/type-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,21 @@ describe('Property types validation takes in account types hierarchy', () => {
});
});

test('No false positive on declared type assignment', async () => {
const validation = await validate(`
interface A {}
interface B extends A {}

interface Test {
value: A;
}

B returns B: {B};

Test returns Test:
value=B
;`);

expectNoIssues(validation);
});
});