From c88566034756eb17c4ff563901b3a1c95b63f788 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 23 Feb 2021 21:57:11 +0000 Subject: [PATCH] support nullish coalescing operator (#4678) --- README.md | 2 +- lib/compress.js | 46 +++++++++++---- lib/output.js | 4 +- lib/parse.js | 4 +- test/compress/arrows.js | 18 +++--- test/compress/classes.js | 2 +- test/compress/exports.js | 4 +- test/compress/functions.js | 2 +- test/compress/loops.js | 4 +- test/compress/new.js | 4 +- test/compress/nullish.js | 111 +++++++++++++++++++++++++++++++++++++ test/compress/numbers.js | 2 +- test/compress/templates.js | 4 +- test/mocha/comments.js | 2 +- test/mocha/parentheses.js | 2 +- test/ufuzz/index.js | 2 + 16 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 test/compress/nullish.js diff --git a/README.md b/README.md index be6eae02..53f0e734 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ a double dash to prevent input files being used as option arguments: 1 - single 2 - double 3 - original - `wrap_iife` Wrap IIFEs in parenthesis. Note: you may + `wrap_iife` Wrap IIFEs in parentheses. Note: you may want to disable `negate_iife` under compressor options. -O, --output-opts [options] Specify output options (`beautify` disabled by default). diff --git a/lib/compress.js b/lib/compress.js index dcd48fa8..7f9a8dd5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2182,6 +2182,7 @@ merge(Compressor.prototype, { var lazy = lazy_op[expr.operator]; if (unused && lazy + && expr.operator != "??" && expr.right instanceof AST_Assign && expr.right.operator == "=" && !(expr.right.left instanceof AST_Destructured)) { @@ -3840,7 +3841,7 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_string", func); }); - var lazy_op = makePredicate("&& ||"); + var lazy_op = makePredicate("&& || ??"); (function(def) { function to_node(value, orig) { @@ -4286,6 +4287,9 @@ merge(Compressor.prototype, { switch (this.operator) { case "&&" : result = left && right; break; case "||" : result = left || right; break; + case "??" : + result = left == null ? right : left; + break; case "|" : result = left | right; break; case "&" : result = left & right; break; case "^" : result = left ^ right; break; @@ -7130,6 +7134,7 @@ merge(Compressor.prototype, { node = this.clone(); node.right = right.drop_side_effect_free(compressor); } + if (this.operator == "??") return node; return (first_in_statement ? best_of_statement : best_of_expression)(node, make_node(AST_Binary, this, { operator: node.operator == "&&" ? "||" : "&&", left: node.left.negate(compressor, first_in_statement), @@ -9159,7 +9164,7 @@ merge(Compressor.prototype, { // (a = b, x || a = c) ---> a = x ? b : c function to_conditional_assignment(compressor, def, value, node) { if (!(node instanceof AST_Binary)) return; - if (!lazy_op[node.operator]) return; + if (!(node.operator == "&&" || node.operator == "||")) return; if (!(node.right instanceof AST_Assign)) return; if (node.right.operator != "=") return; if (!(node.right.left instanceof AST_SymbolRef)) return; @@ -9680,22 +9685,41 @@ merge(Compressor.prototype, { }).optimize(compressor); } break; + case "??": + var nullish = true; case "||": - var ll = fuzzy_eval(self.left); - if (!ll) { - AST_Node.warn("Condition left of || always false [{file}:{line},{col}]", self.start); + var ll = fuzzy_eval(self.left, nullish); + if (nullish ? ll == null : !ll) { + AST_Node.warn("Condition left of {operator} always {value} [{file}:{line},{col}]", { + operator: self.operator, + value: nullish ? "nulish" : "false", + file: self.start.file, + line: self.start.line, + col: self.start.col, + }); return make_sequence(self, [ self.left, self.right ]).optimize(compressor); } else if (!(ll instanceof AST_Node)) { - AST_Node.warn("Condition left of || always true [{file}:{line},{col}]", self.start); + AST_Node.warn("Condition left of {operator} always {value} [{file}:{line},{col}]", { + operator: self.operator, + value: nullish ? "defined" : "true", + file: self.start.file, + line: self.start.line, + col: self.start.col, + }); return maintain_this_binding(compressor, parent, compressor.self(), self.left).optimize(compressor); } var rr = self.right.evaluate(compressor); if (!rr) { if (in_bool || parent.operator == "||" && parent.left === compressor.self()) { - AST_Node.warn("Dropping side-effect-free || [{file}:{line},{col}]", self.start); + AST_Node.warn("Dropping side-effect-free {operator} [{file}:{line},{col}]", { + operator: self.operator, + file: self.start.file, + line: self.start.line, + col: self.start.col, + }); return self.left.optimize(compressor); } - } else if (!(rr instanceof AST_Node)) { + } else if (!nullish && !(rr instanceof AST_Node)) { if (in_bool) { AST_Node.warn("Boolean || always true [{file}:{line},{col}]", self.start); return make_sequence(self, [ @@ -9705,7 +9729,7 @@ merge(Compressor.prototype, { } else self.truthy = true; } // x && true || y ---> x ? true : y - if (self.left.operator == "&&") { + if (!nullish && self.left.operator == "&&") { var lr = self.left.right.is_truthy() || self.left.right.evaluate(compressor, true); if (lr && !(lr instanceof AST_Node)) return make_node(AST_Conditional, self, { condition: self.left.left, @@ -10047,9 +10071,9 @@ merge(Compressor.prototype, { }); } - function fuzzy_eval(node) { + function fuzzy_eval(node, nullish) { if (node.truthy) return true; - if (node.falsy) return false; + if (node.falsy && !nullish) return false; if (node.is_truthy()) return true; return node.evaluate(compressor, true); } diff --git a/lib/output.js b/lib/output.js index 592717b0..49164359 100644 --- a/lib/output.js +++ b/lib/output.js @@ -753,7 +753,9 @@ function OutputStream(options) { if (p instanceof AST_Binary) { var po = p.operator, pp = PRECEDENCE[po]; var so = this.operator, sp = PRECEDENCE[so]; - return pp > sp || (pp == sp && this === p[po == "**" ? "left" : "right"]); + return pp > sp + || po == "??" && (so == "&&" || so == "||") + || (pp == sp && this === p[po == "**" ? "left" : "right"]); } // (foo && bar)() if (p instanceof AST_Call) return p.expression === this; diff --git a/lib/parse.js b/lib/parse.js index 26c618ef..2cb30409 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -107,7 +107,8 @@ var OPERATORS = makePredicate([ "^=", "&=", "&&", - "||" + "||", + "??", ]); var NEWLINE_CHARS = "\n\r\u2028\u2029"; @@ -662,6 +663,7 @@ var PRECEDENCE = function(a, ret) { } return ret; }([ + ["??"], ["||"], ["&&"], ["|"], diff --git a/test/compress/arrows.js b/test/compress/arrows.js index e7322b72..096503d6 100644 --- a/test/compress/arrows.js +++ b/test/compress/arrows.js @@ -34,7 +34,7 @@ destructured_funarg: { node_version: ">=6" } -await_parenthesis: { +await_parentheses: { input: { async function f() { await (a => a); @@ -43,7 +43,7 @@ await_parenthesis: { expect_exact: "async function f(){await(a=>a)}" } -for_parenthesis_init: { +for_parentheses_init: { input: { for (a => (a in a); console.log(42);); } @@ -52,7 +52,7 @@ for_parenthesis_init: { node_version: ">=4" } -for_parenthesis_condition: { +for_parentheses_condition: { input: { for (console.log(42); a => (a in a);) break; @@ -62,7 +62,7 @@ for_parenthesis_condition: { node_version: ">=4" } -for_parenthesis_step: { +for_parentheses_step: { input: { for (; console.log(42); a => (a in a)); } @@ -71,7 +71,7 @@ for_parenthesis_step: { node_version: ">=4" } -for_assign_parenthesis_init: { +for_assign_parentheses_init: { input: { for (f = a => (a in a); console.log(42);); } @@ -80,7 +80,7 @@ for_assign_parenthesis_init: { node_version: ">=4" } -for_assign_parenthesis_condition: { +for_assign_parentheses_condition: { input: { for (console.log(42); f = a => (a in a);) break; @@ -90,7 +90,7 @@ for_assign_parenthesis_condition: { node_version: ">=4" } -for_assign_parenthesis_step: { +for_assign_parentheses_step: { input: { for (; console.log(42); f = a => (a in a)); } @@ -99,7 +99,7 @@ for_assign_parenthesis_step: { node_version: ">=4" } -for_declaration_parenthesis_init: { +for_declaration_parentheses_init: { input: { for (var f = a => (a in a); console.log(42);); } @@ -108,7 +108,7 @@ for_declaration_parenthesis_init: { node_version: ">=4" } -for_statement_parenthesis_init: { +for_statement_parentheses_init: { input: { for (a => { a in a; diff --git a/test/compress/classes.js b/test/compress/classes.js index 8c5575c0..390ae525 100644 --- a/test/compress/classes.js +++ b/test/compress/classes.js @@ -158,7 +158,7 @@ yield: { node_version: ">=12" } -conditional_parenthesis: { +conditional_parentheses: { options = { conditionals: true, } diff --git a/test/compress/exports.js b/test/compress/exports.js index d3a4ffce..e0690503 100644 --- a/test/compress/exports.js +++ b/test/compress/exports.js @@ -38,7 +38,7 @@ defaults: { expect_exact: "export default 42;export default async;export default(x,y)=>x*x;export default class{}export default function*(a,b){}export default async function f({c:c},...[d]){}" } -defaults_parenthesis_1: { +defaults_parentheses_1: { input: { export default function() { console.log("FAIL"); @@ -47,7 +47,7 @@ defaults_parenthesis_1: { expect_exact: 'export default function(){console.log("FAIL")}console.log("PASS");' } -defaults_parenthesis_2: { +defaults_parentheses_2: { input: { export default (async function() { console.log("PASS"); diff --git a/test/compress/functions.js b/test/compress/functions.js index f521433d..fe9307ca 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -2053,7 +2053,7 @@ issue_2898: { expect_stdout: "2" } -deduplicate_parenthesis: { +deduplicate_parentheses: { input: { ({}).a = b; (({}).a = b)(); diff --git a/test/compress/loops.js b/test/compress/loops.js index 0afffae5..b1128f1a 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -501,14 +501,14 @@ do_switch: { } } -in_parenthesis_1: { +in_parentheses_1: { input: { for (("foo" in {});0;); } expect_exact: 'for(("foo"in{});0;);' } -in_parenthesis_2: { +in_parentheses_2: { input: { for ((function(){ "foo" in {}; });0;); } diff --git a/test/compress/new.js b/test/compress/new.js index a823bb9c..9c99f016 100644 --- a/test/compress/new.js +++ b/test/compress/new.js @@ -85,7 +85,7 @@ new_with_unary_prefix: { expect_exact: 'var bar=(+new Date).toString(32);'; } -dot_parenthesis_1: { +dot_parentheses_1: { input: { console.log(new (Math.random().constructor) instanceof Number); } @@ -93,7 +93,7 @@ dot_parenthesis_1: { expect_stdout: "true" } -dot_parenthesis_2: { +dot_parentheses_2: { input: { console.log(typeof new function(){Math.random()}.constructor); } diff --git a/test/compress/nullish.js b/test/compress/nullish.js new file mode 100644 index 00000000..e8f5ad6c --- /dev/null +++ b/test/compress/nullish.js @@ -0,0 +1,111 @@ +parentheses: { + input: { + (console.log("foo") || console.log("bar") ?? console.log("baz")) && console.log("moo"); + } + expect_exact:'((console.log("foo")||console.log("bar"))??console.log("baz"))&&console.log("moo");' + expect_stdout: [ + "foo", + "bar", + "baz", + ] + node_version: ">=14" +} + +evaluate: { + options = { + evaluate: true, + side_effects: true, + } + input: { + void console.log("foo" ?? "bar") ?? console.log("baz"); + } + expect: { + console.log("foo"), + console.log("baz"); + } + expect_stdout: [ + "foo", + "baz", + ] + node_version: ">=14" +} + +conditional_assignment_1: { + options = { + collapse_vars: true, + } + input: { + console.log(function(a, b) { + b ?? (a = "FAIL"); + return a; + }("PASS", !console)); + } + expect: { + console.log(function(a, b) { + b ?? (a = "FAIL"); + return a; + }("PASS", !console)); + } + expect_stdout: "PASS" + node_version: ">=14" +} + +conditional_assignment_2: { + options = { + conditionals: true, + } + input: { + var a, b = false; + a = "PASS", + b ?? (a = "FAIL"), + console.log(a); + } + expect: { + var a, b = false; + a = "PASS", + b ?? (a = "FAIL"), + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=14" +} + +conditional_assignment_3: { + options = { + conditionals: true, + join_vars: true, + } + input: { + var a, b = false; + a = "PASS", + b ?? (a = "FAIL"), + console.log(a); + } + expect: { + var a, b = false, a = "PASS"; + b ?? (a = "FAIL"), + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=14" +} + +conditional_assignment_4: { + options = { + side_effects: true, + } + input: { + console.log(function(a) { + !console ?? (a = "FAIL"); + return a; + }("PASS")); + } + expect: { + console.log(function(a) { + !console ?? (a = "FAIL"); + return a; + }("PASS")); + } + expect_stdout: "PASS" + node_version: ">=14" +} diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 994cd5cf..d7c0360c 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -777,7 +777,7 @@ issue_1710: { expect_stdout: true } -unary_binary_parenthesis: { +unary_binary_parentheses: { options = { evaluate: true, } diff --git a/test/compress/templates.js b/test/compress/templates.js index d9e95975..b75dc7d7 100644 --- a/test/compress/templates.js +++ b/test/compress/templates.js @@ -53,7 +53,7 @@ tagged_chain: { node_version: ">=4" } -tag_parenthesis_arrow: { +tag_parentheses_arrow: { input: { console.log((s => s.raw[0])`\tPASS`.slice(2)); } @@ -62,7 +62,7 @@ tag_parenthesis_arrow: { node_version: ">=4" } -tag_parenthesis_new: { +tag_parentheses_new: { input: { (new function() { return console.log; diff --git a/test/mocha/comments.js b/test/mocha/comments.js index 9a4b3be6..88cf08c4 100644 --- a/test/mocha/comments.js +++ b/test/mocha/comments.js @@ -259,7 +259,7 @@ describe("comments", function() { assert.strictEqual(result.code, code); }); - it("Should handle comments around parenthesis correctly", function() { + it("Should handle comments around parentheses correctly", function() { var code = [ "a();", "/* foo */", diff --git a/test/mocha/parentheses.js b/test/mocha/parentheses.js index 373db2da..0fbb4c95 100644 --- a/test/mocha/parentheses.js +++ b/test/mocha/parentheses.js @@ -84,7 +84,7 @@ describe("parentheses", function() { } }); - it("Should compress leading parenthesis with reasonable performance", function() { + it("Should compress leading parentheses with reasonable performance", function() { this.timeout(30000); var code = [ "({}?0:1)&&x();", diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 0625a267..3d0824b6 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -149,6 +149,7 @@ var SUPPORT = function(matrix) { for_of: "for (var a of []);", generator: "function* f(){}", let: "let a;", + nullish: "0 ?? 0", rest: "var [...a] = [];", rest_object: "var {...a} = {};", spread: "[...[]];", @@ -231,6 +232,7 @@ var BINARY_OPS = [ ",", ]; BINARY_OPS = BINARY_OPS.concat(BINARY_OPS); +if (SUPPORT.nullish) BINARY_OPS.push("??"); BINARY_OPS = BINARY_OPS.concat(BINARY_OPS); BINARY_OPS = BINARY_OPS.concat(BINARY_OPS); if (SUPPORT.exponentiation) BINARY_OPS.push("**"); -- 2.34.1