Skip to content

Commit

Permalink
support optional chaining operator (#4899)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlamsl committed May 3, 2021
1 parent 203f4b7 commit f0de9a8
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 71 deletions.
22 changes: 14 additions & 8 deletions lib/ast.js
Expand Up @@ -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",
$propdoc: {
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",
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) {
var node = this._clone(deep);
Expand Down Expand Up @@ -506,7 +506,7 @@ var AST_Scope = DEFNODE("Scope", "uses_eval uses_with", {
var AST_Toplevel = DEFNODE("Toplevel", "globals", {
$documentation: "The toplevel scope",
$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) {
var body = this.body;
Expand Down Expand Up @@ -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",
$propdoc: {
args: "[AST_Node*] array of arguments",
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) {
var node = this;
Expand All @@ -1315,7 +1317,10 @@ var AST_Call = DEFNODE("Call", "expression args pure", {
});

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);

var AST_Sequence = DEFNODE("Sequence", "expressions", {
Expand All @@ -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\"]`",
$propdoc: {
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() {
var p = this.property;
Expand Down
86 changes: 61 additions & 25 deletions lib/compress.js
Expand Up @@ -82,6 +82,7 @@ function Compressor(options, false_by_default) {
merge_vars : !false_by_default,
negate_iife : !false_by_default,
objects : !false_by_default,
optional_chains : !false_by_default,
passes : 1,
properties : !false_by_default,
pure_funcs : null,
Expand Down Expand Up @@ -698,9 +699,7 @@ merge(Compressor.prototype, {
if (save) fixed = function() {
return make_node(AST_Sub, node, {
expression: save(),
property: make_node(AST_Number, node, {
value: index
})
property: make_node(AST_Number, node, { value: index }),
});
};
node.walk(scanner);
Expand Down Expand Up @@ -958,22 +957,33 @@ merge(Compressor.prototype, {
exp.walk(tw);
if (iife) delete exp.reduce_vars;
return true;
} else if (exp instanceof AST_SymbolRef) {
}
if (exp instanceof AST_SymbolRef) {
var def = exp.definition();
if (this.TYPE == "Call" && tw.in_boolean_context()) def.bool_fn++;
if (!(def.fixed instanceof AST_LambdaDefinition)) return;
var defun = mark_defun(tw, def);
if (!defun) return;
descend();
defun.walk(tw);
return true;
if (def.fixed instanceof AST_LambdaDefinition) {
var defun = mark_defun(tw, def);
if (defun) {
descend();
defun.walk(tw);
return true;
}
}
} else if (this.TYPE == "Call"
&& exp instanceof AST_Assign
&& exp.operator == "="
&& exp.left instanceof AST_SymbolRef
&& tw.in_boolean_context()) {
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) {
var node = this;
Expand Down Expand Up @@ -1143,6 +1153,14 @@ merge(Compressor.prototype, {
walk_defuns(tw, fn);
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) {
this.variables.each(function(def) {
reset_def(tw, compressor, def);
Expand Down Expand Up @@ -2068,9 +2086,11 @@ merge(Compressor.prototype, {
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_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_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) {
Expand Down Expand Up @@ -2132,7 +2152,8 @@ merge(Compressor.prototype, {
var exp = node.expression;
return side_effects
|| 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_SymbolRef) {
Expand Down Expand Up @@ -4983,7 +5004,7 @@ merge(Compressor.prototype, {
return any(this.properties, 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);
});
def(AST_EmptyStatement, return_false);
Expand Down Expand Up @@ -5014,7 +5035,7 @@ merge(Compressor.prototype, {
return this.body.has_side_effects(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.property.has_side_effects(compressor);
});
Expand Down Expand Up @@ -5102,7 +5123,7 @@ merge(Compressor.prototype, {
return any(this.definitions, 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);
});
def(AST_If, function(compressor) {
Expand Down Expand Up @@ -5130,7 +5151,7 @@ merge(Compressor.prototype, {
return this.body.may_throw(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.property.may_throw(compressor);
});
Expand Down Expand Up @@ -7592,7 +7613,7 @@ merge(Compressor.prototype, {
def(AST_Constant, return_null);
def(AST_Dot, function(compressor, first_in_statement) {
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);
});
def(AST_Function, function(compressor) {
Expand Down Expand Up @@ -8510,6 +8531,18 @@ merge(Compressor.prototype, {
OPT(AST_Const, 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) {
var exp = node.expression;
if (!(exp instanceof AST_Sequence)) return node;
Expand Down Expand Up @@ -8616,6 +8649,8 @@ merge(Compressor.prototype, {

OPT(AST_Call, function(self, compressor) {
var exp = self.expression;
var terminated = trim_optional_chain(self, compressor);
if (terminated) return terminated;
if (compressor.option("sequences")) {
if (exp instanceof AST_PropAccess) {
var seq = lift_sequence_in_expression(exp, compressor);
Expand Down Expand Up @@ -8828,7 +8863,7 @@ merge(Compressor.prototype, {
return make_node(AST_Call, self, {
expression: make_node(AST_Dot, exp, {
expression: exp.expression,
property: "call"
property: "call",
}),
args: args
}).optimize(compressor);
Expand Down Expand Up @@ -11370,6 +11405,8 @@ merge(Compressor.prototype, {
OPT(AST_Sub, function(self, compressor) {
var expr = self.expression;
var prop = self.property;
var terminated = trim_optional_chain(self, compressor);
if (terminated) return terminated;
if (compressor.option("properties")) {
var key = prop.evaluate(compressor);
if (key !== prop) {
Expand All @@ -11388,8 +11425,9 @@ merge(Compressor.prototype, {
if (is_identifier_string(property)
&& property.length <= prop.print_to_string().length + 1) {
return make_node(AST_Dot, self, {
optional: self.optional,
expression: expr,
property: property
property: property,
}).optimize(compressor);
}
}
Expand Down Expand Up @@ -11496,12 +11534,8 @@ merge(Compressor.prototype, {
values.push(retValue);
return make_sequence(self, values).optimize(compressor);
} else return make_node(AST_Sub, self, {
expression: make_node(AST_Array, expr, {
elements: values
}),
property: make_node(AST_Number, prop, {
value: index
})
expression: make_node(AST_Array, expr, { elements: values }),
property: make_node(AST_Number, prop, { value: index }),
});
}
}
Expand Down Expand Up @@ -11597,6 +11631,8 @@ merge(Compressor.prototype, {
}
var parent = compressor.parent();
if (is_lhs(compressor.self(), parent)) return self;
var terminated = trim_optional_chain(self, compressor);
if (terminated) return terminated;
if (compressor.option("sequences")
&& parent.TYPE != "Call"
&& !(parent instanceof AST_ForEnumeration && parent.init === self)) {
Expand Down
7 changes: 6 additions & 1 deletion lib/mozilla-ast.js
Expand Up @@ -274,6 +274,7 @@
return new (M.computed ? AST_Sub : AST_Dot)({
start: my_start_token(M),
end: my_end_token(M),
optional: M.optional,
expression: from_moz(M.object),
property: M.computed ? from_moz(M.property) : M.property.name,
});
Expand Down Expand Up @@ -554,6 +555,9 @@
node.end.parens.push(my_end_token(M));
return node;
},
ChainExpression: function(M) {
return from_moz(M.expression);
},
};

MOZ_TO_ME.UpdateExpression =
Expand Down Expand Up @@ -593,7 +597,7 @@
map("AssignmentPattern", AST_DefaultValue, "left>name, right>value");
map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative");
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("SpreadElement", AST_Spread, "argument>expression");
map("ObjectExpression", AST_Object, "properties@properties");
Expand Down Expand Up @@ -868,6 +872,7 @@
type: "MemberExpression",
object: to_moz(M.expression),
computed: computed,
optional: M.optional,
property: computed ? to_moz(M.property) : {
type: "Identifier",
name: M.property,
Expand Down
12 changes: 7 additions & 5 deletions lib/output.js
Expand Up @@ -1473,6 +1473,7 @@ function OutputStream(options) {
var self = this;
print_annotation(self, output);
self.expression.print(output);
if (self.optional) output.print("?.");
print_call_args(self, output);
});
DEFPRINT(AST_New, function(output) {
Expand Down Expand Up @@ -1501,22 +1502,23 @@ function OutputStream(options) {
expr.print(output);
var prop = self.property;
if (output.option("ie8") && RESERVED_WORDS[prop]) {
output.print("[");
output.print(self.optional ? "?.[" : "[");
output.add_mapping(self.end);
output.print_string(prop);
output.print("]");
} else {
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.
output.add_mapping(self.end);
output.print_name(prop);
}
});
DEFPRINT(AST_Sub, function(output) {
this.expression.print(output);
output.print("[");
this.property.print(output);
var self = this;
self.expression.print(output);
output.print(self.optional ? "?.[" : "[");
self.property.print(output);
output.print("]");
});
DEFPRINT(AST_Spread, function(output) {
Expand Down

0 comments on commit f0de9a8

Please sign in to comment.