Skip to content
13 changes: 5 additions & 8 deletions src/transformation/utils/lua-ast.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as ts from "typescript";
import * as assert from "assert";
import * as ts from "typescript";
import { LuaTarget } from "../../CompilerOptions";
import * as lua from "../../LuaAST";
import { TransformationContext } from "../context";
import { getCurrentNamespace } from "../visitors/namespace";
import { createExportedIdentifier, getIdentifierExportScope } from "./export";
import { findScope, peekScope, ScopeType } from "./scope";
import { peekScope, ScopeType } from "./scope";
import { isFunctionType } from "./typescript";

export type OneToManyVisitorResult<T extends lua.Node> = T | T[] | undefined;
Expand Down Expand Up @@ -122,7 +121,6 @@ export function createLocalOrExportedOrGlobalDeclaration(
let declaration: lua.VariableDeclarationStatement | undefined;
let assignment: lua.AssignmentStatement | undefined;

const isVariableDeclaration = tsOriginal !== undefined && ts.isVariableDeclaration(tsOriginal);
const isFunctionDeclaration = tsOriginal !== undefined && ts.isFunctionDeclaration(tsOriginal);

const identifiers = Array.isArray(lhs) ? lhs : [lhs];
Expand All @@ -143,11 +141,10 @@ export function createLocalOrExportedOrGlobalDeclaration(
);
}
} else {
const insideFunction = findScope(context, ScopeType.Function) !== undefined;

if (context.isModule || getCurrentNamespace(context) || insideFunction || isVariableDeclaration) {
const scope = peekScope(context);
const scope = peekScope(context);
const isTopLevelVariable = scope.type === ScopeType.File;

if (context.isModule || !isTopLevelVariable) {
const isPossibleWrappedFunction =
!isFunctionDeclaration &&
tsOriginal &&
Expand Down
2 changes: 1 addition & 1 deletion src/transformation/visitors/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ function moduleHasEmittedBody(
return false;
}

// Static context -> namespace dictionary keeping the current namespace for each transformation context
const currentNamespaces = new WeakMap<TransformationContext, ts.ModuleDeclaration | undefined>();
export const getCurrentNamespace = (context: TransformationContext) => currentNamespaces.get(context);

export const transformModuleDeclaration: FunctionVisitor<ts.ModuleDeclaration> = (node, context) => {
const annotations = getTypeAnnotations(context, context.checker.getTypeAtLocation(node));
Expand Down
48 changes: 35 additions & 13 deletions test/translation/__snapshots__/transformation.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Transformation (blockScopeVariables) 1`] = `
"do
local a = 1
local b = 1
local ____ = {c = 1}
local c = ____.c
end"
`;

exports[`Transformation (characterEscapeSequence) 1`] = `
"local quoteInDoubleQuotes = \\"' ' '\\"
local quoteInTemplateString = \\"' ' '\\"
local doubleQuoteInQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\"
local doubleQuoteInDoubleQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\"
local doubleQuoteInTemplateString = \\"\\\\\\" \\\\\\" \\\\\\"\\"
local backQuoteInQuotes = \\"\` \` \`\\"
local backQuoteInDoubleQuotes = \\"\` \` \`\\"
local backQuoteInTemplateString = \\"\` \` \`\\"
local escapedCharsInQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
local escapedCharsInDoubleQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
local escapedCharsInTemplateString = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
local nonEmptyTemplateString = \\"Level 0: \\\\n\\\\t \\" .. \\"Level 1: \\\\n\\\\t\\\\t \\" .. \\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\" .. \\" \\\\n --\\" .. \\" \\\\n --\\" .. \\" \\\\n --\\""
"quoteInDoubleQuotes = \\"' ' '\\"
quoteInTemplateString = \\"' ' '\\"
doubleQuoteInQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\"
doubleQuoteInDoubleQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\"
doubleQuoteInTemplateString = \\"\\\\\\" \\\\\\" \\\\\\"\\"
backQuoteInQuotes = \\"\` \` \`\\"
backQuoteInDoubleQuotes = \\"\` \` \`\\"
backQuoteInTemplateString = \\"\` \` \`\\"
escapedCharsInQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
escapedCharsInDoubleQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
escapedCharsInTemplateString = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\"
nonEmptyTemplateString = \\"Level 0: \\\\n\\\\t \\" .. \\"Level 1: \\\\n\\\\t\\\\t \\" .. \\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\" .. \\" \\\\n --\\" .. \\" \\\\n --\\" .. \\" \\\\n --\\""
`;

exports[`Transformation (classExtension1) 1`] = `
Expand Down Expand Up @@ -244,7 +253,7 @@ ____exports.foo = \\"bar\\"
return ____exports"
`;

exports[`Transformation (modulesVariableNoExport) 1`] = `"local foo = \\"bar\\""`;
exports[`Transformation (modulesVariableNoExport) 1`] = `"foo = \\"bar\\""`;

exports[`Transformation (namespacePhantom) 1`] = `
"function nsMember(self)
Expand All @@ -257,6 +266,19 @@ exports[`Transformation (returnDefault) 1`] = `
end"
`;

exports[`Transformation (topLevelVariables) 1`] = `
"obj = {value1 = 1, value2 = 2}
value1 = obj.value1
value2 = obj.value2
obj2 = {value3 = 1, value4 = 2}
value3 = obj2.value3
value4 = obj2.value4
function fun1(self)
end
fun2 = function()
end"
`;

exports[`Transformation (unusedDefaultWithNamespaceImport) 1`] = `
"local ____exports = {}
local x = require(\\"module\\")
Expand Down
5 changes: 5 additions & 0 deletions test/translation/transformation/blockScopeVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
const a = 1;
const [b] = [1];
const { c } = { c: 1 };
}
11 changes: 11 additions & 0 deletions test/translation/transformation/topLevelVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const obj = { value1: 1, value2: 2 };
const value1 = obj.value1;
const { value2 } = obj;

let noValueLet;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be better to transform it to a nil assignment instead of just dropping it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should decide and do this in another PR

let obj2 = { value3: 1, value4: 2 };
let value3 = obj2.value3;
let { value4 } = obj2;

function fun1(): void {}
const fun2 = () => {};
20 changes: 14 additions & 6 deletions test/unit/assignments.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import * as util from "../util";

test("const declaration", () => {
const lua = util.transpileString(`const foo = true;`);
expect(lua).toBe(`local foo = true`);
test.each(["const", "let"])("%s declaration not top-level is not global", declarationKind => {
util.testModule`
{
${declarationKind} foo = true;
}
// @ts-ignore
return (globalThis as any).foo;
`.expectToEqual(undefined);
});

test("let declaration", () => {
const lua = util.transpileString(`let foo = true;`);
expect(lua).toBe(`local foo = true`);
test.each(["const", "let"])("%s declaration top-level is global", declarationKind => {
util.testModule`
${declarationKind} foo = true;
// @ts-ignore
return (globalThis as any).foo;
`.expectToEqual(true);
});

test("var declaration is disallowed", () => {
Expand Down
67 changes: 33 additions & 34 deletions test/unit/objectLiteral.spec.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,65 @@
import * as util from "../util";

test.each([
{ inp: `{a:3,b:"4"}`, out: '{a = 3, b = "4"}' },
{ inp: `{"a":3,b:"4"}`, out: '{a = 3, b = "4"}' },
{ inp: `{["a"]:3,b:"4"}`, out: '{a = 3, b = "4"}' },
{ inp: `{["a"+123]:3,b:"4"}`, out: '{["a" .. 123] = 3, b = "4"}' },
{ inp: `{[myFunc()]:3,b:"4"}`, out: '{\n [myFunc(_G)] = 3,\n b = "4"\n}' },
{ inp: `{x}`, out: `{x = x}` },
])("Object Literal (%p)", ({ inp, out }) => {
const lua = util.transpileString(`const myvar = ${inp};`);
expect(lua).toBe(`local myvar = ${out}`);
test.each([`{ a: 3, b: "4" }`, `{ "a": 3, b: "4" }`, `{ ["a"]: 3, b: "4" }`, `{ ["a" + 123]: 3, b: "4" }`])(
"Object Literal (%p)",
inp => {
util.testExpression(inp).expectToMatchJsResult();
}
);

test("object literal with function call to get key", () => {
util.testFunction`
const myFunc = () => "a";
return { [myFunc() + "b"]: 3 };
`.expectToMatchJsResult();
});

test("object literal with shorthand property", () => {
util.testFunction`
const x = 5;
return { x };
`.expectToMatchJsResult();
});

describe("property shorthand", () => {
test("should support property shorthand", () => {
const result = util.transpileAndExecute(`
util.testFunction`
const x = 1;
const o = { x };
return o.x;
`);

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

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

expect(result).toBe(identifier);
util.testExpression`({ ${identifier} }).${identifier}`.expectToMatchJsResult();
});

test("should support _G shorthand", () => {
const result = util.transpileAndExecute(
`return ({ _G })._G.foobar;`,
undefined,
`foobar = "foobar"`,
"declare const _G: any;"
);

expect(result).toBe("foobar");
util.testExpression`({ _G })._G.foobar`
.setTsHeader(`declare const _G: any;`)
.setLuaHeader(`foobar = "foobar"`)
.expectToEqual("foobar");
});

test("should support export property shorthand", () => {
const code = `
util.testModule`
export const x = 1;
const o = { x };
export const y = o.x;
`;
expect(util.transpileExecuteAndReturnExport(code, "y")).toBe(1);
`.expectToMatchJsResult();
});
});

test("undefined as object key", () => {
const code = `const foo = {undefined: "foo"};
return foo.undefined;`;
expect(util.transpileAndExecute(code)).toBe("foo");
util.testFunction`
const foo = {undefined: "foo"};
return foo.undefined;
`.expectToMatchJsResult();
});

test.each([`({x: "foobar"}.x)`, `({x: "foobar"}["x"])`, `({x: () => "foobar"}.x())`, `({x: () => "foobar"}["x"]())`])(
"object literal property access (%p)",
expression => {
const code = `return ${expression}`;
const expectResult = eval(expression);
expect(util.transpileAndExecute(code)).toBe(expectResult);
util.testExpression(expression).expectToMatchJsResult();
}
);