UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

187 lines (164 loc) 7.66 kB
/****************************************************************************** * Copyright 2022 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 { Grammar } from '../../languages/generated/ast.js'; import type { LangiumDocuments } from '../../workspace/documents.js'; import type { AstTypes, InterfaceType, PropertyType, TypeOption, UnionType } from './type-collector/types.js'; import type { ValidationAstTypes } from './type-collector/all-types.js'; import type { PlainAstTypes, PlainInterface, PlainUnion } from './type-collector/plain-types.js'; import { findAstTypes, sortInterfacesTopologically } from './types-util.js'; import { isInterfaceType, isPrimitiveType, isPropertyUnion, isStringType, isUnionType, isValueType } from './type-collector/types.js'; import { collectTypeResources } from './type-collector/all-types.js'; import { plainToTypes } from './type-collector/plain-types.js'; /** * Collects all types for the generated AST. The types collector entry point. * * @param grammars All grammars involved in the type generation process * @param documents Additional documents so that imports can be resolved as necessary */ export function collectAst(grammars: Grammar | Grammar[], documents?: LangiumDocuments): AstTypes { const { inferred, declared } = collectTypeResources(grammars, documents); return createAstTypes(inferred, declared); } /** * Collects all types used during the validation process. * The validation process requires us to compare our inferred types with our declared types. * * @param grammars All grammars involved in the validation process * @param documents Additional documents so that imports can be resolved as necessary */ export function collectValidationAst(grammars: Grammar | Grammar[], documents?: LangiumDocuments): ValidationAstTypes { const { inferred, declared, astResources } = collectTypeResources(grammars, documents); return { astResources, inferred: createAstTypes(declared, inferred), declared: createAstTypes(inferred, declared) }; } export function createAstTypes(first: PlainAstTypes, second?: PlainAstTypes): AstTypes { const astTypes: PlainAstTypes = { interfaces: sortInterfacesTopologically(mergeAndRemoveDuplicates<PlainInterface>(...first.interfaces, ...second?.interfaces ?? [])), unions: mergeAndRemoveDuplicates<PlainUnion>(...first.unions, ...second?.unions ?? []), }; const finalTypes = plainToTypes(astTypes); specifyAstNodeProperties(finalTypes); return finalTypes; } /** * Merges the lists of given elements into a single list and removes duplicates. Elements later in the lists get precedence over earlier elements. * * The distinction is performed over the `name` property of the element. The result is a name-sorted list of elements. */ function mergeAndRemoveDuplicates<T extends { name: string }>(...elements: T[]): T[] { return Array.from(elements .reduce((acc, type) => { acc.set(type.name, type); return acc; }, new Map<string, T>()) .values()).sort((a, b) => a.name.localeCompare(b.name)); } export function specifyAstNodeProperties(astTypes: AstTypes) { const nameToType = filterInterfaceLikeTypes(astTypes); const array = Array.from(nameToType.values()); addSubTypes(array); buildContainerTypes(astTypes.interfaces); buildTypeNames(array); } function buildTypeNames(types: TypeOption[]) { // Recursively collect all subtype names const visited = new Set<TypeOption>(); const collect = (type: TypeOption): void => { if (visited.has(type)) return; visited.add(type); type.typeNames.add(type.name); for (const subtype of type.subTypes) { collect(subtype); subtype.typeNames.forEach(n => type.typeNames.add(n)); } }; types.forEach(collect); } /** * Removes union types that reference only to primitive types or * types that reference only to primitive types. */ function filterInterfaceLikeTypes({ interfaces, unions }: AstTypes): Map<string, TypeOption> { const nameToType = (interfaces as TypeOption[]).concat(unions) .reduce((acc, e) => { acc.set(e.name, e); return acc; }, new Map<string, TypeOption>()); const cache = new Map<UnionType, boolean>(); for (const union of unions) { cache.set(union, isDataType(union.type, new Set())); } for (const [union, isDataType] of cache) { if (isDataType) { nameToType.delete(union.name); } } return nameToType; } function isDataType(property: PropertyType, visited: Set<PropertyType>): boolean { if (visited.has(property)) { return true; } visited.add(property); if (isPropertyUnion(property)) { return property.types.every(e => isDataType(e, visited)); } else if (isValueType(property)) { const value = property.value; if (isUnionType(value)) { return isDataType(value.type, visited); } else { return false; } } else { return isPrimitiveType(property) || isStringType(property); } } function addSubTypes(types: TypeOption[]) { for (const interfaceType of types) { for (const superTypeName of interfaceType.superTypes) { superTypeName.subTypes.add(interfaceType); } } } /** * Builds types of `$container` property. * @param interfaces Interfaces for which container types are calculated. */ function buildContainerTypes(interfaces: InterfaceType[]) { const nameToInterface = interfaces .reduce((acc, type) => { acc.set(type.name, type); return acc; }, new Map<string, InterfaceType>() ); // 1st stage: collect container types for (const containerType of interfaces) { const types = containerType.properties.flatMap(property => findAstTypes(property.type)); for (const type of types) { nameToInterface.get(type)?.containerTypes.add(containerType); } } // 2nd stage: lift the container types of containers to parents // if one of the children has no container types, the parent also loses container types // contains type names that have children and at least one of them has no container types const emptyContainerTypes: Set<string> = new Set(); const queue: InterfaceType[] = interfaces.filter(interf => interf.subTypes.size === 0); const visited: Set<InterfaceType> = new Set(queue); while (queue.length > 0) { const interf = queue.shift(); if (interf) { for (const superType of interf.superTypes) { if (isInterfaceType(superType)) { if (interf.containerTypes.size === 0) { emptyContainerTypes.add(superType.name); superType.containerTypes.clear(); } else if (!emptyContainerTypes.has(superType.name)) { interf.containerTypes.forEach(e => superType.containerTypes.add(e)); } if (!visited.has(superType)) { visited.add(superType); queue.push(superType); } } } } } }