fix corner cases in optional_chains (#5110)
This commit is contained in:
12
lib/ast.js
12
lib/ast.js
@@ -1289,13 +1289,14 @@ function must_be_expressions(node, prop, allow_spread, allow_hole) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var AST_Call = DEFNODE("Call", "args expression optional pure", {
|
var AST_Call = DEFNODE("Call", "args expression optional pure terminal", {
|
||||||
$documentation: "A function call expression",
|
$documentation: "A function call expression",
|
||||||
$propdoc: {
|
$propdoc: {
|
||||||
args: "[AST_Node*] array of arguments",
|
args: "[AST_Node*] array of arguments",
|
||||||
expression: "[AST_Node] expression to invoke as function",
|
expression: "[AST_Node] expression to invoke as function",
|
||||||
optional: "[boolean] whether the expression is optional chaining",
|
optional: "[boolean] whether the expression is optional chaining",
|
||||||
pure: "[string/S] marker for side-effect-free call expression",
|
pure: "[string/S] marker for side-effect-free call expression",
|
||||||
|
terminal: "[boolean] whether the chain has ended",
|
||||||
},
|
},
|
||||||
walk: function(visitor) {
|
walk: function(visitor) {
|
||||||
var node = this;
|
var node = this;
|
||||||
@@ -1316,6 +1317,7 @@ var AST_New = DEFNODE("New", null, {
|
|||||||
$documentation: "An object instantiation. Derives from a function call since it has exactly the same properties",
|
$documentation: "An object instantiation. Derives from a function call since it has exactly the same properties",
|
||||||
_validate: function() {
|
_validate: function() {
|
||||||
if (this.optional) throw new Error("optional must be false");
|
if (this.optional) throw new Error("optional must be false");
|
||||||
|
if (this.terminal) throw new Error("terminal must be false");
|
||||||
},
|
},
|
||||||
}, AST_Call);
|
}, AST_Call);
|
||||||
|
|
||||||
@@ -1338,12 +1340,18 @@ var AST_Sequence = DEFNODE("Sequence", "expressions", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var AST_PropAccess = DEFNODE("PropAccess", "expression optional property", {
|
function root_expr(prop) {
|
||||||
|
while (prop instanceof AST_PropAccess) prop = prop.expression;
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
var AST_PropAccess = DEFNODE("PropAccess", "expression optional property terminal", {
|
||||||
$documentation: "Base class for property access expressions, i.e. `a.foo` or `a[\"foo\"]`",
|
$documentation: "Base class for property access expressions, i.e. `a.foo` or `a[\"foo\"]`",
|
||||||
$propdoc: {
|
$propdoc: {
|
||||||
expression: "[AST_Node] the “container” expression",
|
expression: "[AST_Node] the “container” expression",
|
||||||
optional: "[boolean] whether the expression is optional chaining",
|
optional: "[boolean] whether the expression is optional chaining",
|
||||||
property: "[AST_Node|string] the property to access. For AST_Dot this is always a plain string, while for AST_Sub it's an arbitrary AST_Node",
|
property: "[AST_Node|string] the property to access. For AST_Dot this is always a plain string, while for AST_Sub it's an arbitrary AST_Node",
|
||||||
|
terminal: "[boolean] whether the chain has ended",
|
||||||
},
|
},
|
||||||
get_property: function() {
|
get_property: function() {
|
||||||
var p = this.property;
|
var p = this.property;
|
||||||
|
|||||||
@@ -1721,11 +1721,6 @@ merge(Compressor.prototype, {
|
|||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
function root_expr(prop) {
|
|
||||||
while (prop instanceof AST_PropAccess) prop = prop.expression;
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function is_iife_call(node) {
|
function is_iife_call(node) {
|
||||||
if (node.TYPE != "Call") return false;
|
if (node.TYPE != "Call") return false;
|
||||||
do {
|
do {
|
||||||
@@ -9070,16 +9065,20 @@ merge(Compressor.prototype, {
|
|||||||
OPT(AST_Const, varify);
|
OPT(AST_Const, varify);
|
||||||
OPT(AST_Let, varify);
|
OPT(AST_Let, varify);
|
||||||
|
|
||||||
function trim_optional_chain(self, compressor) {
|
function trim_optional_chain(node, compressor) {
|
||||||
if (!compressor.option("optional_chains")) return;
|
if (!compressor.option("optional_chains")) return;
|
||||||
if (!self.optional) return;
|
if (node.terminal) do {
|
||||||
var expr = self.expression;
|
var expr = node.expression;
|
||||||
|
if (node.optional) {
|
||||||
var ev = fuzzy_eval(compressor, expr, true);
|
var ev = fuzzy_eval(compressor, expr, true);
|
||||||
if (ev == null) return make_node(AST_UnaryPrefix, self, {
|
if (ev == null) return make_node(AST_UnaryPrefix, node, {
|
||||||
operator: "void",
|
operator: "void",
|
||||||
expression: expr,
|
expression: expr,
|
||||||
}).optimize(compressor);
|
}).optimize(compressor);
|
||||||
if (!(ev instanceof AST_Node)) self.optional = false;
|
if (!(ev instanceof AST_Node)) node.optional = false;
|
||||||
|
}
|
||||||
|
node = expr;
|
||||||
|
} while ((node.TYPE == "Call" || node instanceof AST_PropAccess) && !node.terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lift_sequence_in_expression(node, compressor) {
|
function lift_sequence_in_expression(node, compressor) {
|
||||||
|
|||||||
@@ -556,7 +556,9 @@
|
|||||||
return node;
|
return node;
|
||||||
},
|
},
|
||||||
ChainExpression: function(M) {
|
ChainExpression: function(M) {
|
||||||
return from_moz(M.expression);
|
var node = from_moz(M.expression);
|
||||||
|
node.terminal = true;
|
||||||
|
return node;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -868,7 +870,7 @@
|
|||||||
|
|
||||||
def_to_moz(AST_PropAccess, function To_Moz_MemberExpression(M) {
|
def_to_moz(AST_PropAccess, function To_Moz_MemberExpression(M) {
|
||||||
var computed = M instanceof AST_Sub;
|
var computed = M instanceof AST_Sub;
|
||||||
return {
|
var expr = {
|
||||||
type: "MemberExpression",
|
type: "MemberExpression",
|
||||||
object: to_moz(M.expression),
|
object: to_moz(M.expression),
|
||||||
computed: computed,
|
computed: computed,
|
||||||
@@ -878,6 +880,10 @@
|
|||||||
name: M.property,
|
name: M.property,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return M.terminal ? {
|
||||||
|
type: "ChainExpression",
|
||||||
|
expression: expr,
|
||||||
|
} : expr;
|
||||||
});
|
});
|
||||||
|
|
||||||
def_to_moz(AST_Unary, function To_Moz_Unary(M) {
|
def_to_moz(AST_Unary, function To_Moz_Unary(M) {
|
||||||
|
|||||||
@@ -790,35 +790,26 @@ function OutputStream(options) {
|
|||||||
if (p instanceof AST_Unary) return true;
|
if (p instanceof AST_Unary) return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
function lhs_has_optional(node, output) {
|
function need_chain_parens(node, parent) {
|
||||||
var p = output.parent();
|
if (!node.terminal) return false;
|
||||||
if (p instanceof AST_PropAccess && p.expression === node && is_lhs(p, output.parent(1))) {
|
if (!(parent instanceof AST_Call || parent instanceof AST_PropAccess)) return false;
|
||||||
// ++(foo?.bar).baz
|
return parent.expression === node;
|
||||||
// (foo?.()).bar = baz
|
|
||||||
do {
|
|
||||||
if (node.optional) return true;
|
|
||||||
node = node.expression;
|
|
||||||
} while (node.TYPE == "Call" || node instanceof AST_PropAccess);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PARENS(AST_PropAccess, function(output) {
|
PARENS(AST_PropAccess, function(output) {
|
||||||
var node = this;
|
var node = this;
|
||||||
var p = output.parent();
|
var p = output.parent();
|
||||||
if (p instanceof AST_New) {
|
|
||||||
if (p.expression !== node) return false;
|
|
||||||
// i.e. new (foo().bar)
|
// i.e. new (foo().bar)
|
||||||
//
|
//
|
||||||
// if there's one call into this subtree, then we need
|
// if there's one call into this subtree, then we need
|
||||||
// parens around it too, otherwise the call will be
|
// parens around it too, otherwise the call will be
|
||||||
// interpreted as passing the arguments to the upper New
|
// interpreted as passing the arguments to the upper New
|
||||||
// expression.
|
// expression.
|
||||||
do {
|
if (p instanceof AST_New && p.expression === node && root_expr(node).TYPE == "Call") return true;
|
||||||
node = node.expression;
|
// (foo?.bar)()
|
||||||
} while (node instanceof AST_PropAccess);
|
// (foo?.bar).baz
|
||||||
return node.TYPE == "Call";
|
// new (foo?.bar)()
|
||||||
}
|
return need_chain_parens(node, p);
|
||||||
return lhs_has_optional(node, output);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
PARENS(AST_Call, function(output) {
|
PARENS(AST_Call, function(output) {
|
||||||
@@ -833,7 +824,10 @@ function OutputStream(options) {
|
|||||||
var g = output.parent(1);
|
var g = output.parent(1);
|
||||||
if (g instanceof AST_Assign && g.left === p) return true;
|
if (g instanceof AST_Assign && g.left === p) return true;
|
||||||
}
|
}
|
||||||
return lhs_has_optional(node, output);
|
// (foo?.())()
|
||||||
|
// (foo?.()).bar
|
||||||
|
// new (foo?.())()
|
||||||
|
return need_chain_parens(node, p);
|
||||||
});
|
});
|
||||||
|
|
||||||
PARENS(AST_New, function(output) {
|
PARENS(AST_New, function(output) {
|
||||||
|
|||||||
60
lib/parse.js
60
lib/parse.js
@@ -1818,10 +1818,7 @@ function parse($TEXT, options) {
|
|||||||
if (is("punc")) {
|
if (is("punc")) {
|
||||||
switch (start.value) {
|
switch (start.value) {
|
||||||
case "`":
|
case "`":
|
||||||
var tmpl = template(null);
|
return subscripts(template(null), allow_calls);
|
||||||
tmpl.start = start;
|
|
||||||
tmpl.end = prev();
|
|
||||||
return subscripts(tmpl, allow_calls);
|
|
||||||
case "(":
|
case "(":
|
||||||
next();
|
next();
|
||||||
if (is("punc", ")")) {
|
if (is("punc", ")")) {
|
||||||
@@ -2230,6 +2227,7 @@ function parse($TEXT, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function template(tag) {
|
function template(tag) {
|
||||||
|
var start = tag ? tag.start : S.token;
|
||||||
var read = S.input.context().read_template;
|
var read = S.input.context().read_template;
|
||||||
var strings = [];
|
var strings = [];
|
||||||
var expressions = [];
|
var expressions = [];
|
||||||
@@ -2240,58 +2238,60 @@ function parse($TEXT, options) {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
return new AST_Template({
|
return new AST_Template({
|
||||||
|
start: start,
|
||||||
expressions: expressions,
|
expressions: expressions,
|
||||||
strings: strings,
|
strings: strings,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
end: prev(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscripts = function(expr, allow_calls, optional) {
|
function subscripts(expr, allow_calls) {
|
||||||
var start = expr.start;
|
var start = expr.start;
|
||||||
|
var optional = null;
|
||||||
|
while (true) {
|
||||||
|
if (is("operator", "?") && is_token(peek(), "punc", ".")) {
|
||||||
|
next();
|
||||||
|
next();
|
||||||
|
optional = expr;
|
||||||
|
}
|
||||||
if (is("punc", "[")) {
|
if (is("punc", "[")) {
|
||||||
next();
|
next();
|
||||||
var prop = expression();
|
var prop = expression();
|
||||||
expect("]");
|
expect("]");
|
||||||
return subscripts(new AST_Sub({
|
expr = new AST_Sub({
|
||||||
start: start,
|
start: start,
|
||||||
optional: optional,
|
optional: optional === expr,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
property: prop,
|
property: prop,
|
||||||
end: prev(),
|
end: prev(),
|
||||||
}), allow_calls);
|
});
|
||||||
}
|
} else if (allow_calls && is("punc", "(")) {
|
||||||
if (allow_calls && is("punc", "(")) {
|
|
||||||
next();
|
next();
|
||||||
var call = new AST_Call({
|
expr = new AST_Call({
|
||||||
start: start,
|
start: start,
|
||||||
optional: optional,
|
optional: optional === expr,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
args: expr_list(")", !options.strict),
|
args: expr_list(")", !options.strict),
|
||||||
end: prev(),
|
end: prev(),
|
||||||
});
|
});
|
||||||
return subscripts(call, true);
|
} else if (optional === expr || is("punc", ".")) {
|
||||||
}
|
if (optional !== expr) next();
|
||||||
if (optional || is("punc", ".")) {
|
expr = new AST_Dot({
|
||||||
if (!optional) next();
|
|
||||||
return subscripts(new AST_Dot({
|
|
||||||
start: start,
|
start: start,
|
||||||
optional: optional,
|
optional: optional === expr,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
property: as_name(),
|
property: as_name(),
|
||||||
end: prev(),
|
end: prev(),
|
||||||
}), allow_calls);
|
});
|
||||||
|
} else if (is("punc", "`")) {
|
||||||
|
if (optional) croak("Invalid template on optional chain");
|
||||||
|
expr = template(expr);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (is("punc", "`")) {
|
|
||||||
var tmpl = template(expr);
|
|
||||||
tmpl.start = expr.start;
|
|
||||||
tmpl.end = prev();
|
|
||||||
return subscripts(tmpl, allow_calls);
|
|
||||||
}
|
|
||||||
if (is("operator", "?") && is_token(peek(), "punc", ".")) {
|
|
||||||
next();
|
|
||||||
next();
|
|
||||||
return subscripts(expr, allow_calls, true);
|
|
||||||
}
|
}
|
||||||
|
if (optional) expr.terminal = true;
|
||||||
if (expr instanceof AST_Call && !expr.pure) {
|
if (expr instanceof AST_Call && !expr.pure) {
|
||||||
var start = expr.start;
|
var start = expr.start;
|
||||||
var comments = start.comments_before;
|
var comments = start.comments_before;
|
||||||
@@ -2305,7 +2305,7 @@ function parse($TEXT, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return expr;
|
return expr;
|
||||||
};
|
}
|
||||||
|
|
||||||
function maybe_unary(no_in) {
|
function maybe_unary(no_in) {
|
||||||
var start = S.token;
|
var start = S.token;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ assign_parentheses_dot: {
|
|||||||
input: {
|
input: {
|
||||||
(console?.log).name.p = console.log("PASS");
|
(console?.log).name.p = console.log("PASS");
|
||||||
}
|
}
|
||||||
expect_exact: '(console?.log.name).p=console.log("PASS");'
|
expect_exact: '(console?.log).name.p=console.log("PASS");'
|
||||||
expect_stdout: "PASS"
|
expect_stdout: "PASS"
|
||||||
node_version: ">=14"
|
node_version: ">=14"
|
||||||
}
|
}
|
||||||
@@ -72,6 +72,26 @@ assign_no_parentheses: {
|
|||||||
node_version: ">=14"
|
node_version: ">=14"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
call_parentheses: {
|
||||||
|
input: {
|
||||||
|
(function(o) {
|
||||||
|
console.log(o.f("FAIL"), (o.f)("FAIL"), (0, o.f)(42));
|
||||||
|
console.log(o?.f("FAIL"), (o?.f)("FAIL"), (0, o?.f)(42));
|
||||||
|
})({
|
||||||
|
a: "PASS",
|
||||||
|
f(b) {
|
||||||
|
return this.a || b;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect_exact: '(function(o){console.log(o.f("FAIL"),o.f("FAIL"),(0,o.f)(42));console.log(o?.f("FAIL"),(o?.f)("FAIL"),(0,o?.f)(42))})({a:"PASS",f(b){return this.a||b}});'
|
||||||
|
expect_stdout: [
|
||||||
|
"PASS PASS 42",
|
||||||
|
"PASS PASS 42",
|
||||||
|
]
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
unary_parentheses: {
|
unary_parentheses: {
|
||||||
input: {
|
input: {
|
||||||
var o = { p: 41 };
|
var o = { p: 41 };
|
||||||
@@ -237,6 +257,99 @@ trim_2: {
|
|||||||
node_version: ">=14"
|
node_version: ">=14"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trim_dot_call_1: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
console.log(null?.f());
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
console.log(void 0);
|
||||||
|
}
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_dot_call_2: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
unsafe: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
try {
|
||||||
|
(null?.p)();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("PASS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
try {
|
||||||
|
(void 0)();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("PASS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_dot_call_3: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
unsafe: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
try {
|
||||||
|
({ p: null })?.p();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("PASS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
try {
|
||||||
|
null();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("PASS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_dot_sub: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
console.log(null?.p[42]);
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
console.log(void 0);
|
||||||
|
}
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_sub_call_call: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
console.log(null?.[42]()());
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
console.log(void 0);
|
||||||
|
}
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
issue_4906: {
|
issue_4906: {
|
||||||
options = {
|
options = {
|
||||||
toplevel: true,
|
toplevel: true,
|
||||||
|
|||||||
1
test/input/invalid/optional-template.js
Normal file
1
test/input/invalid/optional-template.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console?.log``;
|
||||||
@@ -721,6 +721,20 @@ describe("bin/uglifyjs", function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("Should throw syntax error (console?.log``)", function(done) {
|
||||||
|
var command = uglifyjscmd + " test/input/invalid/optional-template.js";
|
||||||
|
exec(command, function(err, stdout, stderr) {
|
||||||
|
assert.ok(err);
|
||||||
|
assert.strictEqual(stdout, "");
|
||||||
|
assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [
|
||||||
|
"Parse error at test/input/invalid/optional-template.js:1,12",
|
||||||
|
"console?.log``;",
|
||||||
|
" ^",
|
||||||
|
"ERROR: Invalid template on optional chain",
|
||||||
|
].join("\n"));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
it("Should handle literal string as source map input", function(done) {
|
it("Should handle literal string as source map input", function(done) {
|
||||||
var command = [
|
var command = [
|
||||||
uglifyjscmd,
|
uglifyjscmd,
|
||||||
|
|||||||
Reference in New Issue
Block a user