langium
Version:
A language engineering tool for the Language Server Protocol
594 lines (527 loc) • 20.9 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 { expandToNode, expandToStringWithNL, joinToNode, toString, type Generated } from '../../../generate/index.js';
import type { CstNode } from '../../../syntax-tree.js';
import type { Action, Assignment, TypeAttribute } from '../../../languages/generated/ast.js';
import { distinctAndSorted, escapeQuotes } from '../types-util.js';
export interface Property {
name: string;
optional: boolean;
type: PropertyType;
defaultValue?: PropertyDefaultValue;
astNodes: Set<Assignment | Action | TypeAttribute>;
comment?: string;
}
export type PropertyDefaultValue = string | number | boolean | PropertyDefaultValue[];
export type PropertyType =
| ReferenceType
| ArrayType
| PropertyUnion
| ValueType
| PrimitiveType
| StringType;
export interface ReferenceType {
referenceType: PropertyType
isMulti: boolean;
isSingle: boolean;
}
export function isReferenceType(propertyType: PropertyType): propertyType is ReferenceType {
return 'referenceType' in propertyType;
}
export interface ArrayType {
elementType: PropertyType | undefined
}
export function isArrayType(propertyType: PropertyType): propertyType is ArrayType {
return 'elementType' in propertyType;
}
export interface PropertyUnion {
types: PropertyType[]
}
export function isPropertyUnion(propertyType: PropertyType): propertyType is PropertyUnion {
return 'types' in propertyType;
}
export function flattenPropertyUnion(propertyType: PropertyType): PropertyType[] {
if (isPropertyUnion(propertyType)) {
const items: PropertyType[] = [];
for (const type of propertyType.types) {
items.push(...flattenPropertyUnion(type));
}
return items;
} else {
return [propertyType];
}
}
export interface ValueType {
value: TypeOption
}
export function isValueType(propertyType: PropertyType): propertyType is ValueType {
return 'value' in propertyType;
}
export interface PrimitiveType {
primitive: string
regex?: string
}
export function isPrimitiveType(propertyType: PropertyType): propertyType is PrimitiveType {
return 'primitive' in propertyType;
}
export interface StringType {
string: string
}
export function isStringType(propertyType: PropertyType): propertyType is StringType {
return 'string' in propertyType;
}
export type AstTypes = {
interfaces: InterfaceType[],
unions: UnionType[],
}
export function isUnionType(type: TypeOption): type is UnionType {
return type && 'type' in type;
}
export function isInterfaceType(type: TypeOption): type is InterfaceType {
return type && 'properties' in type;
}
export type TypeOption = InterfaceType | UnionType;
export class UnionType {
name: string;
type: PropertyType;
superTypes = new Set<TypeOption>();
subTypes = new Set<TypeOption>();
typeNames = new Set<string>();
declared: boolean;
dataType?: string;
comment?: string;
constructor(name: string, options?: {
declared: boolean,
dataType?: string,
comment?: string,
}) {
this.name = name;
this.declared = options?.declared ?? false;
this.dataType = options?.dataType;
this.comment = options?.comment;
}
toAstTypesString(reflectionInfo: boolean): string {
const unionNode = expandToNode`${this.comment}`
.appendNewLineIfNotEmpty()
.append(`export type ${this.name} = ${propertyTypeToString(this.type, 'AstType')};`)
.appendNewLine();
if (reflectionInfo) {
unionNode.appendNewLine()
.append(addReflectionInfo(this.name));
}
if (this.dataType) {
unionNode.appendNewLine()
.append(addDataTypeReflectionInfo(this));
}
return toString(unionNode);
}
toDeclaredTypesString(reservedWords: Set<string>): string {
return expandToStringWithNL`
type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypeToString(this.type, 'DeclaredType')};
`;
}
}
export class InterfaceType {
name: string;
comment?: string;
superTypes = new Set<TypeOption>();
subTypes = new Set<TypeOption>();
containerTypes = new Set<TypeOption>();
typeNames = new Set<string>();
declared = false;
abstract = false;
properties: Property[] = [];
get superProperties(): Property[] {
return this.getSuperProperties(new Set());
}
private getSuperProperties(visited: Set<string>): Property[] {
if (visited.has(this.name)) {
return [];
} else {
visited.add(this.name);
}
const map = new Map<string, Property>();
for (const property of this.properties) {
map.set(property.name, property);
}
for (const superType of this.interfaceSuperTypes) {
const allSuperProperties = superType.getSuperProperties(visited);
for (const superProp of allSuperProperties) {
if (!map.has(superProp.name)) {
map.set(superProp.name, superProp);
}
}
}
return Array.from(map.values());
}
get allProperties(): Property[] {
const map = new Map(this.superProperties.map(e => [e.name, e]));
for (const subType of this.subTypes) {
this.getSubTypeProperties(subType, map, new Set());
}
const superProps = Array.from(map.values());
return superProps;
}
private getSubTypeProperties(type: TypeOption, map: Map<string, Property>, visited: Set<string>): void {
if (visited.has(this.name)) {
return;
} else {
visited.add(this.name);
}
const props = isInterfaceType(type) ? type.properties : [];
for (const prop of props) {
if (!map.has(prop.name)) {
map.set(prop.name, prop);
}
}
for (const subType of type.subTypes) {
this.getSubTypeProperties(subType, map, visited);
}
}
get interfaceSuperTypes(): InterfaceType[] {
return Array.from(this.superTypes).filter(e => e instanceof InterfaceType);
}
constructor(name: string, declared: boolean, abstract: boolean, comment?: string) {
this.name = name;
this.declared = declared;
this.abstract = abstract;
this.comment = comment;
}
toAstTypesString(reflectionInfo: boolean): string {
const interfaceSuperTypes = this.interfaceSuperTypes.map(e => e.name);
const superTypes = interfaceSuperTypes.length > 0 ? distinctAndSorted([...interfaceSuperTypes]) : ['langium.AstNode'];
const interfaceNode = expandToNode`${this.comment}`
.appendNewLineIfNotEmpty()
.append(`export interface ${this.name} extends ${superTypes.join(', ')} {`)
.appendNewLine();
interfaceNode.indent(body => {
if (this.containerTypes.size > 0) {
body.append(`readonly $container: ${distinctAndSorted([...this.containerTypes].map(e => e.name)).join(' | ')};`).appendNewLine();
}
if (this.typeNames.size > 0) {
body.append(`readonly $type: ${distinctAndSorted([...this.typeNames]).map(e => `'${e}'`).join(' | ')};`).appendNewLine();
}
body.append(
pushProperties(this.properties, 'AstType')
);
});
interfaceNode.append('}').appendNewLine();
if (reflectionInfo) {
interfaceNode
.appendNewLine()
.append(addReflectionInfo(this.name, this.superProperties));
}
return toString(interfaceNode);
}
toDeclaredTypesString(reservedWords: Set<string>): string {
const name = escapeReservedWords(this.name, reservedWords);
const superTypes = distinctAndSorted(this.interfaceSuperTypes.map(e => e.name)).join(', ');
return toString(
expandToNode`
interface ${name}${superTypes.length > 0 ? ` extends ${superTypes}` : ''} {
${pushProperties(this.properties, 'DeclaredType')}
}
`.appendNewLine()
);
}
}
export class TypeResolutionError extends Error {
readonly target: CstNode | undefined;
constructor(message: string, target: CstNode | undefined) {
super(message);
this.name = 'TypeResolutionError';
this.target = target;
}
}
export function isTypeAssignable(from: PropertyType, to: PropertyType): boolean {
return isTypeAssignableInternal(from, to, new Map());
}
function isTypeAssignableInternal(from: PropertyType | undefined, to: PropertyType | undefined, visited: Map<string, boolean>): boolean {
if (!from) {
return true;
} else if (!to) {
return false;
}
const key = `${propertyTypeToKeyString(from)}»${propertyTypeToKeyString(to)}`;
let result = visited.get(key);
if (result !== undefined) {
return result;
}
visited.set(key, false);
result = false;
if (isPropertyUnion(from)) {
result = from.types.every(fromType => isTypeAssignableInternal(fromType, to, visited));
} else if (isPropertyUnion(to)) {
result = to.types.some(toType => isTypeAssignableInternal(from, toType, visited));
} else if (isReferenceType(from)) {
result = isReferenceType(to)
&& from.isMulti === to.isMulti
&& from.isSingle === to.isSingle
&& isTypeAssignableInternal(from.referenceType, to.referenceType, visited);
} else if (isArrayType(from)) {
result = isArrayType(to) && isTypeAssignableInternal(from.elementType, to.elementType, visited);
} else if (isValueType(from)) {
if (isUnionType(from.value)) {
if (from.value.dataType) {
// We can test the primitive data type directly
// This potentially skips a expensive recursive call
// This also helps in case the computed internal data type does not fit the declared data type
const primitiveType: PrimitiveType = {
primitive: from.value.dataType
};
result = isTypeAssignableInternal(primitiveType, to, visited);
}
if (!result) {
result = isTypeAssignableInternal(from.value.type, to, visited);
}
} else if (!isValueType(to)) {
result = false;
} else if (isUnionType(to.value)) {
result = isTypeAssignableInternal(from, to.value.type, visited);
} else {
result = isInterfaceAssignable(from.value, to.value, new Set());
}
} else if (isValueType(to) && isUnionType(to.value)) {
if (isValueType(from) && isUnionType(from.value) && to.value.name === from.value.name) {
result = true;
} else {
result = isTypeAssignableInternal(from, to.value.type, visited);
}
} else if (isPrimitiveType(from)) {
result = isPrimitiveType(to) && from.primitive === to.primitive;
} else if (isStringType(from)) {
result = (isPrimitiveType(to) && to.primitive === 'string') || (isStringType(to) && to.string === from.string);
}
if (result) {
visited.set(key, result);
}
return result;
}
function isInterfaceAssignable(from: InterfaceType, to: InterfaceType, visited: Set<string>): boolean {
const key = from.name;
if (visited.has(key)) {
return false;
} else {
visited.add(key);
}
if (from.name === to.name) {
return true;
}
for (const superType of from.superTypes) {
if (isInterfaceType(superType) && isInterfaceAssignable(superType, to, visited)) {
return true;
}
}
return false;
}
function propertyTypeToKeyString(type: PropertyType): string {
if (isReferenceType(type)) {
return `@(${propertyTypeToKeyString(type.referenceType)})${type.isMulti ? '*' : ''}`;
} else if (isArrayType(type)) {
return type.elementType ? `(${propertyTypeToKeyString(type.elementType)})[]` : 'unknown[]';
} else if (isPropertyUnion(type)) {
const union = type.types.map(e => propertyTypeToKeyString(e)).join(' | ');
if (type.types.length <= 1) {
return `Union<${union}>`;
}
return union;
} else if (isValueType(type)) {
return `Value<${type.value.name}>`;
} else if (isPrimitiveType(type)) {
return type.primitive;
} else if (isStringType(type)) {
return `'${type.string}'`;
}
throw new Error('Invalid type');
}
export function propertyTypeToString(type?: PropertyType, mode: 'AstType' | 'DeclaredType' = 'AstType'): string {
if (!type) {
return 'unknown';
}
if (isReferenceType(type)) {
const refType = propertyTypeToString(type.referenceType, mode);
return mode === 'AstType' ? `langium.${type.isMulti ? 'Multi' : ''}Reference<${refType}>` : `@${typeParenthesis(type.referenceType, refType)}${type.isMulti ? '+' : ''}`;
} else if (isArrayType(type)) {
const arrayType = propertyTypeToString(type.elementType, mode);
return mode === 'AstType' ? `Array<${arrayType}>` : `${type.elementType ? typeParenthesis(type.elementType, arrayType) : 'unknown'}[]`;
} else if (isPropertyUnion(type)) {
const types = type.types.map(e => typeParenthesis(e, propertyTypeToString(e, mode)));
return distinctAndSorted(types).join(' | ');
} else if (isValueType(type)) {
return type.value.name;
} else if (isPrimitiveType(type)) {
return type.primitive;
} else if (isStringType(type)) {
const delimiter = mode === 'AstType' ? "'" : '"';
return `${delimiter}${escapeQuotes(type.string, delimiter)}${delimiter}`;
}
throw new Error('Invalid type');
}
function typeParenthesis(type: PropertyType, name: string): string {
const needsParenthesis = isPropertyUnion(type);
if (needsParenthesis) {
name = `(${name})`;
}
return name;
}
function pushProperties(
properties: Property[],
mode: 'AstType' | 'DeclaredType'
): Generated {
function propertyToNode(property: Property): Generated {
// We don't need to escape reserved words in the property name
// The parser will be able to handle it just fine
const name = property.name;
const optional = property.optional && !isMandatoryPropertyType(property.type);
const propType = propertyTypeToString(property.type, mode);
return expandToNode`${property.comment}`
.appendNewLineIfNotEmpty()
.append(`${name}${optional ? '?' : ''}: ${propType};`);
}
return joinToNode(
distinctAndSorted(properties, (a, b) => a.name.localeCompare(b.name)),
propertyToNode,
{ appendNewLineIfNotEmpty: true }
);
}
export function isMandatoryPropertyType(propertyType: PropertyType): boolean {
if (isArrayType(propertyType)) {
return true;
} else if (isReferenceType(propertyType)) {
return false;
} else if (isPropertyUnion(propertyType)) {
return propertyType.types.every(e => isMandatoryPropertyType(e));
} else if (isPrimitiveType(propertyType)) {
const value = propertyType.primitive;
return value === 'boolean';
} else {
return false;
}
}
function addReflectionInfo(name: string, properties: Property[] = []): Generated {
return expandToNode`
export const ${name} = {
$type: '${name}'${properties.length > 0 ? ',' : ''}
${joinToNode(properties.sort((a, b) => a.name.localeCompare(b.name)), prop => `${prop.name}: '${escapeQuotes(prop.name, "'")}'`, { separator: ',', appendNewLineIfNotEmpty: true })}
} as const;
export function is${name}(item: unknown): item is ${name} {
return reflection.isInstance(item, ${name}.$type);
}
`.appendNewLine();
}
function addDataTypeReflectionInfo(union: UnionType): Generated {
switch (union.dataType) {
case 'string':
if (containsOnlyStringTypes(union.type)) {
const subTypes = Array.from(union.subTypes).map(e => e.name);
const strings = collectStringValuesFromDataType(union.type);
const regexes = collectRegexesFromDataType(union.type);
if (subTypes.length === 0 && strings.length === 0 && regexes.length === 0) {
return generateIsDataTypeFunction(union.name, `typeof item === '${union.dataType}'`);
} else {
const returnString = createDataTypeCheckerFunctionReturnString(subTypes, strings, regexes);
return generateIsDataTypeFunction(union.name, returnString);
}
}
return;
case 'number':
case 'boolean':
case 'bigint':
return generateIsDataTypeFunction(union.name, `typeof item === '${union.dataType}'`);
case 'Date':
return generateIsDataTypeFunction(union.name, 'item instanceof Date');
default:
return;
}
}
function containsOnlyStringTypes(propertyType: PropertyType): boolean {
let result = true;
if (isPrimitiveType(propertyType)) {
if (propertyType.primitive === 'string') {
return true;
} else {
return false;
}
} else if (isStringType(propertyType)) {
return true;
} else if (!isPropertyUnion(propertyType)) {
return false;
} else {
for (const type of propertyType.types) {
if (isValueType(type)) {
if (isUnionType(type.value)) {
if (!containsOnlyStringTypes(type.value.type)) {
return false;
}
} else {
return false;
}
} else if (isPrimitiveType(type)) {
if (type.primitive !== 'string' || !type.regex) {
return false;
}
} else if (isPropertyUnion(type)) {
result = containsOnlyStringTypes(type);
} else if (!isStringType(type)) {
return false;
}
}
}
return result;
}
function createDataTypeCheckerFunctionReturnString(subTypes: string[], strings: string[], regexes: string[]): string {
const allArray = [
...subTypes.map(e => `is${e}(item)`),
...strings.map(e => `item === '${e}'`)
];
if (regexes.length > 0) {
const joinedRegexes = regexes.map(e => `${e}.test(item)`).join(' || ');
allArray.push(`(typeof item === 'string' && (${joinedRegexes}))`);
}
return allArray.join(' || ');
}
function escapeReservedWords(name: string, reserved: Set<string>): string {
return reserved.has(name) ? `^${name}` : name;
}
function collectStringValuesFromDataType(propertyType: PropertyType): string[] {
const values: string[] = [];
if (isStringType(propertyType)) {
return [propertyType.string];
}
if (isPropertyUnion(propertyType)) {
for (const type of propertyType.types) {
if (isStringType(type)) {
values.push(type.string);
} else if (isPropertyUnion(type)) {
values.push(...collectStringValuesFromDataType(type));
}
}
}
return values;
}
function collectRegexesFromDataType(propertyType: PropertyType): string[] {
const regexes: string[] = [];
if (isPrimitiveType(propertyType) && propertyType.primitive === 'string' && propertyType.regex) {
regexes.push(propertyType.regex);
}
if (isPropertyUnion(propertyType)) {
for (const type of propertyType.types) {
if (isPrimitiveType(type) && type.primitive === 'string' && type.regex) {
regexes.push(type.regex);
} else if (isPropertyUnion(type)) {
regexes.push(...collectRegexesFromDataType(type));
}
}
}
return regexes;
}
function generateIsDataTypeFunction(unionName: string, returnString: string): Generated {
return expandToNode`
export function is${unionName}(item: unknown): item is ${unionName} {
return ${returnString};
}
`.appendNewLine();
}