Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
// https://github.com/facebook/jest/issues/5274
"!<rootDir>/src/tstl.ts",
],
watchPathIgnorePatterns: ["cli/watch/[^/]+$"],
watchPathIgnorePatterns: ["cli/watch/[^/]+$", "src/lualib"],

setupFilesAfterEnv: ["<rootDir>/test/setup.ts"],
testEnvironment: "node",
Expand Down
3 changes: 3 additions & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export enum LuaLibFeature {
Iterator = "Iterator",
Map = "Map",
NewIndex = "NewIndex",
Number = "Number",
NumberIsFinite = "NumberIsFinite",
NumberIsNaN = "NumberIsNaN",
ObjectAssign = "ObjectAssign",
ObjectEntries = "ObjectEntries",
ObjectFromEntries = "ObjectFromEntries",
Expand Down
99 changes: 82 additions & 17 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3320,7 +3320,7 @@ export class LuaTransformer {
const expression = this.expectExpression(this.transformExpression(element.initializer));
properties.push(tstl.createTableFieldExpression(expression, name, element));
} else if (ts.isShorthandPropertyAssignment(element)) {
const identifier = this.transformIdentifier(element.name);
const identifier = this.transformIdentifierExpression(element.name);
properties.push(tstl.createTableFieldExpression(identifier, name, element));
} else if (ts.isMethodDeclaration(element)) {
const expression = this.expectExpression(this.transformFunctionExpression(element));
Expand Down Expand Up @@ -3523,6 +3523,14 @@ export class LuaTransformer {
);
}

const expressionType = this.checker.getTypeAtLocation(node.expression);
if (tsHelper.isStandardLibraryType(expressionType, undefined, this.program)) {
const result = this.transformGlobalFunctionCall(node);
if (result) {
return result;
}
}

const callPath = this.expectExpression(this.transformExpression(node.expression));
const signatureDeclaration = signature && signature.getDeclaration();
if (signatureDeclaration
Expand All @@ -3534,15 +3542,35 @@ export class LuaTransformer {
parameters = this.transformArguments(node.arguments, signature, context);
}

const expressionType = this.checker.getTypeAtLocation(node.expression);
if (tsHelper.isStandardLibraryType(expressionType, "SymbolConstructor", this.program)) {
return this.transformLuaLibFunction(LuaLibFeature.Symbol, node, ...parameters);
}

const callExpression = tstl.createCallExpression(callPath, parameters, node);
return wrapResult ? this.wrapInTable(callExpression) : callExpression;
}

private transformGlobalFunctionCall(node: ts.CallExpression): ExpressionVisitResult {
const signature = this.checker.getResolvedSignature(node);
const parameters = this.transformArguments(node.arguments, signature);

const expressionType = this.checker.getTypeAtLocation(node.expression);
const name = expressionType.symbol.name;
switch (name) {
case "SymbolConstructor":
return this.transformLuaLibFunction(LuaLibFeature.Symbol, node, ...parameters);
case "NumberConstructor":
return this.transformLuaLibFunction(LuaLibFeature.Number, node, ...parameters);
case "isNaN":
case "isFinite":
const numberParameters = tsHelper.isNumberType(expressionType)
? parameters
: [this.transformLuaLibFunction(LuaLibFeature.Number, undefined, ...parameters)];

return this.transformLuaLibFunction(
name === "isNaN" ? LuaLibFeature.NumberIsNaN : LuaLibFeature.NumberIsFinite,
node,
...numberParameters
);
}
}

public transformPropertyCall(node: ts.CallExpression): ExpressionVisitResult {
let parameters: tstl.Expression[] = [];

Expand Down Expand Up @@ -3580,6 +3608,10 @@ export class LuaTransformer {
return this.transformSymbolCallExpression(node);
}

if (tsHelper.isStandardLibraryType(ownerType, "NumberConstructor", this.program)) {
return this.transformNumberCallExpression(node);
}

const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker);

if (classDecorators.has(DecoratorKind.LuaTable)) {
Expand Down Expand Up @@ -4290,6 +4322,26 @@ export class LuaTransformer {
}
}

// Transpile a Number._ property
private transformNumberCallExpression(expression: ts.CallExpression): tstl.CallExpression {
const method = expression.expression as ts.PropertyAccessExpression;
const parameters = this.transformArguments(expression.arguments);
const methodName = method.name.escapedText;

switch (methodName) {
case "isNaN":
return this.transformLuaLibFunction(LuaLibFeature.NumberIsNaN, expression, ...parameters);
case "isFinite":
return this.transformLuaLibFunction(LuaLibFeature.NumberIsFinite, expression, ...parameters);
default:
throw TSTLErrors.UnsupportedForTarget(
`number property ${methodName}`,
this.luaTarget,
expression
);
}
}

private validateLuaTableCall(
expression: ts.CallExpression & { expression: ts.PropertyAccessExpression },
isWithinExpressionStatement: boolean
Expand Down Expand Up @@ -4575,18 +4627,13 @@ export class LuaTransformer {
}

private getIdentifierText(identifier: ts.Identifier): string {
let escapedText = identifier.escapedText as string;
const underScoreCharCode = "_".charCodeAt(0);
if (escapedText.length >= 3 && escapedText.charCodeAt(0) === underScoreCharCode &&
escapedText.charCodeAt(1) === underScoreCharCode && escapedText.charCodeAt(2) === underScoreCharCode) {
escapedText = escapedText.substr(1);
}
const text = ts.idText(identifier);

if (this.luaKeywords.has(escapedText)) {
if (this.luaKeywords.has(text)) {
throw TSTLErrors.KeywordIdentifier(identifier);
}

return escapedText;
return text;
}

public transformIdentifier(expression: ts.Identifier): tstl.Identifier {
Expand All @@ -4597,16 +4644,34 @@ export class LuaTransformer {
// at some point.
}

const escapedText = this.getIdentifierText(expression);
const text = this.getIdentifierText(expression);
const symbolId = this.getIdentifierSymbolId(expression);
return tstl.createIdentifier(escapedText, expression, symbolId);
return tstl.createIdentifier(text, expression, symbolId);
}

private transformIdentifierExpression(expression: ts.Identifier): tstl.IdentifierOrTableIndexExpression {
private transformIdentifierExpression(expression: ts.Identifier): tstl.Expression {
const identifier = this.transformIdentifier(expression);
if (this.isIdentifierExported(identifier)) {
return this.createExportedIdentifier(identifier);
}

switch (this.getIdentifierText(expression)) {
case "NaN":
return tstl.createParenthesizedExpression(
tstl.createBinaryExpression(
tstl.createNumericLiteral(0),
tstl.createNumericLiteral(0),
tstl.SyntaxKind.DivisionOperator,
expression
)
);

case "Infinity":
const math = tstl.createIdentifier("math");
const huge = tstl.createStringLiteral("huge");
return tstl.createTableIndexExpression(math, huge, expression);
}

return identifier;
}

Expand Down
14 changes: 11 additions & 3 deletions src/TSHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export class TSHelper {
(type.flags & ts.TypeFlags.StringLiteral) !== 0;
}

public static isNumberType(type: ts.Type): boolean {
return (type.flags & ts.TypeFlags.Number) !== 0 || (type.flags & ts.TypeFlags.NumberLike) !== 0 ||
(type.flags & ts.TypeFlags.NumberLiteral) !== 0;
}

public static isExplicitArrayType(type: ts.Type, checker: ts.TypeChecker, program: ts.Program): boolean {
if (type.isUnionOrIntersection()) {
return type.types.some(t => TSHelper.isExplicitArrayType(t, checker, program));
Expand Down Expand Up @@ -731,9 +736,12 @@ export class TSHelper {
return program.isSourceFileDefaultLibrary(source);
}

public static isStandardLibraryType(type: ts.Type, name: string, program: ts.Program): boolean {
const symbol = type.symbol;
if (!symbol || symbol.escapedName !== name) { return false; }
public static isStandardLibraryType(type: ts.Type, name: string | undefined, program: ts.Program): boolean {
const symbol = type.getSymbol();
if (!symbol || (name ? symbol.escapedName !== name : symbol.escapedName === '__type')) {
return false;
}

const declaration = symbol.valueDeclaration;
if(!declaration) { return true; } // assume to be lib function if no valueDeclaration exists
return this.isStandardLibraryDeclaration(declaration, program);
Expand Down
2 changes: 1 addition & 1 deletion src/lualib/ArraySetLength.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function __TS__ArraySetLength<T>(this: void, arr: T[], length: number): number {
if (length < 0
|| length !== length // NaN
|| length === (1 / 0) // Infinity
|| length === Infinity // Infinity
|| Math.floor(length) !== length) // non-integer
{
// tslint:disable-next-line:no-string-throw
Expand Down
20 changes: 20 additions & 0 deletions src/lualib/Number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function __TS__Number(this: void, value: unknown): number {
const valueType = type(value);
if (valueType === "number") {
return value as number;
} else if (valueType === "string") {
const numberValue = tonumber(value);
if (numberValue) return numberValue;

if (value === "Infinity") return Infinity;
if (value === "-Infinity") return -Infinity;
const [stringWithoutSpaces] = string.gsub(value as string, '%s', '');
if (stringWithoutSpaces === "") return 0;

return NaN;
} else if (valueType === "boolean") {
return value ? 1 : 0;
} else {
return NaN;
}
}
5 changes: 5 additions & 0 deletions src/lualib/NumberIsFinite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function __TS__NumberIsFinite(this: void, value: unknown): boolean {
return (
typeof value === "number" && value === value && value !== Infinity && value !== -Infinity
);
}
3 changes: 3 additions & 0 deletions src/lualib/NumberIsNaN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function __TS__NumberIsNaN(this: void, value: unknown): boolean {
return value !== value;
}
10 changes: 5 additions & 5 deletions src/lualib/Symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ declare function setmetatable<T extends object>(this: void, obj: T, metatable: a
const ____symbolMetatable = {
__tostring(): string {
if (this.description === undefined) {
return 'Symbol()';
return "Symbol()";
} else {
return 'Symbol(' + this.description + ')';
return "Symbol(" + this.description + ")";
}
},
};

function __TS__Symbol(description?: string | number): symbol {
function __TS__Symbol(this: void, description?: string | number): symbol {
return setmetatable({ description }, ____symbolMetatable) as any;
}

Symbol = {
iterator: __TS__Symbol('Symbol.iterator'),
hasInstance: __TS__Symbol('Symbol.hasInstance'),
iterator: __TS__Symbol("Symbol.iterator"),
hasInstance: __TS__Symbol("Symbol.hasInstance"),
} as any;
4 changes: 3 additions & 1 deletion src/lualib/declarations/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** @noSelfInFile */

declare function tonumber(value: any, base?: number): number | undefined;
declare function type(
this: void,
value: any
): "nil" | "number" | "string" | "boolean" | "table" | "function" | "thread" | "userdata";
79 changes: 79 additions & 0 deletions test/unit/numbers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as util from "../util";

test.each([
"NaN === NaN",
"NaN !== NaN",
"NaN + NaN",
"NaN - NaN",
"NaN * NaN",
"NaN / NaN",
"NaN + 1",
"1 + NaN",
"1 / NaN",
"NaN * 0",
])("%s", code => expect(util.transpileAndExecute(`return ${code}`)).toBe(eval(code)));

test("NaN reassignment", () => {
const result = util.transpileAndExecute(`const NaN = 1; return NaN`);

expect(result).toBe(NaN);
});

test.each([
"Infinity",
"Infinity - Infinity",
"Infinity / -1",
"Infinity * -1",
"Infinity + 1",
"Infinity - 1",
])("%s", code => expect(util.transpileAndExecute(`return ${code}`)).toBe(eval(code)));

test("Infinity reassignment", () => {
const result = util.transpileAndExecute(`const Infinity = 1; return Infinity`);

expect(result).toBe(Infinity);
});

const numberCases = [-1, 0, 1, 1.5, Infinity, -Infinity];
const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"];
const restCases: any[] = [true, false, "", " ", "\t", "\n", "foo", {}];
const cases: any[] = [...numberCases, ...stringCases, ...restCases];

// TODO: Add more general utils to serialize values
const valueToString = (value: unknown) =>
value === Infinity || value === -Infinity || (typeof value === "number" && Number.isNaN(value))
? String(value)
: JSON.stringify(value);

describe("Number", () => {
test.each(cases)("constructor(%p)", value => {
const result = util.transpileAndExecute(`return Number(${valueToString(value)})`);
expect(result).toBe(Number(value));
});

test.each(cases)("isNaN(%p)", value => {
const result = util.transpileAndExecute(`
return Number.isNaN(${valueToString(value)} as any)
`);

expect(result).toBe(Number.isNaN(value));
});

test.each(cases)("isFinite(%p)", value => {
const result = util.transpileAndExecute(`
return Number.isFinite(${valueToString(value)} as any)
`);

expect(result).toBe(Number.isFinite(value));
});
});

test.each(cases)("isNaN(%p)", value => {
const result = util.transpileAndExecute(`return isNaN(${valueToString(value)} as any)`);
expect(result).toBe(isNaN(value));
});

test.each(cases)("isFinite(%p)", value => {
const result = util.transpileAndExecute(`return isFinite(${valueToString(value)} as any)`);
expect(result).toBe(isFinite(value));
});
24 changes: 17 additions & 7 deletions test/unit/objectLiteral.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ test.each([
expect(lua).toBe(`local myvar = ${out}`);
});

test.each([{ input: "3", expected: 3 }])(
"Shorthand Property Assignment (%p)",
({ input, expected }) => {
const result = util.transpileAndExecute(`const x = ${input}; const o = {x}; return o.x;`);
expect(result).toBe(expected);
},
);
describe("property shorthand", () => {
test("should support property shorthand", () => {
const result = util.transpileAndExecute(`
const x = 1;
const o = { x };
return o.x;
`);

expect(result).toBe(1);
});

test.each([NaN, Infinity])("should support %p shorthand", identifier => {
const result = util.transpileAndExecute(`return ({ ${identifier} }).${identifier}`);

expect(result).toBe(identifier);
});
});

test("undefined as object key", () => {
const code = `const foo = {undefined: "foo"};
Expand Down