ts-budgie
Version:
Converts TypeScript code to Budgie.
147 lines (116 loc) • 6.29 kB
text/typescript
import { CaseStyle, CommandNames, KeywordNames } from "budgie";
import * as tsutils from "tsutils";
import * as ts from "typescript";
import { BudgieLine } from "../../../output/budgieLine";
import { Transformation } from "../../../output/transformation";
import { PropertyAccessChecker } from "./propertyAccessChecker";
export class MemberOrStaticFunctionChecker extends PropertyAccessChecker {
public visit(node: ts.PropertyAccessExpression): Transformation[] | undefined {
if (node.parent === undefined || !ts.isCallExpression(node.parent)) {
return undefined;
}
// Handles the edge case of new Class().method
if (ts.isCallExpression(node.expression) || ts.isNewExpression(node.expression)) {
return this.handleCallOrNewExpression(node, node.expression, node.parent);
}
const hostContainerAndSignature = this.getHostContainerAndSignature(node);
if (hostContainerAndSignature === undefined) {
return undefined;
}
const { commandName, hostSignature } = hostContainerAndSignature;
if (hostSignature.declarations === undefined) {
return undefined;
}
const caller = this.router.recurseIntoValue(node.expression);
const args = node.parent.arguments.map((arg) => (arg === node ? node.name.text : this.router.recurseIntoValue(arg)));
const [hostDeclaration] = hostSignature.declarations;
const privacy = this.aliaser.getFriendlyPrivacyName(hostDeclaration);
const functionNameSplit = this.nameSplitter.split(node.name.getText(this.sourceFile));
const functionName = this.casing.convertToCase(CaseStyle.PascalCase, functionNameSplit);
return [Transformation.fromNode(node, this.sourceFile, [new BudgieLine(commandName, privacy, caller, functionName, ...args)])];
}
/**
* Handles the edges case of:
* methodCall().method()
* new Class().method()
*/
private handleCallOrNewExpression(
node: ts.PropertyAccessExpression,
expression: ts.CallExpression | ts.NewExpression,
parent: ts.CallExpression,
): Transformation[] | undefined {
const newBudgieLineCall = this.router.recurseIntoValue(expression);
const args = parent.arguments.map((arg) => (arg === node ? node.name.text : this.router.recurseIntoValue(arg)));
const functionNameSplit = this.nameSplitter.split(node.name.getText(this.sourceFile));
const functionName = this.casing.convertToCase(CaseStyle.PascalCase, functionNameSplit);
return [
Transformation.fromNode(node, this.sourceFile, [
new BudgieLine(CommandNames.MemberFunction, KeywordNames.Public, newBudgieLineCall, functionName, ...args),
]),
];
}
private getHostContainerAndSignature(node: ts.PropertyAccessExpression) {
const direct = this.getHostContainerAndSignatureOfPropertyAccess(node.expression, node.name);
if (direct !== undefined) {
return direct;
}
if (ts.isIdentifier(node.expression)) {
return this.getHostContainerAndSignatureOfPropertyAccess(node.expression, node.name);
}
if (ts.isPropertyAccessExpression(node.expression)) {
return this.getHostContainerAndSignatureOfPropertyAccess(node.expression.expression, node.expression.name);
}
if (ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.expression)) {
return this.getHostContainerAndSignatureOfPropertyAccess(
node.expression.expression.expression,
node.expression.expression.name,
);
}
return undefined;
}
private getHostContainerAndSignatureOfPropertyAccess(expression: ts.Identifier | ts.LeftHandSideExpression, name: ts.Identifier) {
const nameSymbol = this.typeChecker.getSymbolAtLocation(name);
if (nameSymbol === undefined) {
return undefined;
}
const classSymbol = this.getClassSymbol(expression, nameSymbol);
if (classSymbol === undefined) {
return undefined;
}
const { escapedName } = nameSymbol;
// If the class was imported from another file, this might be necessary to get the real type
const declaredClassSymbol = this.typeChecker.getDeclaredTypeOfSymbol(classSymbol).symbol;
const trueClassSymbol = declaredClassSymbol === undefined ? classSymbol : declaredClassSymbol;
// Protected properties are only listed as augmented properties (not in .members)
const expressionType = this.typeChecker.getTypeAtLocation(expression);
const classProperties = this.typeChecker.getAugmentedPropertiesOfType(expressionType);
for (const classProperty of classProperties) {
if (classProperty.escapedName !== escapedName) {
continue;
}
if (classProperty.valueDeclaration === undefined) {
return undefined;
}
return {
commandName: tsutils.hasModifier(classProperty.valueDeclaration.modifiers, ts.SyntaxKind.StaticKeyword)
? CommandNames.StaticFunction
: CommandNames.MemberFunction,
hostSignature: classProperty,
trueClassSymbol,
};
}
return undefined;
}
private getClassSymbol(expression: ts.LeftHandSideExpression, nameSymbol: ts.Symbol): ts.Symbol | undefined {
// If the expression is a direct class usage, this will normally work
const direct = this.typeChecker.getSymbolAtLocation(expression);
if (direct !== undefined) {
return direct;
}
// Otherwise, we'll have to try to parse through the function's declarations
if (nameSymbol.valueDeclaration === undefined || nameSymbol.valueDeclaration.parent === undefined) {
return undefined;
}
return this.typeChecker.getTypeAtLocation(nameSymbol.valueDeclaration.parent).symbol;
}
}