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
56 changes: 44 additions & 12 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface Scope {
referencedSymbols?: Set<tstl.SymbolId>;
variableDeclarations?: tstl.VariableDeclarationStatement[];
functionDefinitions?: Map<tstl.SymbolId, FunctionDefinitionInfo>;
importStatements?: tstl.Statement[];
loopContinued?: boolean;
}

Expand Down Expand Up @@ -273,9 +274,9 @@ export class LuaTransformer {
statement.moduleSpecifier
);

const importResult = this.transformImportDeclaration(importDeclaration);

const result = Array.isArray(importResult) ? importResult : [importResult];
// Wrap in block to prevent imports from hoisting out of `do` statement
const block = ts.createBlock([importDeclaration]);
const result = this.transformBlock(block).statements;

// Now the module is imported, add the imports to the export table
for (const exportVariable of statement.exportClause.elements) {
Expand Down Expand Up @@ -327,13 +328,23 @@ export class LuaTransformer {

const result: tstl.Statement[] = [];

const scope = this.peekScope();
if (!this.options.noHoisting && !scope.importStatements) {
scope.importStatements = [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm a little confused why this is not a strict violation, since peeking a scope could be undefined?

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.

That's because of microsoft/TypeScript#13778, but it's possible to change peekScope return type to Scope | undefined.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aha, we probably should make that change. Doesn't have to be in this PR though.

}

const moduleSpecifier = statement.moduleSpecifier as ts.StringLiteral;
const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), "");

if (!statement.importClause) {
const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
result.push(tstl.createExpressionStatement(requireCall));
return result;
if (scope.importStatements) {
scope.importStatements.push(...result);
return undefined;
} else {
return result;
}
}

const imports = statement.importClause.namedBindings;
Expand Down Expand Up @@ -369,30 +380,41 @@ export class LuaTransformer {
if (importSpecifier.propertyName) {
const propertyIdentifier = this.transformIdentifier(importSpecifier.propertyName);
const propertyName = tstl.createStringLiteral(propertyIdentifier.text);
const renamedImport = this.createHoistableVariableDeclarationStatement(
importSpecifier.name,
const renamedImport = tstl.createVariableDeclarationStatement(
this.transformIdentifier(importSpecifier.name),
tstl.createTableIndexExpression(importUniqueName, propertyName),
importSpecifier);
result.push(renamedImport);
} else {
const name = tstl.createStringLiteral(importSpecifier.name.text);
const namedImport = this.createHoistableVariableDeclarationStatement(
importSpecifier.name,
const namedImport = tstl.createVariableDeclarationStatement(
this.transformIdentifier(importSpecifier.name),
tstl.createTableIndexExpression(importUniqueName, name),
importSpecifier
);
result.push(namedImport);
}
});
return result;
if (scope.importStatements) {
scope.importStatements.push(...result);
return undefined;
} else {
return result;
}

} else if (ts.isNamespaceImport(imports)) {
const requireStatement = this.createHoistableVariableDeclarationStatement(
imports.name,
const requireStatement = tstl.createVariableDeclarationStatement(
this.transformIdentifier(imports.name),
requireCall,
statement
);
result.push(requireStatement);
return result;
if (scope.importStatements) {
scope.importStatements.push(...result);
return undefined;
} else {
return result;
}
}
}

Expand Down Expand Up @@ -4882,6 +4904,14 @@ export class LuaTransformer {
}
}

protected hoistImportStatements(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] {
if (!scope.importStatements) {
return statements;
}

return [...scope.importStatements, ...statements];
}

protected hoistFunctionDefinitions(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] {
if (!scope.functionDefinitions) {
return statements;
Expand Down Expand Up @@ -4952,6 +4982,8 @@ export class LuaTransformer {

result = this.hoistVariableDeclarations(scope, result);

result = this.hoistImportStatements(scope, result);

return result;
}

Expand Down
52 changes: 52 additions & 0 deletions test/unit/hoisting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,55 @@ test.each([
TSTLErrors.ReferencedBeforeDeclaration(ts.createIdentifier(identifier)),
);
});

test("Import hoisting (named)", () => {
const importCode = `
const bar = foo;
import {foo} from "myMod";`;
const luaHeader = `
package.loaded["myMod"] = {foo = "foobar"}
${util.transpileString(importCode)}`;
const tsHeader = "declare const bar: any;";
const code = "return bar;";
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
});

test("Import hoisting (namespace)", () => {
const importCode = `
const bar = myMod.foo;
import * as myMod from "myMod";`;
const luaHeader = `
package.loaded["myMod"] = {foo = "foobar"}
${util.transpileString(importCode)}`;
const tsHeader = "declare const bar: any;";
const code = "return bar;";
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
});

test("Import hoisting (side-effect)", () => {
const importCode = `
const bar = foo;
import "myMod";`;
const luaHeader = `
package.loaded["myMod"] = {_ = (function() foo = "foobar" end)()}
${util.transpileString(importCode)}`;
const tsHeader = "declare const bar: any;";
const code = "return bar;";
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
});

test("Import hoisted before function", () => {
const importCode = `
let bar: any;
import {foo} from "myMod";
baz();
function baz() {
bar = foo;
}`;
const luaHeader = `
package.loaded["myMod"] = {foo = "foobar"}
${util.transpileString(importCode)}`;
const tsHeader = "declare const bar: any;";
const code = "return bar;";
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
});