@travetto/transformer
Version:
Functionality for AST transformations, with transformer registration, and general utils
170 lines (143 loc) • 5.38 kB
text/typescript
import ts from 'typescript';
import { path, ManifestIndex, ManifestModuleUtil, IndexedFile } from '@travetto/manifest';
import type { AnyType, TransformResolver } from './types.ts';
import { TypeCategorize, TypeBuilder } from './builder.ts';
import { VisitCache } from './cache.ts';
import { DocUtil } from '../util/doc.ts';
import { DeclarationUtil } from '../util/declaration.ts';
import { transformCast } from '../types/shared.ts';
/**
* Implementation of TransformResolver
*/
export class SimpleResolver implements TransformResolver {
#tsChecker: ts.TypeChecker;
#manifestIndex: ManifestIndex;
constructor(tsChecker: ts.TypeChecker, manifestIndex: ManifestIndex) {
this.#tsChecker = tsChecker;
this.#manifestIndex = manifestIndex;
}
/**
* Get type checker
* @private
*/
getChecker(): ts.TypeChecker {
return this.#tsChecker;
}
/**
* Resolve an import for a file
*/
getFileImport(file: string): IndexedFile | undefined {
let sourceFile = path.toPosix(file);
const type = ManifestModuleUtil.getFileType(file);
if (type !== 'js' && type !== 'ts') {
sourceFile = `${sourceFile}${ManifestModuleUtil.SOURCE_DEF_EXT}`;
}
const sourceType = ManifestModuleUtil.getFileType(sourceFile);
return this.#manifestIndex.getEntry((sourceType === 'ts' || sourceType === 'js') ? sourceFile : undefined!) ??
this.#manifestIndex.getFromImport(ManifestModuleUtil.withoutSourceExtension(sourceFile).replace(/^.*node_modules\//, ''));
}
/**
* Resolve an import name (e.g. @module/path/file) for a file
*/
getFileImportName(file: string, removeExt?: boolean): string {
const imp = this.getFileImport(file)?.import ?? file;
return removeExt ? ManifestModuleUtil.withoutSourceExtension(imp) : imp;
}
/**
* Resolve an import name (e.g. @module/path/file) for a type
*/
getTypeImportName(type: ts.Type, removeExt?: boolean): string | undefined {
const ogSource = DeclarationUtil.getPrimaryDeclarationNode(type)?.getSourceFile()?.fileName;
return ogSource ? this.getFileImportName(ogSource, removeExt) : undefined;
}
/**
* Is the file/import known to the index, helpful for determine ownership
*/
isKnownFile(fileOrImport: string): boolean {
return (this.#manifestIndex.getFromSource(fileOrImport) !== undefined) ||
(this.#manifestIndex.getFromImport(fileOrImport) !== undefined);
}
/**
* Get type from element
* @param el
*/
getType(el: ts.Type | ts.Node): ts.Type {
return 'getSourceFile' in el ? this.#tsChecker.getTypeAtLocation(el) : el;
}
/**
* Fetch all type arguments for a give type
*/
getAllTypeArguments(ref: ts.Type): ts.Type[] {
return transformCast(this.#tsChecker.getTypeArguments(transformCast(ref)));
}
/**
* Resolve the return type for a method
*/
getReturnType(node: ts.MethodDeclaration): ts.Type {
const type = this.getType(node);
const [sig] = type.getCallSignatures();
return this.#tsChecker.getReturnTypeOfSignature(sig);
}
/**
* Get type as a string representation
*/
getTypeAsString(type: ts.Type): string | undefined {
return this.#tsChecker.typeToString(this.#tsChecker.getApparentType(type)) || undefined;
}
/**
* Get list of properties
*/
getPropertiesOfType(type: ts.Type): ts.Symbol[] {
return this.#tsChecker.getPropertiesOfType(type);
}
/**
* Resolve an `AnyType` from a `ts.Type` or a `ts.Node`
*/
resolveType(node: ts.Type | ts.Node, importName: string): AnyType {
const visited = new VisitCache();
const resolve = (resType: ts.Type, alias?: ts.Symbol, depth = 0): AnyType => {
if (depth > 20) { // Max depth is 20
throw new Error('Object structure too nested');
}
const { category, type } = TypeCategorize(this, resType);
const { build, finalize } = TypeBuilder[category];
let result = build(this, type, alias);
// Convert via cache if needed
result = visited.getOrSet(type, result);
// Recurse
if (result) {
result.original = resType;
result.comment = DocUtil.describeDocs(type).description;
if ('tsTypeArguments' in result) {
result.typeArguments = result.tsTypeArguments!.map((elType) => resolve(elType, type.aliasSymbol, depth + 1));
delete result.tsTypeArguments;
}
if ('tsFieldTypes' in result) {
const fields: Record<string, AnyType> = {};
for (const [name, fieldType] of Object.entries(result.tsFieldTypes ?? [])) {
fields[name] = resolve(fieldType, undefined, depth + 1);
}
result.fieldTypes = fields;
delete result.tsFieldTypes;
}
if ('tsSubTypes' in result) {
result.subTypes = result.tsSubTypes!.map((elType) => resolve(elType, type.aliasSymbol, depth + 1));
delete result.tsSubTypes;
}
if (finalize) {
result = finalize(transformCast(result));
}
}
return result ?? { key: 'literal', ctor: Object, name: 'object' };
};
try {
return resolve(this.getType(node));
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
console.error(`Unable to resolve type in ${importName}`, err.stack);
return { key: 'literal', ctor: Object, name: 'object' };
}
}
}