UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

296 lines (265 loc) 10.4 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 { Action, Assignment, TypeAttribute } from '../../../languages/generated/ast.js'; import { hasBooleanType } from '../types-util.js'; import type { AstTypes, Property, PropertyType } from './types.js'; import { InterfaceType, UnionType, isArrayType } from './types.js'; export interface PlainAstTypes { interfaces: PlainInterface[]; unions: PlainUnion[]; } export type PlainType = PlainInterface | PlainUnion; export interface PlainInterface { name: string; superTypes: Set<string>; subTypes: Set<string>; properties: PlainProperty[]; declared: boolean; abstract: boolean; comment?: string; } export function isPlainInterface(type: PlainType): type is PlainInterface { return !isPlainUnion(type); } export interface PlainUnion { name: string; superTypes: Set<string>; subTypes: Set<string>; type: PlainPropertyType; declared: boolean; dataType?: string; comment?: string; } export function isPlainUnion(type: PlainType): type is PlainUnion { return 'type' in type; } export interface PlainProperty { name: string; optional: boolean; astNodes: Set<Assignment | Action | TypeAttribute>; type: PlainPropertyType; defaultValue?: PlainPropertyDefaultValue; comment?: string; } export type PlainPropertyDefaultValue = string | number | boolean | PlainPropertyDefaultValue[]; export type PlainPropertyType = | PlainReferenceType | PlainArrayType | PlainPropertyUnion | PlainValueType | PlainPrimitiveType | PlainStringType; export interface PlainReferenceType { referenceType: PlainPropertyType; isMulti: boolean; isSingle: boolean; } export function isPlainReferenceType(propertyType: PlainPropertyType): propertyType is PlainReferenceType { return 'referenceType' in propertyType; } export interface PlainArrayType { elementType: PlainPropertyType; } export function isPlainArrayType(propertyType: PlainPropertyType): propertyType is PlainArrayType { return 'elementType' in propertyType; } export interface PlainPropertyUnion { types: PlainPropertyType[]; } export function isPlainPropertyUnion(propertyType: PlainPropertyType): propertyType is PlainPropertyUnion { return 'types' in propertyType; } export interface PlainValueType { value: string; } export function isPlainValueType(propertyType: PlainPropertyType): propertyType is PlainValueType { return 'value' in propertyType; } export interface PlainPrimitiveType { primitive: string; regex?: string; } export function isPlainPrimitiveType(propertyType: PlainPropertyType): propertyType is PlainPrimitiveType { return 'primitive' in propertyType; } export interface PlainStringType { string: string; } export function isPlainStringType(propertyType: PlainPropertyType): propertyType is PlainStringType { return 'string' in propertyType; } export function plainToTypes(plain: PlainAstTypes): AstTypes { const interfaceTypes = new Map<string, InterfaceType>(); const unionTypes = new Map<string, UnionType>(); for (const interfaceValue of plain.interfaces) { const type = new InterfaceType(interfaceValue.name, interfaceValue.declared, interfaceValue.abstract, interfaceValue.comment); interfaceTypes.set(interfaceValue.name, type); } for (const unionValue of plain.unions) { const type = new UnionType(unionValue.name, { declared: unionValue.declared, dataType: unionValue.dataType, comment: unionValue.comment, }); unionTypes.set(unionValue.name, type); } for (const interfaceValue of plain.interfaces) { const type = interfaceTypes.get(interfaceValue.name)!; for (const superTypeName of interfaceValue.superTypes) { const superType = interfaceTypes.get(superTypeName) || unionTypes.get(superTypeName); if (superType) { type.superTypes.add(superType); } } for (const subTypeName of interfaceValue.subTypes) { const subType = interfaceTypes.get(subTypeName) || unionTypes.get(subTypeName); if (subType) { type.subTypes.add(subType); } } for (const property of interfaceValue.properties) { const prop = plainToProperty(property, interfaceTypes, unionTypes); type.properties.push(prop); } } for (const unionValue of plain.unions) { const type = unionTypes.get(unionValue.name)!; type.type = plainToPropertyType(unionValue.type, type, interfaceTypes, unionTypes); } return { interfaces: Array.from(interfaceTypes.values()), unions: Array.from(unionTypes.values()) }; } function plainToProperty(property: PlainProperty, interfaces: Map<string, InterfaceType>, unions: Map<string, UnionType>): Property { const prop: Property = { name: property.name, optional: property.optional, astNodes: property.astNodes, type: plainToPropertyType(property.type, undefined, interfaces, unions), comment: property.comment, }; if (property.defaultValue !== undefined) { prop.defaultValue = property.defaultValue; } else if (hasBooleanType(prop.type)) { prop.defaultValue = false; } else if (isArrayType(prop.type)) { prop.defaultValue = []; } return prop; } function plainToPropertyType(type: PlainPropertyType, union: UnionType | undefined, interfaces: Map<string, InterfaceType>, unions: Map<string, UnionType>): PropertyType { if (isPlainArrayType(type)) { return { elementType: plainToPropertyType(type.elementType, union, interfaces, unions) }; } else if (isPlainReferenceType(type)) { return { referenceType: plainToPropertyType(type.referenceType, undefined, interfaces, unions), isMulti: type.isMulti, isSingle: type.isSingle }; } else if (isPlainPropertyUnion(type)) { return { types: type.types.map(e => plainToPropertyType(e, union, interfaces, unions)) }; } else if (isPlainStringType(type)) { return { string: type.string }; } else if (isPlainPrimitiveType(type)) { return { primitive: type.primitive, regex: type.regex }; } else if (isPlainValueType(type)) { const value = interfaces.get(type.value) || unions.get(type.value); if (!value) { return { primitive: 'unknown' }; } if (union) { union.subTypes.add(value); } return { value }; } else { throw new Error('Invalid property type'); } } export function mergePropertyTypes(first: PlainPropertyType, second: PlainPropertyType): PlainPropertyType { const { union: flattenedFirstUnion, array: flattenedFirstArray } = flattenPlainType(first); const { union: flattenedSecondUnion, array: flattenedSecondArray } = flattenPlainType(second); const flattenedUnion = mergeTypeUnion(flattenedFirstUnion, flattenedSecondUnion); const flattenedArray = mergeTypeUnion(flattenedFirstArray, flattenedSecondArray); if (flattenedArray.length > 0) { flattenedUnion.push({ elementType: flattenedArray.length === 1 ? flattenedArray[0] : { types: flattenedArray } }); } if (flattenedUnion.length === 1) { return flattenedUnion[0]; } else { return { types: flattenedUnion }; } } function mergeTypeUnion(first: PlainPropertyType[], second: PlainPropertyType[]): PlainPropertyType[] { const result = [...first]; for (const type of second) { if (!includesType(result, type)) { result.push(type); } else if (isPlainReferenceType(type)) { // Adjust the existing reference type to also include the multi/single flags of the new type const existing = result.find((e): e is PlainReferenceType => isPlainReferenceType(e) && typeEquals(e.referenceType, type.referenceType)); if (existing) { existing.isMulti ||= type.isMulti; existing.isSingle ||= type.isSingle; } } } return result; } function includesType(list: PlainPropertyType[], value: PlainPropertyType): boolean { return list.some(e => typeEquals(e, value)); } function typeEquals(first: PlainPropertyType, second: PlainPropertyType): boolean { if (isPlainArrayType(first) && isPlainArrayType(second)) { return typeEquals(first.elementType, second.elementType); } else if (isPlainReferenceType(first) && isPlainReferenceType(second)) { return typeEquals(first.referenceType, second.referenceType); } else if (isPlainValueType(first) && isPlainValueType(second)) { return first.value === second.value; } else if (isPlainPrimitiveType(first) && isPlainPrimitiveType(second)) { return first.primitive === second.primitive; } else { return false; } } export function flattenPlainType(type: PlainPropertyType): { union: PlainPropertyType[], array: PlainPropertyType[] } { if (isPlainPropertyUnion(type)) { const flattened = type.types.flatMap(e => flattenPlainType(e)); return { union: flattened.map(e => e.union).flat(), array: flattened.map(e => e.array).flat() }; } else if (isPlainArrayType(type)) { return { array: flattenPlainType(type.elementType).union, union: [] }; } else { return { array: [], union: [type] }; } }