|
| 1 | +import * as ts from "typescript"; |
| 2 | +import { TransformationContext } from "../context"; |
| 3 | +import { LuaLibFeature, importLuaLibFeature } from "../utils/lualib"; |
| 4 | + |
| 5 | +export function usingTransformer(context: TransformationContext): ts.TransformerFactory<ts.SourceFile> { |
| 6 | + return ctx => sourceFile => { |
| 7 | + function visit(node: ts.Node): ts.Node { |
| 8 | + if (ts.isBlock(node)) { |
| 9 | + const [hasUsings, newStatements] = transformBlockWithUsing(context, node.statements, node); |
| 10 | + if (hasUsings) { |
| 11 | + // Recurse visitor into updated block to find further usings |
| 12 | + const updatedBlock = ts.factory.updateBlock(node, newStatements); |
| 13 | + const result = ts.visitEachChild(updatedBlock, visit, ctx); |
| 14 | + |
| 15 | + // Set all the synthetic node parents to something that makes sense |
| 16 | + const parent: ts.Node[] = [updatedBlock]; |
| 17 | + function setParent(node2: ts.Node): ts.Node { |
| 18 | + ts.setParent(node2, parent[parent.length - 1]); |
| 19 | + parent.push(node2); |
| 20 | + ts.visitEachChild(node2, setParent, ctx); |
| 21 | + parent.push(); |
| 22 | + return node2; |
| 23 | + } |
| 24 | + ts.visitEachChild(updatedBlock, setParent, ctx); |
| 25 | + ts.setParent(updatedBlock, node.parent); |
| 26 | + |
| 27 | + return result; |
| 28 | + } |
| 29 | + } |
| 30 | + return ts.visitEachChild(node, visit, ctx); |
| 31 | + } |
| 32 | + return ts.visitEachChild(sourceFile, visit, ctx); |
| 33 | + }; |
| 34 | +} |
| 35 | + |
| 36 | +function isUsingDeclarationList(node: ts.Node): node is ts.VariableStatement { |
| 37 | + return ts.isVariableStatement(node) && (node.declarationList.flags & ts.NodeFlags.Using) !== 0; |
| 38 | +} |
| 39 | + |
| 40 | +function transformBlockWithUsing( |
| 41 | + context: TransformationContext, |
| 42 | + statements: ts.NodeArray<ts.Statement> | ts.Statement[], |
| 43 | + block: ts.Block |
| 44 | +): [true, ts.Statement[]] | [false] { |
| 45 | + const newStatements: ts.Statement[] = []; |
| 46 | + |
| 47 | + for (let i = 0; i < statements.length; i++) { |
| 48 | + const statement = statements[i]; |
| 49 | + if (isUsingDeclarationList(statement)) { |
| 50 | + const isAwaitUsing = (statement.declarationList.flags & ts.NodeFlags.AwaitContext) !== 0; |
| 51 | + |
| 52 | + if (isAwaitUsing) { |
| 53 | + importLuaLibFeature(context, LuaLibFeature.UsingAsync); |
| 54 | + } else { |
| 55 | + importLuaLibFeature(context, LuaLibFeature.Using); |
| 56 | + } |
| 57 | + |
| 58 | + // Make declared using variables callback function parameters |
| 59 | + const variableNames = statement.declarationList.declarations.map(d => |
| 60 | + ts.factory.createParameterDeclaration(undefined, undefined, d.name) |
| 61 | + ); |
| 62 | + // Add this: void as first parameter |
| 63 | + variableNames.unshift(createThisVoidParameter(context.checker)); |
| 64 | + |
| 65 | + // Put all following statements in the callback body |
| 66 | + const followingStatements = statements.slice(i + 1); |
| 67 | + const [followingHasUsings, replacedFollowingStatements] = transformBlockWithUsing( |
| 68 | + context, |
| 69 | + followingStatements, |
| 70 | + block |
| 71 | + ); |
| 72 | + const callbackBody = ts.factory.createBlock( |
| 73 | + followingHasUsings ? replacedFollowingStatements : followingStatements |
| 74 | + ); |
| 75 | + |
| 76 | + const callback = ts.factory.createFunctionExpression( |
| 77 | + undefined, |
| 78 | + undefined, |
| 79 | + undefined, |
| 80 | + undefined, |
| 81 | + variableNames, |
| 82 | + undefined, |
| 83 | + callbackBody |
| 84 | + ); |
| 85 | + |
| 86 | + // Replace using variable list with call to lualib function with callback and followed by all variable initializers |
| 87 | + const functionIdentifier = ts.factory.createIdentifier(isAwaitUsing ? "__TS__UsingAsync" : "__TS__Using"); |
| 88 | + let call: ts.Expression = ts.factory.createCallExpression( |
| 89 | + functionIdentifier, |
| 90 | + [], |
| 91 | + [ |
| 92 | + callback, |
| 93 | + ...statement.declarationList.declarations.map( |
| 94 | + d => d.initializer ?? ts.factory.createIdentifier("unidentified") |
| 95 | + ), |
| 96 | + ] |
| 97 | + ); |
| 98 | + |
| 99 | + // If this is an 'await using ...', add an await statement here |
| 100 | + if (isAwaitUsing) { |
| 101 | + call = ts.factory.createAwaitExpression(call); |
| 102 | + } |
| 103 | + |
| 104 | + if (ts.isBlock(block.parent) && block.parent.statements[block.parent.statements.length - 1] !== block) { |
| 105 | + // If this is a free-standing block in a function (not the last statement), dont return the value |
| 106 | + newStatements.push(ts.factory.createExpressionStatement(call)); |
| 107 | + } else { |
| 108 | + newStatements.push(ts.factory.createReturnStatement(call)); |
| 109 | + } |
| 110 | + |
| 111 | + return [true, newStatements]; |
| 112 | + } else { |
| 113 | + newStatements.push(statement); |
| 114 | + } |
| 115 | + } |
| 116 | + return [false]; |
| 117 | +} |
| 118 | + |
| 119 | +function createThisVoidParameter(checker: ts.TypeChecker) { |
| 120 | + const voidType = checker.typeToTypeNode(checker.getVoidType(), undefined, undefined); |
| 121 | + return ts.factory.createParameterDeclaration( |
| 122 | + undefined, |
| 123 | + undefined, |
| 124 | + ts.factory.createIdentifier("this"), |
| 125 | + undefined, |
| 126 | + voidType |
| 127 | + ); |
| 128 | +} |
0 commit comments