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
96 changes: 94 additions & 2 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export class LuaTransformer {
case ts.SyntaxKind.Block:
return this.transformBlockAsDoStatement(node as ts.Block);
// Declaration Statements
case ts.SyntaxKind.ExportDeclaration:
return this.transformExportDeclaration(node as ts.ExportDeclaration);
case ts.SyntaxKind.ImportDeclaration:
return this.transformImportDeclaration(node as ts.ImportDeclaration);
case ts.SyntaxKind.ClassDeclaration:
Expand Down Expand Up @@ -219,6 +221,90 @@ export class LuaTransformer {
return tstl.createDoStatement(statements, block);
}

public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult {
if (statement.moduleSpecifier === undefined) {
const result = [];
for (const exportElement of statement.exportClause.elements) {
result.push(
tstl.createAssignmentStatement(
this.createExportedIdentifier(this.transformIdentifier(exportElement.name)),
this.transformIdentifier(exportElement.propertyName || exportElement.name)
)
);
}
return result;
}

if (statement.exportClause) {
if (statement.exportClause.elements.some(e =>
(e.name && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)
|| (e.propertyName && e.propertyName.originalKeywordKind === ts.SyntaxKind.DefaultKeyword))
) {
throw TSTLErrors.UnsupportedDefaultExport(statement);
}

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

const importDeclaration = ts.createImportDeclaration(
statement.decorators,
statement.modifiers,
importClause,
statement.moduleSpecifier
);

const importResult = this.transformImportDeclaration(importDeclaration);

const result = Array.isArray(importResult) ? importResult : [importResult];

// Now the module is imported, add the imports to the export table
for (const exportVariable of statement.exportClause.elements) {
result.push(
tstl.createAssignmentStatement(
this.createExportedIdentifier(this.transformIdentifier(exportVariable.name)),
this.transformIdentifier(exportVariable.name)
)
);
}

// Wrap this in a DoStatement to prevent polluting the scope.
return tstl.createDoStatement(result, statement);
} else {
const moduleRequire = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);
const tempModuleIdentifier = tstl.createIdentifier("__TSTL_export");

const declaration = tstl.createVariableDeclarationStatement(tempModuleIdentifier, moduleRequire);

const forKey = tstl.createIdentifier("____exportKey");
const forValue = tstl.createIdentifier("____exportValue");

const body = tstl.createBlock(
[tstl.createAssignmentStatement(
tstl.createTableIndexExpression(
tstl.createIdentifier("exports"),
forKey
),
forValue
)]
);

const pairsIdentifier = tstl.createIdentifier("pairs");
const forIn = tstl.createForInStatement(
body,
[tstl.cloneIdentifier(forKey), tstl.cloneIdentifier(forValue)],
[tstl.createCallExpression(pairsIdentifier, [tstl.cloneIdentifier(tempModuleIdentifier)])]
);

// Wrap this in a DoStatement to prevent polluting the scope.
return tstl.createDoStatement([declaration, forIn], statement);
}
}

public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult {
if (statement.importClause && !statement.importClause.namedBindings) {
throw TSTLErrors.DefaultImportsNotSupported(statement);
Expand All @@ -228,9 +314,8 @@ export class LuaTransformer {

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

const requireCall = tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]);
const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral);

if (!statement.importClause) {
result.push(tstl.createExpressionStatement(requireCall));
Expand Down Expand Up @@ -291,6 +376,13 @@ export class LuaTransformer {
}
}

private createModuleRequire(moduleSpecifier: ts.StringLiteral): tstl.CallExpression {
const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), "");
const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath));

return tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]);
}

public transformClassDeclaration(
statement: ts.ClassLikeDeclaration,
nameOverride?: tstl.Identifier
Expand Down
46 changes: 20 additions & 26 deletions src/TSTLErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,23 @@ import {TranspileError} from "./TranspileError";
import {TSHelper as tsHelper} from "./TSHelper";

export class TSTLErrors {
public static CouldNotFindEnumMember =
(enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) => new TranspileError(
`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node
);
public static CouldNotFindEnumMember = (enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) =>
new TranspileError(`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node);

public static DefaultImportsNotSupported = (node: ts.Node) =>
new TranspileError(`Default Imports are not supported, please use named imports instead!`, node);

public static ForbiddenEllipsisDestruction =
(node: ts.Node) => new TranspileError(`Ellipsis destruction is not allowed.`, node);
public static ForbiddenEllipsisDestruction = (node: ts.Node) =>
new TranspileError(`Ellipsis destruction is not allowed.`, node);

public static ForbiddenForIn =
(node: ts.Node) => new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node);
public static ForbiddenForIn = (node: ts.Node) =>
new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node);

public static HeterogeneousEnum = (node: ts.Node) => new TranspileError(
`Invalid heterogeneous enum. Enums should either specify no member values, ` +
`or specify values (of the same type) for all members.`,
node);

public static InvalidEnumMember =
(node: ts.Node) => new TranspileError(`Only numeric or string initializers allowed for enums.`, node);

public static InvalidDecoratorArgumentNumber = (name: string, got: number, expected: number, node: ts.Node) =>
new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node);

