Skip to content
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

Compress more nullish checks into nullish coalescing #985

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 32 additions & 20 deletions lib/compress/index.js
Expand Up @@ -6795,40 +6795,47 @@ function is_nullish(node) {
);
}

function is_nullish_check(check, check_subject, compressor) {
if (check_subject.may_throw(compressor)) return false;

function is_nullish_check(check, consequent, alternative, compressor) {
let nullish_side;

// foo == null
// foo != null
if (
check instanceof AST_Binary
&& check.operator === "=="
&& (check.operator === "==" || check.operator === "!=")
// which side is nullish?
&& (
(nullish_side = is_nullish(check.left) && check.left)
|| (nullish_side = is_nullish(check.right) && check.right)
)
// is the other side the same as the check_subject
&& (
nullish_side === check.left
? check.right
: check.left
).equivalent_to(check_subject)
) {
return true;
const test_subject = nullish_side === check.left ? check.right : check.left;
if (test_subject.may_throw(compressor)) return false;

const check_subject = check.operator == "==" ? alternative : consequent;
if (test_subject.equivalent_to(check_subject)) return true;
}

// foo === null || foo === undefined
if (check instanceof AST_Binary && check.operator === "||") {
// foo !== null && foo !== undefined
if (
check instanceof AST_Binary
&& (check.operator === "||" || check.operator === "&&")
) {
let null_cmp;
let undefined_cmp;

const OR = check.operator == "||";

const find_comparison = cmp => {
if (!(
cmp instanceof AST_Binary
&& (cmp.operator === "===" || cmp.operator === "==")
)) {
if (!(cmp instanceof AST_Binary)) {
return false;
}
if (
OR
? !(cmp.operator === "===" || cmp.operator === "==")
: !(cmp.operator === "!==" || cmp.operator === "!=")
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

return false;
}

Expand Down Expand Up @@ -6860,7 +6867,10 @@ function is_nullish_check(check, check_subject, compressor) {
return false;
}

if (!defined_side.equivalent_to(check_subject)) {
if (
defined_side.may_throw(compressor)
|| !defined_side.equivalent_to(OR ? alternative : consequent)
) {
return false;
}

Expand Down Expand Up @@ -6974,14 +6984,16 @@ def_optimize(AST_Conditional, function(self, compressor) {
}

// a == null ? b : a -> a ?? b
// a != null ? a : b -> a ?? b
if (
compressor.option("ecma") >= 2020 &&
is_nullish_check(condition, alternative, compressor)
is_nullish_check(condition, consequent, alternative, compressor)
) {
let left = condition.operator == "==" || condition.operator == "||" ? alternative : consequent;
return make_node(AST_Binary, self, {
operator: "??",
left: alternative,
right: consequent
left: left,
right: left == alternative ? consequent : alternative,
}).optimize(compressor);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess is_nullish_check could return the left and right hand side:

const nullish_check = get_nullish_check(condition, consequent, alternative, compressor);
if (nullish_check != null) { // very meta line here
  const [left, right] = nullish_check
  // create "??" expression using left and right
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

Expand Down
70 changes: 56 additions & 14 deletions test/compress/nullish.js
Expand Up @@ -10,16 +10,44 @@ conditional_to_nullish_coalescing: {
const foo = id('something');

leak(foo == null ? bar : foo);
leak(foo != null ? foo : bar);
}

expect: {
const foo = id('something');

leak(foo ?? bar);
leak(foo ?? bar);
}
}

conditional_to_nullish_coalescing_strict: {
options = {
ecma: 2020,
toplevel: true,
conditionals: true
}

input: {
const foo = id('something')

foo === null || foo === undefined ? bar : foo;
foo === undefined || foo === null ? bar : foo;
foo !== null && foo !== undefined ? foo : bar;
foo !== undefined && foo !== null ? foo : bar;
}

expect: {
const foo = id('something')

foo ?? bar;
foo ?? bar;
foo ?? bar;
foo ?? bar;
}
}

conditional_to_nullish_coalescing_2: {
conditional_to_nullish_coalescing_strict_negative: {
options = {
ecma: 2020,
toplevel: true,
Expand All @@ -29,31 +57,45 @@ conditional_to_nullish_coalescing_2: {
input: {
const foo = id('something')

console.log('negative cases')
foo === null || foo === null ? bar : foo;
foo === undefined || foo === undefined ? bar : foo;
foo === null || foo === undefined ? foo : bar;
some_global === null || some_global === undefined ? bar : some_global;

console.log('positive cases')
foo === null || foo === void 0 ? bar : foo;
foo === null || foo === undefined ? bar : foo;
foo === undefined || foo === null ? bar : foo;
foo !== null && foo !== null ? foo : bar;
foo !== undefined && foo !== undefined ? foo : bar;
foo !== null && foo !== undefined ? bar : foo;
}

expect: {
const foo = id('something')

console.log('negative cases')
null === foo || null === foo ? bar : foo;
void 0 === foo || void 0 === foo ? bar : foo;
null === foo || void 0 === foo ? foo : bar;
null === some_global || void 0 === some_global ? bar : some_global;
null !== foo && null !== foo ? foo : bar ;
void 0 !== foo && void 0 !== foo ? foo : bar ;
null !== foo && void 0 !== foo ? bar : foo ;
}
}

console.log('positive cases')
foo ?? bar;
foo ?? bar;
foo ?? bar;
conditional_to_nullish_coalescing_global: {
options = {
ecma: 2020,
toplevel: true,
conditionals: true
}

input: {
some_global == null ? bar : some_global;
some_global != null ? some_global : bar;
some_global === null || some_global === undefined ? bar : some_global;
some_global !== null && some_global !== undefined ? some_global : bar;
}

expect: {
null == some_global ? bar : some_global;
null != some_global ? some_global : bar;
null === some_global || void 0 === some_global ? bar : some_global;
null !== some_global && void 0 !== some_global ? some_global : bar;
}
}

Expand Down