UNPKG

ts-budgie

Version:

Converts TypeScript code to Budgie.

159 lines (129 loc) 7.17 kB
import * as tsutils from "tsutils"; import * as ts from "typescript"; import { INodeAliaser, IPrivacyName, IReturningNode } from "../../nodes/aliaser"; import { BudgieLine } from "../../output/budgieLine"; import { TypeFlagsResolver } from "../flags"; import { parseRawTypeToBudgie } from "../types"; import { ArrayLiteralExpressionAliaser } from "./arrayLiteralExpressionAliaser"; import { ElementAccessExpressionAliaser } from "./elementAccessExpressionAliaser"; import { NewExpressionAliaser } from "./newExpressionAliaser"; import { NumericAliaser } from "./numericAliaser"; import { PropertyOrVariableDeclarationAliaser } from "./propertyOrVariableDeclarationAliaser"; import { TypeLiteralAliaser } from "./typeLiteralAliaser"; import { TypeNameAliaser } from "./typeNameAliaser"; type INodeChildPasser = (node: ts.Node) => ts.Node; const createChildGetter = (index = 0) => (node: ts.Node) => node.getChildren()[index]; interface IIntrinsicSignatureReturnType extends ts.Type { /** * @remarks This is private within TypeScript, and might change in the future. */ intrinsicName: string; } const recursiveFriendlyValueDeclarationTypes = new Set<ts.SyntaxKind>([ ts.SyntaxKind.Parameter, ts.SyntaxKind.PropertyDeclaration, ts.SyntaxKind.VariableDeclaration, ]); export class RootAliaser implements RootAliaser { private readonly flagResolver: TypeFlagsResolver; private readonly passThroughTypes: Map<ts.SyntaxKind, INodeChildPasser>; private readonly sourceFile: ts.SourceFile; private readonly typesWithKnownTypeNames: Map<ts.SyntaxKind, INodeAliaser>; private readonly typeChecker: ts.TypeChecker; public constructor(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { this.flagResolver = new TypeFlagsResolver(); this.sourceFile = sourceFile; this.typeChecker = typeChecker; this.passThroughTypes = new Map<ts.SyntaxKind, INodeChildPasser>([ [ts.SyntaxKind.ExpressionStatement, createChildGetter()], [ts.SyntaxKind.ParenthesizedExpression, createChildGetter()], [ts.SyntaxKind.ParenthesizedType, createChildGetter()], [ts.SyntaxKind.SyntaxList, createChildGetter()], ]); this.typesWithKnownTypeNames = new Map<ts.SyntaxKind, INodeAliaser>([ [ts.SyntaxKind.ArrayLiteralExpression, new ArrayLiteralExpressionAliaser(typeChecker, this.getFriendlyTypeName)], [ts.SyntaxKind.BooleanKeyword, new TypeNameAliaser("boolean")], [ts.SyntaxKind.ElementAccessExpression, new ElementAccessExpressionAliaser(typeChecker, this.getFriendlyTypeName)], [ts.SyntaxKind.FalseKeyword, new TypeNameAliaser("boolean")], [ts.SyntaxKind.NewExpression, new NewExpressionAliaser(this.sourceFile)], [ts.SyntaxKind.NumericLiteral, new NumericAliaser(this.sourceFile)], [ts.SyntaxKind.TrueKeyword, new TypeNameAliaser("boolean")], [ts.SyntaxKind.TypeLiteral, new TypeLiteralAliaser(typeChecker, this.getFriendlyTypeName)], [ts.SyntaxKind.StringKeyword, new TypeNameAliaser("string")], [ts.SyntaxKind.StringLiteral, new TypeNameAliaser("string")], [ts.SyntaxKind.VariableDeclaration, new PropertyOrVariableDeclarationAliaser(typeChecker, this.getFriendlyTypeName)], ]); } public readonly getFriendlyTypeName = (node: ts.Node): string | BudgieLine | undefined => { const knownTypeNameConverter = this.typesWithKnownTypeNames.get(node.kind); if (knownTypeNameConverter !== undefined) { const typeNameConverted = knownTypeNameConverter.getFriendlyTypeName(node); if (typeNameConverted !== undefined) { return typeNameConverted; } } const passThroughType = this.passThroughTypes.get(node.kind); if (passThroughType !== undefined) { return this.getFriendlyTypeName(passThroughType(node)); } // We use the real type checker last because our checks can know the difference // between seemingly identical types, such as "float" or "int" within "number" const typeAtLocation = this.typeChecker.getTypeAtLocation(node); const resolvedFlagType = this.flagResolver.resolve(typeAtLocation.flags); if (resolvedFlagType !== undefined) { return resolvedFlagType; } // TypeScript won't give up expression nodes' types, but if they have a type symbol // we can use it to find their value declaration const symbol = this.typeChecker.getSymbolAtLocation(node); if (symbol !== undefined && symbol.valueDeclaration !== undefined) { const valueDeclaration = symbol.valueDeclaration; if (recursiveFriendlyValueDeclarationTypes.has(valueDeclaration.kind)) { return this.getFriendlyTypeName(valueDeclaration); } } // By now, this is probably a node with a non-primitive type, such as a class instance. if (ts.isParameter(node) || ts.isPropertyDeclaration(node) || ts.isVariableDeclaration(node)) { if (node.type !== undefined) { return this.getFriendlyTypeName(node.type); } } if (ts.isTypeNode(node)) { return parseRawTypeToBudgie(node.getText()); } // This seems to sometimes succeed when directly calling getSymbolAtLocation doesn't const typeSymbol = typeAtLocation.symbol; if (typeSymbol !== undefined && typeSymbol.name !== "unknown") { return typeSymbol.name; } return undefined; }; public getFriendlyPrivacyName(node: ts.Node): IPrivacyName { if (tsutils.hasModifier(node.modifiers, ts.SyntaxKind.PrivateKeyword)) { return "private"; } if (tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ProtectedKeyword)) { return "protected"; } return "public"; } public getFriendlyReturnTypeName(node: IReturningNode): string | BudgieLine | undefined { // If the node explicitly mentions a return type, use that if (node.type !== undefined) { return this.getFriendlyTypeName(node.type); } // The rest of this logic attempts to use the type checker to get a computed type symbol const typeAtLocation = this.typeChecker.getTypeAtLocation(node); const signaturesOfType = this.typeChecker.getSignaturesOfType(typeAtLocation, ts.SignatureKind.Call); if (signaturesOfType.length !== 1) { return undefined; } const signatureOfType = signaturesOfType[0]; const signatureReturnType = signatureOfType.getReturnType(); const symbol = signatureReturnType.getSymbol(); if (symbol !== undefined) { return symbol.getName(); } return (signatureReturnType as IIntrinsicSignatureReturnType).intrinsicName; } }