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

Optional Chaining Operator ?. #1593

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
13 changes: 13 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ private void visitExpression(Node node, int contextFlags) {

case Token.REF_CALL:
case Token.CALL:
case Token.CALL_OPTIONAL:
case Token.NEW:
{
if (type == Token.NEW) {
Expand Down Expand Up @@ -686,6 +687,11 @@ private void visitExpression(Node node, int contextFlags) {
resolveForwardGoto(afterElseJumpStart);
}
break;
case Token.GETPROP_OPTIONAL:
visitExpression(child, 0);
child = child.getNext();
addStringOp(type, child.getString());
break;

case Token.GETPROP:
case Token.GETPROPNOWARN:
Expand Down Expand Up @@ -962,6 +968,7 @@ private void visitExpression(Node node, int contextFlags) {
visitArrayComprehension(node, child, child.getNext());
break;

case Token.REF_SPECIAL_OPTIONAL:
case Token.REF_SPECIAL:
visitExpression(child, 0);
addStringOp(type, (String) node.getProp(Node.NAME_PROP));
Expand Down Expand Up @@ -1057,6 +1064,7 @@ private void generateCallFunAndThis(Node left) {
stackChange(2);
break;
}
case Token.GETPROP_OPTIONAL:
case Token.GETPROP:
case Token.GETELEM:
{
Expand All @@ -1068,6 +1076,11 @@ private void generateCallFunAndThis(Node left) {
// stack: ... target -> ... function thisObj
addStringOp(Icode_PROP_AND_THIS, property);
stackChange(1);
} else if (type == Token.GETPROP_OPTIONAL) {
String property = id.getString();
// stack: ... target -> ... function thisObj
addStringOp(Icode_PROP_AND_THIS_OPTIONAL, property);
stackChange(1);
} else {
visitExpression(id, 0);
// stack: ... target id -> ... function thisObj
Expand Down
29 changes: 22 additions & 7 deletions rhino/src/main/java/org/mozilla/javascript/IRFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ private Node transform(AstNode node) {
return transformBlock(node);
case Token.BREAK:
return transformBreak((BreakStatement) node);
case Token.CALL_OPTIONAL:
case Token.CALL:
return transformFunctionCall((FunctionCall) node);
case Token.CONTINUE:
Expand All @@ -161,6 +162,7 @@ private Node transform(AstNode node) {
return transformGenExpr((GeneratorExpression) node);
case Token.GETELEM:
return transformElementGet((ElementGet) node);
case Token.QUESTION_DOT:
case Token.GETPROP:
return transformPropertyGet((PropertyGet) node);
case Token.HOOK:
Expand Down Expand Up @@ -347,7 +349,8 @@ private Node arrayCompTransformHelper(ArrayComprehension node, String arrayName)
Node call =
createCallOrNew(
Token.CALL,
createPropertyGet(parser.createName(arrayName), null, "push", 0));
createPropertyGet(
parser.createName(arrayName), null, "push", 0, node.type));

Node body = new Node(Token.EXPR_VOID, call, lineno);

Expand Down Expand Up @@ -603,7 +606,10 @@ private Node transformFunction(FunctionNode fn) {
}

private Node transformFunctionCall(FunctionCall node) {
Node call = createCallOrNew(Token.CALL, transform(node.getTarget()));
Node call =
createCallOrNew(
node.type == Token.CALL_OPTIONAL ? Token.CALL_OPTIONAL : Token.CALL,
transform(node.getTarget()));
call.setLineno(node.getLineno());
List<AstNode> args = node.getArguments();
for (int i = 0; i < args.size(); i++) {
Expand Down Expand Up @@ -917,7 +923,7 @@ private Node transformComputedPropertyKey(ComputedPropertyKey node) {
private Node transformPropertyGet(PropertyGet node) {
Node target = transform(node.getTarget());
String name = node.getProperty().getIdentifier();
return createPropertyGet(target, null, name, 0);
return createPropertyGet(target, null, name, 0, node.type);
}

private Node transformTemplateLiteral(TemplateLiteral node) {
Expand Down Expand Up @@ -1239,7 +1245,7 @@ private Node transformXmlRef(Node pn, XmlRef node, int memberTypeFlags) {
String ns = namespace != null ? namespace.getIdentifier() : null;
if (node instanceof XmlPropRef) {
String name = ((XmlPropRef) node).getPropName().getIdentifier();
return createPropertyGet(pn, ns, name, memberTypeFlags);
return createPropertyGet(pn, ns, name, memberTypeFlags, node.type);
}
Node expr = transform(((XmlElemRef) node).getExpression());
return createElementGet(pn, ns, expr, memberTypeFlags);
Expand Down Expand Up @@ -1875,18 +1881,27 @@ private static Node createIncDec(int nodeType, boolean post, Node child) {
}

private Node createPropertyGet(
Node target, String namespace, String name, int memberTypeFlags) {
Node target, String namespace, String name, int memberTypeFlags, int type) {
if (namespace == null && memberTypeFlags == 0) {
if (target == null) {
return parser.createName(name);
}
parser.checkActivationName(name, Token.GETPROP);
if (ScriptRuntime.isSpecialProperty(name)) {
Node ref = new Node(Token.REF_SPECIAL, target);
Node ref =
new Node(
type == Token.QUESTION_DOT
? Token.REF_SPECIAL_OPTIONAL
: Token.REF_SPECIAL,
target);
ref.putProp(Node.NAME_PROP, name);
return new Node(Token.GET_REF, ref);
}
return new Node(Token.GETPROP, target, Node.newString(name));

return new Node(
type == Token.QUESTION_DOT ? Token.GETPROP_OPTIONAL : Token.GETPROP,
target,
Node.newString(name));
}
Node elem = Node.newString(name);
memberTypeFlags |= Node.PROPERTY_FLAG;
Expand Down
6 changes: 4 additions & 2 deletions rhino/src/main/java/org/mozilla/javascript/Icode.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ abstract class Icode {
Icode_TEMPLATE_LITERAL_CALLSITE = -74,
Icode_LITERAL_KEYS = -75,
Icode_LITERAL_KEY_SET = -76,

Icode_PROP_AND_THIS_OPTIONAL = -77,
// Last icode
MIN_ICODE = -76;
MIN_ICODE = -77;

static String bytecodeName(int bytecode) {
if (!validBytecode(bytecode)) {
Expand Down Expand Up @@ -190,6 +190,8 @@ static String bytecodeName(int bytecode) {
return "NAME_AND_THIS";
case Icode_PROP_AND_THIS:
return "PROP_AND_THIS";
case Icode_PROP_AND_THIS_OPTIONAL:
return "PROP_AND_THIS_OPTIONAL";
case Icode_ELEM_AND_THIS:
return "ELEM_AND_THIS";
case Icode_VALUE_AND_THIS:
Expand Down
35 changes: 35 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/Interpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,16 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl
lhs, stringReg, cx, frame.scope);
continue Loop;
}
case Token.GETPROP_OPTIONAL:
{
Object lhs = stack[stackTop];
if (lhs == DBL_MRK)
lhs = ScriptRuntime.wrapNumber(sDbl[stackTop]);
stack[stackTop] =
ScriptRuntime.getObjectPropOptional(
lhs, stringReg, cx, frame.scope);
continue Loop;
}
case Token.SETPROP:
{
Object rhs = stack[stackTop];
Expand Down Expand Up @@ -1694,6 +1704,19 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl
++stackTop;
stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx);
continue Loop;
case Icode_PROP_AND_THIS_OPTIONAL:
{
Object obj = stack[stackTop];
if (obj == DBL_MRK)
obj = ScriptRuntime.wrapNumber(sDbl[stackTop]);
// stringReg: property
stack[stackTop] =
ScriptRuntime.getPropFunctionAndThisOptional(
obj, stringReg, cx, frame.scope);
++stackTop;
stack[stackTop] = ScriptRuntime.lastStoredScriptable(cx);
continue Loop;
}
case Icode_PROP_AND_THIS:
{
Object obj = stack[stackTop];
Expand Down Expand Up @@ -1744,6 +1767,7 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl
continue Loop;
}
case Token.CALL:
case Token.CALL_OPTIONAL:
case Icode_TAIL_CALL:
case Token.REF_CALL:
{
Expand Down Expand Up @@ -2260,6 +2284,17 @@ private static Object interpretLoop(Context cx, CallFrame frame, Object throwabl
obj, stringReg, cx, frame.scope);
continue Loop;
}
case Token.REF_SPECIAL_OPTIONAL:
{
// stringReg: name of special property
Object obj = stack[stackTop];
if (obj == DBL_MRK)
obj = ScriptRuntime.wrapNumber(sDbl[stackTop]);
stack[stackTop] =
ScriptRuntime.optionalSpecialRef(
obj, stringReg, cx, frame.scope);
continue Loop;
}
case Token.REF_MEMBER:
{
// indexReg: flags
Expand Down
35 changes: 33 additions & 2 deletions rhino/src/main/java/org/mozilla/javascript/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2795,6 +2795,7 @@ private AstNode memberExprTail(boolean allowCallSyntax, AstNode pn) throws IOExc
int tt = peekToken();
switch (tt) {
case Token.DOT:
case Token.QUESTION_DOT:
case Token.DOTDOT:
lineno = ts.lineno;
pn = propertyAccess(tt, pn);
Expand Down Expand Up @@ -2924,8 +2925,28 @@ private AstNode propertyAccess(int tt, AstNode pn) throws IOException {
}

AstNode ref = null; // right side of . or .. operator

int token = nextToken();
if (token == Token.LP && tt == Token.QUESTION_DOT) {
// optional chaining operator method call, o.func?.()
var pos = pn.getPosition();
pn.setType(Token.QUESTION_DOT);
consumeToken();
checkCallRequiresActivation(pn);
FunctionCall f = new FunctionCall(pos);
f.setTarget(pn);
// Assign the line number for the function call to where
// the paren appeared, not where the name expression started.
f.setLineno(lineno);
f.setLp(ts.tokenBeg - pos);
List<AstNode> args = argumentList();
if (args != null && args.size() > ARGC_LIMIT) reportError("msg.too.many.function.args");
f.setArguments(args);
f.setRp(ts.tokenBeg - pos);
f.setLength(ts.tokenEnd - pos);
f.setType(Token.CALL_OPTIONAL);
return f;
}

switch (token) {
case Token.THROW:
// needed for generator.throw();
Expand Down Expand Up @@ -2957,7 +2978,6 @@ private AstNode propertyAccess(int tt, AstNode pn) throws IOException {
ref = propertyName(-1, memberTypeFlags);
break;
}

default:
if (compilerEnv.isReservedKeywordAsIdentifier()) {
// allow keywords as property names, e.g. ({if: 1})
Expand All @@ -2982,9 +3002,20 @@ private AstNode propertyAccess(int tt, AstNode pn) throws IOException {
result.setLineno(pn.getLineno());
result.setLeft(pn); // do this after setting position
result.setRight(ref);

if (tt == Token.QUESTION_DOT && result instanceof PropertyGet) {
result.setType(Token.QUESTION_DOT);
}
return result;
}

private static AstNode cloneNode(AstNode target) {
var newParser = new Parser();
var root = newParser.parse(target.toSource(), "", 1);
var targetCopy = ((ExpressionStatement) root.first).getExpression();
return targetCopy;
}

/**
* Xml attribute expression:
*
Expand Down
35 changes: 35 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,22 @@ public static Object getObjectProp(Object obj, String property, Context cx, Scri
return getObjectProp(sobj, property, cx);
}

public static Object getObjectPropOptional(
Object obj, String property, Context cx, Scriptable scope) {
if (obj == null || Undefined.isUndefined(obj)) {
return Undefined.instance;
}
return getObjectProp(obj, property, cx, scope);
}

public static Object getObjectPropOptional(
Scriptable obj, String property, Context cx, Scriptable scope) {
if (obj == null || Undefined.isUndefined(obj)) {
return Undefined.instance;
}
return getObjectProp(obj, property, cx);
}

public static Object getObjectProp(Scriptable obj, String property, Context cx) {

Object result = ScriptableObject.getProperty(obj, property);
Expand Down Expand Up @@ -1971,6 +1987,11 @@ public static Ref specialRef(Object obj, String specialProperty, Context cx, Scr
return SpecialRef.createSpecial(cx, scope, obj, specialProperty);
}

public static Ref optionalSpecialRef(
Object obj, String specialProperty, Context cx, Scriptable scope) {
return SpecialOptionalRef.create(cx, scope, obj, specialProperty);
}

/** @deprecated Use {@link #delete(Object, Object, Context, Scriptable, boolean)} instead */
@Deprecated
public static Object delete(Object obj, Object id, Context cx) {
Expand Down Expand Up @@ -2617,6 +2638,20 @@ public static Callable getElemFunctionAndThis(
return (Callable) value;
}

public static Callable getPropFunctionAndThisOptional(
Object obj, String property, Context cx, Scriptable scope) {

Scriptable thisObj = toObjectOrNull(cx, obj, scope);
if (thisObj == null) {
throw undefCallError(obj, property);
}

Object value = ScriptableObject.getProperty(thisObj, property);
if (Scriptable.NOT_FOUND == value || Undefined.isUndefined(value) || value == null)
return (cx1, scope1, thisObj2, args) -> Undefined.instance;
return getPropFunctionAndThisHelper(obj, property, cx, thisObj);
}

/**
* Prepare for calling obj.property(...): return function corresponding to obj.property and make
* obj properly converted to Scriptable available as ScriptRuntime.lastStoredScriptable() for
Expand Down
29 changes: 29 additions & 0 deletions rhino/src/main/java/org/mozilla/javascript/SpecialOptionalRef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.mozilla.javascript;

class SpecialOptionalRef extends Ref {
Ref specialRef;

private SpecialOptionalRef(Ref specialRef) {
this.specialRef = specialRef;
}

public static Ref create(Context cx, Scriptable scope, Object object, String name) {
Scriptable target = ScriptRuntime.toObjectOrNull(cx, object, scope);
if (target != null && target != Undefined.instance) {
return new SpecialOptionalRef(SpecialRef.createSpecial(cx, scope, object, name));
}
return new SpecialOptionalRef(null);
}

@Override
public Object get(Context cx) {
if (specialRef == null) return Undefined.instance;
return specialRef.get(cx);
}

@Deprecated
@Override
public Object set(Context cx, Object value) {
throw new IllegalStateException();
}
}
Loading
Loading