diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 0245dccb7..cbefa7476 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -40,8 +40,10 @@ interface Scope { export class LuaTransformer { public luaKeywords: Set = new Set([ - "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "new", "nil", - "not", "or", "repeat", "return", "self", "then", "until", "while", + "_G", "and", "assert", "break", "coroutine", "debug", "do", "else", "elseif", "end", "error", "false", "for", + "function", "goto", "if", "ipairs", "in", "local", "math", "nil", "not", "or", "pairs", "pcall", "print", + "rawget", "rawset", "repeat", "return", "require", "self", "string", "table", "then", "tostring", "type", + "unpack", "until", "while", ]); private isStrict: boolean; @@ -238,10 +240,23 @@ export class LuaTransformer { const result = []; for (const exportElement of statement.exportClause.elements) { + let exportedIdentifier: tstl.Expression | undefined; + if (exportElement.propertyName !== undefined) { + exportedIdentifier = this.transformIdentifier(exportElement.propertyName); + + } else { + const exportedSymbol = this.checker.getExportSpecifierLocalTargetSymbol(exportElement); + if (exportedSymbol !== undefined) { + exportedIdentifier = this.createIdentifierFromSymbol(exportedSymbol, exportElement.name); + } else { + exportedIdentifier = this.transformIdentifier(exportElement.name); + } + } + result.push( tstl.createAssignmentStatement( this.createExportedIdentifier(this.transformIdentifier(exportElement.name)), - this.transformIdentifier(exportElement.propertyName || exportElement.name) + exportedIdentifier ) ); } @@ -368,7 +383,7 @@ export class LuaTransformer { return undefined; } - const tstlIdentifier = (name: string) => "__TSTL_" + name.replace(new RegExp("-|\\$| |#|'", "g"), "_"); + const tstlIdentifier = (name: string) => "__TSTL_" + tsHelper.fixInvalidLuaIdentifier(name); const importUniqueName = tstl.createIdentifier(tstlIdentifier(path.basename((importPath)))); const requireStatement = tstl.createVariableDeclarationStatement( tstl.createIdentifier(tstlIdentifier(path.basename((importPath)))), @@ -379,8 +394,7 @@ export class LuaTransformer { filteredElements.forEach(importSpecifier => { if (importSpecifier.propertyName) { - const propertyIdentifier = this.transformIdentifier(importSpecifier.propertyName); - const propertyName = tstl.createStringLiteral(propertyIdentifier.text); + const propertyName = this.transformPropertyName(importSpecifier.propertyName); const renamedImport = tstl.createVariableDeclarationStatement( this.transformIdentifier(importSpecifier.name), tstl.createTableIndexExpression(importUniqueName, propertyName), @@ -427,6 +441,14 @@ export class LuaTransformer { return tstl.createCallExpression(tstl.createIdentifier("require"), [modulePath], moduleSpecifier); } + private validateClassElement(element: ts.ClassElement): void { + if (element.name && (ts.isStringLiteral(element.name) || ts.isIdentifier(element.name))) { + if (tsHelper.isStatic(element) && element.name.text === "new") { + throw TSTLErrors.ForbiddenStaticClassPropertyName(element, element.name.text); + } + } + } + public transformClassDeclaration( statement: ts.ClassLikeDeclaration, nameOverride?: tstl.Identifier @@ -532,10 +554,23 @@ export class LuaTransformer { } } + let localClassName: tstl.Identifier; + if (this.isUnsafeName(className.text)) { + localClassName = tstl.createIdentifier( + this.createSafeName(className.text), + undefined, + className.symbolId + ); + tstl.setNodePosition(localClassName, className); + } else { + localClassName = className; + } + if (!isExtension && !isMetaExtension) { const classCreationMethods = this.createClassCreationMethods( statement, className, + localClassName, extendsType ); result.push(...classCreationMethods); @@ -567,7 +602,7 @@ export class LuaTransformer { // Add constructor plus initialization of instance fields const constructorResult = this.transformConstructorDeclaration( constructor, - className, + localClassName, instanceFields, statement ); @@ -576,7 +611,7 @@ export class LuaTransformer { // Generate a constructor if none was defined in a base class const constructorResult = this.transformConstructorDeclaration( ts.createConstructor([], [], [], ts.createBlock([], true)), - className, + localClassName, instanceFields, statement ); @@ -585,7 +620,7 @@ export class LuaTransformer { || statement.members.some(m => tsHelper.isGetAccessorOverride(m, statement, this.checker))) { // Generate a constructor if none was defined in a class with instance fields that need initialization - // className.prototype.____constructor = function(self, ...) + // localClassName.prototype.____constructor = function(self, ...) // baseClassName.prototype.____constructor(self, ...) // ... const constructorBody = this.transformClassInstanceFields(statement, instanceFields); @@ -607,7 +642,7 @@ export class LuaTransformer { tstl.FunctionExpressionFlags.Declaration ); result.push(tstl.createAssignmentStatement( - this.createConstructorName(className), + this.createConstructorName(localClassName), constructorFunction, statement )); @@ -616,29 +651,35 @@ export class LuaTransformer { // Transform get accessors statement.members.filter(ts.isGetAccessor).forEach(getAccessor => { - const transformResult = this.transformGetAccessorDeclaration(getAccessor, className); + const transformResult = this.transformGetAccessorDeclaration(getAccessor, localClassName); result.push(...this.statementVisitResultToArray(transformResult)); }); // Transform set accessors statement.members.filter(ts.isSetAccessor).forEach(setAccessor => { - const transformResult = this.transformSetAccessorDeclaration(setAccessor, className); + const transformResult = this.transformSetAccessorDeclaration(setAccessor, localClassName); result.push(...this.statementVisitResultToArray(transformResult)); }); // Transform methods statement.members.filter(ts.isMethodDeclaration).forEach(method => { - const methodResult = this.transformMethodDeclaration(method, className, isExtension || isMetaExtension); + const methodResult = this.transformMethodDeclaration( + method, + localClassName, + isExtension || isMetaExtension + ); result.push(...this.statementVisitResultToArray(methodResult)); }); // Add static declarations for (const field of staticFields) { + this.validateClassElement(field); + const fieldName = this.transformPropertyName(field.name); const value = field.initializer ? this.transformExpression(field.initializer) : undefined; const classField = tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), fieldName ); @@ -663,6 +704,7 @@ export class LuaTransformer { private createClassCreationMethods( statement: ts.ClassLikeDeclarationBase, className: tstl.Identifier, + localClassName: tstl.Identifier, extendsType?: ts.Type ): tstl.Statement[] { @@ -675,28 +717,28 @@ export class LuaTransformer { result.push(...classVar); if (this.isIdentifierExported(className)) { - // local className = ____exports.className + // local localClassName = ____exports.className result.push( tstl.createVariableDeclarationStatement( - tstl.cloneIdentifier(className, statement), + localClassName, this.addExportToIdentifier(tstl.cloneIdentifier(className)) ) ); } - // className.name = className + // localClassName.name = className result.push( tstl.createAssignmentStatement( - tstl.createTableIndexExpression(tstl.cloneIdentifier(className), tstl.createStringLiteral("name")), - tstl.createStringLiteral(className.text), + tstl.createTableIndexExpression(tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("name")), + tstl.createStringLiteral(localClassName.text), statement ) ); - // className.____getters = {} + // localClassName.____getters = {} if (statement.members.some(m => ts.isGetAccessor(m) && tsHelper.isStatic(m))) { const classGetters = tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("____getters") ); const assignClassGetters = tstl.createAssignmentStatement( @@ -709,18 +751,22 @@ export class LuaTransformer { this.importLuaLibFeature(LuaLibFeature.ClassIndex); } - // className.__index = className + // localClassName.__index = localClassName const classIndex = tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("__index") ); - const assignClassIndex = tstl.createAssignmentStatement(classIndex, tstl.cloneIdentifier(className), statement); + const assignClassIndex = tstl.createAssignmentStatement( + classIndex, + tstl.cloneIdentifier(localClassName), + statement + ); result.push(assignClassIndex); - // className.____setters = {} + // localClassName.____setters = {} if (statement.members.some(m => ts.isSetAccessor(m) && tsHelper.isStatic(m))) { const classSetters = tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("____setters") ); const assignClassSetters = tstl.createAssignmentStatement( @@ -733,9 +779,9 @@ export class LuaTransformer { this.importLuaLibFeature(LuaLibFeature.ClassNewIndex); } - // className.prototype = {} + // localClassName.prototype = {} const createClassPrototype = () => tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("prototype") ); const classPrototypeTable = tstl.createTableExpression(); @@ -746,7 +792,7 @@ export class LuaTransformer { ); result.push(assignClassPrototype); - // className.prototype.____getters = {} + // localClassName.prototype.____getters = {} if (statement.members.some(m => ts.isGetAccessor(m) && !tsHelper.isStatic(m))) { const classPrototypeGetters = tstl.createTableIndexExpression( createClassPrototype(), @@ -765,7 +811,7 @@ export class LuaTransformer { tstl.createStringLiteral("__index") ); if (tsHelper.hasGetAccessorInClassOrAncestor(statement, false, this.checker)) { - // className.prototype.__index = __TS_Index(className.prototype) + // localClassName.prototype.__index = __TS_Index(localClassName.prototype) const assignClassPrototypeIndex = tstl.createAssignmentStatement( classPrototypeIndex, this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype()), @@ -774,7 +820,7 @@ export class LuaTransformer { result.push(assignClassPrototypeIndex); } else { - // className.prototype.__index = className.prototype + // localClassName.prototype.__index = localClassName.prototype const assignClassPrototypeIndex = tstl.createAssignmentStatement( classPrototypeIndex, createClassPrototype(), @@ -784,7 +830,7 @@ export class LuaTransformer { } if (statement.members.some(m => ts.isSetAccessor(m) && !tsHelper.isStatic(m))) { - // className.prototype.____setters = {} + // localClassName.prototype.____setters = {} const classPrototypeSetters = tstl.createTableIndexExpression( createClassPrototype(), tstl.createStringLiteral("____setters") @@ -798,7 +844,7 @@ export class LuaTransformer { } if (tsHelper.hasSetAccessorInClassOrAncestor(statement, false, this.checker)) { - // className.prototype.__newindex = __TS_NewIndex(className.prototype) + // localClassName.prototype.__newindex = __TS_NewIndex(localClassName.prototype) const classPrototypeNewIndex = tstl.createTableIndexExpression( createClassPrototype(), tstl.createStringLiteral("__newindex") @@ -811,14 +857,14 @@ export class LuaTransformer { result.push(assignClassPrototypeIndex); } - // className.prototype.constructor = className + // localClassName.prototype.constructor = localClassName const classPrototypeConstructor = tstl.createTableIndexExpression( createClassPrototype(), tstl.createStringLiteral("constructor") ); const assignClassPrototypeConstructor = tstl.createAssignmentStatement( classPrototypeConstructor, - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), statement ); result.push(assignClassPrototypeConstructor); @@ -832,18 +878,14 @@ export class LuaTransformer { throw TSTLErrors.UndefinedTypeNode(statement); } - const baseName = ts.isIdentifier(extendedTypeNode.expression) - ? this.transformIdentifier(extendedTypeNode.expression) // Skip adding '____exports' - : this.transformExpression(extendedTypeNode.expression); - - // className.____super = baseName + // localClassName.____super = extendsExpression const createClassBase = () => tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("____super") ); const assignClassBase = tstl.createAssignmentStatement( createClassBase(), - baseName, + this.transformExpression(extendedTypeNode.expression), extendedTypeNode.expression ); result.push(assignClassBase); @@ -860,7 +902,7 @@ export class LuaTransformer { ) ); } else { - // __index = className.____super + // __index = localClassName.____super metatableFields.push( tstl.createTableFieldExpression( createClassBase(), @@ -884,25 +926,25 @@ export class LuaTransformer { const setClassMetatable = tstl.createExpressionStatement( tstl.createCallExpression( tstl.createIdentifier("setmetatable"), - [tstl.cloneIdentifier(className), tstl.createTableExpression(metatableFields)], + [tstl.cloneIdentifier(localClassName), tstl.createTableExpression(metatableFields)], extendedTypeNode.expression ) ); result.push(setClassMetatable); } else { - // setmetatable(className, className.____super) + // setmetatable(localClassName, localClassName.____super) const setClassMetatable = tstl.createExpressionStatement( tstl.createCallExpression( tstl.createIdentifier("setmetatable"), - [tstl.cloneIdentifier(className), createClassBase()], + [tstl.cloneIdentifier(localClassName), createClassBase()], extendedTypeNode.expression ) ); result.push(setClassMetatable); } - // setmetatable(className.prototype, className.____super.prototype) + // setmetatable(localClassName.prototype, localClassName.____super.prototype) const basePrototype = tstl.createTableIndexExpression( createClassBase(), tstl.createStringLiteral("prototype") @@ -943,7 +985,7 @@ export class LuaTransformer { const setClassMetatable = tstl.createExpressionStatement( tstl.createCallExpression( tstl.createIdentifier("setmetatable"), - [tstl.cloneIdentifier(className), tstl.createTableExpression(metatableFields)] + [tstl.cloneIdentifier(localClassName), tstl.createTableExpression(metatableFields)] ), statement ); @@ -952,7 +994,7 @@ export class LuaTransformer { const newFuncStatements: tstl.Statement[] = []; - // local self = setmetatable({}, className.prototype) + // local self = setmetatable({}, localClassName.prototype) const assignSelf = tstl.createVariableDeclarationStatement( this.createSelfIdentifier(), tstl.createCallExpression( @@ -976,11 +1018,11 @@ export class LuaTransformer { const returnSelf = tstl.createReturnStatement([this.createSelfIdentifier()]); newFuncStatements.push(returnSelf); - // function className.new(construct, ...) ... end - // or function export.className.new(construct, ...) ... end + // function localClassName.new(construct, ...) ... end + // or function export.localClassName.new(construct, ...) ... end const newFunc = tstl.createAssignmentStatement( tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), + tstl.cloneIdentifier(localClassName), tstl.createStringLiteral("new")), tstl.createFunctionExpression( tstl.createBlock(newFuncStatements), @@ -1003,6 +1045,8 @@ export class LuaTransformer { const statements: tstl.Statement[] = []; for (const f of instanceFields) { + this.validateClassElement(f); + // Get identifier const fieldName = this.transformPropertyName(f.name); @@ -1146,6 +1190,8 @@ export class LuaTransformer { return undefined; } + this.validateClassElement(getAccessor); + const name = this.transformIdentifier(getAccessor.name as ts.Identifier); const [body] = this.transformFunctionBody(getAccessor.parameters, getAccessor.body); @@ -1182,6 +1228,8 @@ export class LuaTransformer { return undefined; } + this.validateClassElement(setAccessor); + const name = this.transformIdentifier(setAccessor.name as ts.Identifier); const [params, dot, restParam] = this.transformParameters(setAccessor.parameters, this.createSelfIdentifier()); @@ -1222,6 +1270,8 @@ export class LuaTransformer { return undefined; } + this.validateClassElement(node); + let methodName = this.transformPropertyName(node.name); if (tstl.isStringLiteral(methodName) && methodName.value === "toString") { methodName = tstl.createStringLiteral("__tostring", node.name); @@ -1394,10 +1444,9 @@ export class LuaTransformer { // The identifier of the new variable const variableName = this.transformIdentifier(element.name as ts.Identifier); // The field to extract - const propertyName = this.transformIdentifier( - (element.propertyName || element.name) as ts.Identifier); + const propertyName = this.transformPropertyName(element.propertyName || element.name); const expression = isObjectBindingPattern - ? tstl.createTableIndexExpression(tableExpression, tstl.createStringLiteral(propertyName.text)) + ? tstl.createTableIndexExpression(tableExpression, propertyName) : tstl.createTableIndexExpression(tableExpression, tstl.createNumericLiteral(index + 1)); result.push(...this.createLocalOrExportedOrGlobalDeclaration(variableName, expression)); if (element.initializer) { @@ -1427,6 +1476,18 @@ export class LuaTransformer { return result; } + protected createModuleLocalNameIdentifier(declaration: ts.ModuleDeclaration): tstl.Identifier { + const moduleSymbol = this.checker.getSymbolAtLocation(declaration.name); + if (moduleSymbol !== undefined && this.isUnsafeName(moduleSymbol.name)) { + return tstl.createIdentifier( + this.createSafeName(declaration.name.text), + declaration.name, + moduleSymbol && this.symbolIds.get(moduleSymbol) + ); + } + return this.transformIdentifier(declaration.name as ts.Identifier); + } + public transformModuleDeclaration(statement: ts.ModuleDeclaration): StatementVisitResult { const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(statement), this.checker); // If phantom namespace elide the declaration and return the body @@ -1447,14 +1508,17 @@ export class LuaTransformer { || (symbol.declarations.findIndex(d => ts.isClassLike(d) || ts.isFunctionDeclaration(d)) === -1 && statement === symbol.declarations.find(ts.isModuleDeclaration)); + const nameIdentifier = this.transformIdentifier(statement.name as ts.Identifier); + if (isFirstDeclaration) { const isExported = (ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export) !== 0; if (isExported && this.currentNamespace) { // outerNS.innerNS = {} const namespaceDeclaration = tstl.createAssignmentStatement( tstl.createTableIndexExpression( - this.transformIdentifier(this.currentNamespace.name as ts.Identifier), - tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text)), + this.createModuleLocalNameIdentifier(this.currentNamespace), + tstl.createStringLiteral(nameIdentifier.text) + ), tstl.createTableExpression() ); @@ -1463,10 +1527,12 @@ export class LuaTransformer { if (hasExports && tsHelper.moduleHasEmittedBody(statement)) { // local innerNS = outerNS.innerNS const localDeclaration = this.createHoistableVariableDeclarationStatement( - statement.name as ts.Identifier, + this.createModuleLocalNameIdentifier(statement), tstl.createTableIndexExpression( - this.transformIdentifier(this.currentNamespace.name as ts.Identifier), - tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text))); + this.createModuleLocalNameIdentifier(this.currentNamespace), + tstl.createStringLiteral(nameIdentifier.text) + ) + ); result.push(localDeclaration); } @@ -1474,7 +1540,7 @@ export class LuaTransformer { } else if (isExported && !this.currentNamespace && this.isModule) { // exports.NS = {} const namespaceDeclaration = tstl.createAssignmentStatement( - this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier)), + this.createExportedIdentifier(nameIdentifier), tstl.createTableExpression() ); @@ -1483,8 +1549,9 @@ export class LuaTransformer { if (hasExports && tsHelper.moduleHasEmittedBody(statement)) { // local NS = exports.NS const localDeclaration = this.createHoistableVariableDeclarationStatement( - statement.name as ts.Identifier, - this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier))); + this.createModuleLocalNameIdentifier(statement), + this.createExportedIdentifier(tstl.cloneIdentifier(nameIdentifier, statement.name)) + ); result.push(localDeclaration); } @@ -1492,7 +1559,7 @@ export class LuaTransformer { } else { // local NS = {} const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration( - this.transformIdentifier(statement.name as ts.Identifier), + this.createModuleLocalNameIdentifier(statement), tstl.createTableExpression() ); @@ -3323,18 +3390,25 @@ export class LuaTransformer { if (ts.isPropertyAssignment(element)) { const expression = this.transformExpression(element.initializer); properties.push(tstl.createTableFieldExpression(expression, name, element)); + } else if (ts.isShorthandPropertyAssignment(element)) { - let identifier = this.transformIdentifierExpression(element.name); - if (tstl.isIdentifier(identifier)) { - const valueSymbol = this.checker.getShorthandAssignmentValueSymbol(element); - if (valueSymbol !== undefined && this.isSymbolExported(valueSymbol)) { - identifier = this.createExportedIdentifier(identifier); - } + let identifier: tstl.Expression | undefined; + const valueSymbol = this.checker.getShorthandAssignmentValueSymbol(element); + if (valueSymbol !== undefined && this.symbolIds.has(valueSymbol)) { + // Ignore unknown symbols so things like NaN still get transformed properly + identifier = this.createIdentifierFromSymbol(valueSymbol, element.name); + } else { + identifier = this.transformIdentifierExpression(element.name); + } + if (tstl.isIdentifier(identifier) && valueSymbol !== undefined && this.isSymbolExported(valueSymbol)) { + identifier = this.createExportedIdentifier(identifier); } properties.push(tstl.createTableFieldExpression(identifier, name, element)); + } else if (ts.isMethodDeclaration(element)) { const expression = this.transformFunctionExpression(element); properties.push(tstl.createTableFieldExpression(expression, name, element)); + } else { throw TSTLErrors.UnsupportedKind("object literal element", element.kind, expression); } @@ -4641,26 +4715,23 @@ export class LuaTransformer { } protected getIdentifierText(identifier: ts.Identifier): string { - const text = ts.idText(identifier); - - if (this.luaKeywords.has(text)) { - throw TSTLErrors.KeywordIdentifier(identifier); - } - - return text; + return ts.idText(identifier); } - public transformIdentifier(expression: ts.Identifier): tstl.Identifier { - if (expression.originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { + public transformIdentifier(identifier: ts.Identifier): tstl.Identifier { + if (identifier.originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { return tstl.createIdentifier("nil"); // TODO this is a hack that allows use to keep Identifier // as return time as changing that would break a lot of stuff. // But this should be changed to return tstl.createNilLiteral() // at some point. } - const text = this.getIdentifierText(expression); - const symbolId = this.getIdentifierSymbolId(expression); - return tstl.createIdentifier(text, expression, symbolId); + const text = this.hasUnsafeIdentifierName(identifier) + ? this.createSafeName(this.getIdentifierText(identifier)) + : this.getIdentifierText(identifier); + + const symbolId = this.getIdentifierSymbolId(identifier); + return tstl.createIdentifier(text, identifier, symbolId); } private transformIdentifierExpression(expression: ts.Identifier): tstl.Expression { @@ -4737,9 +4808,17 @@ export class LuaTransformer { } protected createExportedIdentifier(identifier: tstl.Identifier): tstl.TableIndexExpression { - const exportTable = this.currentNamespace - ? this.transformIdentifier(this.currentNamespace.name as ts.Identifier) - : this.createExportsIdentifier(); + let exportTable: tstl.Identifier; + if (this.currentNamespace !== undefined) { + if (this.isUnsafeName(this.currentNamespace.name.text)) { + exportTable = this.createModuleLocalNameIdentifier(this.currentNamespace); + } else { + exportTable = this.transformIdentifier(this.currentNamespace.name as ts.Identifier); + } + + } else { + exportTable = this.createExportsIdentifier(); + } return tstl.createTableIndexExpression( exportTable, @@ -5112,16 +5191,50 @@ export class LuaTransformer { return tstl.createBinaryExpression(expression, tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator); } + protected createIdentifierFromSymbol(symbol: ts.Symbol, tsOriginal?: ts.Node): tstl.Identifier { + const name = this.hasUnsafeSymbolName(symbol) + ? this.createSafeName(symbol.name) + : symbol.name; + return tstl.createIdentifier(name, tsOriginal, this.symbolIds.get(symbol)); + } + + protected isUnsafeName(name: string): boolean { + return this.luaKeywords.has(name) || !tsHelper.isValidLuaIdentifier(name); + } + + protected hasUnsafeSymbolName(symbol: ts.Symbol): boolean { + if (this.luaKeywords.has(symbol.name)) { + // lua keywords are only unsafe when non-ambient and not exported + const isNonAmbient = symbol.declarations.find(d => !tsHelper.isAmbient(d)) !== undefined; + return isNonAmbient && !this.isSymbolExported(symbol); + } + return this.isUnsafeName(symbol.name); + } + + protected hasUnsafeIdentifierName(identifier: ts.Identifier): boolean { + const symbol = this.checker.getSymbolAtLocation(identifier); + if (symbol !== undefined) { + return this.hasUnsafeSymbolName(symbol); + } + return false; + } + + protected createSafeName(name: string): string { + return "____" + tsHelper.fixInvalidLuaIdentifier(name); + } + protected getIdentifierSymbolId(identifier: ts.Identifier): tstl.SymbolId | undefined { const symbol = this.checker.getSymbolAtLocation(identifier); - let symbolId: number | undefined; + let symbolId: tstl.SymbolId | undefined; if (symbol) { // Track first time symbols are seen if (!this.symbolIds.has(symbol)) { symbolId = this.genSymbolIdCounter++; + const symbolInfo: SymbolInfo = {symbol, firstSeenAtPos: identifier.pos}; this.symbolIds.set(symbol, symbolId); this.symbolInfo.set(symbolId, symbolInfo); + } else { symbolId = this.symbolIds.get(symbol); } @@ -5317,15 +5430,14 @@ export class LuaTransformer { } protected createHoistableVariableDeclarationStatement( - identifier: ts.Identifier, + identifier: tstl.Identifier, initializer?: tstl.Expression, tsOriginal?: ts.Node, parent?: tstl.Node ): tstl.AssignmentStatement | tstl.VariableDeclarationStatement { - const variable = this.transformIdentifier(identifier); - const declaration = tstl.createVariableDeclarationStatement(variable, initializer, tsOriginal, parent); - if (!this.options.noHoisting && variable.symbolId) { + const declaration = tstl.createVariableDeclarationStatement(identifier, initializer, tsOriginal, parent); + if (!this.options.noHoisting && identifier.symbolId) { const scope = this.peekScope(); if (scope === undefined) { throw TSTLErrors.UndefinedScope(); diff --git a/src/TSHelper.ts b/src/TSHelper.ts index ebf86b61b..ecdc50e82 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -676,6 +676,10 @@ export class TSHelper { return match !== undefined && match !== null && match[0] === str; } + public static fixInvalidLuaIdentifier(name: string): string { + return name.replace(/[^a-zA-Z0-9_]/g, c => `_${c.charCodeAt(0).toString(16).toUpperCase()}`); + } + // Checks that a name is valid for use in lua function declaration syntax: // 'foo.bar' => passes ('function foo.bar()' is valid) // 'getFoo().bar' => fails ('function getFoo().bar()' would be illegal) diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 050b77d6f..0a241cad7 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -75,8 +75,8 @@ export class TSTLErrors { public static InvalidThrowExpression = (node: ts.Node) => new TranspileError(`Invalid throw expression, only strings can be thrown.`, node); - public static KeywordIdentifier = (node: ts.Identifier) => - new TranspileError(`Cannot use Lua keyword ${node.escapedText} as identifier.`, node); + public static ForbiddenStaticClassPropertyName = (node: ts.Node, name: string) => + new TranspileError(`Cannot use "${name}" as a static class property or method name.`, node); public static MissingClassName = (node: ts.Node) => new TranspileError(`Class declarations must have a name.`, node); diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 27e78d0c8..5a1e12df6 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -391,16 +391,16 @@ local TestClass = __TSTL_test.TestClass" `; exports[`Transformation (modulesImportNamedSpecialChars) 1`] = ` -"local __TSTL_kebab_module = require(\\"kebab-module\\") -local TestClass = __TSTL_kebab_module.TestClass -local __TSTL_dollar_module = require(\\"dollar$module\\") -local TestClass = __TSTL_dollar_module.TestClass -local __TSTL_singlequote_module = require(\\"singlequote'module\\") -local TestClass = __TSTL_singlequote_module.TestClass -local __TSTL_hash_module = require(\\"hash#module\\") -local TestClass = __TSTL_hash_module.TestClass -local __TSTL_space_module = require(\\"space module\\") -local TestClass = __TSTL_space_module.TestClass" +"local __TSTL_kebab_2Dmodule = require(\\"kebab-module\\") +local TestClass = __TSTL_kebab_2Dmodule.TestClass +local __TSTL_dollar_24module = require(\\"dollar$module\\") +local TestClass = __TSTL_dollar_24module.TestClass +local __TSTL_singlequote_27module = require(\\"singlequote'module\\") +local TestClass = __TSTL_singlequote_27module.TestClass +local __TSTL_hash_23module = require(\\"hash#module\\") +local TestClass = __TSTL_hash_23module.TestClass +local __TSTL_space_20module = require(\\"space module\\") +local TestClass = __TSTL_space_20module.TestClass" `; exports[`Transformation (modulesImportRenamed) 1`] = ` @@ -409,16 +409,16 @@ local RenamedClass = __TSTL_test.TestClass" `; exports[`Transformation (modulesImportRenamedSpecialChars) 1`] = ` -"local __TSTL_kebab_module = require(\\"kebab-module\\") -local RenamedClass = __TSTL_kebab_module.TestClass -local __TSTL_dollar_module = require(\\"dollar$module\\") -local RenamedClass = __TSTL_dollar_module.TestClass -local __TSTL_singlequote_module = require(\\"singlequote'module\\") -local RenamedClass = __TSTL_singlequote_module.TestClass -local __TSTL_hash_module = require(\\"hash#module\\") -local RenamedClass = __TSTL_hash_module.TestClass -local __TSTL_space_module = require(\\"space module\\") -local RenamedClass = __TSTL_space_module.TestClass" +"local __TSTL_kebab_2Dmodule = require(\\"kebab-module\\") +local RenamedClass = __TSTL_kebab_2Dmodule.TestClass +local __TSTL_dollar_24module = require(\\"dollar$module\\") +local RenamedClass = __TSTL_dollar_24module.TestClass +local __TSTL_singlequote_27module = require(\\"singlequote'module\\") +local RenamedClass = __TSTL_singlequote_27module.TestClass +local __TSTL_hash_23module = require(\\"hash#module\\") +local RenamedClass = __TSTL_hash_23module.TestClass +local __TSTL_space_20module = require(\\"space module\\") +local RenamedClass = __TSTL_space_20module.TestClass" `; exports[`Transformation (modulesImportWithoutFromClause) 1`] = `"require(\\"test\\")"`; diff --git a/test/unit/assignments/assignments.spec.ts b/test/unit/assignments/assignments.spec.ts index 4e47bd849..f7abcef6b 100644 --- a/test/unit/assignments/assignments.spec.ts +++ b/test/unit/assignments/assignments.spec.ts @@ -176,15 +176,6 @@ test("TupleReturn in expression", () => { expect(result).toBe("a3"); }); -test.each(["and", "local", "nil", "not", "or", "repeat", "then", "until"])( - "Keyword identifier error (%p)", - identifier => { - expect(() => util.transpileString(`const ${identifier} = 3;`)).toThrowExactError( - TSTLErrors.KeywordIdentifier(ts.createIdentifier(identifier)), - ); - }, -); - test("String table access", () => { const code = ` const dict : {[key:string]:any} = {}; diff --git a/test/unit/class.spec.ts b/test/unit/class.spec.ts index 0ffc8421b..42b575064 100644 --- a/test/unit/class.spec.ts +++ b/test/unit/class.spec.ts @@ -823,3 +823,43 @@ test("Class field override in subclass with constructors", () => { return (new Foo()).field + (new Bar()).field;`; expect(util.transpileAndExecute(code)).toBe("foobar"); }); + +test("Class cannot have static new method", () => { + const code = ` + class Foo { + static new() {} + }`; + expect(() => util.transpileAndExecute(code)).toThrow( + TSTLErrors.ForbiddenStaticClassPropertyName(ts.createEmptyStatement(), "new").message, + ); +}); + +test("Class cannot have static new property", () => { + const code = ` + class Foo { + static new = "foobar"; + }`; + expect(() => util.transpileAndExecute(code)).toThrow( + TSTLErrors.ForbiddenStaticClassPropertyName(ts.createEmptyStatement(), "new").message, + ); +}); + +test("Class cannot have static new get accessor", () => { + const code = ` + class Foo { + static get new() { return "foobar" } + }`; + expect(() => util.transpileAndExecute(code)).toThrow( + TSTLErrors.ForbiddenStaticClassPropertyName(ts.createEmptyStatement(), "new").message, + ); +}); + +test("Class cannot have static new set accessor", () => { + const code = ` + class Foo { + static set new(value: string) {} + }`; + expect(() => util.transpileAndExecute(code)).toThrow( + TSTLErrors.ForbiddenStaticClassPropertyName(ts.createEmptyStatement(), "new").message, + ); +}); diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts new file mode 100644 index 000000000..58ead74ff --- /dev/null +++ b/test/unit/identifiers.spec.ts @@ -0,0 +1,574 @@ +import * as util from "../util"; + +test.each(["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])("invalid lua identifier name (%p)", name => { + const code = ` + const ${name} = "foobar"; + return ${name};`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); +}); + +describe("lua keyword as identifier doesn't interfere with lua's value", () => { + test("variable (nil)", () => { + const code = ` + const nil = "foobar"; + return \`\${undefined}|\${nil}\``; + + expect(util.transpileAndExecute(code)).toBe("nil|foobar"); + }); + + test("variable (and)", () => { + const code = ` + const and = "foobar"; + return true && and;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (elseif)", () => { + const code = ` + const elseif = "foobar"; + if (false) { + } else if (elseif) { + return elseif; + }`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (end)", () => { + const code = ` + const end = "foobar"; + { + return end; + }`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (local)", () => { + const code = ` + const local = "foobar"; + return local;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (not)", () => { + const code = ` + const not = "foobar"; + return (!false) && not;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (or)", () => { + const code = ` + const or = "foobar"; + return false || or;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (repeat)", () => { + const code = ` + const repeat = "foobar"; + do {} while (false); + return repeat;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (then)", () => { + const code = ` + const then = "foobar"; + if (then) { + return then; + }`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (until)", () => { + const code = ` + const until = "foobar"; + do {} while (false); + return until;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (goto)", () => { + const code = ` + const goto = "foobar"; + switch (goto) { + case goto: + return goto; + }`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (print)", () => { + const luaHeader = ` + local result = "" + print = function(s) + result = result .. s + end`; + + const tsHeader = ` + declare let result: string;`; + + const code = ` + const print = "foobar"; + console.log(print); + return result;`; + + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + expect(util.transpileAndExecute(code, compilerOptions, luaHeader, tsHeader)).toBe("foobar"); + }); + + test("variable (type)", () => { + const code = ` + function type(this: void, a: unknown) { + return (typeof a) + "|foobar"; + } + return type(7);`; + + expect(util.transpileAndExecute(code)).toBe("number|foobar"); + }); + + test("variable (error)", () => { + const code = ` + const error = "foobar"; + throw error;`; + + expect(() => util.transpileAndExecute(code)).toThrow(/^LUA ERROR: .+ foobar$/); + }); + + test("variable (assert)", () => { + const code = ` + const assert = false; + console.assert(assert, "foobar");`; + + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + expect(() => util.transpileAndExecute(code, compilerOptions)).toThrow( + /^LUA ERROR: .+ foobar$/, + ); + }); + + test("variable (debug)", () => { + const luaHeader = ` + local result = "" + print = function(s) + result = result .. s + end`; + + const tsHeader = ` + declare let result: string;`; + + const code = ` + const debug = "foobar"; + console.trace(debug); + return result;`; + + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + expect(util.transpileAndExecute(code, compilerOptions, luaHeader, tsHeader)).toMatch( + /^foobar\nstack traceback.+/, + ); + }); + + test("variable (string)", () => { + const code = ` + const string = "foobar"; + return string[0];`; + + expect(util.transpileAndExecute(code)).toBe("f"); + }); + + test("variable (math)", () => { + const code = ` + const math = -17; + return Math.abs(math);`; + + expect(util.transpileAndExecute(code)).toBe(17); + }); + + test("variable (table)", () => { + const code = ` + const table = ["foobar"]; + return table.pop();`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (coroutine)", () => { + const code = ` + const coroutine = "foobar"; + function *foo() { yield coroutine; } + return foo().next().value;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (pairs)", () => { + const code = ` + const pairs = {foobar: "foobar"}; + let result = ""; + for (const key in pairs) { + result += key; + } + return result;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (pcall)", () => { + const code = ` + const pcall = "foobar"; + try {} finally {} + return pcall;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (rawget)", () => { + const code = ` + const rawget = {foobar: "foobar"}; + return rawget.hasOwnProperty("foobar");`; + + expect(util.transpileAndExecute(code)).toBe(true); + }); + + test("variable (rawset)", () => { + const code = ` + const rawset = "foobar"; + class A { + prop = "prop"; + } + class B extends A { + get prop() { return rawset; } + } + const b = new B(); + return b.prop;`; + + expect(util.transpileAndExecute(code)).toBe("foobar"); + }); + + test("variable (require)", () => { + const code = ` + const require = "foobar"; + export { foo } from "someModule"; + export const result = require;`; + + const lua = ` + package.loaded.someModule = {foo = "bar"} + return (function() + ${util.transpileString(code, undefined, true)} + end)().result`; + + expect(util.executeLua(lua)).toBe("foobar"); + }); + + test("variable (tostring)", () => { + const code = ` + const tostring = 17; + return tostring.toString();`; + + expect(util.transpileAndExecute(code)).toBe(17); + }); + + test("variable (unpack)", () => { + const code = ` + const unpack = ["foo", "bar"]; + const [foo, bar] = unpack;`; + + const lua = ` + unpack = table.unpack + ${util.transpileString(code, undefined, false)} + return foo .. bar`; + + expect(util.executeLua(lua)).toBe("foobar"); + }); + + test("variable (_G)", () => { + const tsHeader = ` + var foobar = "foo";`; + + const code = ` + const _G = "bar"; + function foo(this: any) { + return this.foobar + _G; + } + return foo();`; + + expect(util.transpileAndExecute(code, undefined, undefined, tsHeader)).toBe("foobar"); + }); + + test("function parameter", () => { + const code = ` + function foo(type: unknown) { + return \`\${typeof type}|\${type}\`; + } + return foo("foobar");`; + + expect(util.transpileAndExecute(code)).toBe("string|foobar"); + }); + + test("destructured property function parameter", () => { + const code = ` + function foo({type}: any) { + return \`\${typeof type}|\${type}\`; + } + return foo({type: "foobar"});`; + + expect(util.transpileAndExecute(code)).toBe("string|foobar"); + }); + + test("destructured array element function parameter", () => { + const code = ` + function foo([type]: any) { + return \`\${typeof type}|\${type}\`; + } + return foo(["foobar"]);`; + + expect(util.transpileAndExecute(code)).toBe("string|foobar"); + }); + + test("property", () => { + const code = ` + const type = "foobar"; + const foo = { type: type }; + return type + "|" + foo.type + "|" + typeof type;`; + + expect(util.transpileAndExecute(code)).toBe("foobar|foobar|string"); + }); + + test("shorthand property", () => { + const code = ` + const type = "foobar"; + const foo = { type }; + return type + "|" + foo.type + "|" + typeof type;`; + + expect(util.transpileAndExecute(code)).toBe("foobar|foobar|string"); + }); + + test("destructured property", () => { + const code = ` + const foo = { type: "foobar" }; + const { type: type } = foo; + return type + "|" + foo.type + "|" + typeof type;`; + + expect(util.transpileAndExecute(code)).toBe("foobar|foobar|string"); + }); + + test("destructured shorthand property", () => { + const code = ` + const foo = { type: "foobar" }; + const { type } = foo; + return type + "|" + foo.type + "|" + typeof type;`; + + expect(util.transpileAndExecute(code)).toBe("foobar|foobar|string"); + }); + + test("destructured array element", () => { + const code = ` + const foo = ["foobar"]; + const [type] = foo; + return type + "|" + typeof type;`; + + expect(util.transpileAndExecute(code)).toBe("foobar|string"); + }); + + test.each(["type", "type as type"])("imported variable (%p)", importName => { + const luaHeader = ` + package.loaded.someModule = {type = "foobar"}`; + + const code = ` + import {${importName}} from "someModule"; + return typeof 7 + "|" + type;`; + + const lua = util.transpileString(code); + + expect(util.executeLua(`${luaHeader} ${lua}`)).toBe("number|foobar"); + }); + + test.each([ + { returnExport: "type", expectResult: "foobar" }, + { returnExport: "mytype", expectResult: "foobar" }, + { returnExport: "result", expectResult: "string|foobar" }, + ])("separately exported variable (%p)", ({ returnExport, expectResult }) => { + const code = ` + const type = "foobar"; + export { type } + export { type as mytype } + export const result = typeof type + "|" + type;`; + + expect(util.transpileExecuteAndReturnExport(code, returnExport)).toBe(expectResult); + }); + + test.each(["type", "type as type"])( + "re-exported variable with lua keyword as name (%p)", + importName => { + const code = ` + export { ${importName} } from "someModule"`; + + const lua = ` + package.loaded.someModule = {type = "foobar"} + return (function() + ${util.transpileString(code)} + end)().type`; + + expect(util.executeLua(lua)).toBe("foobar"); + }, + ); + + test("class", () => { + const code = ` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + const t = new type(); + return t.method() + "|" + type.staticMethod();`; + + expect(util.transpileAndExecute(code)).toBe("number|string"); + }); + + test("subclass of class", () => { + const code = ` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + class Foo extends type {} + const foo = new Foo(); + return foo.method() + "|" + Foo.staticMethod();`; + + expect(util.transpileAndExecute(code)).toBe("number|string"); + }); + + test.each([ + { returnExport: "result", expectResult: "number|string" }, + { returnExport: "type ~= nil", expectResult: true }, + ])("exported class (%p)", ({ returnExport, expectResult }) => { + const code = ` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + const t = new type(); + export const result = t.method() + "|" + type.staticMethod();`; + + expect(util.transpileExecuteAndReturnExport(code, returnExport)).toBe(expectResult); + }); + + test.each([ + { returnExport: "result", expectResult: "number|string" }, + { returnExport: "type ~= nil", expectResult: true }, + ])("subclass of exported class (%p)", ({ returnExport, expectResult }) => { + const code = ` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + class Foo extends type {} + const foo = new Foo(); + export const result = foo.method() + "|" + Foo.staticMethod();`; + + expect(util.transpileExecuteAndReturnExport(code, returnExport)).toBe(expectResult); + }); + + test("namespace", () => { + const tsHeader = ` + namespace type { + export const foo = "foobar"; + }`; + + const code = ` + return typeof type.foo + "|" + type.foo`; + + expect(util.transpileAndExecute(code, undefined, undefined, tsHeader)).toBe( + "string|foobar", + ); + }); + + test.each([ + { returnExport: "result", expectResult: "string|foobar" }, + { returnExport: "type ~= nil", expectResult: true }, + ])("exported namespace (%p)", ({ returnExport, expectResult }) => { + const code = ` + export namespace type { + export const foo = "foobar"; + } + export const result = typeof type.foo + "|" + type.foo;`; + + expect(util.transpileExecuteAndReturnExport(code, returnExport)).toBe(expectResult); + }); + + test("merged namespace", () => { + const tsHeader = ` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof true; } + } + + namespace type { + export const foo = "foo"; + } + + namespace type { + export const bar = "bar"; + }`; + + const code = ` + const t = new type(); + return \`\${t.method()}|\${type.staticMethod()}|\${typeof type.foo}|\${type.foo}|\${type.bar}\`;`; + + expect(util.transpileAndExecute(code, undefined, undefined, tsHeader)).toBe( + "number|boolean|string|foo|bar", + ); + }); + + test.each([ + { returnExport: "result", expectResult: "number|boolean|string|foo|bar" }, + { returnExport: "type ~= nil", expectResult: true }, + ])("exported merged namespace (%p)", ({ returnExport, expectResult }) => { + const code = ` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof true; } + } + + export namespace type { + export const foo = "foo"; + } + + export namespace type { + export const bar = "bar"; + } + + const t = new type(); + export const result = \`\${t.method()}|\${type.staticMethod()}|\${typeof type.foo}|\${type.foo}|\${type.bar}\`;`; + + expect(util.transpileExecuteAndReturnExport(code, returnExport)).toBe(expectResult); + }); +}); + +test("declaration-only variable with lua keyword as name is not renamed", () => { + const code = ` + declare function type(this: void, a: unknown): string; + type(7);`; + + expect(util.transpileString(code, undefined, false)).toBe("type(7)"); +}); + +test("exported variable with lua keyword as name is not renamed", () => { + const code = ` + export const print = "foobar";`; + + expect(util.transpileExecuteAndReturnExport(code, "print")).toBe("foobar"); +}); diff --git a/test/unit/importexport.spec.ts b/test/unit/importexport.spec.ts index cd98b0b4f..ee50cb4e0 100644 --- a/test/unit/importexport.spec.ts +++ b/test/unit/importexport.spec.ts @@ -10,3 +10,18 @@ test.each([ TSTLErrors.UnsupportedDefaultExport(util.nodeStub), ); }); + +test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])( + "Import module names with invalid lua identifier characters (%p)", + name => { + const code = ` + import { foo } from "${name}";`; + + const lua = ` + setmetatable(package.loaded, {__index = function() return {foo = "bar"} end}) + ${util.transpileString(code)} + return foo;`; + + expect(util.executeLua(lua)).toBe("bar"); + }, +);