diff --git a/lib/ast.js b/lib/ast.js index fc03a2c0..abc277d3 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -282,9 +282,10 @@ var AST_With = DEFNODE("With", "expression", { /* -----[ scope and functions ]----- */ -var AST_Scope = DEFNODE("Scope", "directives variables functions uses_with uses_eval parent_scope enclosed cname", { +var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions uses_with uses_eval parent_scope enclosed cname", { $documentation: "Base class for all statements introducing a lexical scope", $propdoc: { + is_block_scope: "[boolean] identifies a block scope", directives: "[string*/S] an array of directives declared in this scope", variables: "[Object/S] a map of name -> SymbolDef for all variables/functions defined in this scope", functions: "[Object/S] like `variables`, but only lists function declarations", @@ -1077,9 +1078,17 @@ var AST_SymbolVar = DEFNODE("SymbolVar", null, { $documentation: "Symbol defining a variable", }, AST_SymbolDeclaration); +var AST_SymbolBlockDeclaration = DEFNODE("SymbolBlockDeclaration", null, { + $documentation: "Base class for block-scoped declaration symbols" +}, AST_SymbolDeclaration); + var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); + +var AST_SymbolLet = DEFNODE("SymbolLet", null, { + $documentation: "A block-scoped `let` declaration" +}, AST_SymbolBlockDeclaration); var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", @@ -1099,7 +1108,7 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, { $documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class." -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolClass = DEFNODE("SymbolClass", null, { $documentation: "Symbol naming a class's name. Lexically scoped to the class." @@ -1107,11 +1116,11 @@ var AST_SymbolClass = DEFNODE("SymbolClass", null, { var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolImport = DEFNODE("SymbolImport", null, { $documentation: "Symbol refering to an imported name", -}, AST_SymbolDeclaration); +}, AST_SymbolBlockDeclaration); var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { $documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes", diff --git a/lib/compress.js b/lib/compress.js index f96fb04a..3dcb922a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -190,6 +190,14 @@ merge(Compressor.prototype, { return false; }; + function can_be_evicted_from_block(node) { + return !( + node instanceof AST_DefClass || + node instanceof AST_Let || + node instanceof AST_Const + ); + } + function loop_body(x) { if (x instanceof AST_Switch) return x; if (x instanceof AST_For || x instanceof AST_ForIn || x instanceof AST_DWLoop) { @@ -311,7 +319,7 @@ merge(Compressor.prototype, { function eliminate_spurious_blocks(statements) { var seen_dirs = []; return statements.reduce(function(a, stat){ - if (stat instanceof AST_BlockStatement) { + if (stat instanceof AST_BlockStatement && all(stat.body, can_be_evicted_from_block)) { CHANGED = true; a.push.apply(a, eliminate_spurious_blocks(stat.body)); } else if (stat instanceof AST_EmptyStatement) { @@ -633,7 +641,7 @@ merge(Compressor.prototype, { function extract_declarations_from_unreachable_code(compressor, stat, target) { compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); stat.walk(new TreeWalker(function(node){ - if (node instanceof AST_Definitions) { + if (node instanceof AST_Var) { compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start); node.remove_initializers(); target.push(node); @@ -957,6 +965,8 @@ merge(Compressor.prototype, { }); def(AST_Defun, function(compressor){ return true }); def(AST_Function, function(compressor){ return false }); + def(AST_Class, function(compressor){ return false }); + def(AST_DefClass, function(compressor){ return true }); def(AST_Binary, function(compressor){ return this.left.has_side_effects(compressor) || this.right.has_side_effects(compressor); @@ -1067,7 +1077,11 @@ merge(Compressor.prototype, { OPT(AST_BlockStatement, function(self, compressor){ self.body = tighten_body(self.body, compressor); switch (self.body.length) { - case 1: return self.body[0]; + case 1: + if (can_be_evicted_from_block(self.body[0])) { + return self.body[0]; + } + break; case 0: return make_node(AST_EmptyStatement, self); } return self; @@ -1077,7 +1091,6 @@ merge(Compressor.prototype, { var self = this; if (compressor.has_directive("use asm")) return self; if (compressor.option("unused") - && !(self instanceof AST_Toplevel) && !self.uses_eval ) { var in_use = []; @@ -1166,8 +1179,11 @@ merge(Compressor.prototype, { } } } - if (node instanceof AST_Defun && node !== self) { - if (!member(node.name.definition(), in_use)) { + if ((node instanceof AST_Defun || node instanceof AST_DefClass) && node !== self) { + var keep = + member(node.name.definition(), in_use) || + node.name.definition().global; + if (!keep) { compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, file : node.name.start.file, @@ -1182,6 +1198,7 @@ merge(Compressor.prototype, { var def = node.definitions.filter(function(def){ if (def.is_destructuring()) return true; if (member(def.name.definition(), in_use)) return true; + if (def.name.definition().global) return true; var w = { name : def.name.name, file : def.name.start.file, @@ -1260,6 +1277,12 @@ merge(Compressor.prototype, { }); } } + if (node instanceof AST_BlockStatement) { + descend(node, this); + if (in_list && all(node.body, can_be_evicted_from_block)) { + return MAP.splice(node.body); + } + } if (node instanceof AST_Scope && node !== self) return node; } diff --git a/lib/parse.js b/lib/parse.js index c2be35e7..bf90476f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1214,11 +1214,14 @@ function parse($TEXT, options) { }); }; - function vardefs(no_in, in_const) { + function vardefs(no_in, kind) { var a = []; var def; for (;;) { - var sym_type = in_const ? AST_SymbolConst : AST_SymbolVar; + var sym_type = + kind === "var" ? AST_SymbolVar : + kind === "const" ? AST_SymbolConst : + kind === "let" ? AST_SymbolLet : null; if (is("punc", "{") || is("punc", "[")) { def = new AST_VarDef({ start: S.token, @@ -1287,7 +1290,7 @@ function parse($TEXT, options) { var var_ = function(no_in) { return new AST_Var({ start : prev(), - definitions : vardefs(no_in, false), + definitions : vardefs(no_in, "var"), end : prev() }); }; @@ -1295,7 +1298,7 @@ function parse($TEXT, options) { var let_ = function(no_in) { return new AST_Let({ start : prev(), - definitions : vardefs(no_in, false), + definitions : vardefs(no_in, "let"), end : prev() }); }; @@ -1303,7 +1306,7 @@ function parse($TEXT, options) { var const_ = function() { return new AST_Const({ start : prev(), - definitions : vardefs(false, true), + definitions : vardefs(false, "const"), end : prev() }); }; diff --git a/lib/scope.js b/lib/scope.js index f9241f2d..ef92329b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -106,11 +106,15 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var in_destructuring = null; var in_export; var tw = new TreeWalker(function(node, descend){ - if (options.screw_ie8 && node instanceof AST_Catch) { + var create_a_block_scope = + (options.screw_ie8 && node instanceof AST_Catch) || + ((node instanceof AST_Block) && node.creates_block_scope()); + if (create_a_block_scope) { var save_scope = scope; scope = new AST_Scope(node); scope.init_scope_vars(nesting); scope.parent_scope = save_scope; + scope.is_block_scope = true; descend(); scope = save_scope; return true; @@ -174,7 +178,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node, in_export); + var parent_lambda = defun.parent_scope; + while (parent_lambda.is_block_scope) { + parent_lambda = parent_lambda.parent_scope; + } + (node.scope = parent_lambda).def_function(node, in_export); } else if (node instanceof AST_SymbolClass) { defun.def_variable(node, in_export); @@ -188,8 +196,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolVar - || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node, in_export); + || node instanceof AST_SymbolConst + || node instanceof AST_SymbolLet) { + var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node, in_export); def.constant = node instanceof AST_SymbolConst; def.destructuring = in_destructuring; def.init = tw.parent().value; @@ -279,6 +288,14 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ this.nesting = nesting; // the nesting level of this scope (0 means toplevel) }); +AST_Block.DEFMETHOD("creates_block_scope", function() { + return ( + !(this instanceof AST_Lambda) && + !(this instanceof AST_Toplevel) && + !(this instanceof AST_Class) + ); +}); + AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; @@ -312,17 +329,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){ def = new SymbolDef(this, this.variables.size(), symbol); this.variables.set(symbol.name, def); def.object_destructuring_arg = symbol.object_destructuring_arg; - def.global = !this.parent_scope; - if (symbol instanceof AST_SymbolImport) { - // Imports are not global - def.global = false; - // TODO The real fix comes with block scoping being first class in uglifyJS, - // enabling import definitions to behave like module-level let declarations - } - if (!this.parent_scope && in_export) { - def.global = false; + if (in_export) { def.export = true; } + def.global = !this.parent_scope && !(symbol instanceof AST_SymbolBlockDeclaration); } else { def = this.variables.get(symbol.name); def.orig.push(symbol); @@ -466,7 +476,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ node.mangled_name = name; return true; } - if (options.screw_ie8 && node instanceof AST_SymbolCatch) { + var mangle_with_block_scope = + (options.screw_ie8 && node instanceof AST_SymbolCatch) || + node instanceof AST_SymbolBlockDeclaration; + if (mangle_with_block_scope) { to_mangle.push(node.definition()); return; } diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index d1953ce5..47a38f74 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -31,3 +31,103 @@ do_not_hoist_let: { } } +do_not_remove_anon_blocks_if_they_have_decls: { + input: { + function x() { + { + let x; + } + { + var x; + } + { + const y; + class Zee {}; + } + } + { + let y; + } + { + var y; + } + } + expect: { + function x(){ + { + let x + } + var x; + { + const y; + class Zee {} + } + } + { + let y + } + var y; + } +} + +remove_unused_in_global_block: { + options = { + unused: true, + } + input: { + { + let x; + const y; + class Zee {}; + var w; + } + let ex; + const why; + class Zed {}; + var wut; + console.log(x, y, Zee); + } + expect: { + var w; + var wut; + console.log(x, y, Zee); + } +} + +regression_block_scope_resolves: { + mangle = { }; + options = { + dead_code: false + }; + input: { + (function () { + if(1) { + let x; + const y; + class Zee {}; + } + if(1) { + let ex; + const why; + class Zi {}; + } + console.log(x, y, Zee, ex, why, Zi); + }()); + } + expect: { + (function () { + if (1) { + let a; + const b; + class c {}; + } + if (1) { + let a; + const b; + class c {}; + } + console.log(x, y, Zee, ex, why, Zi); + }()); + } +} + diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 5009ae1e..dddc9159 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -87,3 +87,25 @@ dead_code_constant_boolean_should_warn_more: { var moo; } } + +dead_code_block_decls_die: { + options = { + dead_code : true, + conditionals : true, + booleans : true, + evaluate : true + }; + input: { + if (0) { + let foo = 6; + const bar = 12; + class Baz {}; + var qux; + } + console.log(foo, bar, Baz); + } + expect: { + var qux; + console.log(foo, bar, Baz); + } +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index eebb81c6..5c4ffde9 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -164,6 +164,72 @@ used_var_in_catch: { } } +unused_block_decls_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + let x = 10; + const y = 10; + class Zee {}; + } + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) {} + } + } +} + +used_block_decls_in_catch: { + options = { unused: true }; + input: { + function foo() { + try { + foo(); + } catch(ex) { + let x = 10; + const y = 10; + class Zee {}; + } + console.log(x, y, Zee); + } + } + expect: { + function foo() { + try { + foo(); + } catch(ex) {} + console.log(x, y, Zee); + } + } +} + +unused_block_decls: { + options = { unused: true }; + input: { + function foo() { + { + const x; + } + { + let y; + } + console.log(x, y); + } + } + expect: { + function foo() { + console.log(x, y); + } + } +} + unused_keep_harmony_destructuring: { options = { unused: true }; input: {