mishoo / UglifyJS

JavaScript parser / mangler / compressor / beautifier toolkit

Home Page:http://lisperator.net/uglifyjs/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Class declaration in IIFE considered as side effect

sonicoder86 opened this issue · comments

I'm sending a bug report

When i declare a class inside an IIFE, but class is not used, it isn't stripped by UglifyJS, because it is considered as a side effect.

var V6Engine = (function () {
    function V6Engine() {
    }
    V6Engine.prototype.toString = function () {
        return 'V6';
    };
    return V6Engine;
}());

The warning i get:

WARN: Side effects in initialization of unused variable V6Engine [./dist/car.bundle.js:74,4]

I suspect it is because it assigns values to the prototype.
The biggest pain point with it that it blocks tree-shaking of exported classes generated by Typescript.

Can it be fixed or is their a class declaration that is not considered a side effect?
Here is an example repository where i use UglifyJS through Webpack with Typescript.

cc @DanielRosenwasser This may be of interest to you. This prevents TS users from being able to have tree shaking/dead code elim with UglifyJs

@mishoo if we can help resolve this, please point us in the right direction.

commented

@BlackSonic Uglify doesn't perform program flow analysis. It won't drop the code because of the the side effects you've noted.

You probably want to use something like rollup which supports that sort of speculative unused code dropping.

Since I don't understand specifically what you mean by speculative code dropping, could you elaborate on this please?

commented

One truly doesn't truly know the global impact of any program unless you run it or use SSA. Barring that you can trap all assignments and make some assumptions as to what is likely going on. That seems to be what rollup is doing. But one could craft code to make such a scheme fail. That's why it's speculative.

True, it is possible one could write something like export const impure = (() => window.foo = 'bar')(), which would have different global effects depending on whether it is removed or not.

The problem is the interaction with pure declarative code that ends up using IIFE by accident of transpilation...

Speaking of "pure declarative", assuming uglify got support for this use case, how would decorators sit in this? Is there something to prevent, say, a class decorator from having global side-effects? I presume class and static member decorators have to execute at class declaration time, so if they can have side-effects, tree-shaking would affect the actual runtime result of the code.

commented

Certain transforms are simpler to perform at the higher ES6 level, rather than with the lower level transpiled ES5 where it's more difficult to reason about side effects.

The following is using Uglify's harmony branch. Notice that the ES6 class Foo will be dropped by uglify harmony if not referenced:

$ echo 'class Foo { method(){console.log("method");} };' | bin/uglifyjs -c

WARN: Dropping unused function Foo [-:1,6]

Uglify is pretty conservative in dropping code. Rollup appears to have more aggressive code dropping heuristics.

commented

Uglify's harmony branch for ES6 is still a work in progress and doesn't support class decorators as far as I know. @avdg could better answer what's supported in harmony.

On es 6 I'm more focused on getting stuff working (I prefer wide support
and less compression rather than supporting only few es6 features and have
scripts without any compression).

But we still haven't good destructuring or modules support for example.
There is a lot to be done there on the parser and AST side.

We have to make decisions on where to spend our time if we can't spend our
time on implementing all features. (Although, fixes are mostly relative
small, maybe we should keep more obvious things in the repo so others who
want can fix them instead?).

Although where possible, there is some constant evaluation being done, for
objects and classes that applies to computed properties (still needs to be
merged with harmony though).

Feel free to open issues with clear examples where UglifyJS could do a
better job, but UglifyJS like nearly (or) all compilers will have
limitations and some transformations may be unsafe due to a lack of agreed
concensus with the compiler on what is safe to compile.

Op ma 22 aug. 2016 05:20 schreef kzc notifications@github.com:

Uglify's harmony branch for ES6 is still a work in progress and doesn't
support class decorators as far as I know. @avdg https://github.com/avdg
could better answer what's supported in harmony.


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
#1261 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAM9GPsIdPloMwdaQWy-0vyvL6bbtQyXks5qiRVigaJpZM4JpWFl
.

Would it be possible to add this to the list of 'unsafe' compress features. These wrapped IIEF's are generated from transpilers? This is a pretty important optimization to both babel and Typescript users. I can't think of a code example where a transpiler would output cases like the one @kzc described due to the generative nature of the code.

commented

It's a non-trivial amount of work to recognize code for ES6 classes converted to ES5 IIFEs and drop them if the side effects are collectively deemed to be acceptable to ignore. Uglify does not have any program flow analysis. Pull requests are welcome.

As I mentioned before, dealing with this issue at a higher level with rollup is probably your best bet.

@kzc Is it a trivial amount of work to detect an IIFE or is it also considered program flow analysis? If not, with the flag one can decide if the codebase and the packages used can be trusted and no global variable manipulation occurs there.

commented

IIFEs are trivial to detect. Deciding whether the code in them can be considered pure and side-effect free in the context of the entire program is not.

Then there's the exports assignment issue that I mentioned in the other thread that would prevent the IIFE from being dropped anyway.

If you want to put together a PR to address these issues, go for it.

But it would be the developers choice to decide wether IIFEs can be considered side effect free. The transpiler won't produce side effects.

The exports variable is not interfering with this statement, which causes the side effect detection.

var V6Engine = (function () {
    function V6Engine() {
    }
    V6Engine.prototype.toString = function () {
        return 'V6';
    };
    return V6Engine;
}());

If you consider it trivial can you give us hints on how and where to implement it? At first sight to understanding 1k+ files seemed hard for me who is not familiar with the codebase.

This would be a big help for everyone using Webpack with Babel or Typescript, it would be not a trivial nor easy thing to migrate to Rollup.

commented

Uglify consists of a dozen or so JS files in lib, tools and bin. It's pretty easy to follow.

Some important api docs can be found on http://lisperator.net/uglifyjs/

Is it ok if i add a new option parameter to skip the check on this line?
https://github.com/mishoo/UglifyJS2/blob/8430123e9d2d12442312bbdcdf54004dc6d29c12/lib/compress.js#L1447

@avdg thanks, those docs are really helpful for newcomers

commented

@BlackSonic wrote:

Is it ok if i add a new option parameter to skip the check on this line?

I wouldn't recommend it. It would break a lot of uglified code.

How can this break existing things if it is turned off by default?

commented

Just because you want to drop code with a side effect in a very specific case does not mean it should be done in the general case.

$ echo '(function(){ var x = console.log("whatever"); })();' | uglifyjs -c | node
WARN: Side effects in initialization of unused variable x [-:1,17]
whatever

Maybe put a check there for IIFE also besides the config check?
If so this is where i would ask for your or anyone's help, because i'm not familiar with AST.

commented

I don't think you understand the nature of the problem you're trying to solve. Please re-read: #1261 (comment)

Can you explain it in detail?

var V6Engine = (function () {
    function V6Engine() {
    }
    V6Engine.prototype.toString = function () {
        return 'V6';
    };
    return V6Engine;
}());

This part has nothing to do with the exports assignment. It is not assigned to exports.

commented

I've tried to explain that the problem is not as simple as you believe it to be. Just examine the uglify code and you'll figure it out. Good luck.

This is probably a bad idea due to the additional coupling, but could there be, say, an _uglifyPureIIFE "global meta-function" that, when given a function, in the default case, would be transpiled by uglify to become its argument (_uglifyPureIIFE(function () {})(args) -> +function(){}(args)), but would also allow uglify to treat that function as safe to remove when the result is unused?

commented

Or implement a /** @pure */ annotation to tell the compressor there are no side effects for a function or function call.

I think the problem should be splitted into two parts:

  1. functions need to be removes as unused even when properties are added to the prototype.
!function() {
  function a() {}
  a.prototype.b = 1;
  console.log("x");
}();

should be compressed to (as a is unused)

!function() {
  console.log("x");
}();

Currently uglifyjs is not able to do this, and it's a very general purpose optimization which would be great for many codebases. Especially with Tree Shaking.


  1. Uglifyjs usage tracker should be able to enter IIFE of this special style: var x = (function() { return y; })()
!function() {
  var a = (function() {
    console.log("y");
    return a;
  }());
}();

should compress to

!function() {
  (function() {
    console.log("y");
  }());
}();

