Skip to content

Commit 043b87d

Browse files
authored
Import hoisting (#526)
* Hoisting all imports * Fixed hoisting order * Preventing "export...from" imports from being hoisted out of block * Variable rename
1 parent e9a7b0f commit 043b87d

File tree

2 files changed

+96
-12
lines changed

2 files changed

+96
-12
lines changed

src/LuaTransformer.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface Scope {
3434
referencedSymbols?: Set<tstl.SymbolId>;
3535
variableDeclarations?: tstl.VariableDeclarationStatement[];
3636
functionDefinitions?: Map<tstl.SymbolId, FunctionDefinitionInfo>;
37+
importStatements?: tstl.Statement[];
3738
loopContinued?: boolean;
3839
}
3940

@@ -273,9 +274,9 @@ export class LuaTransformer {
273274
statement.moduleSpecifier
274275
);
275276

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

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

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

331+
const scope = this.peekScope();
332+
if (!this.options.noHoisting && !scope.importStatements) {
333+
scope.importStatements = [];
334+
}
335+
330336
const moduleSpecifier = statement.moduleSpecifier as ts.StringLiteral;
331337
const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), "");
332338

333339
if (!statement.importClause) {
334340
const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
335341
result.push(tstl.createExpressionStatement(requireCall));
336-
return result;
342+
if (scope.importStatements) {
343+
scope.importStatements.push(...result);
344+
return undefined;
345+
} else {
346+
return result;
347+
}
337348
}
338349

339350
const imports = statement.importClause.namedBindings;
@@ -369,30 +380,41 @@ export class LuaTransformer {
369380
if (importSpecifier.propertyName) {
370381
const propertyIdentifier = this.transformIdentifier(importSpecifier.propertyName);
371382
const propertyName = tstl.createStringLiteral(propertyIdentifier.text);
372-
const renamedImport = this.createHoistableVariableDeclarationStatement(
373-
importSpecifier.name,
383+
const renamedImport = tstl.createVariableDeclarationStatement(
384+
this.transformIdentifier(importSpecifier.name),
374385
tstl.createTableIndexExpression(importUniqueName, propertyName),
375386
importSpecifier);
376387
result.push(renamedImport);
377388
} else {
378389
const name = tstl.createStringLiteral(importSpecifier.name.text);
379-
const namedImport = this.createHoistableVariableDeclarationStatement(
380-
importSpecifier.name,
390+
const namedImport = tstl.createVariableDeclarationStatement(
391+
this.transformIdentifier(importSpecifier.name),
381392
tstl.createTableIndexExpression(importUniqueName, name),
382393
importSpecifier
383394
);
384395
result.push(namedImport);
385396
}
386397
});
387-
return result;
398+
if (scope.importStatements) {
399+
scope.importStatements.push(...result);
400+
return undefined;
401+
} else {
402+
return result;
403+
}
404+
388405
} else if (ts.isNamespaceImport(imports)) {
389-
const requireStatement = this.createHoistableVariableDeclarationStatement(
390-
imports.name,
406+
const requireStatement = tstl.createVariableDeclarationStatement(
407+
this.transformIdentifier(imports.name),
391408
requireCall,
392409
statement
393410
);
394411
result.push(requireStatement);
395-
return result;
412+
if (scope.importStatements) {
413+
scope.importStatements.push(...result);
414+
return undefined;
415+
} else {
416+
return result;
417+
}
396418
}
397419
}
398420

@@ -4882,6 +4904,14 @@ export class LuaTransformer {
48824904
}
48834905
}
48844906

4907+
protected hoistImportStatements(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] {
4908+
if (!scope.importStatements) {
4909+
return statements;
4910+
}
4911+
4912+
return [...scope.importStatements, ...statements];
4913+
}
4914+
48854915
protected hoistFunctionDefinitions(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] {
48864916
if (!scope.functionDefinitions) {
48874917
return statements;
@@ -4952,6 +4982,8 @@ export class LuaTransformer {
49524982

49534983
result = this.hoistVariableDeclarations(scope, result);
49544984

4985+
result = this.hoistImportStatements(scope, result);
4986+
49554987
return result;
49564988
}
49574989

test/unit/hoisting.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,55 @@ test.each([
243243
TSTLErrors.ReferencedBeforeDeclaration(ts.createIdentifier(identifier)),
244244
);
245245
});
246+
247+
test("Import hoisting (named)", () => {
248+
const importCode = `
249+
const bar = foo;
250+
import {foo} from "myMod";`;
251+
const luaHeader = `
252+
package.loaded["myMod"] = {foo = "foobar"}
253+
${util.transpileString(importCode)}`;
254+
const tsHeader = "declare const bar: any;";
255+
const code = "return bar;";
256+
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
257+
});
258+
259+
test("Import hoisting (namespace)", () => {
260+
const importCode = `
261+
const bar = myMod.foo;
262+
import * as myMod from "myMod";`;
263+
const luaHeader = `
264+
package.loaded["myMod"] = {foo = "foobar"}
265+
${util.transpileString(importCode)}`;
266+
const tsHeader = "declare const bar: any;";
267+
const code = "return bar;";
268+
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
269+
});
270+
271+
test("Import hoisting (side-effect)", () => {
272+
const importCode = `
273+
const bar = foo;
274+
import "myMod";`;
275+
const luaHeader = `
276+
package.loaded["myMod"] = {_ = (function() foo = "foobar" end)()}
277+
${util.transpileString(importCode)}`;
278+
const tsHeader = "declare const bar: any;";
279+
const code = "return bar;";
280+
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
281+
});
282+
283+
test("Import hoisted before function", () => {
284+
const importCode = `
285+
let bar: any;
286+
import {foo} from "myMod";
287+
baz();
288+
function baz() {
289+
bar = foo;
290+
}`;
291+
const luaHeader = `
292+
package.loaded["myMod"] = {foo = "foobar"}
293+
${util.transpileString(importCode)}`;
294+
const tsHeader = "declare const bar: any;";
295+
const code = "return bar;";
296+
expect(util.transpileAndExecute(code, undefined, luaHeader, tsHeader)).toBe("foobar");
297+
});

0 commit comments

Comments
 (0)