Bug report or feature request?
bug
ES5 or ES6+ input?
ES6
Uglify version (uglifyjs -V)
uglify-es 3.3.8
JavaScript input
var test = (function(){
function inlinedFunction(data) {
return {
children: data,
count: data.reduce(function(a, b) {
return a + b;
})
}
}
function testMinify(){
if(true) {
const data = inlinedFunction([1,2,3]);
if(this.someFunction) {
this.someFunction(data);
}
}
}
return testMinify();
})();
The uglifyjs CLI command executed or minify() options used.
uglifyjs -b -c -m -- test.js
JavaScript output or error produced.
Chrome gives an 'Uncaught ReferenceError: n is not defined' error on line 5 when the minified output is pasted into the console and run, because it's trying to assign to 'n' as a variable and a constant.
The minified output looks like the following:
var test = function() {
return function() {
{
const n = {
children: n = [ 1, 2, 3 ],
count: n.reduce(function(n, t) {
return n + t;
})
};
this.someFunction && this.someFunction(n);
}
var n;
}();
}();
Actually encountered it elsewhere, but this is as simple as I can get the test case. Both the inlined function and the containing function seem to need to use their variables twice, otherwise it's all optimized away. The conditional is also necessary, though it doesn't have to be if(true) , just using that as an example.
@alexlamsl This is a case where the rename pass in [email protected] masked the issue.
$ bin/uglifyjs -V
uglify-es 3.3.7
$
$ cat t2842.js | bin/uglifyjs -bmc | node
$
$ cat t2842.js | bin/uglifyjs -bmc --no-rename | node
[stdin]:5
children: n = [ 1, 2, 3 ],
^
ReferenceError: n is not defined
Thanks, that's useful to know! Both inline=false and inline=1 work, it only gives errors on inline >= 2.
@kzc may be the variable name collision logic of inline not working for AST_SymbolBlockDeclaration inside AST_Block (as opposed to AST_Scope).
Above my pay grade.
Can you suggest a simple fix?
OT - function inlining is a lot trickier than I first thought. So many bloody edge cases.
Let me finish my $day_job in a couple of hours, and I'll see what I can do.
@kzc AST_Node.block_scope is easily lost during compress, so unless we track down and propagate this there is no way inline can safely operate.
So the simplest fix would be to change inline default value to 1 and call it a day.
FWIW, here's a slighly more reduced test case:
(function() {
function inlinedFunction(data) {
return data[data[0]];
}
function testMinify() {
if (true) {
const data = inlinedFunction([1, 2, 3]);
console.log(data);
}
}
return testMinify();
})();
So the simplest fix would be to change inline default value to 1 and call it a day.
Then we'd lose inline=2 for any ES5 code processed with uglify-es.
There must be way to identify whether symbols within the function to be inlined conflict with any non-var variable names in the caller at the point of the call.
Let me think about it.
Then we'd lose
inline=2for any ES5 code processed withuglify-es.
They should be using uglify-js in the first place.
They should be using uglify-js in the first place.
Most new projects favor uglify-es over uglify-js@3.
I haven't looked at the uglify-es download count in a while. Seems to be getting a fair bit of use.
186 170 downloads in the last day
920 228 downloads in the last week
Most new projects favor
uglify-esoveruglify-js@3.
In which case, they are on their own - I have no plans to support harmony in the foreseeable future.
@alexlamsl so, does that mean uglify-es will not get bug fixes/new features from now on?
@krassx you are more than welcomed to work on harmony (uglify-es) - this is an OpenSource project.
I will review and take Pull Request on both branches, but my primary focus is a stable and usable master (uglify-js).
@alexlamsl Correct me if I'm wrong...
For the benefit of those not familiar with the uglify code base, this harmony branch bug (a.k.a. uglify-es) can be fixed in one of three ways from least to most effort:
inline to 1 as discussed above. This is the easiest stop-gap measure but has the disadvantage of not inlining ES5 single-use functions with arguments and/or variables. This is the safest option.rename pass (AST_Toplevel.expand_names) to respect ES6 constructs and block scope. This allows for the inlining of more single-use functions than the other options.@kzc (3) does not really fix the underlying cause of this issue - it just so happens the order of optimize() being called in the particular test case above.
I was just in the process of making a comment edit. ;-)
Edit: Technically option (2) would be also be required if option (3) were to be implemented.
And perhaps the best option:
Jokes aside - I think we are getting close to the point where the issue of block scopes can no longer be relied on half-hearted workarounds within harmony.
Certainly not something I would undertake lightly.
@kzc if you really want to tackle (2), here's what I can contribute to your efforts:
--- a/lib/compress.js
+++ b/lib/compress.js
@@ -4718,10 +4718,15 @@ merge(Compressor.prototype, {
in_loop = [];
} else if (scope instanceof AST_SymbolRef) {
if (scope.fixed_value() instanceof AST_Scope) return false;
+ } else if (scope.block_scope) {
+ scope = scope.block_scope;
}
} while (!(scope instanceof AST_Scope) || scope instanceof AST_Arrow);
var safe_to_inject = !(scope instanceof AST_Toplevel) || compressor.toplevel.vars;
var inline = compressor.option("inline");
+console.error(self.print_to_string());
+console.error(scope.TYPE, scope.var_names());
+console.error();
if (!can_inject_vars(catches, inline >= 3 && safe_to_inject)) return false;
if (!can_inject_args(catches, inline >= 2 && safe_to_inject)) return false;
return !in_loop || in_loop.length == 0 || !is_reachable(fn, in_loop);
$ uglifyjs test.js -bc
function(data){return data[data[0]]}([1,2,3])
Scope { data: true, inlinedFunction: true, console: true }
function(data){return data[data[0]]}([1,2,3])
Function { inlinedFunction: true, console: true, arguments: true }
!function() {
(function() {
{
const data = (data = [ 1, 2, 3 ])[data[0]];
console.log(data);
}
var data;
})();
}();
This is what I meant in https://github.com/mishoo/UglifyJS2/issues/2842#issuecomment-359586277
Jokes aside
@alexlamsl I wasn't kidding. You're doing a fantastic job maintaining and improving uglify in your free time and you should be compensated for it.
if you really want to tackle (2), here's what I can contribute to your efforts
I'll let someone else deal with it. Now that uglify-es is out the door and is on solid footing I was going to step back from this project anyway and now is as good time as any. People using this software every day should step up and contribute.
I found a nice way to fix this. But it's going to take quite some work.
There's already block-scope support in inlining functions, but it's only used for catch blocks.
function can_inject_symbols() {
var catches = Object.create(null);
(...)
if (scope instanceof AST_Catch) {
catches[scope.argname.name] = true;
I propose changing that variable name to block_scoped and adding variables from block scopes to it.
} else if (scope instanceof AST_Block && !(scope instanceof AST_Lambda)) {
scope.block_scope.variables.each(function (variable) {
catches[variable.name] = true;
});
However, it will be quite some work because compress.js destroys and creates blocks like they don't have any scope! For example, our if (true) in this issue, since it always evaluates to true, is compressed to a AST_BlockStatement by a simple make_node call. However, the block_scope property which was duly integrated in scope.js into the if body isn't added to the newly created block statement. Therefore, all information about the block scope is lost.
I'm currently working on finding places where make_node(AST_BlockStatement is called, and passing the block_scope property to it.
False alarm. It looks like since compress.js calls figure_out_scope, the block scopes turn out to be there.
The following code also breaks after function inlining:
Source:
export default ( () => {
return function() {
return def => {
const abc = def( abc );
return foo( abc );
};
}
function foo( abc ) {
abc();
return abc;
}
} )();
After minification:
export default function() {
return def => {
const abc = def(abc);
return (abc = abc)(), abc;
};
var abc;
};
Note: this issue is also causing a problem with babel-plugin-transform-typeof-symbol, which generates this function:
function _typeof(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
This, when inlined, results in something like this (minified):
function u(t) {
return o = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function o(t) {
return typeof t
} : function o(t) {
return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
}, o(t)
}
When executed in the browser (e.g. with browserify), this can cause all kinds of trouble, like overwriting the local module variable (edit: which shouldn't be happening anyway, should it?).
I don't see this behavior anymore in v3.4.8, is that correct?
@larsgw latest version of uglify-js should be working correctly.
If not, please follow the issue template & file a new issue with a reproducible test case.
Most helpful comment
Regression from
[email protected].Workaround: