rbxts-transformer-t-new-new
Version:
TypeScript transformer which converts TypeScript types to t entities
264 lines (204 loc) • 5.59 kB
text/typescript
import ts from "typescript";
import path from "path";
/**
* Concatenates two arrays and removes duplicates
*/
export function mergeArrays<T>(array1: T[], array2: T[]): T[] {
return [...new Set([...array1, ...array2])];
}
/**
* Separates an array into two arrays:
* the first array contains elements matched
* by predicate as true, the second as false.
*/
export function separateArray<T>(
array: T[],
predicate: (element: T) => boolean,
): [T[], T[]] {
const nextIteration = (array: T[], onTrue: T[], onFalse: T[]): [T[], T[]] => {
if (array.length === 0) return [onTrue, onFalse];
const [head, ...tail] = array;
if (predicate(head)) return nextIteration(tail, [...onTrue, head], onFalse);
return nextIteration(tail, onTrue, [...onFalse, head]);
};
return nextIteration(array, [], []);
}
/**
* Checks if type is an EnumItem
*/
export function isEnum(type: ts.Type): boolean {
return (<any>type.aliasSymbol)?.parent?.escapedName === "Enum";
}
/**
* Checks if ts.Type is a function
*/
export function isFunctionType(type: ts.Type): boolean {
return type.getCallSignatures().length !== 0;
}
/**
* Checks if ts.Type is of ts.TupleType
*/
export function isTupleType(
type: ts.Type,
typeChecker: ts.TypeChecker,
): type is ts.TupleType {
return (<any>typeChecker).isTupleType(type);
}
/**
* Checks if ts.Type is of Array type
*/
export function isArrayType(
type: ts.Type,
typeChecker: ts.TypeChecker,
): type is ts.GenericType {
return (<any>typeChecker).isArrayType(type);
}
/**
* Checks if ts.Type is of Map type
*/
export function isMapType(type: ts.Type): type is ts.GenericType {
return type.symbol?.getName() === "Map";
}
/**
* Checks if ts.Type is of Map type
*/
export function isBrickColorType(type: ts.Type): type is ts.GenericType {
return type.symbol?.getName() === "BrickColor";
}
/**
* Converts array of ts.Types to array of ts.Expressions
*/
/**
* Checks if ts.Type being is of ts.ObjectType
*/
export function isObjectType(type: ts.Type): type is ts.ObjectType {
return type.symbol?.getName() === "__type";
}
/**
* Checks if property declaration matched is optional
*/
export function isOptionalPropertyDeclaration(prop: ts.Symbol): boolean {
const property = prop.valueDeclaration as ts.PropertyDeclaration;
return property.questionToken !== undefined;
}
/**
* Checks if type is a literal
*/
export function isLiteral(
typeChecker: ts.TypeChecker,
): (type: ts.Type) => boolean {
return (type: ts.Type): boolean =>
type.isLiteral() ||
["true", "false"].includes(typeChecker.typeToString(type));
}
export function isCustomEnum(type: ts.Type) {
return type.symbol?.valueDeclaration?.kind === ts.SyntaxKind.EnumDeclaration;
}
/**
* Extracts property type and name from ts.Symbol
*/
export function extractProperty(
prop: ts.Symbol,
typeChecker: ts.TypeChecker,
): { name: string; type: ts.Type } {
const declaration = prop.valueDeclaration;
const type =
(<any>prop).type === undefined
? typeChecker.getTypeFromTypeNode((<any>declaration).type)
: ((<any>prop).type.target ?? (<any>prop).type);
const name = String(prop.escapedName);
return { name, type };
}
/**
* Builds name for property.
* If it is simple, use just string.
* For complex names wrap it into ts.StringLiteral
*/
export function buildPropertyName(name: string): string | ts.StringLiteral {
if (name.match(/^[a-zA-Z_]+[\w_]+$/) !== null) return name;
return ts.factory.createStringLiteral(name);
}
/**
* Represents function that maps single node
*/
type MappingFunction = {
(node: ts.Node, program: ts.Program): [ts.Node, boolean];
};
/**
* Predicate accepts ts.Node or it"s subtypes
*/
type NodePredicate<T extends ts.Node = ts.Node> = (node: T) => boolean;
/**
* Returns mapping function that replaces
* nodes that satisfy the passed predicate
* with a replacement node passed but skips the first appearance
*/
export function getNodeReplacer(
predicate: NodePredicate,
replacement: ts.Node,
): MappingFunction {
let replaced = false;
return (node: ts.Node) => {
if (replaced && predicate(node)) return [replacement, replaced];
if (replaced || !predicate(node)) return [node, replaced];
replaced = true;
return [node, replaced];
};
}
/**
* Returns aliased symbol of ts.Symbol or
* this symbol itself if it is aliased
*/
export function getAliasedSymbol(
symbol: ts.Symbol,
typeChecker: ts.TypeChecker,
): ts.Symbol {
try {
return typeChecker.getAliasedSymbol(symbol);
} catch (_) {
return symbol;
}
}
/**
* Returns aliased symbol of ts.Identifier
*/
export function getAliasedSymbolOfNode(
node: ts.Identifier,
typeChecker: ts.TypeChecker,
): ts.Symbol | undefined {
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol === undefined) return undefined;
return getAliasedSymbol(symbol, typeChecker);
}
/**
* Checks is symbol belongs to declaration
* with name "name" located in file filePath
*/
export function isSymbolOf(
symbol: ts.Symbol,
name: string,
filePath: string,
): boolean {
if (symbol.escapedName !== name) return false;
return [symbol.valueDeclaration, ...symbol.declarations!]
.filter(Boolean) // TODO
.some(
(declaration) =>
path.join(declaration!.getSourceFile().fileName) === filePath,
);
}
/**
* Returns id of ts.Type
*/
export function getTypeId(type: ts.Type): number {
return (<any>type).id;
}
/**
* Merges two objects
*/
export function mergeObjects<K extends string | number, V>(
obj1: Record<K, V>,
obj2: Record<K, V>,
): Record<K, V> {
return { ...obj1, ...obj2 };
}