langium
Version:
A language engineering tool for the Language Server Protocol
296 lines (265 loc) • 10.4 kB
text/typescript
/******************************************************************************
* 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]
};
}
}