diff --git a/lib/ast.js b/lib/ast.js index 1ae990d8f..9f6ff9ad0 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -2271,6 +2271,24 @@ var AST_Class = DEFNODE("Class", "name extends properties", function AST_Class(p } }); }, + is_self_referential: function() { + const this_id = this.name && this.name.definition().id; + let found = false; + let class_this = true; + this.visit_nondeferred_class_parts(new TreeWalker((node, descend) => { + if (found) return true; + if (node instanceof AST_This) return (found = class_this); + if (node instanceof AST_SymbolRef) return (found = node.definition().id === this_id); + if (node instanceof AST_Lambda && !(node instanceof AST_Arrow)) { + const class_this_save = class_this; + class_this = false; + descend(); + class_this = class_this_save; + return true; + } + })); + return found; + }, }, AST_Scope /* TODO a class might have a scope but it's not a scope */); var AST_ClassProperty = DEFNODE("ClassProperty", "static quote", function AST_ClassProperty(props) { diff --git a/lib/compress/drop-side-effect-free.js b/lib/compress/drop-side-effect-free.js index 8f31df7fb..75b0bfce6 100644 --- a/lib/compress/drop-side-effect-free.js +++ b/lib/compress/drop-side-effect-free.js @@ -155,9 +155,13 @@ def_drop_side_effect_free(AST_Arrow, return_null); def_drop_side_effect_free(AST_Class, function (compressor) { const with_effects = []; + + if (this.is_self_referential() && this.has_side_effects(compressor)) { + return this; + } + const trimmed_extends = this.extends && this.extends.drop_side_effect_free(compressor); - if (trimmed_extends) - with_effects.push(trimmed_extends); + if (trimmed_extends) with_effects.push(trimmed_extends); for (const prop of this.properties) { if (prop instanceof AST_ClassStaticBlock) { @@ -166,11 +170,7 @@ def_drop_side_effect_free(AST_Class, function (compressor) { } } else { const trimmed_prop = prop.drop_side_effect_free(compressor); - if (trimmed_prop) { - if (trimmed_prop.contains_this()) return this; - - with_effects.push(trimmed_prop); - } + if (trimmed_prop) with_effects.push(trimmed_prop); } } diff --git a/lib/compress/drop-unused.js b/lib/compress/drop-unused.js index 29a3d2875..fb68f22ff 100644 --- a/lib/compress/drop-unused.js +++ b/lib/compress/drop-unused.js @@ -70,7 +70,6 @@ import { AST_SymbolFunarg, AST_SymbolRef, AST_SymbolVar, - AST_This, AST_Toplevel, AST_Unary, AST_Var, @@ -127,7 +126,6 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { return node.expression; } }; - var this_def = null; var in_use_ids = new Map(); var fixed_ids = new Map(); if (self instanceof AST_Toplevel && compressor.top_retain) { @@ -139,6 +137,7 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { } var var_defs_by_id = new Map(); var initializations = new Map(); + var self_referential_classes = new Set(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). @@ -152,6 +151,10 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { }); } if (node === self) return; + if (node instanceof AST_Class && node.has_side_effects(compressor)) { + if (node.is_self_referential()) self_referential_classes.add(node); + node.visit_nondeferred_class_parts(tw); + } if (node instanceof AST_Defun || node instanceof AST_DefClass) { var node_def = node.name.definition(); const in_export = tw.parent() instanceof AST_Export; @@ -161,22 +164,11 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { } } - if (node instanceof AST_DefClass && node.has_side_effects(compressor)) { - const save_this_def = this_def; - this_def = node_def; - node.visit_nondeferred_class_parts(tw); - this_def = save_this_def; - } - map_add(initializations, node_def.id, node); return true; // don't go in nested scopes } // In the root scope, we drop things. In inner scopes, we just check for uses. const in_root_scope = scope === self; - if (node instanceof AST_This && this_def && in_root_scope) { - in_use_ids.set(this_def.id, this_def); - return true; - } if (node instanceof AST_SymbolFunarg && in_root_scope) { map_add(var_defs_by_id, node.definition().id, node); } @@ -225,6 +217,9 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) { init.walk(tw); }); }); + self_referential_classes.forEach(function (cls) { + cls.walk(tw); + }); // pass 3: we should drop declarations not in_use var tt = new TreeTransformer( function before(node, descend, in_list) { diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index c475a42c8..2982c4bb7 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -3306,6 +3306,492 @@ class_used_within_itself_3: { } } +class_used_within_itself_4: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + let c; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + class r { + static #t = (c = n(this, [], e)); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + } + })(); + x = 1; + constructor() { } + })(); + + console.log(c.ok); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS", "true"] +} + +class_used_within_itself_5: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + let c; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + (class { + static #t = (c = n(this, [], e)); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + }) + })(); + x = 1; + constructor() { } + })(); + + console.log(c.ok); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS", "true"] +} + +class_used_within_itself_6: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + class X { + static prop = "X" + static Y = class Y extends this { } + } + + console.log(X.Y.prop) + } + expect: { + class X { + static prop = "X" + static Y = class extends this { } + } + + console.log(X.Y.prop) + } + node_version = ">=20.0.0" + expect_stdout: "X" +} + +class_used_within_itself_7: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + class X { + static prop = "X" + static Y = class Y extends this { } + } + } + expect: { } + node_version = ">=20.0.0" + expect_stdout: true +} + +class_used_within_itself_var: { + options = { + toplevel: true, + unused: true, + side_effects: true, + pure_getters: true, + }; + input: { + var C = class { + static [C.name] = 1; + } + } + expect: { } +} + +class_used_within_itself_2_var: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + globalThis.useThis = function(obj) { + obj.prototype.method() + } + + const List = ['P', 'A', 'S', 'S']; + var Class = class { + method(t) { + List.forEach(letter => console.log(letter)); + } + static prop = useThis(this); + } + } + expect: { + globalThis.useThis = function(obj) { + obj.prototype.method() + } + + const List = ['P', 'A', 'S', 'S']; + (class { + method(t) { + List.forEach(letter => console.log(letter)); + } + static prop = useThis(this); + }); + } + expect_stdout: [ 'P', 'A', 'S', 'S' ] +} + +class_used_within_itself_3_var: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + const importedfn = () => console.log("------------"); + + const cls = (() => { + var ErrorBoundary = importedfn; + + let _Wrapper; + var Wrapper = class { + static _ = ((cls) => (_Wrapper = cls))(Wrapper); + render() { + ErrorBoundary("foobar"); + } + } + + return _Wrapper; + })(); + + new cls().render(); + } + expect: { + const importedfn = () => console.log("------------"); + + const cls = (() => { + var ErrorBoundary = importedfn; + + let _Wrapper; + var Wrapper = class { + static _ = ((cls) => (_Wrapper = cls))(Wrapper); + render() { + ErrorBoundary("foobar"); + } + } + + return _Wrapper; + })(); + + new cls().render(); + } +} + +class_used_within_itself_4_var: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + var r = class { + static #t = n(this, [], e); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + } + })(); + x = 1; + constructor() { } + })(); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS"] +} + +class_used_within_itself_5_var: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + let c; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + var r = class { + static #t = (c = n(this, [], e)); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + } + })(); + x = 1; + constructor() { } + })(); + + console.log(c.ok); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS", "true"] +} + +class_used_within_itself_6_var: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + var X = class { + static prop = "X" + static Y = class Y extends this { } + } + + console.log(X.Y.prop) + } + expect: { + var X = class { + static prop = "X" + static Y = class extends this { } + } + + console.log(X.Y.prop) + } + node_version = ">=20.0.0" + expect_stdout: "X" +} + +class_used_within_itself_var_expname: { + options = { + toplevel: true, + unused: true, + side_effects: true, + pure_getters: true, + }; + input: { + var C = class C { + static [C.name] = 1; + } + } + expect: { } +} + +class_used_within_itself_2_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + globalThis.useThis = function(obj) { + obj.prototype.method() + } + + const List = ['P', 'A', 'S', 'S']; + var Class = class Class { + method(t) { + List.forEach(letter => console.log(letter)); + } + static prop = useThis(Class); + } + } + expect: { + globalThis.useThis = function(obj) { + obj.prototype.method() + } + + const List = ['P', 'A', 'S', 'S']; + (class Class { + method(t) { + List.forEach(letter => console.log(letter)); + } + static prop = useThis(Class); + }); + } + expect_stdout: [ 'P', 'A', 'S', 'S' ] +} + +class_used_within_itself_3_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + const importedfn = () => console.log("------------"); + + const cls = (() => { + var ErrorBoundary = importedfn; + + let _Wrapper; + var Wrapper = class Wrapper { + static _ = ((cls) => (_Wrapper = cls))(Wrapper); + render() { + ErrorBoundary("foobar"); + } + } + + return _Wrapper; + })(); + + new cls().render(); + } + expect: { + const importedfn = () => console.log("------------"); + + const cls = (() => { + var ErrorBoundary = importedfn; + + let _Wrapper; + (class Wrapper { + static _ = ((cls) => (_Wrapper = cls))(Wrapper); + render() { + ErrorBoundary("foobar"); + } + }); + + return _Wrapper; + })(); + + new cls().render(); + } +} + +class_used_within_itself_4_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + let c; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + var r = class r { + static #t = (c = n(r, [], e)); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + } + })(); + x = 1; + constructor() { } + })(); + + console.log(c.ok); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS", "true"] +} + +class_used_within_itself_5_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + let List = [1]; + let c; + globalThis.n = t => new t(1); + (e = [ + ((r = 'x-button'), + (t, e) => { + let { addInitializer: n } = e; + return n(function () { + customElements.define(r, this); + }); + }), + ]), + new (class { + static #t = (() => { + var r = class r { + static #t = (c = n(r, [], e)); + constructor(t) { + this.ok = List.includes(t); + if (this.ok) { + console.log("PASS"); + } + } + } + })(); + x = 1; + constructor() { } + })(); + + console.log(c.ok); + } + node_version = ">=20.0.0" + expect_stdout: ["PASS", "true"] +} + +class_used_within_itself_6_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + var X = class X { + static prop = "X" + static Y = class Y extends this { } + } + + console.log(X.Y.prop) + } + expect: { + var X = class { + static prop = "X" + static Y = class extends this { } + } + + console.log(X.Y.prop) + } + node_version = ">=20.0.0" + expect_stdout: "X" +} + +class_used_within_itself_7_var_expname: { + options = { toplevel: true, unused: true, side_effects: true }; + input: { + var X = class X { + static prop = "X" + static Y = class Y extends X { } + } + } + expect: { } + node_version = ">=20.0.0" + expect_stdout: true +} issue_t1447_var: { options = { unused: true, inline: true, reduce_vars: true }