diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 658664488..23a9711ad 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -55,6 +55,7 @@ export enum LuaLibFeature { StringStartsWith = "StringStartsWith", Symbol = "Symbol", SymbolRegistry = "SymbolRegistry", + TypeOf = "TypeOf", } const luaLibDependencies: { [lib in LuaLibFeature]?: LuaLibFeature[] } = { diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 09317db82..dc3b7f1ac 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2655,10 +2655,42 @@ export class LuaTransformer { } } + protected transformTypeOfLiteralComparison( + typeOfExpression: ts.TypeOfExpression, + comparedExpression: tstl.StringLiteral, + operator: ts.BinaryOperator, + tsOriginal: ts.Node + ): ExpressionVisitResult { + if (comparedExpression.value === "object") { + comparedExpression.value = "table"; + } else if (comparedExpression.value === "undefined") { + comparedExpression.value = "nil"; + } + const innerExpression = this.transformExpression(typeOfExpression.expression); + const typeCall = tstl.createCallExpression(tstl.createIdentifier("type"), [innerExpression], typeOfExpression); + return this.transformBinaryOperation(typeCall, comparedExpression, operator, tsOriginal); + } + + protected transformComparisonExpression(expression: ts.BinaryExpression): ExpressionVisitResult { + const left = this.transformExpression(expression.left); + const right = this.transformExpression(expression.right); + const operator = expression.operatorToken.kind; + + // Custom handling for 'typeof(foo) === "type"' + if (ts.isTypeOfExpression(expression.left) && tstl.isStringLiteral(right)) { + return this.transformTypeOfLiteralComparison(expression.left, right, operator, expression); + } else if (ts.isTypeOfExpression(expression.right) && tstl.isStringLiteral(left)) { + return this.transformTypeOfLiteralComparison(expression.right, left, operator, expression); + } + + return this.transformBinaryOperation(left, right, operator, expression); + } + public transformBinaryExpression(expression: ts.BinaryExpression): ExpressionVisitResult { - // Check if this is an assignment token, then handle accordingly + const operator = expression.operatorToken.kind; - const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(expression.operatorToken.kind); + // Check if this is an assignment token, then handle accordingly + const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(operator); if (isCompound && replacementOperator) { return this.transformCompoundAssignmentExpression( expression, @@ -2669,18 +2701,14 @@ export class LuaTransformer { ); } - const lhs = this.transformExpression(expression.left); - const rhs = this.transformExpression(expression.right); - // Transpile operators - switch (expression.operatorToken.kind) { + switch (operator) { case ts.SyntaxKind.AmpersandToken: case ts.SyntaxKind.BarToken: case ts.SyntaxKind.CaretToken: case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return this.transformBinaryBitOperation(expression, lhs, rhs, expression.operatorToken.kind); case ts.SyntaxKind.PlusToken: case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: @@ -2688,7 +2716,12 @@ export class LuaTransformer { case ts.SyntaxKind.AsteriskToken: case ts.SyntaxKind.AsteriskAsteriskToken: case ts.SyntaxKind.SlashToken: - case ts.SyntaxKind.PercentToken: + case ts.SyntaxKind.PercentToken: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); + return this.transformBinaryOperation(lhs, rhs, operator, expression); + } + case ts.SyntaxKind.GreaterThanToken: case ts.SyntaxKind.GreaterThanEqualsToken: case ts.SyntaxKind.LessThanToken: @@ -2697,10 +2730,14 @@ export class LuaTransformer { case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return this.transformBinaryOperation(lhs, rhs, expression.operatorToken.kind, expression); + return this.transformComparisonExpression(expression); + case ts.SyntaxKind.EqualsToken: return this.transformAssignmentExpression(expression); - case ts.SyntaxKind.InKeyword: + + case ts.SyntaxKind.InKeyword: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); const indexExpression = tstl.createTableIndexExpression(rhs, lhs); return tstl.createBinaryExpression( indexExpression, @@ -2708,8 +2745,11 @@ export class LuaTransformer { tstl.SyntaxKind.InequalityOperator, expression ); + } - case ts.SyntaxKind.InstanceOfKeyword: + case ts.SyntaxKind.InstanceOfKeyword: { + const lhs = this.transformExpression(expression.left); + const rhs = this.transformExpression(expression.right); const rhsType = this.checker.getTypeAtLocation(expression.right); const decorators = tsHelper.getCustomDecorators(rhsType, this.checker); @@ -2727,16 +2767,19 @@ export class LuaTransformer { } return this.transformLuaLibFunction(LuaLibFeature.InstanceOf, expression, lhs, rhs); + } - case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.CommaToken: { + const rhs = this.transformExpression(expression.right); return this.createImmediatelyInvokedFunctionExpression( this.statementVisitResultToArray(this.transformExpressionStatement(expression.left)), rhs, expression ); + } default: - throw TSTLErrors.UnsupportedKind("binary operator", expression.operatorToken.kind, expression); + throw TSTLErrors.UnsupportedKind("binary operator", operator, expression); } } @@ -4489,16 +4532,7 @@ export class LuaTransformer { public transformTypeOfExpression(expression: ts.TypeOfExpression): ExpressionVisitResult { const innerExpression = this.transformExpression(expression.expression); - const typeFunctionIdentifier = tstl.createIdentifier("type"); - const typeCall = tstl.createCallExpression(typeFunctionIdentifier, [innerExpression]); - const tableString = tstl.createStringLiteral("table"); - const objectString = tstl.createStringLiteral("object"); - const condition = tstl.createBinaryExpression(typeCall, tableString, tstl.SyntaxKind.EqualityOperator); - const andClause = tstl.createBinaryExpression(condition, objectString, tstl.SyntaxKind.AndOperator); - - return tstl.createParenthesizedExpression( - tstl.createBinaryExpression(andClause, tstl.cloneNode(typeCall), tstl.SyntaxKind.OrOperator, expression) - ); + return this.transformLuaLibFunction(LuaLibFeature.TypeOf, expression, innerExpression); } public transformSpreadElement(expression: ts.SpreadElement): ExpressionVisitResult { diff --git a/src/lualib/TypeOf.ts b/src/lualib/TypeOf.ts new file mode 100644 index 000000000..af531d732 --- /dev/null +++ b/src/lualib/TypeOf.ts @@ -0,0 +1,12 @@ +declare function type(this: void, value: unknown): string; + +function __TS__TypeOf(this: void, value: unknown): string { + const luaType = type(value); + if (luaType === "table") { + return "object"; + } else if (luaType === "nil") { + return "undefined"; + } else { + return luaType; + } +} diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts index 3d5014336..166921e92 100644 --- a/test/unit/typechecking.spec.ts +++ b/test/unit/typechecking.spec.ts @@ -40,7 +40,7 @@ test("typeof function", () => { test.each(["null", "undefined"])("typeof undefined (%p)", inp => { const result = util.transpileAndExecute(`return typeof ${inp};`); - expect(result).toBe("nil"); + expect(result).toBe("undefined"); }); test("instanceof", () => { @@ -140,3 +140,44 @@ test("instanceof Symbol.hasInstance", () => { expect(result).toBe(true); }); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, +])("typeof literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + return typeof val ${operator} "${compareTo}";`; + + expect(util.transpileAndExecute(code)).toBe(expectResult); +}); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, +])("typeof non-literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + let compareTo = "${compareTo}"; + return typeof val ${operator} compareTo;`; + + expect(util.transpileAndExecute(code)).toBe(expectResult); +});