Note that the wrapping !function() { /*...*/ }(); is only required to create a separate scope. Elsewise variable would be in the global scope which would make it impossible to mark as unused.


These two steps should be able to solve the TS issue when combined.

commented

@sokra The issues involved are well known. The contributors to this project are aware of javascript scoping rules. Most of uglify's present mangle and compress optimizations would not be possible without respecting these rules. What you are describing is data flow analysis. Uglify does not use a control flow graph. It's a significant amount of work to implement correctly.

I read this as you won't fix it. At least not short-term.

@sokra The changes involved are not trivial to implement, and it's a voluntary effort.

Sure, I just wanted to confirm that you are not working on it, before starting on another solution to the problem.

commented

The other options mentioned in this thread are straight forward:

Drop unused class at ES6 level: #1261 (comment)

Pure function annotation: #1261 (comment)

This issue can be split in two:

  1. IIFE wrappers can be safely removed (in other words, their content can be extracted to the outer scope) if the variables are correctly renamed and the scope isn't top-level. This doesn't seem to involve any sophisticated flow analysis, does it?

    In a non-top-level scope

      var V6Engine = (function () {
          function V6Engine() {
          }
          V6Engine.prototype.toString = function () {
              return 'V6';
          };
          return V6Engine;
      }());

    can be safely replaced with

      function V6Engine_1_() {}
      V6Engine_1_.prototype.toString = function() {
          return 'V6';
      };
      var V6Engine = V6Engine_1_;
  2. Even without an IIFE, foo.prototype.bar = ... isn't removed because Uglify thinks that the prototype.bar setter may have side effects. So a pure_setters option needs to be implemented. Just like the existing pure_getters option, it wouldn't be absolutely safe, yet safe enough for many situations. Some well-known exceptions like location.href probably should be hard-coded.

    UPD: realised that "pure setters" is an oxymoron. Some other name is needed.

@thorn0 Babel would actually use Object.defineProperty, unless you use loose mode; that'd be even worse for uglify as the value of foo.prototype escapes, and it (probably) doesn't know that Object.defineProperty is a standard library function with known behavior.

pure_setters isn't something I'm interested in - by definition assignments are a side effect. Whichever the solution, such an option isn't it.

@rvanvelzen Sure, "pure setters" is a stupid name. What I mean is that this option (named somehow else) would make a setter be considered safe to remove if its only side effect is modifying a (sub)property of a variable that is a candidate for removal.

commented

The author of rollup just did a complete rewrite in the past week to more aggressively remove unreachable code. It now handles removing unreferenced side-effect-free IIFEs like the one at the top of this Issue:

$ node_modules/.bin/rollup -v
rollup version 0.35.9
$ cat i1.js 
var V8Engine = (function() {
    function V8Engine() {}
    V8Engine.prototype.toString = function() { return 'V8'; };
    return V8Engine;
}());
var V6Engine = (function() {
    function V6Engine() {}
    V6Engine.prototype.toString = function() { return 'V6'; };
    return V6Engine;
}());
console.log(new V8Engine().toString());
$ node i1.js 
V8
$ node_modules/.bin/rollup i1.js && echo
var V8Engine = (function() {
    function V8Engine() {}
    V8Engine.prototype.toString = function() { return 'V8'; };
    return V8Engine;
}());
console.log(new V8Engine().toString());
$ node_modules/.bin/rollup i1.js | node
V8

So far, so good.

However, rollup (at time of this writing) still has some issues to iron out with side effects. Below is a modified version of the V6Engine IIFE. See the line marked "side effect":

$ cat i2.js 
var V8Engine = (function() {
    function V8Engine() {}
    V8Engine.prototype.toString = function() { return 'V8'; };
    return V8Engine;
}());
var V6Engine = (function() {
    function V6Engine() {}
    V6Engine.prototype = V8Engine.prototype; // <---- side effect
    V6Engine.prototype.toString = function() { return 'V6'; };
    return V6Engine;
}());
console.log(new V8Engine().toString());

Expected result:

$ node i2.js 
V6

Notice that the modified V6Engine IIFE with side effect was incorrectly dropped by rollup:

