From c2b75e58b3c4138f5e4fe2241b084d21e4dd1546 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Tue, 19 Jul 2022 22:35:10 +0800 Subject: [PATCH] support string namespace in `import` & `export` --- lib/ast.js | 52 ++++++--------- lib/compress.js | 2 +- lib/mozilla-ast.js | 137 ++++++++++++++++++++++----------------- lib/output.js | 28 +++++--- lib/parse.js | 85 ++++++++++++++---------- test/compress/exports.js | 11 ++++ test/compress/imports.js | 11 ++++ test/mocha/exports.js | 50 ++++++++++++++ test/mocha/imports.js | 53 +++++++++++++++ 9 files changed, 295 insertions(+), 134 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 28fcd079..2ecb95aa 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1366,34 +1366,29 @@ var AST_ExportDefault = DEFNODE("ExportDefault", "body", { }, }, AST_Statement); -var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path quote", { +var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path", { $documentation: "An `export ... from '...'` statement", $propdoc: { - aliases: "[string*] array of aliases to export", - keys: "[string*] array of keys to import", - path: "[string] the path to import module", - quote: "[string?] the original quote character", + aliases: "[AST_String*] array of aliases to export", + keys: "[AST_String*] array of keys to import", + path: "[AST_String] the path to import module", }, _equals: function(node) { - return this.path == node.path - && list_equals(this.aliases, node.aliases) - && list_equals(this.keys, node.keys); + return this.path.equals(node.path) + && all_equals(this.aliases, node.aliases) + && all_equals(this.keys, node.keys); }, _validate: function() { if (this.aliases.length != this.keys.length) { throw new Error("aliases:key length mismatch: " + this.aliases.length + " != " + this.keys.length); } this.aliases.forEach(function(name) { - if (typeof name != "string") throw new Error("aliases must contain string"); + if (!(name instanceof AST_String)) throw new Error("aliases must contain AST_String"); }); this.keys.forEach(function(name) { - if (typeof name != "string") throw new Error("keys must contain string"); + if (!(name instanceof AST_String)) throw new Error("keys must contain AST_String"); }); - if (typeof this.path != "string") throw new Error("path must be string"); - if (this.quote != null) { - if (typeof this.quote != "string") throw new Error("quote must be string"); - if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote); - } + if (!(this.path instanceof AST_String)) throw new Error("path must be AST_String"); }, }, AST_Statement); @@ -1420,17 +1415,16 @@ var AST_ExportReferences = DEFNODE("ExportReferences", "properties", { }, }, AST_Statement); -var AST_Import = DEFNODE("Import", "all default path properties quote", { +var AST_Import = DEFNODE("Import", "all default path properties", { $documentation: "An `import` statement", $propdoc: { all: "[AST_SymbolImport?] the imported namespace, or null if not specified", default: "[AST_SymbolImport?] the alias for default `export`, or null if not specified", - path: "[string] the path to import module", + path: "[AST_String] the path to import module", properties: "[(AST_SymbolImport*)?] array of aliases, or null if not specified", - quote: "[string?] the original quote character", }, _equals: function(node) { - return this.path == node.path + return this.path.equals(node.path) && prop_equals(this.all, node.all) && prop_equals(this.default, node.default) && !this.properties == !node.properties @@ -1453,16 +1447,12 @@ var AST_Import = DEFNODE("Import", "all default path properties quote", { } if (this.default != null) { if (!(this.default instanceof AST_SymbolImport)) throw new Error("default must be AST_SymbolImport"); - if (this.default.key !== "") throw new Error("invalid default key: " + this.default.key); + if (this.default.key.value !== "") throw new Error("invalid default key: " + this.default.key.value); } - if (typeof this.path != "string") throw new Error("path must be string"); + if (!(this.path instanceof AST_String)) throw new Error("path must be AST_String"); if (this.properties != null) this.properties.forEach(function(node) { if (!(node instanceof AST_SymbolImport)) throw new Error("properties must contain AST_SymbolImport"); }); - if (this.quote != null) { - if (typeof this.quote != "string") throw new Error("quote must be string"); - if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote); - } }, }, AST_Statement); @@ -2005,14 +1995,14 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { var AST_SymbolImport = DEFNODE("SymbolImport", "key", { $documentation: "Symbol defined by an `import` statement", $propdoc: { - key: "[string] the original `export` name", + key: "[AST_String] the original `export` name", }, _equals: function(node) { return this.name == node.name - && this.key == node.key; + && this.key.equals(node.key); }, _validate: function() { - if (typeof this.key != "string") throw new Error("key must be string"); + if (!(this.key instanceof AST_String)) throw new Error("key must be AST_String"); }, }, AST_SymbolConst); @@ -2066,14 +2056,14 @@ var AST_SymbolRef = DEFNODE("SymbolRef", "fixed in_arg redef", { var AST_SymbolExport = DEFNODE("SymbolExport", "alias", { $documentation: "Reference in an `export` statement", $propdoc: { - alias: "[string] the `export` alias", + alias: "[AST_String] the `export` alias", }, _equals: function(node) { return this.name == node.name - && this.alias == node.alias; + && this.alias.equals(node.alias); }, _validate: function() { - if (typeof this.alias != "string") throw new Error("alias must be string"); + if (!(this.alias instanceof AST_String)) throw new Error("alias must be AST_String"); }, }, AST_SymbolRef); diff --git a/lib/compress.js b/lib/compress.js index 9d51fad8..a1e6497a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -294,7 +294,7 @@ Compressor.prototype.compress = function(node) { function export_symbol(sym) { if (!(sym instanceof AST_SymbolDeclaration)) return; var node = make_node(AST_SymbolExport, sym, sym); - node.alias = node.name; + node.alias = make_node(AST_String, node, { value: node.name }); props.push(node); } }); diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js index 749bb3be..3e0534c8 100644 --- a/lib/mozilla-ast.js +++ b/lib/mozilla-ast.js @@ -316,13 +316,22 @@ }); }, ExportAllDeclaration: function(M) { - var alias = M.exported ? read_name(M.exported) : "*"; + var start = my_start_token(M); + var end = my_end_token(M); return new AST_ExportForeign({ - start: my_start_token(M), - end: my_end_token(M), - aliases: [ alias ], - keys: [ "*" ], - path: M.source.value, + start: start, + end: end, + aliases: [ M.exported ? from_moz_alias(M.exported) : new AST_String({ + start: start, + value: "*", + end: end, + }) ], + keys: [ new AST_String({ + start: start, + value: "*", + end: end, + }) ], + path: from_moz(M.source), }); }, ExportDefaultDeclaration: function(M) { @@ -359,15 +368,15 @@ if (M.source) { var aliases = [], keys = []; M.specifiers.forEach(function(prop) { - aliases.push(read_name(prop.exported)); - keys.push(read_name(prop.local)); + aliases.push(from_moz_alias(prop.exported)); + keys.push(from_moz_alias(prop.local)); }); return new AST_ExportForeign({ start: my_start_token(M), end: my_end_token(M), aliases: aliases, keys: keys, - path: M.source.value, + path: from_moz(M.source), }); } return new AST_ExportReferences({ @@ -375,38 +384,48 @@ end: my_end_token(M), properties: M.specifiers.map(function(prop) { var sym = new AST_SymbolExport(from_moz(prop.local)); - sym.alias = read_name(prop.exported); + sym.alias = from_moz_alias(prop.exported); return sym; }), }); }, ImportDeclaration: function(M) { + var start = my_start_token(M); + var end = my_end_token(M); var all = null, def = null, props = null; M.specifiers.forEach(function(prop) { var sym = new AST_SymbolImport(from_moz(prop.local)); switch (prop.type) { case "ImportDefaultSpecifier": def = sym; - def.key = ""; + def.key = new AST_String({ + start: start, + value: "", + end: end, + }); break; case "ImportNamespaceSpecifier": all = sym; - all.key = "*"; + all.key = new AST_String({ + start: start, + value: "*", + end: end, + }); break; default: - sym.key = prop.imported.name || syn.name; + sym.key = from_moz_alias(prop.imported); if (!props) props = []; props.push(sym); break; } }); return new AST_Import({ - start: my_start_token(M), - end: my_end_token(M), + start: start, + end: end, all: all, default: def, properties: props, - path: M.source.value, + path: from_moz(M.source), }); }, ImportExpression: function(M) { @@ -797,38 +816,26 @@ }); def_to_moz(AST_ExportForeign, function To_Moz_ExportAllDeclaration_ExportNamedDeclaration(M) { - if (M.keys[0] == "*") return { + if (M.keys[0].value == "*") return { type: "ExportAllDeclaration", - exported: M.aliases[0] == "*" ? null : { - type: "Identifier", - name: M.aliases[0], - }, - source: { - type: "Literal", - value: M.path, - }, + exported: M.aliases[0].value == "*" ? null : to_moz_alias(M.aliases[0]), + source: to_moz(M.path), }; var specifiers = []; for (var i = 0; i < M.aliases.length; i++) { - specifiers.push({ + specifiers.push(set_moz_loc({ + start: M.keys[i].start, + end: M.aliases[i].end, + }, { type: "ExportSpecifier", - exported: { - type: "Identifier", - name: M.aliases[i], - }, - local: { - type: "Identifier", - name: M.keys[i], - }, - }); + local: to_moz_alias(M.keys[i]), + exported: to_moz_alias(M.aliases[i]), + })); } return { type: "ExportNamedDeclaration", specifiers: specifiers, - source: { - type: "Literal", - value: M.path, - }, + source: to_moz(M.path), }; }); @@ -836,44 +843,41 @@ return { type: "ExportNamedDeclaration", specifiers: M.properties.map(function(prop) { - return { + return set_moz_loc({ + start: prop.start, + end: prop.alias.end, + }, { type: "ExportSpecifier", local: to_moz(prop), - exported: { - type: "Identifier", - name: prop.alias, - }, - }; + exported: to_moz_alias(prop.alias), + }); }), }; }); def_to_moz(AST_Import, function To_Moz_ImportDeclaration(M) { var specifiers = M.properties ? M.properties.map(function(prop) { - return { + return set_moz_loc({ + start: prop.key.start, + end: prop.end, + }, { type: "ImportSpecifier", local: to_moz(prop), - imported: { - type: "Identifier", - name: prop.key, - }, - }; + imported: to_moz_alias(prop.key), + }); }) : []; - if (M.all) specifiers.unshift({ + if (M.all) specifiers.unshift(set_moz_loc(M.all, { type: "ImportNamespaceSpecifier", local: to_moz(M.all), - }); - if (M.default) specifiers.unshift({ + })); + if (M.default) specifiers.unshift(set_moz_loc(M.default, { type: "ImportDefaultSpecifier", local: to_moz(M.default), - }); + })); return { type: "ImportDeclaration", specifiers: specifiers, - source: { - type: "Literal", - value: M.path, - }, + source: to_moz(M.path), }; }); @@ -1220,6 +1224,14 @@ return node; } + function from_moz_alias(moz) { + return new AST_String({ + start: my_start_token(moz), + value: read_name(moz), + end: my_end_token(moz), + }); + } + AST_Node.from_mozilla_ast = function(node) { var save_stack = FROM_MOZ_STACK; FROM_MOZ_STACK = []; @@ -1271,6 +1283,13 @@ return node != null ? node.to_mozilla_ast() : null; } + function to_moz_alias(alias) { + return is_identifier_string(alias.value) ? set_moz_loc(alias, { + type: "Identifier", + name: alias.value, + }) : to_moz(alias); + } + function to_moz_block(node) { return { type: "BlockStatement", diff --git a/lib/output.js b/lib/output.js index 02c22e6a..ea63f146 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1061,6 +1061,14 @@ function OutputStream(options) { } output.semicolon(); }); + function print_alias(alias, output) { + var value = alias.value; + if (value == "*" || is_identifier_string(value)) { + output.print_name(value); + } else { + output.print_string(value, alias.quote); + } + } DEFPRINT(AST_ExportForeign, function(output) { var self = this; output.print("export"); @@ -1068,7 +1076,7 @@ function OutputStream(options) { var len = self.keys.length; if (len == 0) { print_braced_empty(self, output); - } else if (self.keys[0] == "*") { + } else if (self.keys[0].value == "*") { print_entry(0); } else output.with_block(function() { output.indent(); @@ -1084,18 +1092,18 @@ function OutputStream(options) { output.space(); output.print("from"); output.space(); - output.print_string(self.path, self.quote); + self.path.print(output); output.semicolon(); function print_entry(index) { var alias = self.aliases[index]; var key = self.keys[index]; - output.print_name(key); - if (alias != key) { + print_alias(key, output); + if (alias.value != key.value) { output.space(); output.print("as"); output.space(); - output.print_name(alias); + print_alias(alias, output); } } }); @@ -1124,7 +1132,7 @@ function OutputStream(options) { output.print("from"); output.space(); } - output.print_string(self.path, self.quote); + self.path.print(output); output.semicolon(); }); @@ -1734,19 +1742,19 @@ function OutputStream(options) { var name = get_symbol_name(self); output.print_name(name); var alias = self.alias; - if (alias != name) { + if (alias.value != name) { output.space(); output.print("as"); output.space(); - output.print_name(alias); + print_alias(alias, output); } }); DEFPRINT(AST_SymbolImport, function(output) { var self = this; var name = get_symbol_name(self); var key = self.key; - if (key && key != name) { - output.print_name(key); + if (key.value && key.value != name) { + print_alias(key, output); output.space(); output.print("as"); output.space(); diff --git a/lib/parse.js b/lib/parse.js index 3a71245d..00c038fd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1442,28 +1442,41 @@ function parse($TEXT, options) { } function is_alias() { - return is("name") || is_identifier_string(S.token.value); + return is("name") || is("string") || is_identifier_string(S.token.value); + } + + function make_string(token) { + return new AST_String({ + start: token, + quote: token.quote, + value: token.value, + end: token, + }); + } + + function as_path() { + var path = S.token; + expect_token("string"); + semicolon(); + return make_string(path); } function export_() { if (is("operator", "*")) { + var key = S.token; + var alias = key; next(); - var alias = "*"; if (is("name", "as")) { next(); if (!is_alias()) expect_token("name"); - alias = S.token.value; + alias = S.token; next(); } expect_token("name", "from"); - var path = S.token; - expect_token("string"); - semicolon(); return new AST_ExportForeign({ - aliases: [ alias ], - keys: [ "*" ], - path: path.value, - quote: path.quote, + aliases: [ make_string(alias) ], + keys: [ make_string(key) ], + path: as_path(), }); } if (is("punc", "{")) { @@ -1477,26 +1490,20 @@ function parse($TEXT, options) { if (is("name", "as")) { next(); if (!is_alias()) expect_token("name"); - aliases.push(S.token.value); + aliases.push(S.token); next(); } else { - aliases.push(key.value); + aliases.push(key); } if (!is("punc", "}")) expect(","); } expect("}"); if (is("name", "from")) { next(); - var path = S.token; - expect_token("string"); - semicolon(); return new AST_ExportForeign({ - aliases: aliases, - keys: keys.map(function(token) { - return token.value; - }), - path: path.value, - quote: path.quote, + aliases: aliases.map(make_string), + keys: keys.map(make_string), + path: as_path(), }); } semicolon(); @@ -1504,7 +1511,7 @@ function parse($TEXT, options) { properties: keys.map(function(token, index) { if (!is_token(token, "name")) token_error(token, "Name expected"); var sym = _make_symbol(AST_SymbolExport, token); - sym.alias = aliases[index]; + sym.alias = make_string(aliases[index]); return sym; }), }); @@ -1594,26 +1601,42 @@ function parse($TEXT, options) { var all = null; var def = as_symbol(AST_SymbolImport, true); var props = null; - if (def ? (def.key = "", is("punc", ",") && next()) : !is("string")) { + var cont; + if (def) { + def.key = new AST_String({ + start: def.start, + value: "", + end: def.end, + }); + if (cont = is("punc", ",")) next(); + } else { + cont = !is("string"); + } + if (cont) { if (is("operator", "*")) { + var key = S.token; next(); expect_token("name", "as"); all = as_symbol(AST_SymbolImport); - all.key = "*"; + all.key = make_string(key); } else { expect("{"); props = []; while (is_alias()) { var alias; if (is_token(peek(), "name", "as")) { - var key = S.token.value; + var key = S.token; next(); next(); alias = as_symbol(AST_SymbolImport); - alias.key = key; + alias.key = make_string(key); } else { alias = as_symbol(AST_SymbolImport); - alias.key = alias.name; + alias.key = new AST_String({ + start: alias.start, + value: alias.name, + end: alias.end, + }); } props.push(alias); if (!is("punc", "}")) expect(","); @@ -1622,15 +1645,11 @@ function parse($TEXT, options) { } } if (all || def || props) expect_token("name", "from"); - var path = S.token; - expect_token("string"); - semicolon(); return new AST_Import({ all: all, default: def, - path: path.value, + path: as_path(), properties: props, - quote: path.quote, }); } @@ -1808,7 +1827,7 @@ function parse($TEXT, options) { ret = new AST_BigInt({ value: value }); break; case "string": - ret = new AST_String({ value : value, quote : tok.quote }); + ret = new AST_String({ value: value, quote: tok.quote }); break; case "regexp": ret = new AST_RegExp({ value: value }); diff --git a/test/compress/exports.js b/test/compress/exports.js index abcd13b0..a5d0f1bd 100644 --- a/test/compress/exports.js +++ b/test/compress/exports.js @@ -109,6 +109,17 @@ foreign: { expect_exact: 'export*from"foo";export{}from"bar";export*as a from"baz";export{default}from"moo";export{b,c as case,default as delete,d}from"moz";' } +non_identifiers: { + beautify = { + quote_style: 3, + } + input: { + export * as "42" from 'foo'; + export { '42', "delete" as 'foo' } from "bar"; + } + expect_exact: "export*as\"42\"from'foo';export{'42',delete as foo}from\"bar\";" +} + same_quotes: { beautify = { beautify: true, diff --git a/test/compress/imports.js b/test/compress/imports.js index 2f671633..d8233827 100644 --- a/test/compress/imports.js +++ b/test/compress/imports.js @@ -40,6 +40,17 @@ default_keys: { expect_exact: 'import foo,{bar}from"baz";' } +non_identifiers: { + beautify = { + quote_style: 3, + } + input: { + import { '42' as foo } from "bar"; + import { "foo" as bar } from 'baz'; + } + expect_exact: "import{'42'as foo}from\"bar\";import{foo as bar}from'baz';" +} + dynamic: { input: { (async a => await import(a))("foo").then(bar); diff --git a/test/mocha/exports.js b/test/mocha/exports.js index 70d806e7..6f1bf204 100644 --- a/test/mocha/exports.js +++ b/test/mocha/exports.js @@ -1,3 +1,4 @@ +var acorn = require("acorn"); var assert = require("assert"); var UglifyJS = require("../node"); @@ -25,6 +26,7 @@ describe("export", function() { "export { * };", "export { * as A };", "export { 42 as A };", + "export { 'A' as B };", "export { A as B-C };", "export { default as A };", ].forEach(function(code) { @@ -51,8 +53,11 @@ describe("export", function() { it("Should reject invalid `export ... from ...` statement syntax", function() { [ "export from 'path';", + "export A from 'path';", "export * from `path`;", + "export 'A' from 'path';", "export A as B from 'path';", + "export 'A' as B from 'path';", "export default from 'path';", "export { A }, B from 'path';", "export * as A, B from 'path';", @@ -128,4 +133,49 @@ describe("export", function() { }); }); }); + it("Should interoperate with ESTree correctly", function() { + [ + "export var A = 42;", + "export default (class A {});", + "var A; export { A as '42' };", + "export { '42' } from 'path';", + "export * as '42' from 'path';", + ].forEach(function(code) { + var ast = UglifyJS.parse(code); + try { + var spidermonkey = ast.to_mozilla_ast(); + } catch (ex) { + assert.fail("[to_mozilla_ast] " + ex.stack); + } + try { + var ast2 = UglifyJS.AST_Node.from_mozilla_ast(spidermonkey); + } catch (ex) { + assert.fail("[from_mozilla_ast] " + ex.stack); + } + assert.strictEqual(ast2.print_to_string(), ast.print_to_string(), [ + "spidermonkey:", + ast.print_to_string(), + ast2.print_to_string(), + ].join("\n")); + try { + var acorn_est = acorn.parse(code, { + ecmaVersion: "latest", + locations: true, + sourceType: "module", + }); + } catch (ex) { + assert.fail("[acorn.parse] " + ex.stack); + } + try { + var ast3 = UglifyJS.AST_Node.from_mozilla_ast(acorn_est); + } catch (ex) { + assert.fail("[from_acorn] " + ex.stack); + } + assert.strictEqual(ast3.print_to_string(), ast.print_to_string(), [ + "acorn:", + ast.print_to_string(), + ast3.print_to_string(), + ].join("\n")); + }); + }); }); diff --git a/test/mocha/imports.js b/test/mocha/imports.js index 9ca23666..3b2298ee 100644 --- a/test/mocha/imports.js +++ b/test/mocha/imports.js @@ -1,3 +1,4 @@ +var acorn = require("acorn"); var assert = require("assert"); var UglifyJS = require("../node"); @@ -12,14 +13,21 @@ describe("import", function() { "import from 'path';", "if (0) import 'path';", "import * from 'path';", + "import 'A' from 'path';", + "import A-B from 'path';", "import A as B from 'path';", "import { A }, B from 'path';", + "import * as 'A' from 'path';", + "import * as A-B from 'path';", "import * as A, B from 'path';", "import * as A, {} from 'path';", "import { * as A } from 'path';", + "import { * as 'A' } from 'path';", + "import { * as A-B } from 'path';", "function f() { import 'path'; }", "import { 42 as A } from 'path';", "import { A-B as C } from 'path';", + "import { 'A' as 'B' } from 'path';", ].forEach(function(code) { assert.throws(function() { UglifyJS.parse(code); @@ -53,4 +61,49 @@ describe("import", function() { }); }); }); + it("Should interoperate with ESTree correctly", function() { + [ + "import A from 'path';", + "import * as A from 'path';", + "import A, * as B from 'path';", + "import { '42' as A, B } from 'path';", + "import A, { '42' as B } from 'path';", + ].forEach(function(code) { + var ast = UglifyJS.parse(code); + try { + var spidermonkey = ast.to_mozilla_ast(); + } catch (ex) { + assert.fail("[to_mozilla_ast] " + ex.stack); + } + try { + var ast2 = UglifyJS.AST_Node.from_mozilla_ast(spidermonkey); + } catch (ex) { + assert.fail("[from_mozilla_ast] " + ex.stack); + } + assert.strictEqual(ast2.print_to_string(), ast.print_to_string(), [ + "spidermonkey:", + ast.print_to_string(), + ast2.print_to_string(), + ].join("\n")); + try { + var acorn_est = acorn.parse(code, { + ecmaVersion: "latest", + locations: true, + sourceType: "module", + }); + } catch (ex) { + assert.fail("[acorn.parse] " + ex.stack); + } + try { + var ast3 = UglifyJS.AST_Node.from_mozilla_ast(acorn_est); + } catch (ex) { + assert.fail("[from_acorn] " + ex.stack); + } + assert.strictEqual(ast3.print_to_string(), ast.print_to_string(), [ + "acorn:", + ast.print_to_string(), + ast3.print_to_string(), + ].join("\n")); + }); + }); });