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
106 changes: 67 additions & 39 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ interface Scope {
loopContinued?: boolean;
}

export interface EmitResolver {
isValueAliasDeclaration(node: ts.Node): boolean;
isReferencedAliasDeclaration(node: ts.Node, checkChildren?: boolean): boolean;
moduleExportsSomeValue(moduleReferenceExpression: ts.Expression): boolean;
}

export interface DiagnosticsProducingTypeChecker extends ts.TypeChecker {
getEmitResolver(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): EmitResolver;
}

export class LuaTransformer {
public luaKeywords: Set<string> = new Set([
"_G", "and", "assert", "break", "coroutine", "debug", "do", "else", "elseif", "end", "error", "false", "for",
Expand All @@ -49,11 +59,13 @@ export class LuaTransformer {
private isStrict: boolean;
private luaTarget: LuaTarget;

private checker: ts.TypeChecker;
private checker: DiagnosticsProducingTypeChecker;
protected options: CompilerOptions;

private isModule = false;
// Resolver is lazy-initialized in transformSourceFile to avoid type-checking all files
private resolver!: EmitResolver;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It might be better to do this as an optional ? so it has to be verified before used, since it's set in a public api function (transformSourceFile) which could be overridden.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's true, but I think it complicates it's usage quite a lot with all these undefined checks. If it's not defined it would fail immediately anyway, just with a bit less clear error message.
Maybe we could have some different (internal?) method that would initialize state and then call transformSourceFile as a public visitor?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking at this more, setupState couldn't be called if someone overrode transformSourceFile without calling the super version. So, I think you're suggestion here is probably necessary. Transformation should probably be kicked off by a non-override-able function.


private isModule = false;
private currentSourceFile?: ts.SourceFile;

private currentNamespace: ts.ModuleDeclaration | undefined;
Expand All @@ -72,7 +84,7 @@ export class LuaTransformer {
private readonly typeValidationCache: Map<ts.Type, Set<ts.Type>> = new Map<ts.Type, Set<ts.Type>>();

public constructor(protected program: ts.Program) {
this.checker = program.getTypeChecker();
this.checker = (program as any).getDiagnosticsProducingTypeChecker();
this.options = program.getCompilerOptions();
this.isStrict = this.options.alwaysStrict !== undefined
|| (this.options.strict !== undefined && this.options.alwaysStrict !== false)
Expand Down Expand Up @@ -102,6 +114,7 @@ export class LuaTransformer {
this.setupState();

this.currentSourceFile = node;
this.resolver = this.checker.getEmitResolver(node);

let statements: tstl.Statement[] = [];
if (node.flags & ts.NodeFlags.JsonFile) {
Expand Down Expand Up @@ -233,36 +246,6 @@ export class LuaTransformer {
}

public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult {
if (statement.moduleSpecifier === undefined) {
if (statement.exportClause === undefined) {
throw TSTLErrors.InvalidExportDeclaration(statement);
}

const result = [];
for (const exportElement of statement.exportClause.elements) {
let exportedIdentifier: tstl.Expression | undefined;
if (exportElement.propertyName !== undefined) {
exportedIdentifier = this.transformIdentifier(exportElement.propertyName);

} else {
const exportedSymbol = this.checker.getExportSpecifierLocalTargetSymbol(exportElement);
if (exportedSymbol !== undefined) {
exportedIdentifier = this.createIdentifierFromSymbol(exportedSymbol, exportElement.name);
} else {
exportedIdentifier = this.transformIdentifier(exportElement.name);
}
}

result.push(
tstl.createAssignmentStatement(
this.createExportedIdentifier(this.transformIdentifier(exportElement.name)),
exportedIdentifier
)
);
}
return result;
}

if (statement.exportClause) {
if (statement.exportClause.elements.some(e =>
(e.name !== undefined && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)
Expand All @@ -272,11 +255,40 @@ export class LuaTransformer {
throw TSTLErrors.UnsupportedDefaultExport(statement);
}

if (!this.resolver.isValueAliasDeclaration(statement)) {
return undefined;
}

const exportSpecifiers = statement.exportClause.elements.filter(e =>
this.resolver.isValueAliasDeclaration(e)
);

if (statement.moduleSpecifier === undefined) {
return exportSpecifiers.map(specifier => {
let exportedIdentifier: tstl.Expression | undefined;
if (specifier.propertyName !== undefined) {
exportedIdentifier = this.transformIdentifier(specifier.propertyName);
} else {
const exportedSymbol = this.checker.getExportSpecifierLocalTargetSymbol(specifier);
if (exportedSymbol !== undefined) {
exportedIdentifier = this.createIdentifierFromSymbol(exportedSymbol, specifier.name);
} else {
exportedIdentifier = this.transformIdentifier(specifier.name);
}
}

return tstl.createAssignmentStatement(
this.createExportedIdentifier(this.transformIdentifier(specifier.name)),
exportedIdentifier
);
});
}

// First transpile as import clause
const importClause = ts.createImportClause(
undefined,
ts.createNamedImports(statement.exportClause.elements
.map(e => ts.createImportSpecifier(e.propertyName, e.name))
ts.createNamedImports(
exportSpecifiers.map(s => ts.createImportSpecifier(s.propertyName, s.name))
)
);

Expand All @@ -292,18 +304,26 @@ export class LuaTransformer {
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) {
for (const specifier of exportSpecifiers) {
result.push(
tstl.createAssignmentStatement(
this.createExportedIdentifier(this.transformIdentifier(exportVariable.name)),
this.transformIdentifier(exportVariable.name)
this.createExportedIdentifier(this.transformIdentifier(specifier.name)),
this.transformIdentifier(specifier.name)
)
);
}

// Wrap this in a DoStatement to prevent polluting the scope.
return tstl.createDoStatement(this.filterUndefined(result), statement);
} else {
if (statement.moduleSpecifier === undefined) {
throw TSTLErrors.InvalidExportDeclaration(statement);
}

if (!this.resolver.moduleExportsSomeValue(statement.moduleSpecifier)) {
return undefined;
}

const moduleRequire = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
const tempModuleIdentifier = tstl.createIdentifier("__TSTL_export");

Expand Down Expand Up @@ -375,7 +395,11 @@ export class LuaTransformer {
if (ts.isNamedImports(imports)) {
const filteredElements = imports.elements.filter(e => {
const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(e), this.checker);
return !decorators.has(DecoratorKind.Extension) && !decorators.has(DecoratorKind.MetaExtension);
return (
this.resolver.isReferencedAliasDeclaration(e)
&& !decorators.has(DecoratorKind.Extension)
&& !decorators.has(DecoratorKind.MetaExtension)
);
});

// Elide import if all imported types are extension classes
Expand Down Expand Up @@ -418,6 +442,10 @@ export class LuaTransformer {
}

} else if (ts.isNamespaceImport(imports)) {
if (!this.resolver.isReferencedAliasDeclaration(imports)) {
return undefined;
}

const requireStatement = tstl.createVariableDeclarationStatement(
this.transformIdentifier(imports.name),
requireCall,
Expand Down
41 changes: 28 additions & 13 deletions test/translation/__snapshots__/transformation.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -383,42 +383,57 @@ exports[`Transformation (modulesFunctionNoExport) 1`] = `
end"
`;

exports[`Transformation (modulesImportAll) 1`] = `"local Test = require(\\"test\\")"`;
exports[`Transformation (modulesImportAll) 1`] = `
"local Test = require(\\"test\\")
local ____ = Test"
`;

exports[`Transformation (modulesImportNamed) 1`] = `
"local __TSTL_test = require(\\"test\\")
local TestClass = __TSTL_test.TestClass"
local TestClass = __TSTL_test.TestClass
local ____ = TestClass"
`;

exports[`Transformation (modulesImportNamedSpecialChars) 1`] = `
"local __TSTL_kebab_2Dmodule = require(\\"kebab-module\\")
local TestClass = __TSTL_kebab_2Dmodule.TestClass
local TestClass1 = __TSTL_kebab_2Dmodule.TestClass1
local __TSTL_dollar_24module = require(\\"dollar$module\\")
local TestClass = __TSTL_dollar_24module.TestClass
local TestClass2 = __TSTL_dollar_24module.TestClass2
local __TSTL_singlequote_27module = require(\\"singlequote'module\\")
local TestClass = __TSTL_singlequote_27module.TestClass
local TestClass3 = __TSTL_singlequote_27module.TestClass3
local __TSTL_hash_23module = require(\\"hash#module\\")
local TestClass = __TSTL_hash_23module.TestClass
local TestClass4 = __TSTL_hash_23module.TestClass4
local __TSTL_space_20module = require(\\"space module\\")
local TestClass = __TSTL_space_20module.TestClass"
local TestClass5 = __TSTL_space_20module.TestClass5
local ____ = TestClass1
local ____ = TestClass2
local ____ = TestClass3
local ____ = TestClass4
local ____ = TestClass5"
`;

exports[`Transformation (modulesImportRenamed) 1`] = `
"local __TSTL_test = require(\\"test\\")
local RenamedClass = __TSTL_test.TestClass"
local RenamedClass = __TSTL_test.TestClass
local ____ = RenamedClass"
`;

exports[`Transformation (modulesImportRenamedSpecialChars) 1`] = `
"local __TSTL_kebab_2Dmodule = require(\\"kebab-module\\")
local RenamedClass = __TSTL_kebab_2Dmodule.TestClass
local RenamedClass1 = __TSTL_kebab_2Dmodule.TestClass
local __TSTL_dollar_24module = require(\\"dollar$module\\")
local RenamedClass = __TSTL_dollar_24module.TestClass
local RenamedClass2 = __TSTL_dollar_24module.TestClass
local __TSTL_singlequote_27module = require(\\"singlequote'module\\")
local RenamedClass = __TSTL_singlequote_27module.TestClass
local RenamedClass3 = __TSTL_singlequote_27module.TestClass
local __TSTL_hash_23module = require(\\"hash#module\\")
local RenamedClass = __TSTL_hash_23module.TestClass
local RenamedClass4 = __TSTL_hash_23module.TestClass
local __TSTL_space_20module = require(\\"space module\\")
local RenamedClass = __TSTL_space_20module.TestClass"
local RenamedClass5 = __TSTL_space_20module.TestClass
local ____ = RenamedClass1
local ____ = RenamedClass2
local ____ = RenamedClass3
local ____ = RenamedClass4
local ____ = RenamedClass5"
`;

exports[`Transformation (modulesImportWithoutFromClause) 1`] = `"require(\\"test\\")"`;
Expand Down
2 changes: 2 additions & 0 deletions test/translation/transformation/modulesImportAll.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import * as Test from "test";

Test;
2 changes: 2 additions & 0 deletions test/translation/transformation/modulesImportNamed.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import { TestClass } from "test";

TestClass;
16 changes: 11 additions & 5 deletions test/translation/transformation/modulesImportNamedSpecialChars.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { TestClass } from "kebab-module";
import { TestClass } from "dollar$module";
import { TestClass } from "singlequote'module";
import { TestClass } from "hash#module";
import { TestClass } from "space module";
import { TestClass1 } from "kebab-module";
import { TestClass2 } from "dollar$module";
import { TestClass3 } from "singlequote'module";
import { TestClass4 } from "hash#module";
import { TestClass5 } from "space module";

TestClass1;
TestClass2;
TestClass3;
TestClass4;
TestClass5;
2 changes: 2 additions & 0 deletions test/translation/transformation/modulesImportRenamed.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import { TestClass as RenamedClass } from "test";

RenamedClass;
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { TestClass as RenamedClass } from "kebab-module";
import { TestClass as RenamedClass } from "dollar$module";
import { TestClass as RenamedClass } from "singlequote'module";
import { TestClass as RenamedClass } from "hash#module";
import { TestClass as RenamedClass } from "space module";
import { TestClass as RenamedClass1 } from "kebab-module";
import { TestClass as RenamedClass2 } from "dollar$module";
import { TestClass as RenamedClass3 } from "singlequote'module";
import { TestClass as RenamedClass4 } from "hash#module";
import { TestClass as RenamedClass5 } from "space module";

RenamedClass1;
RenamedClass2;
RenamedClass3;
RenamedClass4;
RenamedClass5;
8 changes: 5 additions & 3 deletions test/unit/identifiers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,12 +388,14 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => {
package.loaded.someModule = {type = "foobar"}`;

const code = `
import {${importName}} from "someModule";
return typeof 7 + "|" + type;`;
import {${importName}} from "someModule";
export const result = typeof 7 + "|" + type;
`;

const lua = util.transpileString(code);
const result = util.executeLua(`${luaHeader} return (function() ${lua} end)().result`);

expect(util.executeLua(`${luaHeader} ${lua}`)).toBe("number|foobar");
expect(result).toBe("number|foobar");
});

test.each([
Expand Down
27 changes: 0 additions & 27 deletions test/unit/importexport.spec.ts

This file was deleted.

Loading