From 0f80bc42d049863e82ba483867bf508b6dd0584c Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Tue, 20 Jun 2017 15:01:01 +0800 Subject: [PATCH] support `export` statements properly fixes #2038 fixes #2124 --- lib/ast.js | 32 ++++++++++++++----- lib/compress.js | 13 ++++++-- lib/output.js | 6 ++-- lib/parse.js | 36 +++++++++++---------- lib/transform.js | 15 ++++++++- test/compress/export.js | 69 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 32 deletions(-) create mode 100644 test/compress/export.js diff --git a/lib/ast.js b/lib/ast.js index 234616f7..7668dabf 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -629,11 +629,11 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { - $documentation: "The part of the import statement that imports names from a module.", +var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", { + $documentation: "The part of the export/import statement that declare names from a module.", $propdoc: { - foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", - name: "[AST_SymbolImport] The name as it becomes available to this module." + foreign_name: "[AST_SymbolExportForeign|AST_SymbolImportForeign] The name being exported/imported (as specified in the module)", + name: "[AST_SymbolExport|AST_SymbolImport] The name as it is visible to this module." }, _walk: function (visitor) { return visitor._visit(this, function() { @@ -647,7 +647,7 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { $documentation: "An `import` statement", $propdoc: { imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", - imported_names: "[AST_NameImport*] The names of non-default imported variables", + imported_names: "[AST_NameMapping*] The names of non-default imported variables", module_name: "[AST_String] String literal describing where this module came from", }, _walk: function(visitor) { @@ -656,7 +656,7 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { this.imported_name._walk(visitor); } if (this.imported_names) { - this.imported_names.forEach(function (name_import) { + this.imported_names.forEach(function(name_import) { name_import._walk(visitor); }); } @@ -670,7 +670,7 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul $propdoc: { exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition", exported_value: "[AST_Node?] An exported value", - exported_names: "[AST_NameImport*?] List of exported names", + exported_names: "[AST_NameMapping*?] List of exported names", module_name: "[AST_String?] Name of the file to load exports from", is_default: "[Boolean] Whether this is the default exported value of this module" }, @@ -682,6 +682,14 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul if (this.exported_value) { this.exported_value._walk(visitor); } + if (this.exported_names) { + this.exported_names.forEach(function(name_export) { + name_export._walk(visitor); + }); + } + if (this.module_name) { + this.module_name._walk(visitor); + } }); } }, AST_Statement); @@ -996,7 +1004,7 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { }, AST_SymbolBlockDeclaration); var AST_SymbolImport = DEFNODE("SymbolImport", null, { - $documentation: "Symbol refering to an imported name", + $documentation: "Symbol referring to an imported name", }, AST_SymbolBlockDeclaration); var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { @@ -1018,6 +1026,14 @@ var AST_SymbolRef = DEFNODE("SymbolRef", null, { $documentation: "Reference to some symbol (not definition/declaration)", }, AST_Symbol); +var AST_SymbolExport = DEFNODE("SymbolExport", null, { + $documentation: "Symbol referring to a name to export", +}, AST_SymbolRef); + +var AST_SymbolExportForeign = DEFNODE("SymbolExportForeign", null, { + $documentation: "A symbol exported from this module, but it is used in the other module, and its real name is irrelevant for this module's purposes", +}, AST_Symbol); + var AST_LabelRef = DEFNODE("LabelRef", null, { $documentation: "Reference to a label symbol", }, AST_Symbol); diff --git a/lib/compress.js b/lib/compress.js index d6a1830a..c67a3f40 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -672,7 +672,9 @@ merge(Compressor.prototype, { node instanceof AST_DefClass || node instanceof AST_Defun || node instanceof AST_Let || - node instanceof AST_Const + node instanceof AST_Const || + node instanceof AST_Export || + node instanceof AST_Import ); } @@ -2099,15 +2101,16 @@ merge(Compressor.prototype, { return true; // don't go in nested scopes } if (node instanceof AST_Definitions && scope === self) { + var in_export = tw.parent() instanceof AST_Export; node.definitions.forEach(function(def){ if (def.name instanceof AST_SymbolVar) { var_defs_by_id.add(def.name.definition().id, def); } - if (!drop_vars) { + if (in_export || !drop_vars) { def.name.walk(new TreeWalker(function(node) { if (node instanceof AST_SymbolDeclaration) { var def = node.definition(); - if (def.global && !(def.id in in_use_ids)) { + if ((in_export || def.global) && !(def.id in in_use_ids)) { in_use_ids[def.id] = true; in_use.push(def); } @@ -4037,6 +4040,10 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_SymbolExport, function(self, compressor){ + return self; + }); + OPT(AST_SymbolRef, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { diff --git a/lib/output.js b/lib/output.js index a30800c1..3c04e3ea 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1296,7 +1296,7 @@ function OutputStream(options) { output.semicolon(); }); - DEFPRINT(AST_NameImport, function(self, output) { + DEFPRINT(AST_NameMapping, function(self, output) { var definition = self.name.definition(); var names_are_different = (definition && definition.mangled_name || self.name.name) !== @@ -1326,9 +1326,9 @@ function OutputStream(options) { self.exported_names[0].print(output); } else { output.print("{"); - self.exported_names.forEach(function (name_import, i) { + self.exported_names.forEach(function(name_export, i) { output.space(); - name_import.print(output); + name_export.print(output); if (i < self.exported_names.length - 1) { output.print(","); output.space(); diff --git a/lib/parse.js b/lib/parse.js index 9a5e75cb..84696890 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2302,7 +2302,7 @@ function parse($TEXT, options) { next(); } - imported_names = import_names(true); + imported_names = map_names(true); if (imported_names || imported_name) { expect_token("name", "from"); @@ -2326,26 +2326,28 @@ function parse($TEXT, options) { }); } - function import_name() { + function map_name(is_import) { + var foreign_type = is_import ? AST_SymbolImportForeign : AST_SymbolExportForeign; + var type = is_import ? AST_SymbolImport : AST_SymbolExport; var start = S.token; var foreign_name; var name; if (peek().value === "as" && peek().type === "name") { - foreign_name = as_symbol(AST_SymbolImportForeign); + foreign_name = as_symbol(foreign_type); next(); // The "as" word } - name = as_symbol(AST_SymbolImport); + name = as_symbol(type); if (foreign_name === undefined) { - foreign_name = new AST_SymbolImportForeign({ + foreign_name = new foreign_type({ name: name.name, start: name.start, end: name.end, }); } - return new AST_NameImport({ + return new AST_NameMapping({ start: start, foreign_name: foreign_name, name: name, @@ -2353,26 +2355,26 @@ function parse($TEXT, options) { }) } - function import_nameAsterisk(name) { + function map_nameAsterisk(is_import, name) { + var foreign_type = is_import ? AST_SymbolImportForeign : AST_SymbolExportForeign; + var type = is_import ? AST_SymbolImport : AST_SymbolExport; var start = S.token; var foreign_name; - - var end = prev(); - name = name || new AST_SymbolImport({ + name = name || new type({ name: '*', start: start, end: end, }); - foreign_name = new AST_SymbolImportForeign({ + foreign_name = new foreign_type({ name: '*', start: start, end: end, }); - return new AST_NameImport({ + return new AST_NameMapping({ start: start, foreign_name: foreign_name, name: name, @@ -2380,13 +2382,13 @@ function parse($TEXT, options) { }) } - function import_names(allow_as) { + function map_names(is_import) { var names; if (is("punc", "{")) { next(); names = []; while (!is("punc", "}")) { - names.push(import_name()); + names.push(map_name(is_import)); if (is("punc", ",")) { next(); } @@ -2395,11 +2397,11 @@ function parse($TEXT, options) { } else if (is("operator", "*")) { var name; next(); - if (allow_as && is("name", "as")) { + if (is_import && is("name", "as")) { next(); // The "as" word name = as_symbol(AST_SymbolImportForeign); } - names = [import_nameAsterisk(name)]; + names = [map_nameAsterisk(is_import, name)]; } return names; } @@ -2415,7 +2417,7 @@ function parse($TEXT, options) { is_default = true; next(); } else { - exported_names = import_names(false); + exported_names = map_names(false); if (exported_names) { if (is("name", "from")) { diff --git a/lib/transform.js b/lib/transform.js index 1f10e274..0d52199e 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -243,9 +243,22 @@ TreeTransformer.prototype = new TreeWalker; self.expression = self.expression.transform(tw); }); - _(AST_Export, function(self, tw){ + _(AST_NameMapping, function(self, tw) { + self.foreign_name = self.foreign_name.transform(tw); + self.name = self.name.transform(tw); + }); + + _(AST_Import, function(self, tw) { + if (self.imported_name) self.imported_name = self.imported_name.transform(tw); + if (self.imported_names) do_list(self.imported_names, tw); + self.module_name = self.module_name.transform(tw); + }); + + _(AST_Export, function(self, tw) { if (self.exported_definition) self.exported_definition = self.exported_definition.transform(tw); if (self.exported_value) self.exported_value = self.exported_value.transform(tw); + if (self.exported_names) do_list(self.exported_names, tw); + if (self.module_name) self.module_name = self.module_name.transform(tw); }); _(AST_TemplateString, function(self, tw) { diff --git a/test/compress/export.js b/test/compress/export.js new file mode 100644 index 00000000..a773aad6 --- /dev/null +++ b/test/compress/export.js @@ -0,0 +1,69 @@ +issue_2038_1: { + options = { + toplevel: true, + unused: true, + } + mangle = { + toplevel: true, + } + input: { + export var V = 1; + export let L = 2; + export const C = 3; + } + expect: { + export var V = 1; + export let L = 2; + export const C = 3; + } +} + +issue_2038_2: { + options = { + toplevel: true, + unused: true, + } + mangle = { + toplevel: true, + } + input: { + let LET = 1; + const CONST = 2; + var VAR = 3; + export { LET, CONST, VAR }; + } + expect: { + let a = 1; + const c = 2; + var n = 3; + export { LET as a, CONST as c, VAR as n }; + } +} + +issue_2124: { + options = { + unused: true, + } + input: { + { + export var V = 1; + } + { + export let L = 2; + } + { + export const C = 3; + } + } + expect: { + { + export var V = 1; + } + { + export let L = 2; + } + { + export const C = 3; + } + } +}