From 791eebfeba8b56244e76970e31e9f2b2126485a6 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sun, 17 Feb 2019 19:11:00 +0100 Subject: [PATCH 1/4] add support for export declaration --- src/LuaTransformer.ts | 36 ++++++++++++++++++++++++ test/translation/lua/exportStatement.lua | 9 ++++++ test/translation/ts/exportStatement.ts | 1 + 3 files changed, 46 insertions(+) create mode 100644 test/translation/lua/exportStatement.lua create mode 100644 test/translation/ts/exportStatement.ts diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 56ff0ae66..c4525e560 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -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: @@ -219,6 +221,40 @@ export class LuaTransformer { return tstl.createDoStatement(statements, block); } + public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult { + // 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.addExportToIdentifier(this.transformIdentifier(exportVariable.name)), + this.transformIdentifier(exportVariable.name) + ) + ); + } + + // Wrap this in a DoStatement to prevent polluting the scope. + return tstl.createDoStatement(result, statement); + } + public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult { if (statement.importClause && !statement.importClause.namedBindings) { throw TSTLErrors.DefaultImportsNotSupported(statement); diff --git a/test/translation/lua/exportStatement.lua b/test/translation/lua/exportStatement.lua new file mode 100644 index 000000000..f1d646494 --- /dev/null +++ b/test/translation/lua/exportStatement.lua @@ -0,0 +1,9 @@ +local exports = exports or {}; +do + local __TSTL_xyz = require("xyz"); + local abc = __TSTL_xyz.abc; + local def = __TSTL_xyz.def; + exports.abc = abc; + exports.def = def; +end +return exports; \ No newline at end of file diff --git a/test/translation/ts/exportStatement.ts b/test/translation/ts/exportStatement.ts new file mode 100644 index 000000000..128ea1568 --- /dev/null +++ b/test/translation/ts/exportStatement.ts @@ -0,0 +1 @@ +export {abc, def} from "xyz"; \ No newline at end of file From d997f6cfde989b74919388ba47cd67bcea6d1248 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sun, 17 Feb 2019 21:39:32 +0100 Subject: [PATCH 2/4] Added support for other variants of the the export declaration --- src/LuaTransformer.ts | 103 +++++++++++++++++------ test/translation/lua/exportStatement.lua | 14 +++ test/translation/ts/exportStatement.ts | 7 +- 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index cb85efe72..10f7f7f19 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -222,37 +222,80 @@ export class LuaTransformer { } public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult { - // First transpile as import clause - const importClause = ts.createImportClause( - undefined, - ts.createNamedImports(statement.exportClause.elements - .map(e => ts.createImportSpecifier(e.propertyName, e.name)) - ) - ); + 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; + } - const importDeclaration = ts.createImportDeclaration( - statement.decorators, - statement.modifiers, - importClause, - statement.moduleSpecifier - ); + if (statement.exportClause) { + // First transpile as import clause + const importClause = ts.createImportClause( + undefined, + ts.createNamedImports(statement.exportClause.elements + .map(e => ts.createImportSpecifier(e.propertyName, e.name)) + ) + ); - const importResult = this.transformImportDeclaration(importDeclaration); + const importDeclaration = ts.createImportDeclaration( + statement.decorators, + statement.modifiers, + importClause, + statement.moduleSpecifier + ); - const result = Array.isArray(importResult) ? importResult : [importResult]; + const importResult = this.transformImportDeclaration(importDeclaration); - // Now the module is imported, add the imports to the export table - for (const exportVariable of statement.exportClause.elements) { - result.push( - tstl.createAssignmentStatement( - this.addExportToIdentifier(this.transformIdentifier(exportVariable.name)), - this.transformIdentifier(exportVariable.name) - ) + 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 + )] ); - } - // Wrap this in a DoStatement to prevent polluting the scope. - return tstl.createDoStatement(result, statement); + 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 { @@ -264,9 +307,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)); @@ -327,6 +369,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 diff --git a/test/translation/lua/exportStatement.lua b/test/translation/lua/exportStatement.lua index f1d646494..2369b004b 100644 --- a/test/translation/lua/exportStatement.lua +++ b/test/translation/lua/exportStatement.lua @@ -1,4 +1,13 @@ 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; @@ -6,4 +15,9 @@ do exports.abc = abc; exports.def = def; end +do + local __TSTL_xyz = require("xyz"); + local def = __TSTL_xyz.abc; + exports.def = def; +end return exports; \ No newline at end of file diff --git a/test/translation/ts/exportStatement.ts b/test/translation/ts/exportStatement.ts index 128ea1568..524be0bd9 100644 --- a/test/translation/ts/exportStatement.ts +++ b/test/translation/ts/exportStatement.ts @@ -1 +1,6 @@ -export {abc, def} from "xyz"; \ No newline at end of file +const xyz = 4; +export {xyz}; +export {xyz as uwv}; +export * from "xyz"; +export {abc, def} from "xyz"; +export {abc as def} from "xyz"; From 9a4ad98280a6e56a6638aacd9c0e300934ba1e9b Mon Sep 17 00:00:00 2001 From: Perryvw Date: Mon, 18 Feb 2019 20:30:59 +0100 Subject: [PATCH 3/4] Added error for default exports and cleaned up TSTLErrors a little --- src/LuaTransformer.ts | 7 ++++++ src/TSTLErrors.ts | 46 +++++++++++++++------------------- test/unit/importexport.spec.ts | 17 +++++++++++++ 3 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 test/unit/importexport.spec.ts diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 10f7f7f19..a1e5f414b 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -236,6 +236,13 @@ export class LuaTransformer { } 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, diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 793fdbf54..7f9717b6e 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -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); @@ -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); @@ -56,19 +54,23 @@ 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); @@ -76,12 +78,6 @@ export class TSTLErrors { 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( @@ -141,6 +137,4 @@ export class TSTLErrors { node ); } - - public static InvalidJsonFileContent = (node: ts.Node) => new TranspileError("Invalid JSON file content", node); } diff --git a/test/unit/importexport.spec.ts b/test/unit/importexport.spec.ts new file mode 100644 index 000000000..7aca64caf --- /dev/null +++ b/test/unit/importexport.spec.ts @@ -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); + } +} From d7940fa2a19976bb85f394dc8e4e6e6d6507bb4b Mon Sep 17 00:00:00 2001 From: Perryvw Date: Mon, 18 Feb 2019 21:00:09 +0100 Subject: [PATCH 4/4] Fixed broken test --- test/unit/decoratorMetaExtension.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts index f0a5eabdc..6fdedf191 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -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 { @@ -29,6 +30,7 @@ export class DecoratorMetaExtension { @Test("IncorrectUsage") public incorrectUsage(): void { + const expectedMessage = TSTLErrors.MissingMetaExtension(undefined).message; Expect(() => { util.transpileString( ` @@ -40,8 +42,7 @@ export class DecoratorMetaExtension { } ` ); - }).toThrowError(TranspileError, - "!MetaExtension requires the extension of the metatable class."); + }).toThrowError(TranspileError, expectedMessage); } @Test("DontAllowInstantiation")