From 7130f4c27fcb7e7f5d2be1cc10f778fb2ac604d0 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Sun, 5 May 2019 12:44:33 +0500 Subject: [PATCH 1/3] Add module import/export elision --- src/LuaTransformer.ts | 81 +++++++++++++------ .../__snapshots__/transformation.spec.ts.snap | 41 +++++++--- .../transformation/modulesImportAll.ts | 2 + .../transformation/modulesImportNamed.ts | 2 + .../modulesImportNamedSpecialChars.ts | 16 ++-- .../transformation/modulesImportRenamed.ts | 2 + .../modulesImportRenamedSpecialChars.ts | 16 ++-- test/unit/importexport.spec.ts | 12 --- test/unit/modules.spec.ts | 63 +++++++++++++++ test/unit/require.spec.ts | 4 +- 10 files changed, 176 insertions(+), 63 deletions(-) delete mode 100644 test/unit/importexport.spec.ts diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 9aee708ba..b6357f35e 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -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 = new Set([ "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "new", "nil", @@ -47,11 +57,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; + private isModule = false; private currentSourceFile?: ts.SourceFile; private currentNamespace: ts.ModuleDeclaration | undefined; @@ -70,7 +82,7 @@ export class LuaTransformer { private readonly typeValidationCache: Map> = new Map>(); 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) @@ -100,6 +112,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) { @@ -231,23 +244,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) { - 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 !== undefined && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword) @@ -257,11 +253,28 @@ 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 => + tstl.createAssignmentStatement( + this.createExportedIdentifier(this.transformIdentifier(specifier.name)), + this.transformIdentifier(specifier.propertyName || specifier.name) + ) + ); + } + // 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)) ) ); @@ -277,11 +290,11 @@ 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) ) ); } @@ -289,6 +302,14 @@ export class LuaTransformer { // 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"); @@ -360,7 +381,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 @@ -404,6 +429,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, diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 27e78d0c8..d44516987 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -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_module = require(\\"kebab-module\\") -local TestClass = __TSTL_kebab_module.TestClass +local TestClass1 = __TSTL_kebab_module.TestClass1 local __TSTL_dollar_module = require(\\"dollar$module\\") -local TestClass = __TSTL_dollar_module.TestClass +local TestClass2 = __TSTL_dollar_module.TestClass2 local __TSTL_singlequote_module = require(\\"singlequote'module\\") -local TestClass = __TSTL_singlequote_module.TestClass +local TestClass3 = __TSTL_singlequote_module.TestClass3 local __TSTL_hash_module = require(\\"hash#module\\") -local TestClass = __TSTL_hash_module.TestClass +local TestClass4 = __TSTL_hash_module.TestClass4 local __TSTL_space_module = require(\\"space module\\") -local TestClass = __TSTL_space_module.TestClass" +local TestClass5 = __TSTL_space_module.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_module = require(\\"kebab-module\\") -local RenamedClass = __TSTL_kebab_module.TestClass +local RenamedClass1 = __TSTL_kebab_module.TestClass local __TSTL_dollar_module = require(\\"dollar$module\\") -local RenamedClass = __TSTL_dollar_module.TestClass +local RenamedClass2 = __TSTL_dollar_module.TestClass local __TSTL_singlequote_module = require(\\"singlequote'module\\") -local RenamedClass = __TSTL_singlequote_module.TestClass +local RenamedClass3 = __TSTL_singlequote_module.TestClass local __TSTL_hash_module = require(\\"hash#module\\") -local RenamedClass = __TSTL_hash_module.TestClass +local RenamedClass4 = __TSTL_hash_module.TestClass local __TSTL_space_module = require(\\"space module\\") -local RenamedClass = __TSTL_space_module.TestClass" +local RenamedClass5 = __TSTL_space_module.TestClass +local ____ = RenamedClass1 +local ____ = RenamedClass2 +local ____ = RenamedClass3 +local ____ = RenamedClass4 +local ____ = RenamedClass5" `; exports[`Transformation (modulesImportWithoutFromClause) 1`] = `"require(\\"test\\")"`; diff --git a/test/translation/transformation/modulesImportAll.ts b/test/translation/transformation/modulesImportAll.ts index b452afb73..28b470640 100644 --- a/test/translation/transformation/modulesImportAll.ts +++ b/test/translation/transformation/modulesImportAll.ts @@ -1 +1,3 @@ import * as Test from "test"; + +Test; diff --git a/test/translation/transformation/modulesImportNamed.ts b/test/translation/transformation/modulesImportNamed.ts index b4c16e54b..d147081ff 100644 --- a/test/translation/transformation/modulesImportNamed.ts +++ b/test/translation/transformation/modulesImportNamed.ts @@ -1 +1,3 @@ import { TestClass } from "test"; + +TestClass; diff --git a/test/translation/transformation/modulesImportNamedSpecialChars.ts b/test/translation/transformation/modulesImportNamedSpecialChars.ts index 7237d70a2..dbb54990c 100644 --- a/test/translation/transformation/modulesImportNamedSpecialChars.ts +++ b/test/translation/transformation/modulesImportNamedSpecialChars.ts @@ -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; diff --git a/test/translation/transformation/modulesImportRenamed.ts b/test/translation/transformation/modulesImportRenamed.ts index 42129a291..74c82598b 100644 --- a/test/translation/transformation/modulesImportRenamed.ts +++ b/test/translation/transformation/modulesImportRenamed.ts @@ -1 +1,3 @@ import { TestClass as RenamedClass } from "test"; + +RenamedClass; diff --git a/test/translation/transformation/modulesImportRenamedSpecialChars.ts b/test/translation/transformation/modulesImportRenamedSpecialChars.ts index 59e7ed862..a200c07e9 100644 --- a/test/translation/transformation/modulesImportRenamedSpecialChars.ts +++ b/test/translation/transformation/modulesImportRenamedSpecialChars.ts @@ -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; diff --git a/test/unit/importexport.spec.ts b/test/unit/importexport.spec.ts deleted file mode 100644 index cd98b0b4f..000000000 --- a/test/unit/importexport.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TSTLErrors } from "../../src/TSTLErrors"; -import * as util from "../util"; - -test.each([ - "export { default } from '...'", - "export { x as default } from '...';", - "export { default as x } from '...';", -])("Export default keyword disallowed (%p)", exportStatement => { - expect(() => util.transpileString(exportStatement)).toThrowExactError( - TSTLErrors.UnsupportedDefaultExport(util.nodeStub), - ); -}); diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts index 679e6e615..17719fbb1 100644 --- a/test/unit/modules.spec.ts +++ b/test/unit/modules.spec.ts @@ -2,6 +2,69 @@ import * as tstl from "../../src"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; +describe("module import/export elision", () => { + const moduleDeclaration = ` + declare module "module" { + export type Type = string; + export declare const value: string; + } + `; + + const expectToElideImport = (code: string) => { + const lua = util.transpileString( + { "module.d.ts": moduleDeclaration, "main.ts": code }, + undefined, + false, + ); + + expect(() => util.executeLua(lua)).not.toThrow(); + }; + + test("should elide named type imports", () => { + expectToElideImport(` + import { Type } from "module"; + const foo: Type = "bar"; + `); + }); + + test("should elide named value imports used only as a type", () => { + expectToElideImport(` + import { value } from "module"; + const foo: typeof value = "bar"; + `); + }); + + test("should elide namespace imports with unused values", () => { + expectToElideImport(` + import * as module from "module"; + const foo: module.Type = "bar"; + `); + }); + + test("should elide type exports", () => { + const code = ` + // TODO: Add some Lua Types to transpileStringResult + declare const _G: any; + + _G.foo = true; + type foo = boolean; + export { foo }; + `; + + expect(util.transpileExecuteAndReturnExport(code, "foo")).toBeUndefined(); + }); +}); + +test.each([ + "export { default } from '...'", + "export { x as default } from '...';", + "export { default as x } from '...';", +])("Export default keyword disallowed (%p)", exportStatement => { + expect(() => util.transpileString(exportStatement)).toThrowExactError( + TSTLErrors.UnsupportedDefaultExport(util.nodeStub), + ); +}); + test("defaultImport", () => { expect(() => { const lua = util.transpileString(`import TestClass from "test"`); diff --git a/test/unit/require.spec.ts b/test/unit/require.spec.ts index f8d6d9a4f..183bac892 100644 --- a/test/unit/require.spec.ts +++ b/test/unit/require.spec.ts @@ -74,7 +74,7 @@ test.each([ ])( "require paths root from --baseUrl or --rootDir (%p)", ({ filePath, usedPath, expectedPath, options, throwsError }) => { - const input = { [filePath]: `import * from "${usedPath}";` }; + const input = { [filePath]: `import * as module from "${usedPath}"; module;` }; if (throwsError) { expect(() => util.transpileString(input, options)).toThrow(); } else { @@ -96,7 +96,7 @@ test.each([ "noResolution on ambient modules causes no path alterations (%p)", ({ comment, expectedPath }) => { const lua = util.transpileString({ - "src/main.ts": `import * as fake from "fake";`, + "src/main.ts": `import * as fake from "fake"; fake;`, "module.d.ts": `${comment} declare module "fake" {}`, }); const regex = /require\("(.*?)"\)/; From 35a9cafa6519a8ef63cef2aad1f41b94842169eb Mon Sep 17 00:00:00 2001 From: ark120202 Date: Tue, 21 May 2019 05:27:57 +0500 Subject: [PATCH 2/3] Reference imported values in tests to avoid elision --- test/unit/identifiers.spec.ts | 8 +++++--- test/unit/modules.spec.ts | 4 +++- test/unit/sourcemaps.spec.ts | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index 58ead74ff..20a889a34 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -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([ diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts index 0598a7d8c..2f4056ee0 100644 --- a/test/unit/modules.spec.ts +++ b/test/unit/modules.spec.ts @@ -69,7 +69,9 @@ test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌ "Import module names with invalid lua identifier characters (%p)", name => { const code = ` - import { foo } from "${name}";`; + import { foo } from "${name}"; + foo; + `; const lua = ` setmetatable(package.loaded, {__index = function() return {foo = "bar"} end}) diff --git a/test/unit/sourcemaps.spec.ts b/test/unit/sourcemaps.spec.ts index f4bda8503..5225ab6b2 100644 --- a/test/unit/sourcemaps.spec.ts +++ b/test/unit/sourcemaps.spec.ts @@ -48,6 +48,7 @@ test.each([ { typeScriptSource: ` import {Foo} from "foo"; + Foo; `, assertPatterns: [ @@ -58,6 +59,7 @@ test.each([ { typeScriptSource: ` import * as Foo from "foo"; + Foo; `, assertPatterns: [ From d77ecfe260f0e5a291022abd7cc364d8dd44bb9f Mon Sep 17 00:00:00 2001 From: ark120202 Date: Wed, 22 May 2019 01:48:44 +0500 Subject: [PATCH 3/3] Remove TODO comment --- test/unit/modules.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts index 2f4056ee0..3869beadd 100644 --- a/test/unit/modules.spec.ts +++ b/test/unit/modules.spec.ts @@ -43,7 +43,6 @@ describe("module import/export elision", () => { test("should elide type exports", () => { const code = ` - // TODO: Add some Lua Types to transpileStringResult declare const _G: any; _G.foo = true;