Expand All @@ -44,6 +39,9 @@ export class TSTLErrors {
public static InvalidInstanceOfExtension = (node: ts.Node) =>
new TranspileError(`Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'.`, node);

public static InvalidJsonFileContent = (node: ts.Node) =>
new TranspileError("Invalid JSON file content", node);

public static InvalidPropertyCall = (node: ts.Node) =>
new TranspileError(`Tried to transpile a non-property call as property call.`, node);

Expand All @@ -56,32 +54,30 @@ export class TSTLErrors {
public static KeywordIdentifier = (node: ts.Identifier) =>
new TranspileError(`Cannot use Lua keyword ${node.escapedText} as identifier.`, node);

public static MissingClassName =
(node: ts.Node) => new TranspileError(`Class declarations must have a name.`, node);
public static MissingClassName = (node: ts.Node) =>
new TranspileError(`Class declarations must have a name.`, node);

public static MissingMetaExtension = (node: ts.Node) =>
new TranspileError(`!MetaExtension requires the extension of the metatable class.`, node);
new TranspileError(`@metaExtension requires the extension of the metatable class.`, node);

public static UnsupportedDefaultExport = (node: ts.Node) =>
new TranspileError(`Default exports are not supported.`, node);

public static UnsupportedImportType = (node: ts.Node) => new TranspileError(`Unsupported import type.`, node);
public static UnsupportedImportType = (node: ts.Node) =>
new TranspileError(`Unsupported import type.`, node);

public static UnsupportedKind =
(description: string, kind: ts.SyntaxKind, node: ts.Node) => {
public static UnsupportedKind = (description: string, kind: ts.SyntaxKind, node: ts.Node) =>
{
const kindName = tsHelper.enumName(kind, ts.SyntaxKind);
return new TranspileError(`Unsupported ${description} kind: ${kindName}`, node);
}
};

public static UnsupportedProperty = (parentName: string, property: string, node: ts.Node) =>
new TranspileError(`Unsupported property on ${parentName}: ${property}`, node);

public static UnsupportedForTarget = (functionality: string, version: string, node: ts.Node) =>
new TranspileError(`${functionality} is/are not supported for target Lua ${version}.`, node);

public static UnsupportedObjectLiteralElement = (elementKind: ts.SyntaxKind, node: ts.Node) =>
new TranspileError(`Unsupported object literal element: ${elementKind}.`, node);

public static UnsupportedUnionAccessor = (node: ts.Node) =>
new TranspileError(`Unsupported mixed union of accessor and non-accessor types for the same property.`, node);

public static UnsupportedFunctionConversion = (node: ts.Node, name?: string) => {
if (name) {
return new TranspileError(
Expand Down Expand Up @@ -141,6 +137,4 @@ export class TSTLErrors {
node
);
}

public static InvalidJsonFileContent = (node: ts.Node) => new TranspileError("Invalid JSON file content", node);
}
23 changes: 23 additions & 0 deletions test/translation/lua/exportStatement.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
local exports = exports or {};
local xyz = 4;
exports.xyz = xyz;
exports.uwv = xyz;
do
local __TSTL_export = require("xyz");
for ____exportKey, ____exportValue in pairs(__TSTL_export) do
exports[____exportKey] = ____exportValue;
end
end
do
local __TSTL_xyz = require("xyz");
local abc = __TSTL_xyz.abc;
local def = __TSTL_xyz.def;
exports.abc = abc;
exports.def = def;
end
do
local __TSTL_xyz = require("xyz");
local def = __TSTL_xyz.abc;
exports.def = def;
end
return exports;
6 changes: 6 additions & 0 deletions test/translation/ts/exportStatement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const xyz = 4;
export {xyz};
export {xyz as uwv};
export * from "xyz";
export {abc, def} from "xyz";
export {abc as def} from "xyz";
5 changes: 3 additions & 2 deletions test/unit/decoratorMetaExtension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Expect, Test } from "alsatian";
import * as util from "../src/util";

import { TranspileError } from "../../src/TranspileError";
import { TSTLErrors } from "../../src/TSTLErrors";

export class DecoratorMetaExtension {

Expand Down Expand Up @@ -29,6 +30,7 @@ export class DecoratorMetaExtension {

@Test("IncorrectUsage")
public incorrectUsage(): void {
const expectedMessage = TSTLErrors.MissingMetaExtension(undefined).message;
Expect(() => {
util.transpileString(
`
Expand All @@ -40,8 +42,7 @@ export class DecoratorMetaExtension {
}
`
);
}).toThrowError(TranspileError,
"!MetaExtension requires the extension of the metatable class.");
}).toThrowError(TranspileError, expectedMessage);
}

@Test("DontAllowInstantiation")
Expand Down
17 changes: 17 additions & 0 deletions test/unit/importexport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Expect, Test, TestCase } from "alsatian";

import * as util from "../src/util";
import { TSTLErrors } from "../../src/TSTLErrors";
import { TranspileError } from "../../src/TranspileError";

export class ImportExportTests
{
@TestCase("export { default } from '...'")
@TestCase("export { x as default } from '...';")
@TestCase("export { default as x } from '...';")
@Test("Export default keyword disallowed")
public exportDefaultKeywordError(exportStatement: string): void {
const expectedTest = TSTLErrors.UnsupportedDefaultExport(undefined).message;
Expect(() => util.transpileString(exportStatement)).toThrowError(TranspileError, expectedTest);
}
}