src/languages/default.ts (290 lines of code) (raw):

import * as ts from 'typescript'; import type { TargetLanguage } from './target-language'; import { analyzeObjectLiteral, ObjectLiteralStruct } from '../jsii/jsii-types'; import { isNamedLikeStruct, isJsiiProtocolType } from '../jsii/jsii-utils'; import { OTree, NO_SYNTAX } from '../o-tree'; import { AstRenderer, AstHandler, nimpl, CommentSyntax } from '../renderer'; import { SubmoduleReference } from '../submodule-reference'; import { voidExpressionString } from '../typescript/ast-utils'; import { ImportStatement } from '../typescript/imports'; import { inferredTypeOfExpression } from '../typescript/types'; /** * A basic visitor that applies for most curly-braces-based languages */ export abstract class DefaultVisitor<C> implements AstHandler<C> { public abstract readonly defaultContext: C; public abstract readonly language: TargetLanguage; protected statementTerminator = ';'; public abstract mergeContext(old: C, update: C): C; public commentRange(comment: CommentSyntax, _context: AstRenderer<C>): OTree { return new OTree([comment.isTrailing ? ' ' : '', comment.text, comment.hasTrailingNewLine ? '\n' : '']); } public sourceFile(node: ts.SourceFile, context: AstRenderer<C>): OTree { return new OTree(context.convertAll(node.statements)); } public jsDoc(_node: ts.JSDoc, _context: AstRenderer<C>): OTree { // Already handled by other doc handlers return new OTree([]); } public importStatement(node: ImportStatement, context: AstRenderer<C>): OTree { return this.notImplemented(node.node, context); } public functionDeclaration(node: ts.FunctionDeclaration, children: AstRenderer<C>): OTree { return this.notImplemented(node, children); } public stringLiteral(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral, _renderer: AstRenderer<C>): OTree { return new OTree([JSON.stringify(node.text)]); } public numericLiteral(node: ts.NumericLiteral, _children: AstRenderer<C>): OTree { return new OTree([node.text]); } public identifier(node: ts.Identifier, _children: AstRenderer<C>): OTree { return new OTree([node.text]); } public block(node: ts.Block, children: AstRenderer<C>): OTree { return new OTree(['{'], ['\n', ...children.convertAll(node.statements)], { indent: 4, suffix: '}', }); } public parameterDeclaration(node: ts.ParameterDeclaration, children: AstRenderer<C>): OTree { return this.notImplemented(node, children); } public returnStatement(node: ts.ReturnStatement, children: AstRenderer<C>): OTree { return new OTree(['return ', children.convert(node.expression), this.statementTerminator], [], { canBreakLine: true, }); } public binaryExpression(node: ts.BinaryExpression, context: AstRenderer<C>): OTree { const operator = context.textOf(node.operatorToken); if (operator === '??') { context.reportUnsupported(node.operatorToken, undefined); } const operatorToken = this.translateBinaryOperator(operator); return new OTree([context.convert(node.left), ' ', operatorToken, ' ', context.convert(node.right)]); } public prefixUnaryExpression(node: ts.PrefixUnaryExpression, context: AstRenderer<C>): OTree { return new OTree([this.translateUnaryOperator(node.operator), context.convert(node.operand)]); } public translateUnaryOperator(operator: ts.PrefixUnaryOperator) { return UNARY_OPS[operator]; } public translateBinaryOperator(operator: string) { if (operator === '===') { return '=='; } return operator; } public ifStatement(node: ts.IfStatement, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public propertyAccessExpression( node: ts.PropertyAccessExpression, context: AstRenderer<C>, _submoduleReference: SubmoduleReference | undefined, ): OTree { return new OTree([context.convert(node.expression), '.', context.convert(node.name)]); } /** * Do some work on property accesses to translate common JavaScript-isms to language-specific idioms */ public callExpression(node: ts.CallExpression, context: AstRenderer<C>): OTree { const functionText = context.textOf(node.expression); if (functionText === 'console.log' || functionText === 'console.error') { return this.printStatement(node.arguments, context); } if (functionText === 'super') { return this.superCallExpression(node, context); } return this.regularCallExpression(node, context); } public awaitExpression(node: ts.AwaitExpression, context: AstRenderer<C>): OTree { return context.convert(node.expression); } public regularCallExpression(node: ts.CallExpression, context: AstRenderer<C>): OTree { return new OTree([context.convert(node.expression), '(', this.argumentList(node.arguments, context), ')']); } public superCallExpression(node: ts.CallExpression, context: AstRenderer<C>): OTree { return this.regularCallExpression(node, context); } public printStatement(args: ts.NodeArray<ts.Expression>, context: AstRenderer<C>) { return new OTree(['<PRINT>', '(', this.argumentList(args, context), ')']); } public expressionStatement(node: ts.ExpressionStatement, context: AstRenderer<C>): OTree { return new OTree([context.convert(node.expression)], [], { canBreakLine: true, }); } public token<A extends ts.SyntaxKind>(node: ts.Token<A>, context: AstRenderer<C>): OTree { return new OTree([context.textOf(node)]); } /** * An object literal can render as one of three things: * * - Don't know the type (render as an unknown struct) * - Know the type: * - It's a struct (render as known struct) * - It's not a struct (render as key-value map) */ public objectLiteralExpression(node: ts.ObjectLiteralExpression, context: AstRenderer<C>): OTree { // If any of the elements of the objectLiteralExpression are not a literal property // assignment, report them. We can't support those. const unsupported = node.properties.filter( (p) => !ts.isPropertyAssignment(p) && !ts.isShorthandPropertyAssignment(p), ); for (const unsup of unsupported) { context.report(unsup, `Use of ${ts.SyntaxKind[unsup.kind]} in an object literal is not supported.`); } const anyMembersFunctions = node.properties.some((p) => ts.isPropertyAssignment(p) ? isExpressionOfFunctionType(context.typeChecker, p.initializer) : ts.isShorthandPropertyAssignment(p) ? isExpressionOfFunctionType(context.typeChecker, p.name) : false, ); const inferredType = inferredTypeOfExpression(context.typeChecker, node); if ((inferredType && isJsiiProtocolType(context.typeChecker, inferredType)) || anyMembersFunctions) { context.report( node, `You cannot use an object literal to make an instance of an interface. Define a class instead.`, ); } const lit = analyzeObjectLiteral(context.typeChecker, node); switch (lit.kind) { case 'unknown': return this.unknownTypeObjectLiteralExpression(node, context); case 'struct': case 'local-struct': return this.knownStructObjectLiteralExpression(node, lit, context); case 'map': return this.keyValueObjectLiteralExpression(node, context); } } public unknownTypeObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public knownStructObjectLiteralExpression( node: ts.ObjectLiteralExpression, _structType: ObjectLiteralStruct, context: AstRenderer<C>, ): OTree { return this.notImplemented(node, context); } public keyValueObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public newExpression(node: ts.NewExpression, context: AstRenderer<C>): OTree { return new OTree( ['new ', context.convert(node.expression), '(', this.argumentList(node.arguments, context), ')'], [], { canBreakLine: true }, ); } public propertyAssignment(node: ts.PropertyAssignment, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public variableStatement(node: ts.VariableStatement, context: AstRenderer<C>): OTree { return new OTree([context.convert(node.declarationList)], [], { canBreakLine: true, }); } public variableDeclarationList(node: ts.VariableDeclarationList, context: AstRenderer<C>): OTree { return new OTree([], context.convertAll(node.declarations)); } public variableDeclaration(node: ts.VariableDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public arrayLiteralExpression(node: ts.ArrayLiteralExpression, context: AstRenderer<C>): OTree { return new OTree(['['], context.convertAll(node.elements), { separator: ', ', suffix: ']', }); } public shorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public forOfStatement(node: ts.ForOfStatement, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public classDeclaration(node: ts.ClassDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public constructorDeclaration(node: ts.ConstructorDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public propertyDeclaration(node: ts.PropertyDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public computedPropertyName(node: ts.Expression, context: AstRenderer<C>): OTree { return context.convert(node); } public methodDeclaration(node: ts.MethodDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public interfaceDeclaration(node: ts.InterfaceDeclaration, context: AstRenderer<C>): OTree { if (isNamedLikeStruct(context.textOf(node.name))) { return this.structInterfaceDeclaration(node, context); } return this.regularInterfaceDeclaration(node, context); } public structInterfaceDeclaration(node: ts.InterfaceDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public regularInterfaceDeclaration(node: ts.InterfaceDeclaration, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public propertySignature(node: ts.PropertySignature, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public methodSignature(node: ts.MethodSignature, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public asExpression(node: ts.AsExpression, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public spreadElement(node: ts.SpreadElement, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public spreadAssignment(node: ts.SpreadAssignment, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public ellipsis(_node: ts.SpreadElement | ts.SpreadAssignment, _context: AstRenderer<C>): OTree { return new OTree(['...']); } public templateExpression(node: ts.TemplateExpression, context: AstRenderer<C>): OTree { return this.notImplemented(node, context); } public elementAccessExpression(node: ts.ElementAccessExpression, context: AstRenderer<C>): OTree { const expression = context.convert(node.expression); const index = context.convert(node.argumentExpression); return new OTree([expression, '[', index, ']']); } public nonNullExpression(node: ts.NonNullExpression, context: AstRenderer<C>): OTree { // We default we drop the non-null assertion return context.convert(node.expression); } public parenthesizedExpression(node: ts.ParenthesizedExpression, context: AstRenderer<C>): OTree { return new OTree(['(', context.convert(node.expression), ')']); } public maskingVoidExpression(node: ts.VoidExpression, context: AstRenderer<C>): OTree { // Don't render anything by default when nodes are masked const arg = voidExpressionString(node); if (arg === 'block') { return this.commentRange( { pos: context.getPosition(node).start, text: '\n// ...', kind: ts.SyntaxKind.SingleLineCommentTrivia, hasTrailingNewLine: false, }, context, ); } if (arg === '...') { return new OTree(['...']); } return NO_SYNTAX; } protected argumentList(args: readonly ts.Node[] | undefined, context: AstRenderer<C>): OTree { return new OTree([], args ? context.convertAll(args) : [], { separator: ', ', }); } private notImplemented(node: ts.Node, context: AstRenderer<C>) { context.reportUnsupported(node, this.language); return nimpl(node, context); } } const UNARY_OPS: { [op in ts.PrefixUnaryOperator]: string } = { [ts.SyntaxKind.PlusPlusToken]: '++', [ts.SyntaxKind.MinusMinusToken]: '--', [ts.SyntaxKind.PlusToken]: '+', [ts.SyntaxKind.MinusToken]: '-', [ts.SyntaxKind.TildeToken]: '~', [ts.SyntaxKind.ExclamationToken]: '!', }; /** * Whether the given expression evaluates to a value that is of type "function" * * Examples of function types: * * ```ts * // GIVEN * function someFunction() { } * * // THEN * const x = someFunction; // <- function type * const y = () => 42; // <- function type * const z = x; // <- function type * Array.isArray; // <- function type * ``` */ function isExpressionOfFunctionType(typeChecker: ts.TypeChecker, expr: ts.Expression) { const type = typeChecker.getTypeAtLocation(expr).getNonNullableType(); return type.getCallSignatures().length > 0; }