UNPKG

@travetto/transformer

Version:

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

425 lines (380 loc) 14 kB
import ts from 'typescript'; import { path, ManifestIndex } from '@travetto/manifest'; import { ManagedType, AnyType, ForeignType } from './resolver/types.ts'; import { State, DecoratorMeta, Transformer, ModuleNameSymbol } from './types/visitor.ts'; import { SimpleResolver } from './resolver/service.ts'; import { ImportManager } from './importer.ts'; import { Import } from './types/shared.ts'; import { DocUtil } from './util/doc.ts'; import { DecoratorUtil } from './util/decorator.ts'; import { DeclarationUtil } from './util/declaration.ts'; import { CoreUtil } from './util/core.ts'; import { LiteralUtil } from './util/literal.ts'; import { SystemUtil } from './util/system.ts'; function hasOriginal(n: ts.Node): n is ts.Node & { original: ts.Node } { return !!n && !n.parent && 'original' in n && !!n.original; } function hasEscapedName(n: ts.Node): n is ts.Node & { name: { escapedText: string } } { return !!n && 'name' in n && typeof n.name === 'object' && !!n.name && 'escapedText' in n.name && !!n.name.escapedText; } function isRedefinableDeclaration(x: ts.Node): x is ts.InterfaceDeclaration | ts.ClassDeclaration | ts.FunctionDeclaration { return ts.isFunctionDeclaration(x) || ts.isClassDeclaration(x) || ts.isInterfaceDeclaration(x); } /** * Transformer runtime state */ export class TransformerState implements State { #resolver: SimpleResolver; #imports: ImportManager; #modIdent: ts.Identifier; #manifestIndex: ManifestIndex; #syntheticIdentifiers = new Map<string, ts.Identifier>(); #decorators = new Map<string, ts.PropertyAccessExpression>(); added = new Map<number, ts.Statement[]>(); importName: string; file: string; source: ts.SourceFile; factory: ts.NodeFactory; constructor(source: ts.SourceFile, factory: ts.NodeFactory, checker: ts.TypeChecker, manifestIndex: ManifestIndex) { this.#manifestIndex = manifestIndex; this.#resolver = new SimpleResolver(checker, manifestIndex); this.#imports = new ImportManager(source, factory, this.#resolver); this.file = path.toPosix(source.fileName); this.importName = this.#resolver.getFileImportName(this.file); this.source = source; this.factory = factory; } /** * Rewrite module specifier normalizing output */ normalizeModuleSpecifier<T extends ts.Expression | undefined>(spec: T): T { return this.#imports.normalizeModuleSpecifier(spec); } /** * Get or import the node or external type */ getOrImport(type: ManagedType): ts.Identifier | ts.PropertyAccessExpression { return this.#imports.getOrImport(this.factory, type); } /** * Import a given file */ importFile(file: string, name?: string): Import { return this.#imports.importFile(file, name); } /** * Resolve an `AnyType` from a `ts.Type` or `ts.Node` */ resolveType(node: ts.Type | ts.Node): AnyType { const resolved = this.#resolver.resolveType(node, this.importName); this.#imports.importFromResolved(resolved); return resolved; } /** * Resolve external type */ resolveManagedType(node: ts.Node): ManagedType { const resolved = this.resolveType(node); if (resolved.key !== 'managed') { const file = node.getSourceFile().fileName; const src = this.#resolver.getFileImportName(file); throw new Error(`Unable to import non-external type: ${node.getText()} ${resolved.key}: ${src}`); } return resolved; } /** * Convert a type to it's identifier, will return undefined if none match */ typeToIdentifier(node: ts.Type | AnyType): ts.Identifier | ts.PropertyAccessExpression | undefined { const type = 'flags' in node ? this.resolveType(node) : node; switch (type.key) { case 'literal': return this.factory.createIdentifier(type.ctor!.name); case 'managed': return this.getOrImport(type); case 'shape': return; } } /** * Resolve the return type */ resolveReturnType(node: ts.MethodDeclaration): AnyType { const typeNode = ts.getJSDocReturnType(node); if (typeNode) { const resolved = this.#resolver.getChecker().getTypeFromTypeNode(typeNode); return this.resolveType(resolved); } else { return this.resolveType(this.#resolver.getReturnType(node)); } } /** * Read all JSDoc tags */ readDocTag(node: ts.Declaration, name: string): string[] { return DocUtil.readDocTag(this.#resolver.getType(node), name); } /** * Import a decorator, generally to handle erasure */ importDecorator(pth: string, name: string): ts.PropertyAccessExpression | undefined { if (!this.#decorators.has(`${pth}:${name}`)) { const ref = this.#imports.importFile(pth); const ident = this.factory.createIdentifier(name); this.#decorators.set(name, this.factory.createPropertyAccessExpression(ref.ident, ident)); } return this.#decorators.get(name); } /** * Create a decorator to add functionality to a declaration */ createDecorator(pth: string, name: string, ...contents: (ts.Expression | undefined)[]): ts.Decorator { this.importDecorator(pth, name); return CoreUtil.createDecorator(this.factory, this.#decorators.get(name)!, ...contents); } /** * Read a decorator's metadata */ getDecoratorMeta(dec: ts.Decorator): DecoratorMeta | undefined { const ident = DecoratorUtil.getDecoratorIdent(dec); const decl = DeclarationUtil.getPrimaryDeclarationNode( this.#resolver.getType(ident) ); const src = decl?.getSourceFile().fileName; const mod = src ? this.#resolver.getFileImportName(src, true) : undefined; const file = this.#manifestIndex.getFromImport(mod ?? '')?.outputFile; const targets = DocUtil.readAugments(this.#resolver.getType(ident)); const module = file ? mod : undefined; const name = ident ? ident.escapedText?.toString()! : undefined; if (ident && name) { return { dec, ident, file, module, targets, name }; } } /** * Get list of all #decorators for a node */ getDecoratorList(node: ts.Node): DecoratorMeta[] { return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) .map(dec => this.getDecoratorMeta(dec)) .filter(x => !!x) : []; } /** * Get all declarations for a node */ getDeclarations(node: ts.Node): ts.Declaration[] { return DeclarationUtil.getDeclarations(this.#resolver.getType(node)); } /** * Register statement for inclusion in final output * @param stmt * @param before */ addStatements(added: ts.Statement[], before?: ts.Node | number): void { const stmts = this.source.statements.slice(0); let idx = stmts.length + 1000; if (before && typeof before !== 'number') { let n = before; if (hasOriginal(n)) { n = n.original; } while (n && !ts.isSourceFile(n.parent) && n !== n.parent) { n = n.parent; } if (!ts.isStatement(n)) { throw new Error('Unable to find statement at top level'); } if (n && ts.isSourceFile(n.parent) && stmts.indexOf(n) >= 0) { idx = stmts.indexOf(n) - 1; } } else if (before !== undefined) { idx = before; } if (!this.added.has(idx)) { this.added.set(idx, []); } this.added.get(idx)!.push(...added); } /** * Finalize the source file for emission */ finalize(source: ts.SourceFile): ts.SourceFile { return this.#imports.finalize(source); } /** * From literal */ fromLiteral<T extends ts.Expression>(val: T): T; fromLiteral(val: undefined): ts.Identifier; fromLiteral(val: null): ts.NullLiteral; fromLiteral(val: object): ts.ObjectLiteralExpression; fromLiteral(val: unknown[]): ts.ArrayLiteralExpression; fromLiteral(val: string | boolean | number): ts.LiteralExpression; fromLiteral(val: unknown): ts.Node { return LiteralUtil.fromLiteral(this.factory, val!); } /** * Extend */ extendObjectLiteral(src: object | ts.Expression, ...rest: (object | ts.Expression)[]): ts.ObjectLiteralExpression { return LiteralUtil.extendObjectLiteral(this.factory, src, ...rest); } /** * Create property access */ createAccess(first: string | ts.Expression, second: string | ts.Identifier, ...items: (string | number | ts.Identifier)[]): ts.Expression { return CoreUtil.createAccess(this.factory, first, second, ...items); } /** * Create a static field for a class */ createStaticField(name: string, val: ts.Expression): ts.PropertyDeclaration { return CoreUtil.createStaticField(this.factory, name, val); } /** * Create identifier from node or text * @param name */ createIdentifier(name: string | { getText(): string }): ts.Identifier { return this.factory.createIdentifier(typeof name === 'string' ? name : name.getText()); } /** * Get filename identifier, regardless of module system */ getModuleIdentifier(): ts.Expression { if (this.#modIdent === undefined) { this.#modIdent = this.factory.createUniqueName('mod'); const entry = this.#resolver.getFileImport(this.source.fileName); const decl = this.factory.createVariableDeclaration(this.#modIdent, undefined, undefined, this.fromLiteral([entry?.module, entry?.relativeFile ?? '']) ); this.addStatements([ this.factory.createVariableStatement([], this.factory.createVariableDeclarationList([decl])) ], -1); } return this.#modIdent; } /** * Find decorator, relative to registered key * @param state * @param node * @param name * @param module */ findDecorator(mod: string | Transformer, node: ts.Node, name: string, module?: string): ts.Decorator | undefined { module = module?.replace(/[.]ts$/, ''); // Replace extension if exists mod = typeof mod === 'string' ? mod : mod[ModuleNameSymbol]!; const target = `${mod}:${name}`; const list = this.getDecoratorList(node); return list.find(x => x.targets?.includes(target) && (!module || x.name === name && x.module === module))?.dec; } /** * Generate unique identifier for node * @param node * @param type */ generateUniqueIdentifier(node: ts.Node, type: AnyType, suffix?: string): string { let unique: string[] = []; let name = type.name && !type.name.startsWith('_') ? type.name : ''; if (!name && hasEscapedName(node)) { name = `${node.name.escapedText}`; } name ||= 'Shape'; try { // Tie to source location if possible const tgt = DeclarationUtil.getPrimaryDeclarationNode(type.original!); const fileName = tgt.getSourceFile().fileName; if (fileName === this.source.fileName) { // if in same file suffix with location let child: ts.Node = tgt; while (child && !ts.isSourceFile(child)) { if (isRedefinableDeclaration(child) || ts.isMethodDeclaration(child) || ts.isParameter(child)) { if (child.name) { unique.push(child.name.getText()); } } child = child.parent; } if (!unique.length) { unique.push(ts.getLineAndCharacterOfPosition(tgt.getSourceFile(), tgt.getStart()).line.toString()); } } else { // Otherwise treat it as external and add nothing to it } } catch { unique = [type.name ?? 'unknown']; // Type is only unique piece } if (unique.length) { // Make unique to file unique.unshift(this.#resolver.getFileImportName(this.source.fileName)); return `${name}__${SystemUtil.naiveHashString(unique.join(':'), 12)}${suffix || ''}`; } else { return name; } } /** * Register synthetic identifier */ registerIdentifier(id: string): [identifier: ts.Identifier, exists: boolean] { let exists = true; if (!this.#syntheticIdentifiers.has(id)) { this.#syntheticIdentifiers.set(id, this.factory.createIdentifier(id)); exists = false; } return [this.#syntheticIdentifiers.get(id)!, exists]; } /** * Find a method declaration, by name * @param cls * @param method */ findMethodByName(cls: ts.ClassLikeDeclaration | ts.Type, method: string): ts.MethodDeclaration | undefined { if ('getSourceFile' in cls) { return cls.members.find( (m): m is ts.MethodDeclaration => ts.isMethodDeclaration(m) && ts.isIdentifier(m.name) && m.name.escapedText === method ); } else { const props = this.#resolver.getPropertiesOfType(cls); for (const prop of props) { const decl = prop.declarations?.[0]; if (decl && prop.escapedName === method && ts.isMethodDeclaration(decl)) { return decl; } } } } /** * Get import name for a given file * @param file */ getFileImportName(file: string): string { return this.#resolver.getFileImportName(file); } /** * Produce a foreign target type */ getForeignTarget(ret: ForeignType): ts.Expression { return this.fromLiteral({ Ⲑid: `${ret.source.split('node_modules/')[1]}+${ret.name}` }); } /** * Return a concrete type the given type of a node */ getConcreteType(node: ts.Node): ts.Expression { const type = this.resolveType(node); if (type.key === 'managed') { return this.getOrImport(type); } else if (type.key === 'foreign') { return this.getForeignTarget(type); } else { const file = node.getSourceFile().fileName; const src = this.getFileImportName(file); throw new Error(`Unable to import non-external type: ${node.getText()} ${type.key}: ${src}`); } } /** * Get apparent type of requested field */ getApparentTypeOfField(value: ts.Type, field: string): AnyType | undefined { const checker = this.#resolver.getChecker(); const props = checker.getApparentType(value).getApparentProperties().find(x => x.escapedName === field); return props ? this.resolveType(checker.getTypeOfSymbol(props)) : undefined; } }