$ node_modules/.bin/rollup i2.js && echo
var V8Engine = (function() {
    function V8Engine() {}
    V8Engine.prototype.toString = function() { return 'V8'; };
    return V8Engine;
}());
console.log(new V8Engine().toString());

and produces the incorrect result:

$ node_modules/.bin/rollup i2.js | node
V8

Dead code elimination is a tricky problem.

commented

Reading this thread, I have the impression that this issue is really hard and may go nowhere.
And the fact that some transpilers might use global functions like Object.defineProperty will make it even harder.
Worse: when using decorators (opaque function calls), that's not going to work either.

Isn't the idea of an annotation inside a comment the low-hanging fruit here?
Someone suggested /** @pure */.

  • A comment has no effect when it's not understood;
  • It is removed by all minifiers, so induces no bloat;
  • It would be much easier to implement than some complex flow-analysis and heuristics;
  • It could even speed up analysis as once the function is said pure, its contents can quickly be skipped;
  • It can easily be used in many other places to remove complex code that couldn't be determined to be side-effect free;
  • In fact, one can even use it to lie and remove code that has side-effects but which we might not care about.

A good, important example for the last two bullets:
Consider decorators again. A common use is to store metadata about the class (either on the class itself or some global registry; note that the later is definitely not side-effects free). In that case I could put /** @pure */ on my decorator and so if it the class is not used, it could still be dropped.
Note that conveniently, the decorator result has to be used and linked back to the class, like so:

@meta
class X {}

// converts into:
let X = function() {
  class X {}
  X = meta(X) || X;
  return X;
}();

which makes this work (the decorator call has to be kept when the class is used).

I only see two drawbacks:

  • We need to convince code generators (Typescript, Babel & co.) to include it in their output.
  • It's not a generic solution that all existing code would benefit from. But given how hard this problem seems to be, I think /** @pure */ might actually be more generally useful!

What do you think?

I like the pure annotation.

A little thing: I know IE8 is dead but is /** @ affect by the conditional compilation bug on IE? Should we switch to /** #pure */ instead. Same reason because // @sourceMappingURL was changed to // #sourceMappingURL.

commented

convince code generators (Typescript, Babel & co.) to include it in their output

Good luck with that.

FWIW, this will support pure annotation at the function call level:

