Uglifyjs: `let` bug in `reduce_vars`

Created on 10 Jan 2018  路  14Comments  路  Source: mishoo/UglifyJS

Bug report or feature request?

bug

ES5 or ES6+ input?

ES6

Uglify version (uglifyjs -V)

uglify-es 3.3.5

JavaScript input

Bug found running npm test on uglified [email protected] source code before they switched from ES6 to TypeScript:

$ curl -s https://raw.githubusercontent.com/rollup/rollup/v0.53.0/src/utils/transform.js > transform.js
$ bin/uglifyjs transform.js -bc reduce_vars=0 > 0.js
$ bin/uglifyjs transform.js -bc reduce_vars=1 > 1.js
$ diff -u 0.js 1.js
--- 0.js    2018-01-09 19:27:23.000000000 -0500
+++ 1.js    2018-01-09 19:27:34.000000000 -0500
@@ -49,7 +49,7 @@
             try {
                 transformed = plugin.transform.call(context, previous, id);
             } catch (err) {
-                throwing || context.error(err), error(err);
+                0, error(err);
             }
             return Promise.resolve(transformed).then(result => null == result ? previous : ("string" == typeof result ? result = {
                 code: result,

Reduced test case:

$ cat let_reduce_vars.js 
(function() {
    let bar;
    const unused = function() {
        bar = true;
    };
    if (!bar) {
        console.log(1);
    }
    console.log(2);
}());
$ cat let_reduce_vars.js | node
1
2



md5-1d72dae677dd8ee6f450369dd428b9b5



$ cat let_reduce_vars.js | bin/uglifyjs -bc reduce_vars=0 | node
1
2



md5-1d72dae677dd8ee6f450369dd428b9b5



$ cat let_reduce_vars.js | bin/uglifyjs -bc reduce_vars=1 | node
2



md5-1d72dae677dd8ee6f450369dd428b9b5



$ cat let_reduce_vars.js | bin/uglifyjs -bc reduce_vars=1
console.log(2);
bug harmony

All 14 comments

Worked correctly in last release:

$ bin/uglifyjs -V
uglify-es 3.3.4

```
$ cat let_reduce_vars.js | bin/uglifyjs -bc | node
1
2

```js
$ cat let_reduce_vars.js | bin/uglifyjs -bc
!function() {
    let bar;
    bar || console.log(1), console.log(2);
}();

related issue: #2747

Noticed if the let variable is initialized then the bug goes away:

$ bin/uglifyjs -V
uglify-es 3.3.5
$ cat let_reduce_vars_2.js 
(function() {
    let bar = void 0;
    const unused = function() {
        bar = true;
    };
    if (!bar) {
        console.log(1);
    }
    console.log(2);
}());



md5-1d72dae677dd8ee6f450369dd428b9b5



$ cat let_reduce_vars_2.js | bin/uglifyjs -bc | node
1
2

@alexlamsl This hack will make your skin crawl. It fixes both this issue and #2747 and passes all tests.

diff --git a/lib/ast.js b/lib/ast.js
index 7407bac..dc958a6 100644
--- a/lib/ast.js
+++ b/lib/ast.js
@@ -1086,8 +1086,11 @@ var AST_NaN = DEFNODE("NaN", null, {
     value: 0/0
 }, AST_Atom);

-var AST_Undefined = DEFNODE("Undefined", null, {
+var AST_Undefined = DEFNODE("Undefined", "synthetic", {
     $documentation: "The `undefined` value",
+    $propdoc: {
+        synthetic: "[Boolean] is this a synthetic `undefined` value",
+    },
     value: (function(){}())
 }, AST_Atom);

diff --git a/lib/compress.js b/lib/compress.js
index e80d7a9..2ad784a 100644
--- a/lib/compress.js
+++ b/lib/compress.js
@@ -5325,6 +5325,7 @@ merge(Compressor.prototype, {
     }

     OPT(AST_Undefined, function(self, compressor){
+        if (self.synthetic) return self;
         if (compressor.option("unsafe")) {
             var undef = find_variable(compressor, "undefined");
             if (undef) {
diff --git a/lib/output.js b/lib/output.js
index b2353a4..dc86920 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1474,7 +1474,7 @@ function OutputStream(options) {

     DEFPRINT(AST_VarDef, function(self, output){
         self.name.print(output);
-        if (self.value) {
+        if (self.value && !self.value.synthetic) {
             output.space();
             output.print("=");
             output.space();
diff --git a/lib/parse.js b/lib/parse.js
index 9387ad9..67a88d7 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -1863,7 +1863,8 @@ function parse($TEXT, options) {
                     value : is("operator", "=")
                         ? (next(), expression(false, no_in))
                         : !no_in && kind === "const"
-                            ? croak("Missing initializer in const declaration") : null,
+                            ? croak("Missing initializer in const declaration")
+                            : kind == "let" ? new AST_Undefined({synthetic: true}) : null,
                     end   : prev()
                 });
                 if (def.name.name == "import") croak("Unexpected token: import");

```js
$ cat let_reduce_vars.js | bin/uglifyjs -bc passes=1
!function() {
let bar;
bar || console.log(1), console.log(2);
}();

```js
$ cat let_reduce_vars.js | bin/uglifyjs -bc passes=2
console.log(1), console.log(2);

I welcome a better fix.

Interesting concept - you may be able to achieve the same effects with SymbolDef.init = null:

--- a/lib/scope.js
+++ b/lib/scope.js
@@ -202,7 +202,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
             || node instanceof AST_SymbolConst) {
             var def;
             if (node instanceof AST_SymbolBlockDeclaration) {
-                def = scope.def_variable(node);
+                def = scope.def_variable(node, null);
             } else {
                 def = defun.def_variable(node, node.TYPE == "SymbolVar" ? null : undefined);
             }

Your proposed patch fixes this issue:

$ cat let_reduce_vars.js | bin/uglifyjs -bc passes=2
console.log(1), console.log(2);

but not #2747:

$ cat t2747b.js | bin/uglifyjs -bc passes=3 | node
[stdin]:4
    return 0 === baz ? null : r = baz > 2 ? 4 : 5;
                                ^
ReferenceError: r is not defined

Thanks for validation - I'll put that one-liner & your test case into a new PR in a moment.

@alexlamsl I'll leave these issues with you.

The uninitialized lets seen in both these issues are rather common in ES6 code. Rollup is a case in point.

The combo of PRs are up - let's hope Travis NAT fixes lead to CI testing of this project instead of test-compiling node from scratch...

@kzc thanks a lot for https://github.com/mishoo/UglifyJS2/issues/2757#issuecomment-356529719 - it gives vital clues towards the root cause.

The combination of PRs #2759 and #2760 allow Rollup to pass "npm test" after its ES6 sources were uglified.

@alexlamsl Why did changing the init value from undefined to null in this scope patch work anyway?

-                def = scope.def_variable(node);
+                def = scope.def_variable(node, null);

Where is SymbolDef.init used in a non-boolean context where the undefined versus null distinction would make a difference?

SymbolDef.init is used to prime SymbolDef.fixed:
https://github.com/mishoo/UglifyJS2/blob/6a0af85c8bf8673498e89ad7226d6e8cc80b2c15/lib/compress.js#L319-L320

Setting it to null signals the variable is safe for use without initialisation:
https://github.com/mishoo/UglifyJS2/blob/6a0af85c8bf8673498e89ad7226d6e8cc80b2c15/lib/compress.js#L625-L626

Upon first read, it will materialise into AST_Undefined:
https://github.com/mishoo/UglifyJS2/blob/6a0af85c8bf8673498e89ad7226d6e8cc80b2c15/lib/compress.js#L357-L361

Ok, I see. Thanks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gabmontes picture gabmontes  路  5Comments

Havunen picture Havunen  路  5Comments

alexlamsl picture alexlamsl  路  4Comments

diegocr picture diegocr  路  3Comments

buu700 picture buu700  路  5Comments