langium
Version:
A language engineering tool for the Language Server Protocol
503 lines (502 loc) • 18.3 kB
JavaScript
/******************************************************************************
* 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 } from '../../../generate/index.js';
import { distinctAndSorted, escapeQuotes } from '../types-util.js';
export function isReferenceType(propertyType) {
return 'referenceType' in propertyType;
}
export function isArrayType(propertyType) {
return 'elementType' in propertyType;
}
export function isPropertyUnion(propertyType) {
return 'types' in propertyType;
}
export function flattenPropertyUnion(propertyType) {
if (isPropertyUnion(propertyType)) {
const items = [];
for (const type of propertyType.types) {
items.push(...flattenPropertyUnion(type));
}
return items;
}
else {
return [propertyType];
}
}
export function isValueType(propertyType) {
return 'value' in propertyType;
}
export function isPrimitiveType(propertyType) {
return 'primitive' in propertyType;
}
export function isStringType(propertyType) {
return 'string' in propertyType;
}
export function isUnionType(type) {
return type && 'type' in type;
}
export function isInterfaceType(type) {
return type && 'properties' in type;
}
export class UnionType {
constructor(name, options) {
this.superTypes = new Set();
this.subTypes = new Set();
this.typeNames = new Set();
this.name = name;
this.declared = options?.declared ?? false;
this.dataType = options?.dataType;
this.comment = options?.comment;
}
toAstTypesString(reflectionInfo) {
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) {
return expandToStringWithNL `
type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypeToString(this.type, 'DeclaredType')};
`;
}
}
export class InterfaceType {
get superProperties() {
return this.getSuperProperties(new Set());
}
getSuperProperties(visited) {
if (visited.has(this.name)) {
return [];
}
else {
visited.add(this.name);
}
const map = new Map();
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() {
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;
}
getSubTypeProperties(type, map, visited) {
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() {
return Array.from(this.superTypes).filter(e => e instanceof InterfaceType);
}
constructor(name, declared, abstract, comment) {
this.superTypes = new Set();
this.subTypes = new Set();
this.containerTypes = new Set();
this.typeNames = new Set();
this.declared = false;
this.abstract = false;
this.properties = [];
this.name = name;
this.declared = declared;
this.abstract = abstract;
this.comment = comment;
}
toAstTypesString(reflectionInfo) {
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) {
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 {
constructor(message, target) {
super(message);
this.name = 'TypeResolutionError';
this.target = target;
}
}
export function isTypeAssignable(from, to) {
return isTypeAssignableInternal(from, to, new Map());
}
function isTypeAssignableInternal(from, to, visited) {
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 (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 (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 = {
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 (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, to, visited) {
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) {
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, mode = 'AstType') {
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, name) {
const needsParenthesis = isPropertyUnion(type);
if (needsParenthesis) {
name = `(${name})`;
}
return name;
}
function pushProperties(properties, mode) {
function propertyToNode(property) {
// 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) {
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, properties = []) {
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) {
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) {
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, strings, regexes) {
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, reserved) {
return reserved.has(name) ? `^${name}` : name;
}
function collectStringValuesFromDataType(propertyType) {
const values = [];
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) {
const regexes = [];
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, returnString) {
return expandToNode `
export function is${unionName}(item: unknown): item is ${unionName} {
return ${returnString};
}
`.appendNewLine();
}
//# sourceMappingURL=types.js.map