support optional chaining operator (#4899)
This commit is contained in:
22
lib/ast.js
22
lib/ast.js
@@ -261,9 +261,9 @@ var AST_BlockScope = DEFNODE("BlockScope", "enclosed functions make_def parent_s
|
|||||||
$documentation: "Base class for all statements introducing a lexical scope",
|
$documentation: "Base class for all statements introducing a lexical scope",
|
||||||
$propdoc: {
|
$propdoc: {
|
||||||
enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes",
|
enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes",
|
||||||
functions: "[Object/S] like `variables`, but only lists function declarations",
|
functions: "[Dictionary/S] like `variables`, but only lists function declarations",
|
||||||
parent_scope: "[AST_Scope?/S] link to the parent scope",
|
parent_scope: "[AST_Scope?/S] link to the parent scope",
|
||||||
variables: "[Object/S] a map of name ---> SymbolDef for all variables/functions defined in this scope",
|
variables: "[Dictionary/S] a map of name ---> SymbolDef for all variables/functions defined in this scope",
|
||||||
},
|
},
|
||||||
clone: function(deep) {
|
clone: function(deep) {
|
||||||
var node = this._clone(deep);
|
var node = this._clone(deep);
|
||||||
@@ -506,7 +506,7 @@ var AST_Scope = DEFNODE("Scope", "uses_eval uses_with", {
|
|||||||
var AST_Toplevel = DEFNODE("Toplevel", "globals", {
|
var AST_Toplevel = DEFNODE("Toplevel", "globals", {
|
||||||
$documentation: "The toplevel scope",
|
$documentation: "The toplevel scope",
|
||||||
$propdoc: {
|
$propdoc: {
|
||||||
globals: "[Object/S] a map of name ---> SymbolDef for all undeclared names",
|
globals: "[Dictionary/S] a map of name ---> SymbolDef for all undeclared names",
|
||||||
},
|
},
|
||||||
wrap: function(name) {
|
wrap: function(name) {
|
||||||
var body = this.body;
|
var body = this.body;
|
||||||
@@ -1293,11 +1293,13 @@ function must_be_expressions(node, prop, allow_spread, allow_hole) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var AST_Call = DEFNODE("Call", "expression args pure", {
|
var AST_Call = DEFNODE("Call", "args expression optional pure", {
|
||||||
$documentation: "A function call expression",
|
$documentation: "A function call expression",
|
||||||
$propdoc: {
|
$propdoc: {
|
||||||
|
args: "[AST_Node*] array of arguments",
|
||||||
expression: "[AST_Node] expression to invoke as function",
|
expression: "[AST_Node] expression to invoke as function",
|
||||||
args: "[AST_Node*] array of arguments"
|
optional: "[boolean] whether the expression is optional chaining",
|
||||||
|
pure: "[string/S] marker for side-effect-free call expression",
|
||||||
},
|
},
|
||||||
walk: function(visitor) {
|
walk: function(visitor) {
|
||||||
var node = this;
|
var node = this;
|
||||||
@@ -1315,7 +1317,10 @@ var AST_Call = DEFNODE("Call", "expression args pure", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var AST_New = DEFNODE("New", null, {
|
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() {
|
||||||
|
if (this.optional) throw new Error("optional must be false");
|
||||||
|
},
|
||||||
}, AST_Call);
|
}, AST_Call);
|
||||||
|
|
||||||
var AST_Sequence = DEFNODE("Sequence", "expressions", {
|
var AST_Sequence = DEFNODE("Sequence", "expressions", {
|
||||||
@@ -1337,11 +1342,12 @@ var AST_Sequence = DEFNODE("Sequence", "expressions", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var AST_PropAccess = DEFNODE("PropAccess", "expression property", {
|
var AST_PropAccess = DEFNODE("PropAccess", "expression optional property", {
|
||||||
$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",
|
||||||
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"
|
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",
|
||||||
},
|
},
|
||||||
getProperty: function() {
|
getProperty: function() {
|
||||||
var p = this.property;
|
var p = this.property;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ function Compressor(options, false_by_default) {
|
|||||||
merge_vars : !false_by_default,
|
merge_vars : !false_by_default,
|
||||||
negate_iife : !false_by_default,
|
negate_iife : !false_by_default,
|
||||||
objects : !false_by_default,
|
objects : !false_by_default,
|
||||||
|
optional_chains : !false_by_default,
|
||||||
passes : 1,
|
passes : 1,
|
||||||
properties : !false_by_default,
|
properties : !false_by_default,
|
||||||
pure_funcs : null,
|
pure_funcs : null,
|
||||||
@@ -698,9 +699,7 @@ merge(Compressor.prototype, {
|
|||||||
if (save) fixed = function() {
|
if (save) fixed = function() {
|
||||||
return make_node(AST_Sub, node, {
|
return make_node(AST_Sub, node, {
|
||||||
expression: save(),
|
expression: save(),
|
||||||
property: make_node(AST_Number, node, {
|
property: make_node(AST_Number, node, { value: index }),
|
||||||
value: index
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
node.walk(scanner);
|
node.walk(scanner);
|
||||||
@@ -958,15 +957,18 @@ merge(Compressor.prototype, {
|
|||||||
exp.walk(tw);
|
exp.walk(tw);
|
||||||
if (iife) delete exp.reduce_vars;
|
if (iife) delete exp.reduce_vars;
|
||||||
return true;
|
return true;
|
||||||
} else if (exp instanceof AST_SymbolRef) {
|
}
|
||||||
|
if (exp instanceof AST_SymbolRef) {
|
||||||
var def = exp.definition();
|
var def = exp.definition();
|
||||||
if (this.TYPE == "Call" && tw.in_boolean_context()) def.bool_fn++;
|
if (this.TYPE == "Call" && tw.in_boolean_context()) def.bool_fn++;
|
||||||
if (!(def.fixed instanceof AST_LambdaDefinition)) return;
|
if (def.fixed instanceof AST_LambdaDefinition) {
|
||||||
var defun = mark_defun(tw, def);
|
var defun = mark_defun(tw, def);
|
||||||
if (!defun) return;
|
if (defun) {
|
||||||
descend();
|
descend();
|
||||||
defun.walk(tw);
|
defun.walk(tw);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.TYPE == "Call"
|
} else if (this.TYPE == "Call"
|
||||||
&& exp instanceof AST_Assign
|
&& exp instanceof AST_Assign
|
||||||
&& exp.operator == "="
|
&& exp.operator == "="
|
||||||
@@ -974,6 +976,14 @@ merge(Compressor.prototype, {
|
|||||||
&& tw.in_boolean_context()) {
|
&& tw.in_boolean_context()) {
|
||||||
exp.left.definition().bool_fn++;
|
exp.left.definition().bool_fn++;
|
||||||
}
|
}
|
||||||
|
if (!this.optional) return;
|
||||||
|
exp.walk(tw);
|
||||||
|
push(tw);
|
||||||
|
this.args.forEach(function(arg) {
|
||||||
|
arg.walk(tw);
|
||||||
|
});
|
||||||
|
pop(tw);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
def(AST_Class, function(tw, descend, compressor) {
|
def(AST_Class, function(tw, descend, compressor) {
|
||||||
var node = this;
|
var node = this;
|
||||||
@@ -1143,6 +1153,14 @@ merge(Compressor.prototype, {
|
|||||||
walk_defuns(tw, fn);
|
walk_defuns(tw, fn);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
def(AST_Sub, function(tw) {
|
||||||
|
if (!this.optional) return;
|
||||||
|
this.expression.walk(tw);
|
||||||
|
push(tw);
|
||||||
|
this.property.walk(tw);
|
||||||
|
pop(tw);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
def(AST_Switch, function(tw, descend, compressor) {
|
def(AST_Switch, function(tw, descend, compressor) {
|
||||||
this.variables.each(function(def) {
|
this.variables.each(function(def) {
|
||||||
reset_def(tw, compressor, def);
|
reset_def(tw, compressor, def);
|
||||||
@@ -2068,9 +2086,11 @@ merge(Compressor.prototype, {
|
|||||||
function in_conditional(node, parent) {
|
function in_conditional(node, parent) {
|
||||||
if (parent instanceof AST_Assign) return parent.left !== node && lazy_op[parent.operator.slice(0, -1)];
|
if (parent instanceof AST_Assign) return parent.left !== node && lazy_op[parent.operator.slice(0, -1)];
|
||||||
if (parent instanceof AST_Binary) return parent.left !== node && lazy_op[parent.operator];
|
if (parent instanceof AST_Binary) return parent.left !== node && lazy_op[parent.operator];
|
||||||
|
if (parent instanceof AST_Call) return parent.optional && parent.expression !== node;
|
||||||
if (parent instanceof AST_Case) return parent.expression !== node;
|
if (parent instanceof AST_Case) return parent.expression !== node;
|
||||||
if (parent instanceof AST_Conditional) return parent.condition !== node;
|
if (parent instanceof AST_Conditional) return parent.condition !== node;
|
||||||
return parent instanceof AST_If && parent.condition !== node;
|
if (parent instanceof AST_If) return parent.condition !== node;
|
||||||
|
if (parent instanceof AST_Sub) return parent.optional && parent.expression !== node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_last_node(node, parent) {
|
function is_last_node(node, parent) {
|
||||||
@@ -2132,7 +2152,8 @@ merge(Compressor.prototype, {
|
|||||||
var exp = node.expression;
|
var exp = node.expression;
|
||||||
return side_effects
|
return side_effects
|
||||||
|| exp instanceof AST_SymbolRef && is_arguments(exp.definition())
|
|| exp instanceof AST_SymbolRef && is_arguments(exp.definition())
|
||||||
|| !value_def && (in_try || !lhs_local) && exp.may_throw_on_access(compressor);
|
|| !value_def && (in_try || !lhs_local)
|
||||||
|
&& !node.optional && exp.may_throw_on_access(compressor);
|
||||||
}
|
}
|
||||||
if (node instanceof AST_Spread) return true;
|
if (node instanceof AST_Spread) return true;
|
||||||
if (node instanceof AST_SymbolRef) {
|
if (node instanceof AST_SymbolRef) {
|
||||||
@@ -4983,7 +5004,7 @@ merge(Compressor.prototype, {
|
|||||||
return any(this.properties, compressor);
|
return any(this.properties, compressor);
|
||||||
});
|
});
|
||||||
def(AST_Dot, function(compressor) {
|
def(AST_Dot, function(compressor) {
|
||||||
return this.expression.may_throw_on_access(compressor)
|
return !this.optional && this.expression.may_throw_on_access(compressor)
|
||||||
|| this.expression.has_side_effects(compressor);
|
|| this.expression.has_side_effects(compressor);
|
||||||
});
|
});
|
||||||
def(AST_EmptyStatement, return_false);
|
def(AST_EmptyStatement, return_false);
|
||||||
@@ -5014,7 +5035,7 @@ merge(Compressor.prototype, {
|
|||||||
return this.body.has_side_effects(compressor);
|
return this.body.has_side_effects(compressor);
|
||||||
});
|
});
|
||||||
def(AST_Sub, function(compressor) {
|
def(AST_Sub, function(compressor) {
|
||||||
return this.expression.may_throw_on_access(compressor)
|
return !this.optional && this.expression.may_throw_on_access(compressor)
|
||||||
|| this.expression.has_side_effects(compressor)
|
|| this.expression.has_side_effects(compressor)
|
||||||
|| this.property.has_side_effects(compressor);
|
|| this.property.has_side_effects(compressor);
|
||||||
});
|
});
|
||||||
@@ -5102,7 +5123,7 @@ merge(Compressor.prototype, {
|
|||||||
return any(this.definitions, compressor);
|
return any(this.definitions, compressor);
|
||||||
});
|
});
|
||||||
def(AST_Dot, function(compressor) {
|
def(AST_Dot, function(compressor) {
|
||||||
return this.expression.may_throw_on_access(compressor)
|
return !this.optional && this.expression.may_throw_on_access(compressor)
|
||||||
|| this.expression.may_throw(compressor);
|
|| this.expression.may_throw(compressor);
|
||||||
});
|
});
|
||||||
def(AST_If, function(compressor) {
|
def(AST_If, function(compressor) {
|
||||||
@@ -5130,7 +5151,7 @@ merge(Compressor.prototype, {
|
|||||||
return this.body.may_throw(compressor);
|
return this.body.may_throw(compressor);
|
||||||
});
|
});
|
||||||
def(AST_Sub, function(compressor) {
|
def(AST_Sub, function(compressor) {
|
||||||
return this.expression.may_throw_on_access(compressor)
|
return !this.optional && this.expression.may_throw_on_access(compressor)
|
||||||
|| this.expression.may_throw(compressor)
|
|| this.expression.may_throw(compressor)
|
||||||
|| this.property.may_throw(compressor);
|
|| this.property.may_throw(compressor);
|
||||||
});
|
});
|
||||||
@@ -7592,7 +7613,7 @@ merge(Compressor.prototype, {
|
|||||||
def(AST_Constant, return_null);
|
def(AST_Constant, return_null);
|
||||||
def(AST_Dot, function(compressor, first_in_statement) {
|
def(AST_Dot, function(compressor, first_in_statement) {
|
||||||
var expr = this.expression;
|
var expr = this.expression;
|
||||||
if (expr.may_throw_on_access(compressor)) return this;
|
if (!this.optional && expr.may_throw_on_access(compressor)) return this;
|
||||||
return expr.drop_side_effect_free(compressor, first_in_statement);
|
return expr.drop_side_effect_free(compressor, first_in_statement);
|
||||||
});
|
});
|
||||||
def(AST_Function, function(compressor) {
|
def(AST_Function, function(compressor) {
|
||||||
@@ -8510,6 +8531,18 @@ 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) {
|
||||||
|
if (!compressor.option("optional_chains")) return;
|
||||||
|
if (!self.optional) return;
|
||||||
|
var expr = self.expression;
|
||||||
|
var ev = expr.evaluate(compressor, true);
|
||||||
|
if (ev == null) return make_node(AST_UnaryPrefix, self, {
|
||||||
|
operator: "void",
|
||||||
|
expression: expr,
|
||||||
|
}).optimize(compressor);
|
||||||
|
if (!(ev instanceof AST_Node)) self.optional = false;
|
||||||
|
}
|
||||||
|
|
||||||
function lift_sequence_in_expression(node, compressor) {
|
function lift_sequence_in_expression(node, compressor) {
|
||||||
var exp = node.expression;
|
var exp = node.expression;
|
||||||
if (!(exp instanceof AST_Sequence)) return node;
|
if (!(exp instanceof AST_Sequence)) return node;
|
||||||
@@ -8616,6 +8649,8 @@ merge(Compressor.prototype, {
|
|||||||
|
|
||||||
OPT(AST_Call, function(self, compressor) {
|
OPT(AST_Call, function(self, compressor) {
|
||||||
var exp = self.expression;
|
var exp = self.expression;
|
||||||
|
var terminated = trim_optional_chain(self, compressor);
|
||||||
|
if (terminated) return terminated;
|
||||||
if (compressor.option("sequences")) {
|
if (compressor.option("sequences")) {
|
||||||
if (exp instanceof AST_PropAccess) {
|
if (exp instanceof AST_PropAccess) {
|
||||||
var seq = lift_sequence_in_expression(exp, compressor);
|
var seq = lift_sequence_in_expression(exp, compressor);
|
||||||
@@ -8828,7 +8863,7 @@ merge(Compressor.prototype, {
|
|||||||
return make_node(AST_Call, self, {
|
return make_node(AST_Call, self, {
|
||||||
expression: make_node(AST_Dot, exp, {
|
expression: make_node(AST_Dot, exp, {
|
||||||
expression: exp.expression,
|
expression: exp.expression,
|
||||||
property: "call"
|
property: "call",
|
||||||
}),
|
}),
|
||||||
args: args
|
args: args
|
||||||
}).optimize(compressor);
|
}).optimize(compressor);
|
||||||
@@ -11370,6 +11405,8 @@ merge(Compressor.prototype, {
|
|||||||
OPT(AST_Sub, function(self, compressor) {
|
OPT(AST_Sub, function(self, compressor) {
|
||||||
var expr = self.expression;
|
var expr = self.expression;
|
||||||
var prop = self.property;
|
var prop = self.property;
|
||||||
|
var terminated = trim_optional_chain(self, compressor);
|
||||||
|
if (terminated) return terminated;
|
||||||
if (compressor.option("properties")) {
|
if (compressor.option("properties")) {
|
||||||
var key = prop.evaluate(compressor);
|
var key = prop.evaluate(compressor);
|
||||||
if (key !== prop) {
|
if (key !== prop) {
|
||||||
@@ -11388,8 +11425,9 @@ merge(Compressor.prototype, {
|
|||||||
if (is_identifier_string(property)
|
if (is_identifier_string(property)
|
||||||
&& property.length <= prop.print_to_string().length + 1) {
|
&& property.length <= prop.print_to_string().length + 1) {
|
||||||
return make_node(AST_Dot, self, {
|
return make_node(AST_Dot, self, {
|
||||||
|
optional: self.optional,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
property: property
|
property: property,
|
||||||
}).optimize(compressor);
|
}).optimize(compressor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11496,12 +11534,8 @@ merge(Compressor.prototype, {
|
|||||||
values.push(retValue);
|
values.push(retValue);
|
||||||
return make_sequence(self, values).optimize(compressor);
|
return make_sequence(self, values).optimize(compressor);
|
||||||
} else return make_node(AST_Sub, self, {
|
} else return make_node(AST_Sub, self, {
|
||||||
expression: make_node(AST_Array, expr, {
|
expression: make_node(AST_Array, expr, { elements: values }),
|
||||||
elements: values
|
property: make_node(AST_Number, prop, { value: index }),
|
||||||
}),
|
|
||||||
property: make_node(AST_Number, prop, {
|
|
||||||
value: index
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11597,6 +11631,8 @@ merge(Compressor.prototype, {
|
|||||||
}
|
}
|
||||||
var parent = compressor.parent();
|
var parent = compressor.parent();
|
||||||
if (is_lhs(compressor.self(), parent)) return self;
|
if (is_lhs(compressor.self(), parent)) return self;
|
||||||
|
var terminated = trim_optional_chain(self, compressor);
|
||||||
|
if (terminated) return terminated;
|
||||||
if (compressor.option("sequences")
|
if (compressor.option("sequences")
|
||||||
&& parent.TYPE != "Call"
|
&& parent.TYPE != "Call"
|
||||||
&& !(parent instanceof AST_ForEnumeration && parent.init === self)) {
|
&& !(parent instanceof AST_ForEnumeration && parent.init === self)) {
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
return new (M.computed ? AST_Sub : AST_Dot)({
|
return new (M.computed ? AST_Sub : AST_Dot)({
|
||||||
start: my_start_token(M),
|
start: my_start_token(M),
|
||||||
end: my_end_token(M),
|
end: my_end_token(M),
|
||||||
|
optional: M.optional,
|
||||||
expression: from_moz(M.object),
|
expression: from_moz(M.object),
|
||||||
property: M.computed ? from_moz(M.property) : M.property.name,
|
property: M.computed ? from_moz(M.property) : M.property.name,
|
||||||
});
|
});
|
||||||
@@ -554,6 +555,9 @@
|
|||||||
node.end.parens.push(my_end_token(M));
|
node.end.parens.push(my_end_token(M));
|
||||||
return node;
|
return node;
|
||||||
},
|
},
|
||||||
|
ChainExpression: function(M) {
|
||||||
|
return from_moz(M.expression);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
MOZ_TO_ME.UpdateExpression =
|
MOZ_TO_ME.UpdateExpression =
|
||||||
@@ -593,7 +597,7 @@
|
|||||||
map("AssignmentPattern", AST_DefaultValue, "left>name, right>value");
|
map("AssignmentPattern", AST_DefaultValue, "left>name, right>value");
|
||||||
map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative");
|
map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative");
|
||||||
map("NewExpression", AST_New, "callee>expression, arguments@args, pure=pure");
|
map("NewExpression", AST_New, "callee>expression, arguments@args, pure=pure");
|
||||||
map("CallExpression", AST_Call, "callee>expression, arguments@args, pure=pure");
|
map("CallExpression", AST_Call, "callee>expression, arguments@args, optional=optional, pure=pure");
|
||||||
map("SequenceExpression", AST_Sequence, "expressions@expressions");
|
map("SequenceExpression", AST_Sequence, "expressions@expressions");
|
||||||
map("SpreadElement", AST_Spread, "argument>expression");
|
map("SpreadElement", AST_Spread, "argument>expression");
|
||||||
map("ObjectExpression", AST_Object, "properties@properties");
|
map("ObjectExpression", AST_Object, "properties@properties");
|
||||||
@@ -868,6 +872,7 @@
|
|||||||
type: "MemberExpression",
|
type: "MemberExpression",
|
||||||
object: to_moz(M.expression),
|
object: to_moz(M.expression),
|
||||||
computed: computed,
|
computed: computed,
|
||||||
|
optional: M.optional,
|
||||||
property: computed ? to_moz(M.property) : {
|
property: computed ? to_moz(M.property) : {
|
||||||
type: "Identifier",
|
type: "Identifier",
|
||||||
name: M.property,
|
name: M.property,
|
||||||
|
|||||||
@@ -1473,6 +1473,7 @@ function OutputStream(options) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
print_annotation(self, output);
|
print_annotation(self, output);
|
||||||
self.expression.print(output);
|
self.expression.print(output);
|
||||||
|
if (self.optional) output.print("?.");
|
||||||
print_call_args(self, output);
|
print_call_args(self, output);
|
||||||
});
|
});
|
||||||
DEFPRINT(AST_New, function(output) {
|
DEFPRINT(AST_New, function(output) {
|
||||||
@@ -1501,22 +1502,23 @@ function OutputStream(options) {
|
|||||||
expr.print(output);
|
expr.print(output);
|
||||||
var prop = self.property;
|
var prop = self.property;
|
||||||
if (output.option("ie8") && RESERVED_WORDS[prop]) {
|
if (output.option("ie8") && RESERVED_WORDS[prop]) {
|
||||||
output.print("[");
|
output.print(self.optional ? "?.[" : "[");
|
||||||
output.add_mapping(self.end);
|
output.add_mapping(self.end);
|
||||||
output.print_string(prop);
|
output.print_string(prop);
|
||||||
output.print("]");
|
output.print("]");
|
||||||
} else {
|
} else {
|
||||||
if (expr instanceof AST_Number && !/[ex.)]/i.test(output.last())) output.print(".");
|
if (expr instanceof AST_Number && !/[ex.)]/i.test(output.last())) output.print(".");
|
||||||
output.print(".");
|
output.print(self.optional ? "?." : ".");
|
||||||
// the name after dot would be mapped about here.
|
// the name after dot would be mapped about here.
|
||||||
output.add_mapping(self.end);
|
output.add_mapping(self.end);
|
||||||
output.print_name(prop);
|
output.print_name(prop);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
DEFPRINT(AST_Sub, function(output) {
|
DEFPRINT(AST_Sub, function(output) {
|
||||||
this.expression.print(output);
|
var self = this;
|
||||||
output.print("[");
|
self.expression.print(output);
|
||||||
this.property.print(output);
|
output.print(self.optional ? "?.[" : "[");
|
||||||
|
self.property.print(output);
|
||||||
output.print("]");
|
output.print("]");
|
||||||
});
|
});
|
||||||
DEFPRINT(AST_Spread, function(output) {
|
DEFPRINT(AST_Spread, function(output) {
|
||||||
|
|||||||
34
lib/parse.js
34
lib/parse.js
@@ -2245,44 +2245,52 @@ function parse($TEXT, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscripts = function(expr, allow_calls) {
|
var subscripts = function(expr, allow_calls, optional) {
|
||||||
var start = expr.start;
|
var start = expr.start;
|
||||||
if (is("punc", ".")) {
|
|
||||||
next();
|
|
||||||
return subscripts(new AST_Dot({
|
|
||||||
start : start,
|
|
||||||
expression : expr,
|
|
||||||
property : as_name(),
|
|
||||||
end : prev()
|
|
||||||
}), allow_calls);
|
|
||||||
}
|
|
||||||
if (is("punc", "[")) {
|
if (is("punc", "[")) {
|
||||||
next();
|
next();
|
||||||
var prop = expression();
|
var prop = expression();
|
||||||
expect("]");
|
expect("]");
|
||||||
return subscripts(new AST_Sub({
|
return subscripts(new AST_Sub({
|
||||||
start: start,
|
start: start,
|
||||||
|
optional: optional,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
property: prop,
|
property: prop,
|
||||||
end : prev()
|
end: prev(),
|
||||||
}), allow_calls);
|
}), allow_calls);
|
||||||
}
|
}
|
||||||
if (allow_calls && is("punc", "(")) {
|
if (allow_calls && is("punc", "(")) {
|
||||||
next();
|
next();
|
||||||
var call = new AST_Call({
|
var call = new AST_Call({
|
||||||
start: start,
|
start: start,
|
||||||
|
optional: optional,
|
||||||
expression: expr,
|
expression: expr,
|
||||||
args: expr_list(")", !options.strict),
|
args: expr_list(")", !options.strict),
|
||||||
end : prev()
|
end: prev(),
|
||||||
});
|
});
|
||||||
return subscripts(call, true);
|
return subscripts(call, true);
|
||||||
}
|
}
|
||||||
|
if (optional || is("punc", ".")) {
|
||||||
|
if (!optional) next();
|
||||||
|
return subscripts(new AST_Dot({
|
||||||
|
start: start,
|
||||||
|
optional: optional,
|
||||||
|
expression: expr,
|
||||||
|
property: as_name(),
|
||||||
|
end: prev(),
|
||||||
|
}), allow_calls);
|
||||||
|
}
|
||||||
if (is("punc", "`")) {
|
if (is("punc", "`")) {
|
||||||
var tmpl = template(expr);
|
var tmpl = template(expr);
|
||||||
tmpl.start = expr.start;
|
tmpl.start = expr.start;
|
||||||
tmpl.end = prev();
|
tmpl.end = prev();
|
||||||
return subscripts(tmpl, allow_calls);
|
return subscripts(tmpl, allow_calls);
|
||||||
}
|
}
|
||||||
|
if (is("operator", "?") && is_token(peek(), "punc", ".")) {
|
||||||
|
next();
|
||||||
|
next();
|
||||||
|
return subscripts(expr, allow_calls, 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;
|
||||||
@@ -2405,7 +2413,7 @@ function parse($TEXT, options) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function is_assignable(expr) {
|
function is_assignable(expr) {
|
||||||
return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef;
|
return expr instanceof AST_PropAccess && !expr.optional || expr instanceof AST_SymbolRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
function to_destructured(node) {
|
function to_destructured(node) {
|
||||||
|
|||||||
196
test/compress/optional-chains.js
Normal file
196
test/compress/optional-chains.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
call: {
|
||||||
|
input: {
|
||||||
|
console.log?.(undefined?.(console.log("FAIL")));
|
||||||
|
}
|
||||||
|
expect_exact: 'console.log?.((void 0)?.(console.log("FAIL")));'
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
dot: {
|
||||||
|
input: {
|
||||||
|
console?.log((void 0)?.p);
|
||||||
|
}
|
||||||
|
expect_exact: "console?.log((void 0)?.p);"
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
dot_in: {
|
||||||
|
input: {
|
||||||
|
var o = { in: 42 };
|
||||||
|
console.log(o.in, o?.in);
|
||||||
|
}
|
||||||
|
expect_exact: "var o={in:42};console.log(o.in,o?.in);"
|
||||||
|
expect_stdout: "42 42"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
sub: {
|
||||||
|
input: {
|
||||||
|
console?.["log"](null?.[console.log("FAIL")]);
|
||||||
|
}
|
||||||
|
expect_exact: 'console?.["log"](null?.[console.log("FAIL")]);'
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
ternary_decimal: {
|
||||||
|
input: {
|
||||||
|
null ? .42 : console.log("PASS");
|
||||||
|
}
|
||||||
|
expect_exact: 'null?.42:console.log("PASS");'
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse_vars_1: {
|
||||||
|
options = {
|
||||||
|
collapse_vars: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a;
|
||||||
|
A = 42;
|
||||||
|
a?.[42];
|
||||||
|
console.log(typeof A);
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a;
|
||||||
|
A = 42;
|
||||||
|
a?.[42];
|
||||||
|
console.log(typeof A);
|
||||||
|
}
|
||||||
|
expect_stdout: "number"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse_vars_2: {
|
||||||
|
options = {
|
||||||
|
collapse_vars: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a;
|
||||||
|
A = 42;
|
||||||
|
a?.(42);
|
||||||
|
console.log(typeof A);
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a;
|
||||||
|
A = 42;
|
||||||
|
a?.(42);
|
||||||
|
console.log(typeof A);
|
||||||
|
}
|
||||||
|
expect_stdout: "number"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
properties: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a;
|
||||||
|
console.log(a?.["FAIL"]);
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a;
|
||||||
|
console.log(a?.FAIL);
|
||||||
|
}
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
reduce_vars_1: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
reduce_vars: true,
|
||||||
|
toplevel: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a = 1;
|
||||||
|
null?.[a = 0];
|
||||||
|
console.log(a ? "PASS" : "FAIL");
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a = 1;
|
||||||
|
null?.[a = 0];
|
||||||
|
console.log(a ? "PASS" : "FAIL");
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
reduce_vars_2: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
reduce_vars: true,
|
||||||
|
toplevel: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a = 1;
|
||||||
|
null?.(a = 0);
|
||||||
|
console.log(a ? "PASS" : "FAIL");
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a = 1;
|
||||||
|
null?.(a = 0);
|
||||||
|
console.log(a ? "PASS" : "FAIL");
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
side_effects: {
|
||||||
|
options = {
|
||||||
|
side_effects: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
var a;
|
||||||
|
a?.[a = "FAIL"];
|
||||||
|
console.log(a);
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
var a;
|
||||||
|
a?.[a = "FAIL"];
|
||||||
|
console.log(a);
|
||||||
|
}
|
||||||
|
expect_stdout: "undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_1: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
reduce_vars: true,
|
||||||
|
unsafe: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
(function(a, b) {
|
||||||
|
console?.log?.(a?.p, b?.[console.log("FAIL")]);
|
||||||
|
})?.({ p: "PASS" });
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
(function(a, b) {
|
||||||
|
console?.log?.(a.p, void 0);
|
||||||
|
})({ p: "PASS" });
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS undefined"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_2: {
|
||||||
|
options = {
|
||||||
|
evaluate: true,
|
||||||
|
optional_chains: true,
|
||||||
|
side_effects: true,
|
||||||
|
}
|
||||||
|
input: {
|
||||||
|
(void console.log("PASS"))?.[console.log("FAIL")];
|
||||||
|
}
|
||||||
|
expect: {
|
||||||
|
console.log("PASS");
|
||||||
|
}
|
||||||
|
expect_stdout: "PASS"
|
||||||
|
node_version: ">=14"
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
++null
|
console.log(4 || (null = 4));
|
||||||
|
|||||||
1
test/input/invalid/assign_5.js
Normal file
1
test/input/invalid/assign_5.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log(5 || ([]?.length ^= 5));
|
||||||
@@ -427,16 +427,30 @@ describe("bin/uglifyjs", function() {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("Should throw syntax error (++null)", function(done) {
|
it("Should throw syntax error (null = 4)", function(done) {
|
||||||
var command = uglifyjscmd + " test/input/invalid/assign_4.js";
|
var command = uglifyjscmd + " test/input/invalid/assign_4.js";
|
||||||
exec(command, function(err, stdout, stderr) {
|
exec(command, function(err, stdout, stderr) {
|
||||||
assert.ok(err);
|
assert.ok(err);
|
||||||
assert.strictEqual(stdout, "");
|
assert.strictEqual(stdout, "");
|
||||||
assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [
|
assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [
|
||||||
"Parse error at test/input/invalid/assign_4.js:1,0",
|
"Parse error at test/input/invalid/assign_4.js:1,23",
|
||||||
"++null",
|
"console.log(4 || (null = 4));",
|
||||||
" ^",
|
" ^",
|
||||||
"ERROR: Invalid use of ++ operator",
|
"ERROR: Invalid assignment",
|
||||||
|
].join("\n"));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Should throw syntax error ([]?.length ^= 5)", function(done) {
|
||||||
|
var command = uglifyjscmd + " test/input/invalid/assign_5.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/assign_5.js:1,29",
|
||||||
|
"console.log(5 || ([]?.length ^= 5));",
|
||||||
|
" ^",
|
||||||
|
"ERROR: Invalid assignment",
|
||||||
].join("\n"));
|
].join("\n"));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ minify_in_situ() {
|
|||||||
do
|
do
|
||||||
echo "$i"
|
echo "$i"
|
||||||
CODE=`cat "$i"`
|
CODE=`cat "$i"`
|
||||||
node_modules/.bin/esbuild --loader=ts --target=es2019 > "$i" <<EOF
|
node_modules/.bin/esbuild --loader=ts --target=esnext > "$i" <<EOF
|
||||||
$CODE
|
$CODE
|
||||||
EOF
|
EOF
|
||||||
ARGS="$ARGS $i"
|
ARGS="$ARGS $i"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ minify_in_situ() {
|
|||||||
do
|
do
|
||||||
echo "$i"
|
echo "$i"
|
||||||
CODE=`cat "$i"`
|
CODE=`cat "$i"`
|
||||||
node_modules/.bin/esbuild --loader=ts --target=es2019 > "$i" <<EOF
|
node_modules/.bin/esbuild --loader=ts --target=esnext > "$i" <<EOF
|
||||||
$CODE
|
$CODE
|
||||||
EOF
|
EOF
|
||||||
ARGS="$ARGS $i"
|
ARGS="$ARGS $i"
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ var SUPPORT = function(matrix) {
|
|||||||
logical_assignment: "[].p ??= 0;",
|
logical_assignment: "[].p ??= 0;",
|
||||||
new_target: "function f() { new.target; }",
|
new_target: "function f() { new.target; }",
|
||||||
nullish: "0 ?? 0",
|
nullish: "0 ?? 0",
|
||||||
|
optional_chaining: "0?.p",
|
||||||
rest: "var [...a] = [];",
|
rest: "var [...a] = [];",
|
||||||
rest_object: "var {...a} = {};",
|
rest_object: "var {...a} = {};",
|
||||||
spread: "[...[]];",
|
spread: "[...[]];",
|
||||||
@@ -1487,12 +1488,16 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) {
|
|||||||
return createValue() + " in " + createObjectLiteral(recurmax, stmtDepth, canThrow);
|
return createValue() + " in " + createObjectLiteral(recurmax, stmtDepth, canThrow);
|
||||||
case p++:
|
case p++:
|
||||||
var name = getVarName();
|
var name = getVarName();
|
||||||
var s = name + "[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]";
|
var prop = "[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]";
|
||||||
return canThrow && rng(20) == 0 ? s : name + " && " + s;
|
if (SUPPORT.optional_chaining && rng(50) == 0) return name + "?." + prop;
|
||||||
|
if (canThrow && rng(20) == 0) return name + prop;
|
||||||
|
return name + " && " + name + prop;
|
||||||
case p++:
|
case p++:
|
||||||
var name = getVarName();
|
var name = getVarName();
|
||||||
var s = name + "." + getDotKey();
|
var prop = getDotKey();
|
||||||
return canThrow && rng(20) == 0 ? s : name + " && " + s;
|
if (SUPPORT.optional_chaining && rng(50) == 0) return name + "?." + prop;
|
||||||
|
if (canThrow && rng(20) == 0) return name + "." + prop;
|
||||||
|
return name + " && " + name + "." + prop;
|
||||||
case p++:
|
case p++:
|
||||||
case p++:
|
case p++:
|
||||||
var name = getVarName();
|
var name = getVarName();
|
||||||
@@ -1534,7 +1539,16 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) {
|
|||||||
name = rng(3) == 0 ? getVarName() : "f" + rng(funcs + 2);
|
name = rng(3) == 0 ? getVarName() : "f" + rng(funcs + 2);
|
||||||
} while (name in called && !called[name]);
|
} while (name in called && !called[name]);
|
||||||
called[name] = true;
|
called[name] = true;
|
||||||
return mayDefer("typeof " + name + ' == "function" && --_calls_ >= 0 && ' + name + createArgs(recurmax, stmtDepth, canThrow));
|
var args = createArgs(recurmax, stmtDepth, canThrow);
|
||||||
|
var call = "typeof " + name + ' == "function" && --_calls_ >= 0 && ' + name + args;
|
||||||
|
if (canThrow) {
|
||||||
|
if (SUPPORT.optional_chaining && args[0] != "`" && rng(50) == 0) {
|
||||||
|
call = name + "?." + args;
|
||||||
|
} else if (rng(20) == 0) {
|
||||||
|
call = name + args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mayDefer(call);
|
||||||
}
|
}
|
||||||
_createExpression.N = p;
|
_createExpression.N = p;
|
||||||
return _createExpression(recurmax, noComma, stmtDepth, canThrow);
|
return _createExpression(recurmax, noComma, stmtDepth, canThrow);
|
||||||
|
|||||||
Reference in New Issue
Block a user