--- a/lib/compress.js
+++ b/lib/compress.js
@@ -1273,7 +1273,14 @@ merge(Compressor.prototype, {
         def(AST_Constant, return_false);
         def(AST_This, return_false);
 
+        var pure_regex = /[@#]pure/;
         def(AST_Call, function(compressor){
+            if (this.start
+                && this.start.comments_before
+                && this.start.comments_before.length
+                && pure_regex.test(this.start.comments_before[0].value)) {
+                return false;
+            }
             var pure = compressor.option("pure_funcs");
             if (!pure) return true;
             if (typeof pure == "function") return pure(this);
$ echo '(function(){console.log("class iife");})(); foo(); bar();' | bin/uglifyjs -c
!function(){console.log("class iife")}(),foo(),bar();
$ echo '/*@pure*/(function(){console.log("class iife");})(); foo(); /*#pure*/bar();' | bin/uglifyjs -c
WARN: Dropping side-effect-free statement [-:1,9]
WARN: Dropping side-effect-free statement [-:1,69]
foo();
commented

Good luck with that

I don't know. MS / Typescript is quite open and reaches out to major users. It's quite trivial for them to add a comment in their template and if it enables massive reductions when minified with the most popular JS minifier I think they'll consider it.

And rather than speculating, if Uglify is seriously considering this option it's easy to ask them and find out.

commented

@kzc Those issues are not the same thing, they ask for language extensions.

So I created a new issue to ask if they would add a comment in the emit: microsoft/TypeScript#13721.
We'll see what MS answers.
If you want to see this happen make your voice heard over there.

commented

Evidently Typescript static class members can have side effects even if the class is not referenced:

webpack/webpack#2899 (comment)

class Foo {
  static bar = (() => { console.log('side-effect :(') })()
}

===>

var Foo = (function () {
    function Foo() {
    }
    return Foo;
}());
Foo.bar = (function () { console.log('side-effect :('); })();

Luckily if the IIFE was @pure annotated and uglified with the annotation patch it would not drop the code because of the reference to Foo in Foo.bar.

commented

@kzc
if a static initializer can be proved side-effects free Foo.bar = 42
or if a decorator is marked pure Foo = /** #pure */decorator(Foo) || Foo;
and Foo is not used anywhere else,
would uglify be able to remove it?

I have a feeling that in the second case maybe it could, but the first is more difficult because assigning bar might have a side-effect :(

commented

@kzc @sokra Just in case you don't watch the issue I linked in my previous comment, here's the answer:

If uglify adds this support we can consider emitting the comment. I am assuming this does not apply to classes with static intializers or decorated ones.
I am also assuming this applies to enums, and namespaces with only "side-effect-free" class declarations

commented

@sokra Can you confirm that the patch in #1261 (comment) is acceptable for webpack's purposes?

Notice that the pure annotation is constrained to a comment immediately before a function call, so it would also work for class IIFEs. Should a pure function call return value be assigned to a var that is not referenced, it would only be dropped by uglify if the var is declared within a function.

For example, given this input:

$ cat pure.js
var iife1 = /*@pure*/(function(){ return 1; })();
(function(){
    var iife2 = /*@pure*/(function(){ return 2; })();
})();

output without uglify patch:

$ uglifyjs pure.js -c
WARN: Side effects in initialization of unused variable iife2 [pure.js:3,8]
var iife1=function(){return 1}();!function(){(function(){return 2})()}();

output with uglify patch:

$ bin/uglifyjs pure.js -c
WARN: Dropping unused variable iife2 [pure.js:3,8]
WARN: Dropping side-effect-free statement [pure.js:2,0]
var iife1=function(){return 1}();

Notice that iife1 was not dropped, as uglify has always behaved with unreferenced side-effect-free top level vars.

Assuming the patch is fine, I will create a PR for an upcoming uglify release.

commented

if a static initializer can be proved side-effects free Foo.bar = 42 would uglify be able to remove it?

No.

The act of adding the property bar to Foo is considered a side effect from uglify's point of view. As often mentioned in this thread, uglify lacks data flow analysis.

Rollup could probably handle this case though.

commented

Just to clarify, for this input:

class Foo {
    static bar = 42;
}

Typescript produces:

var Foo = (function () {
    function Foo() {
    }
    return Foo;
}());
Foo.bar = 42;   // <----

In this situation uglify would not be able to drop var Foo and Foo.bar with a pure annotation.

However, if Typescript were to output:

var Foo = /*#pure*/(function () {
    function Foo() {
    }
    Foo.bar = 42;    // <----
    return Foo;
}());

then uglify would be able to drop an unreferenced var Foo with the pure annotation because Foo.bar = 42; is declared within the IIFE deemed to be "pure".

So it's up to the Typescript folks.

@kzc I haven't tried the patch. That's something for @mishoo to validate. Best you just start a PR and let him and us review it.

We should have a clear spec for the annotation before starting to implement it. Uglify doesn't have to implement optimization for the whole spec, we can start with a part of it.

I would propose something like this for the spec:


If comment content matches /^\s*#\s*pure\s*$/ followed by an expression: tools can assume that this expression doesn't have side-effects (other than the expression value) when evaluated. It has precedence between Assignment and Conditional.

If comment content matches /^\s*#\s*pure\s*$/ followed by a declaration statement: tools can assume that using the declared name doesn't have side-effect (other than the return value). I. e. calling the declared name doesn't have side-effects, accessing properties of the declared name doesn't have side-effects. Calling properties may have side-effects. This is a flat annotation.

If comment content matches /^\s*#\s*pure\s*$/ followed by an import statement: tools can assume that this import doesn't have side-effect (other than the imported bindings).

If comment content matches /^\s*#\s*pure\s*$/ followed by an export from statement: tools can assume that this import doesn't have side-effect (other than the reexported bindings).

If comment content matches /^\s*#\s*pure\s*$/ followed by an export statement: This is equal to using pure on the declaration.

If comment content matches /^\s*#\s*pure\s*$/ followed by an export default statement: This is equal to using pure on the expression.

If comment content matches /^\s*#\s*pure\s*$/ followed by anything else: currently ignored

Tools may ignore any pure annotation if they want too.

Examples:

/*#pure*/new X() constructor has no side-effects, if return value is not used it can be removed.

/*#pure*/a.b.c.d getters b, c, d have no side-effects.

/*#pure*/function myFunc() { ... } calling myFunc() has no side-effects, if return value is not used it can be removed.

/*#pure*/var myFunc = abc(); calling myFunc() has no side-effects, if return value is not used it can be removed. This annotation doesn't affect abc(). this may have side-effects.

var myFunc = /*#pure*/abc(); calling myFunc() may have side-effects. Calling abc() has no side-effects.

/*#pure*/var myFunc = /*#pure*/abc(); This handles both.

/*#pure*/var obj = { a: function() { ... } } obj.a has no side-effects. obj.a() may have side-effects.

var x = /*#pure*/new X() cannot be removed when x is used. Return value is not affected by pure.

(/*#pure*/a(), b(), c()) This only affects a().

(/*#pure*/a.b = c.d) this only affects a.b. (getter b of object a has no side-effect)

(/*pure*/a() ? b() : c()) this affects a() ? b() : c().

/*#pure*/import { a } from "./a" importing file ./a has no side-effects. If a is not used this statement can be removed. ./a don't have to be imported.

/*#pure*/export * from "./b" importing file ./b has no side-effects. If exports of ./b are not used in modules importing this module, ./b don't have to be imported.

You can use /*#pure*/ also if the stuff has side-effects but you are not interested whether they are executed or no. /*#pure*/(sideeffect); It's undefined if sideeffect is executed or not.

/*#pure*/export default new X() constructor has no side-effects, if exported value is not used export statement can be removed.

/*pure*/export let a = b() == export /*#pure*/let a = b() != export let a = /*#pure*/b()


Note: the import export stuff is not relevant for uglify. It's relevant for module bundlers.

commented

@sokra

If comment content matches /^\s*#\spure\s$/ followed by an import statement: tools can assume that this import doesn't have side-effect (other than the imported bindings).
If comment content matches /^\s*#\spure\s$/ followed by an export from statement: tools can assume that this import doesn't have side-effect (other than the reexported bindings).

This seems backwards. It's hard for the consumer to know if the imported module is side-effect free or not. This could change over time with new versions. It would seem more logical that the module itself says if it's side-effect free or not.

The idea is very good though, because currently webpack tree-shakes exports but often has a hard time removing imports completely, for it's almost impossible to prove that it's side-effect free.

What about if an annotation is found in a module, then it means importing it is side-effect free.
Maybe /*#pure*/ module: or /*#pure*/"module"; or /*#pure module*/ or just /*#pure*/; at the top of file (let's say before any code)?

commented

@sokra Sorry for not being more clear. The pure annotation I'm proposing will only handle a comment immediately before a function call - not before var or any other node type. This should be adequate for dropping unused Typescript-generated class IIFEs if properly annotated. PR to follow.

commented

Fix: #1448

The pure annotation comment regex is now /[@#]__PURE__/

So looking at the example in #1261 (comment) I can see two optimisations which should make it work:

  • function f() {var a; a = 1;} ➡️ function f() {}
  • !function() {return 1}() ➡️ eliminated

Obviously there may be more complex cases generated by TypeScript or others which will still require /* @__PURE__ */ to aid optimisation efforts.

commented

@alexlamsl I don't think the problem is variable assignment, the problem is object assignment:

function f() { var a = {}; a.foo = 1; return a; }

Because assignment to an object can have arbitrary side effects (because of getters/setters), and because UglifyJS doesn't do data flow analysis, it can't determine that the above code is safe to remove.

commented

For what it's worth, the pure function call comment annotation feature has been released in uglify-js@2.8.1:

$ echo 'foo(); var a=/*#__PURE__*/(function(){console.log("Hello");}()); bar();' | bin/uglifyjs -c toplevel
WARN: Dropping __PURE__ call [-:1,26]
WARN: Dropping unused variable a [-:1,11]
foo();bar();

@sokra any idea when this will land into webpack ?

commented

Before TS might release this in class emit, I am wondering if we should ask them some support for decorators.

Decorators being arbitrary functions, it's nearly impossible to detect if they have side effects or not. So any class with a decorator is going to be kept, even when unused.

Unfortunately, frameworks like Angular or Aurelia make extensive use of decorators, since they are a very natural syntax to attach metadata.

Typescript generates this code when using decorators:

// class emit:
var ClassX = /*#__PURE__*/(...);
// decorators emit:
ClassX = /*(1)*/__decorate([deco1, deco2, deco3], ClassX);

Question: if there was a __PURE__ annotation in (1), would uglify completely drop the ClassX?

My idea behind this is to ask TS to also move a pure comment before decorators in this spot.

Concretely, if I know my decorators are harmless and I want my class to be dismissable (particularly in large frameworks/libs) I would write this:

/*#__PURE__*/
@cacheable
class X { }

And TS could maybe move that pure comment in spot (1) above, and my class could be dropped completely...

An even better option but depending on TS goodwill would be to annotate the decorators themselves and have the annotation inserted automatically when applicable. :/

commented

@jods4 Sorry, decorators are out of scope for Uglify and should be implemented at the higher level language level.

By the way, dropping unused classes is easy at the ES6 level and uglify harmony already does this without any need for annotations. It's only at the ES5 generated code level does this problem become difficult because any ES5 IIFE can potentially have side effects:

$ echo 'class Foo{ bar(){} } baz();' | bin/uglifyjs -c toplevel
WARN: Dropping unused function Foo [-:1,6]
baz();
commented

@kzc I know.
The poorly expressed question was:
If TS emitted the following for a class with decorators:

var ClassX = /*#__PURE__*/(function() { ... }());
ClassX = /*#__PURE__*/__decorate([deco1, deco2, deco3], ClassX);

Would it be completely dropped ?

commented

With uglify-js@2.8.10 neither statement above would be dropped because ClassX is assigned to twice. There is no data flow analysis in Uglify, so the worst case was assumed and the code was retained.

However, the following would work:

$ cat q.js 

    var ClassX = /*#__PURE__*/(function() { whatever(); }());
    var DecoratedClassX = /*#__PURE__*/__decorate([deco1, deco2, deco3], ClassX);

$ bin/uglifyjs q.js -c toplevel,passes=2

    WARN: Dropping __PURE__ call [q.js:3,39]
    WARN: Side effects in initialization of unused variable DecoratedClassX [q.js:3,8]
    WARN: Dropping __PURE__ call [q.js:2,30]
    WARN: Dropping unused variable ClassX [q.js:2,8]

    deco1,deco2,deco3;

as would:

$ cat r.js

    var ClassX = /*#__PURE__*/__decorate([deco1, deco2, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );

$ bin/uglifyjs r.js -c toplevel

    WARN: Dropping __PURE__ call [r.js:2,30]
    WARN: Dropping __PURE__ call [r.js:3,21]
    WARN: Side effects in initialization of unused variable ClassX [r.js:2,8]

    deco1,deco2,deco3;

The reason that the arguments deco1,deco2,deco3 are not dropped is because they are not declared and considered global and could throw if not defined. Had they been declared or been constants, then all code would be dropped:

    var deco1, deco2, deco3;
    var ClassX = /*#__PURE__*/__decorate([deco1, deco2, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );
commented

interesting, thanks!

commented

Furthermore, if the decorators were functions not used elsewhere then they would also be dropped accordingly:

$ cat s.js

    function deco1(obj) { obj.one   = 1; return obj; }
    function deco2(obj) { obj.two   = 2; return obj; }
    function deco3(obj) { obj.three = 3; return obj; }

    var ClassX = /*#__PURE__*/__decorate([deco1, deco2, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    var ClassY = /*#__PURE__*/__decorate([deco1, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    var ClassZ = /*#__PURE__*/__decorate([deco1],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    foo( new ClassZ );

$ bin/uglifyjs s.js -c toplevel,passes=2 -b

    WARN: Dropping unused function deco2 [s.js:3,13]
    WARN: Dropping unused function deco3 [s.js:4,13]
    WARN: Dropping __PURE__ call [s.js:6,30]
    WARN: Dropping __PURE__ call [s.js:7,21]
    WARN: Dropping unused variable ClassX [s.js:6,8]
    WARN: Dropping __PURE__ call [s.js:10,30]
    WARN: Dropping __PURE__ call [s.js:11,21]
    WARN: Dropping unused variable ClassY [s.js:10,8]

    function deco1(obj) {
        return obj.one = 1, obj;
    }

    var ClassZ = __decorate([ deco1 ], function() {
        whatever();
    }());

    foo(new ClassZ());
commented

The __decorate function can be declared to be a pure function in an Uglify option so it does not have to be annotated:

$ cat t.js

    function deco1(obj) { obj.one   = 1; return obj; }
    function deco2(obj) { obj.two   = 2; return obj; }
    function deco3(obj) { obj.three = 3; return obj; }

    var ClassX = __decorate([deco1, deco2, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    var ClassY = __decorate([deco1, deco3],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    var ClassZ = __decorate([deco1],
        /*#__PURE__*/(function(){ whatever(); })()
    );

    foo( new ClassZ );

$ bin/uglifyjs t.js -c toplevel,passes=2 -b --pure-funcs __decorate

    WARN: Dropping unused function deco2 [t.js:3,13]
    WARN: Dropping unused function deco3 [t.js:4,13]
    WARN: Dropping __PURE__ call [t.js:7,21]
    WARN: Dropping unused variable ClassX [t.js:6,8]
    WARN: Dropping __PURE__ call [t.js:11,21]
    WARN: Dropping unused variable ClassY [t.js:10,8]

    function deco1(obj) {
        return obj.one = 1, obj;
    }

    var ClassZ = __decorate([ deco1 ], function() {
        whatever();
    }());

    foo(new ClassZ());
commented

@kzc but this is highly dependent of what the invoked decorators actually do!

Some decorators may have wanted side-effects and removing the __decorate call can be plain wrong.
Unlike the class code gen, this is not an always-or-never situation, more a case-by-case basis.

commented

Marking a function as pure via --pure-funcs does not mean it will be necessarily be removed. It will only be removed if its result is not used. Study the example above.

commented

@kzc the point is that even if its result (the class) is not used, you may want to keep it because of decorators side-effects. My point is that you can't say globally __decorate is pure, remove it when the class is not used. It depends on each instantiation.

For example:

// Here the decorators are just metadata, you can drop the class if it's not used
@cacheable
class A {}

const A = /*#__PURE__*/__decorate([cacheable], class { });

// Here the decorator registers the class in a pub-sub event bus, you should NOT remove it!
@subscribe('login')
class B { }

const B = __decorate([subscribe('login')], class { });
commented

Okay, you have the flexibility to annotate specific __decorate calls with /*#__PURE__*/ if you choose.

Related discussion about introducing this annotation in Rollup and Webpack to enable correct (non-speculative) tree shaking: d3/d3#3076

commented

@alexlamsl It appears that you quietly implemented this feature - dropping unused side-effect-free class IIFEs in 0b0eac1:

$ bin/uglifyjs -V
uglify-js 3.3.9
$ cat t1261.js
var Foo = (function() {
    function Foo() {
    }
    Foo.prototype.toString = function() {
        return 'V6';
    };
    return Foo;
}());
var Bar = (function() {
    function Bar() {
    }
    Bar.prototype.toString = function() {
        return 'V8';
    };
    return Bar;
}());
console.log(new Foo().toString());
$ cat t1261.js | bin/uglifyjs -bc toplevel,passes=3
var Foo = function() {
    function Foo() {}
    return Foo.prototype.toString = function() {
        return "V6";
    }, Foo;
}();

console.log(new Foo().toString());

Nice!

@kzc I was staring at those (function(){}).prototype.destroy = ... under test/benchmark.js and thought we can compress those.

I don't think #2612 is general enough – a second Bar.prototype.prop=... would defeat it.