-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: compound function call unsafe to combine #1631
Comments
Details: https://qfox.nl/weblog/404 |
Looks like ES6: https://github.com/mishoo/UglifyJS2#uglifyjs-2
With the stated version of
|
Ah excuse me, the problem is not related to @alexlamsl Please re-open this. The problem is the compound operator. I'll update the original description not to use |
With the updated example I get: function f(){return x=20,5}var x=10,y=f();x+=y; which gives me |
Cannot reproduce with default compress options:
Please provide an example that will fail from the command line as per example above. |
Fair enough. The case was burried in a large auto-generated build file. Here's the concise test case that breaks:
The problem being
|
Thank you for the working example.
|
@kzc so I think this is related to our discovery back in #1620 (comment), and in this case |
I have a fix. Some --- a/lib/compress.js
+++ b/lib/compress.js
@@ -614,6 +614,7 @@ merge(Compressor.prototype, {
|| node instanceof AST_IterationStatement
|| (parent instanceof AST_If && node !== parent.condition)
|| (parent instanceof AST_Conditional && node !== parent.condition)
+ || (node instanceof AST_Assign && node.operator.length > 1)
|| (parent instanceof AST_Binary
&& (parent.operator == "&&" || parent.operator == "||")
&& node === parent.right) Edit: patch was revised. |
Maybe fix can be refined if the assignment RHS does not have side effects. |
Trying to construct the test case I noticed that the case was already handled properly in general. It was only when it got wrapped in a local function that it broke. Unfortunately I'm not up to speed with uglify internals to be of much more help. |
Found a similar but different failure mode which #1631 (comment) does not cover: // test.js
var a = 0, b = 1;
function f() {
a = 2;
return 4;
}
function g() {
var t = f();
b = a + t;
return b;
}
console.log(g()); gives: $ node test.js
6
$ uglify-js test.js -c | node
WARN: Collapsing variable t [test.js:8,10]
4
$ uglify-js test.js -c collapse_vars=0 | node
6 |
@alexlamsl Oh boy. That's a problem that is not easily solved. In the expression Tracing into each function for possible side-effects is not tenable - full program flow analysis would be required. This could be a show-stopper for Perhaps a side-effect-free variable used in the statement (such as |
That is probably true, just as with the case of undeclared variables (though for a different reason). |
This is a more general fix that should handle both cases: --- a/lib/compress.js
+++ b/lib/compress.js
@@ -614,6 +614,7 @@ merge(Compressor.prototype, {
|| node instanceof AST_IterationStatement
|| (parent instanceof AST_If && node !== parent.condition)
|| (parent instanceof AST_Conditional && node !== parent.condition)
+ || (node instanceof AST_SymbolRef && node.definition().scope !== self)
|| (parent instanceof AST_Binary
&& (parent.operator == "&&" || parent.operator == "||")
&& node === parent.right) @alexlamsl PTAL |
Bad news 😅 // test.js
function g() {
var a = 0, b = 1;
function f() {
a = 2;
return 4;
}
var t = f();
b = a + t;
return b;
}
console.log(g()); gives: $ node test.js
6
$ uglify-js test.js -c reduce_vars=0 | node
WARN: Collapsing variable t [test.js:8,10]
WARN: Dropping unused variable b [test.js:2,13]
4 But as illustrated, $ uglify-js test.js -c | node
WARN: Dropping unused function f [test.js:3,11]
WARN: Dropping unused variable b [test.js:2,13]
6
$ uglify-js test.js -c
WARN: Dropping unused function f [test.js:3,11]
WARN: Dropping unused variable b [test.js:2,13]
function g(){var a=0,t=function(){return a=2,4}();return a+t}console.log(g()); |
Why would scope make a difference here? function g() {
var a = 0, b = 1;
function f() {
a = 2;
return 4;
}
var t = f();
b = a + t;
return b;
}
console.log(g()); Same scope, why would this not have the same problem? |
A function call can only change the value of a |
It would have worked, I think, if The external function can never alter the inner/same-scope variable, that's true. But the inner function could - but so could the |
@kzc #1631 (comment) is tested with #1631 (comment) applied. |
It would never end - everything within it would have to scanned as well, ad nauseam. |
... except that inner function is already being scanned, which is why Can we reuse that information somehow? |
I see that but how exactly did
|
Every function called within it would have to be scanned as well. It's non-trivial at that point. |
It optimises |
Only if they are also function defined within this scope. Any outer functions won't be able to access the inner variable in question. And any functions within this scope would have been scanned anyway, so no need to trace and re-scan? |
The |
Nice. If you're good with the newly revised patch, I'm good with it. |
Two mocha failures: // actual
function foo(o){var n=2*o;print("Foo:",n)}var print=console.log.bind(console);
// expected
function foo(o){print("Foo:",2*o)}var print=console.log.bind(console); // actual
var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){var o=2*n;print("Foo:",o)}(11);
// expected
var print=console.log.bind(console);print("qux",function(n){return 3*n}(3),function(n){return n/2}(12)),function(n){print("Foo:",2*n)}(11); |
Ah, they have the same issue - |
We can live with that I think. |
... and because we can't tell So yeah, let's accept this sub-optimal case for now. |
@alexlamsl Thanks for your help. |
@kzc any time! 😎 |
Great work. Thanks! |
I've been thinking about the collapse var general case. You don't know what happens underwater with observable (and affectable) side effects. I think you can't really deal with this. Consider this contrived example: function f() {
var a = 'fail';
function g() {
a = 'passe';
return 's';
}
var o = {
toString: g,
};
var t = '' + o;
a += t;
console.log(a);
}
f(); // "passes"
$ node_modules/.bin/uglifyjs z -c
WARN: Collapsing variable t [z:15,7]
WARN: Collapsing variable o [z:14,15]
function f(){function g(){return a="passe","s"}var a="fail";a+=""+{toString:g},console.log(a)}f();
// "fails" (Side note: that could have safely been While the inlining of the object is nice, the point is that the assignment of the function may happen sneakily. The string concat may look innocent but it may not be. I'm not sure you could ever statically detect this case. There might be a generic workaround by rewriting On that note, here's another case that you may want to consider; function f() {
var a = 'fail';
function g() {
a = 'passe';
return 's';
}
var o = {
toString: g,
};
var t = '' + o;
a = a + t;
console.log(a);
}
f();
to be precise; it rewrites the explicit The Now Initially I thought there were counter examples to that rewrite but right now I can't think of any. All examples I can think of actually rely on the compound assignment "caching" the value like |
And #1631 (comment) is going to be an even worse problem for ES6+ code. |
Your example works on the latest version of
|
That's great (I mean that) but do you think it holds in the generic case? This is contrived and the artifact is pretty obvious and only contained in the same scope. Does it actually taint and track tainted variables to conclude |
If you are asking for a formal mathematical proof on the level of how Regular Expression is computable and correct, no I haven't come up with one yet. I think we are taking the approach of incremental progress, which means if you can find a corner case which |
I'm just worried that somewhere a developer is going to cry because his code magically breaks due to a minifier being too eager. However, I can't judge on how safe the heuristics are for applying the collapse-var rewrite and I think you can. So while you don't have to proof something about regular expressions, you could at least let me know what you think about the generic case outlined. Right now all I see you do is check the explicit test case and bail, like happened when I opened this ticket. It still led to exposing a clear bug. I'm not asking for sugar. But could you at least give me your thoughts about why this is or isn't going to be a problem for uglifyjs? Rather than just copy-paste-run-shrug, again. There's many syntactical cues that could cause the heuristics in uglifyjs to decide to bail on the collapse. Just like the initial example in this ticket needed some more work before actually exposing a bug. In this case it could be the object literal or the literal appearance of So please, if you tell me why you think this generic case won't be a problem for uglifyjs, I could be convinced to dig deeper into building a test case that it fails on. I do have some related background on the matter. |
FWIW, did some quick testing with 2.8.15 and couldn't produce a failing case. So that's good! |
Thanks for the testing. I'm afraid the best I can do is point you to the code, as it is the layout of which we tackle each optimisation. A good place to start is perhaps understanding what |
@qfox This is a volunteer effort. No one is paid to work on Uglify. We work on it because we find it to be interesting. Although we try to ensure code correctness with extensive unit tests while balancing minification size and speed, no results are guaranteed. Every new release can potentially inadvertantly introduce a new bug while fixing an old one despite our best efforts. Users are encouraged to lock a version of |
@kzc Well that wasn't necessary. I think you got me wrong there. I'm not getting paid to help out either. But that also means that I don't have the time to dig deep into uglifyjs code and heuristics. Somebody familiar with the code should be able to point out easily whether or not and why a certain problem would or wouldn't be a problem. That's all I was asking. My offered help here is just as voluntary, sans the credits. Which I don't need. |
@qfox No slight intended. Just offering my perspective on the thankless job of helping to maintain open source projects in general. Any help is welcome - and indeed your bug report brought to light a flaw in collapse_vars. We could certainly beef up testing. In another post you mentioned you have some experience in code fuzzing. We were hoping to build a fuzz test that executes original and minified code and compares the results. There's already some basic fuzzing in |
So, it's old, but here's the fuzzer for my ES5 parser: http://qfox.github.io/zeparser2/test/tokenfuzzer.html It concats random tokens then runs it through eval and through the parser to check whether or not they both pass or fail. It uncovered some obscure bugs in my parser, but also a few super obscure ones in browsers. Good times. You can do a similar approach for minification testing. Create random code, include the emergency brake boilerplate I mentioned for loops, check the outcome, and match that against minified outcome. Rinse and repeat. I know OS can be hard and the reward is knowing the software is used in millions of setups. |
Btw I would recommend writing a fuzzer from scratch. It's quite fun and rewarding, especially once it starts finding its first bugs. They're surprisingly easy to write, especially once you're at the level of maintaining a minifier anyways. Seeing a fuzzer run is just magic :) |
@qfox That's good advice. Thank you. As @alexlamsl mentioned this minifier doesn't have formal proofs to back it up, just heuristics we think are safe and generally produce smaller code. A fuzzer that executes code would certainly help. |
@kzc here's a setup: https://github.com/qfox/uglyfuzzer So far it didn't find anything, and it's relatively slow. I think something that just calls uglifyjs from within a nodejs script would be much faster. But whatever, I just whipped this up quickly. Should get you going if you want to build on that. If you need more help let me know (would suggest a new ticket for it). |
@qfox - Great stuff. Clearly it's already paying off as evidenced by #1639 If you look at |
Bug report or feature request?
bug
uglify-js
version (uglifyjs -V
)uglify-js 2.8.14
JavaScript input - ideally as small as possible.
(code updated)
uglifyjs
CLI command executed orminify()
options used.Got bitten by this bug today because there was something asserting the variable (
y
above) which was stripped from dist build. So when minifying that build it would make thatx += f()
which broke the build.The text was updated successfully, but these errors were encountered: