@travetto/transformer
Version:
Functionality for AST transformations, with transformer registration, and general utils
195 lines (183 loc) • 7.09 kB
text/typescript
import ts from 'typescript';
import { transformCast, TemplateLiteral } from '../types/shared.ts';
const TypedObject: {
keys<T = unknown, K extends keyof T = keyof T>(o: T): K[];
} & ObjectConstructor = Object;
function isNode(n: unknown): n is ts.Node {
return !!n && typeof n === 'object' && 'kind' in n;
}
const KNOWN_FNS = new Set<unknown>([String, Number, Boolean, Date, RegExp]);
function isKnownFn(n: unknown): n is Function {
return KNOWN_FNS.has(n);
}
/**
* Utilities for dealing with literals
*/
export class LiteralUtil {
/**
* Determine if a type is a literal type
* @param type
*/
static isLiteralType(type: ts.Type): type is ts.LiteralType {
const flags = type.getFlags();
// eslint-disable-next-line no-bitwise
return (flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.StringLiteral)) > 0;
}
/**
* Convert literal to a `ts.Node` type
*/
static fromLiteral<T extends ts.Expression>(factory: ts.NodeFactory, val: T): T;
static fromLiteral(factory: ts.NodeFactory, val: undefined): ts.Identifier;
static fromLiteral(factory: ts.NodeFactory, val: null): ts.NullLiteral;
static fromLiteral(factory: ts.NodeFactory, val: object): ts.ObjectLiteralExpression;
static fromLiteral(factory: ts.NodeFactory, val: unknown[]): ts.ArrayLiteralExpression;
static fromLiteral(factory: ts.NodeFactory, val: string): ts.StringLiteral;
static fromLiteral(factory: ts.NodeFactory, val: number): ts.NumericLiteral;
static fromLiteral(factory: ts.NodeFactory, val: boolean): ts.BooleanLiteral;
static fromLiteral(factory: ts.NodeFactory, val: unknown): ts.Node {
if (isNode(val)) { // If already a node
return val;
} else if (Array.isArray(val)) {
val = factory.createArrayLiteralExpression(val.map(v => this.fromLiteral(factory, v)));
} else if (val === undefined) {
val = factory.createIdentifier('undefined');
} else if (val === null) {
val = factory.createNull();
} else if (typeof val === 'string') {
val = factory.createStringLiteral(val);
} else if (typeof val === 'number') {
const res = factory.createNumericLiteral(Math.abs(val));
val = val < 0 ? factory.createPrefixMinus(res) : res;
} else if (typeof val === 'boolean') {
val = val ? factory.createTrue() : factory.createFalse();
} else if (val instanceof RegExp) {
val = factory.createRegularExpressionLiteral(`/${val.source}/${val.flags ?? ''}`);
} else if (isKnownFn(val)) {
val = factory.createIdentifier(val.name);
} else {
const ov = val;
const pairs: ts.PropertyAssignment[] = [];
for (const k of TypedObject.keys(ov)) {
if (ov[k] !== undefined) {
pairs.push(
factory.createPropertyAssignment(k, this.fromLiteral(factory, ov[k]))
);
}
}
return factory.createObjectLiteralExpression(pairs);
}
return transformCast(val);
}
/**
* Convert a `ts.Node` to a JS literal
*/
static toLiteral(val: ts.NullLiteral, strict?: boolean): null;
static toLiteral(val: ts.NumericLiteral, strict?: boolean): number;
static toLiteral(val: ts.StringLiteral, strict?: boolean): string;
static toLiteral(val: ts.BooleanLiteral, strict?: boolean): boolean;
static toLiteral(val: ts.ObjectLiteralExpression, strict?: boolean): object;
static toLiteral(val: ts.ArrayLiteralExpression, strict?: boolean): unknown[];
static toLiteral(val: undefined, strict?: boolean): undefined;
static toLiteral(val: ts.Node, strict?: boolean): unknown;
static toLiteral(val?: ts.Node, strict = true): unknown {
if (!val) {
throw new Error('Val is not defined');
} else if (ts.isArrayLiteralExpression(val)) {
return val.elements.map(x => this.toLiteral(x, strict));
} else if (ts.isIdentifier(val)) {
if (val.getText() === 'undefined') {
return undefined;
} else if (!strict) {
return val.getText();
}
} else if (val.kind === ts.SyntaxKind.NullKeyword) {
return null;
} else if (ts.isStringLiteral(val)) {
return val.text;
} else if (ts.isNumericLiteral(val)) {
const txt = val.text;
if (txt.includes('.')) {
return parseFloat(txt);
} else {
return parseInt(txt, 10);
}
} else if (ts.isPrefixUnaryExpression(val) && val.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(val.operand)) {
const txt = val.operand.text;
if (txt.includes('.')) {
return -parseFloat(txt);
} else {
return -parseInt(txt, 10);
}
} else if (val.kind === ts.SyntaxKind.FalseKeyword) {
return false;
} else if (val.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (ts.isObjectLiteralExpression(val)) {
const out: Record<string, unknown> = {};
for (const pair of val.properties) {
if (ts.isPropertyAssignment(pair)) {
out[pair.name.getText()] = this.toLiteral(pair.initializer, strict);
}
}
return out;
}
if (strict) {
throw new Error(`Not a valid input, should be a valid ts.Node: ${val.kind}`);
}
}
/**
* Extend object literal, whether JSON or ts.ObjectLiteralExpression
*/
static extendObjectLiteral(factory: ts.NodeFactory, src: object | ts.Expression, ...rest: (object | ts.Expression)[]): ts.ObjectLiteralExpression {
let literal = this.fromLiteral(factory, src);
if (rest.find(x => !!x)) {
literal = factory.createObjectLiteralExpression([
factory.createSpreadAssignment(literal),
...(rest.filter(x => !!x).map(r => factory.createSpreadAssignment(this.fromLiteral(factory, r))))
]);
}
return literal;
}
/**
* Get a value from the an object expression
*/
static getObjectValue(node: ts.Expression | undefined, key: string): ts.Expression | undefined {
if (node && ts.isObjectLiteralExpression(node) && node.properties) {
for (const prop of node.properties) {
if (prop.name!.getText() === key) {
if (ts.isPropertyAssignment(prop)) {
return prop.initializer;
} else if (ts.isShorthandPropertyAssignment(prop)) {
return prop.name;
}
}
}
}
return undefined;
}
/**
* Flatten a template literal into a regex
*/
static templateLiteralToRegex(template: TemplateLiteral, exact = true): string {
const out: string[] = [];
for (const el of template.values) {
if (el === Number) {
out.push('\\d+');
} else if (el === Boolean) {
out.push('(?:true|false)');
} else if (el === String) {
out.push('.+');
} else if (typeof el === 'string' || typeof el === 'number' || typeof el === 'boolean') {
out.push(`${el}`);
} else {
out.push(`(?:${this.templateLiteralToRegex(transformCast(el), false)})`);
}
}
const body = out.join(template.op === 'and' ? '' : '|');
if (exact) {
return `^(?:${body})$`;
} else {
return body;
}
}
}