UNPKG

@travetto/transformer

Version:

Functionality for AST transformations, with transformer registration, and general utils

170 lines (143 loc) 5.38 kB
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' }; } } }