feat: add support for the safe navigation (aka Elvis) operator
fixes #791
This commit is contained in:
@ -91,6 +91,20 @@ export class AccessMember extends AST {
|
||||
visit(visitor) { return visitor.visitAccessMember(this); }
|
||||
}
|
||||
|
||||
export class SafeAccessMember extends AST {
|
||||
constructor(public receiver: AST, public name: string, public getter: Function,
|
||||
public setter: Function) {
|
||||
super();
|
||||
}
|
||||
|
||||
eval(context, locals) {
|
||||
var evaluatedReceiver = this.receiver.eval(context, locals);
|
||||
return isBlank(evaluatedReceiver) ? null : this.getter(evaluatedReceiver);
|
||||
}
|
||||
|
||||
visit(visitor) { return visitor.visitSafeAccessMember(this); }
|
||||
}
|
||||
|
||||
export class KeyedAccess extends AST {
|
||||
constructor(public obj: AST, public key: AST) { super(); }
|
||||
|
||||
@ -251,6 +265,22 @@ export class MethodCall extends AST {
|
||||
visit(visitor) { return visitor.visitMethodCall(this); }
|
||||
}
|
||||
|
||||
export class SafeMethodCall extends AST {
|
||||
constructor(public receiver: AST, public name: string, public fn: Function,
|
||||
public args: List<any>) {
|
||||
super();
|
||||
}
|
||||
|
||||
eval(context, locals) {
|
||||
var evaluatedReceiver = this.receiver.eval(context, locals);
|
||||
if (isBlank(evaluatedReceiver)) return null;
|
||||
var evaluatedArgs = evalList(context, locals, this.args);
|
||||
return this.fn(evaluatedReceiver, evaluatedArgs);
|
||||
}
|
||||
|
||||
visit(visitor) { return visitor.visitSafeMethodCall(this); }
|
||||
}
|
||||
|
||||
export class FunctionCall extends AST {
|
||||
constructor(public target: AST, public args: List<any>) { super(); }
|
||||
|
||||
@ -300,6 +330,8 @@ export class AstVisitor {
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive) {}
|
||||
visitMethodCall(ast: MethodCall) {}
|
||||
visitPrefixNot(ast: PrefixNot) {}
|
||||
visitSafeAccessMember(ast: SafeAccessMember) {}
|
||||
visitSafeMethodCall(ast: SafeMethodCall) {}
|
||||
}
|
||||
|
||||
export class AstTransformer {
|
||||
@ -315,10 +347,18 @@ export class AstTransformer {
|
||||
return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
|
||||
}
|
||||
|
||||
visitSafeAccessMember(ast: SafeAccessMember) {
|
||||
return new SafeAccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
|
||||
}
|
||||
|
||||
visitMethodCall(ast: MethodCall) {
|
||||
return new MethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall) {
|
||||
return new SafeMethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitFunctionCall(ast: FunctionCall) {
|
||||
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
|
||||
}
|
||||
|
@ -219,15 +219,15 @@ class _Scanner {
|
||||
case $DQ:
|
||||
return this.scanString();
|
||||
case $HASH:
|
||||
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
||||
case $PLUS:
|
||||
case $MINUS:
|
||||
case $STAR:
|
||||
case $SLASH:
|
||||
case $PERCENT:
|
||||
case $CARET:
|
||||
case $QUESTION:
|
||||
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
||||
case $QUESTION:
|
||||
return this.scanComplexOperator(start, $PERIOD, '?', '.');
|
||||
case $LT:
|
||||
case $GT:
|
||||
case $BANG:
|
||||
@ -434,7 +434,8 @@ var OPERATORS = SetWrapper.createFromList([
|
||||
'|',
|
||||
'!',
|
||||
'?',
|
||||
'#'
|
||||
'#',
|
||||
'?.'
|
||||
]);
|
||||
|
||||
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
EmptyExpr,
|
||||
ImplicitReceiver,
|
||||
AccessMember,
|
||||
SafeAccessMember,
|
||||
LiteralPrimitive,
|
||||
Binary,
|
||||
PrefixNot,
|
||||
@ -40,6 +41,7 @@ import {
|
||||
LiteralMap,
|
||||
Interpolation,
|
||||
MethodCall,
|
||||
SafeMethodCall,
|
||||
FunctionCall,
|
||||
TemplateBinding,
|
||||
ASTWithSource
|
||||
@ -360,7 +362,10 @@ class _ParseAST {
|
||||
var result = this.parsePrimary();
|
||||
while (true) {
|
||||
if (this.optionalCharacter($PERIOD)) {
|
||||
result = this.parseAccessMemberOrMethodCall(result);
|
||||
result = this.parseAccessMemberOrMethodCall(result, false);
|
||||
|
||||
} else if (this.optionalOperator('?.')) {
|
||||
result = this.parseAccessMemberOrMethodCall(result, true);
|
||||
|
||||
} else if (this.optionalCharacter($LBRACKET)) {
|
||||
var key = this.parseExpression();
|
||||
@ -405,7 +410,7 @@ class _ParseAST {
|
||||
return this.parseLiteralMap();
|
||||
|
||||
} else if (this.next.isIdentifier()) {
|
||||
return this.parseAccessMemberOrMethodCall(_implicitReceiver);
|
||||
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
|
||||
|
||||
} else if (this.next.isNumber()) {
|
||||
var value = this.next.toNumber();
|
||||
@ -451,19 +456,21 @@ class _ParseAST {
|
||||
return new LiteralMap(keys, values);
|
||||
}
|
||||
|
||||
parseAccessMemberOrMethodCall(receiver): AST {
|
||||
parseAccessMemberOrMethodCall(receiver, isSafe: boolean = false): AST {
|
||||
var id = this.expectIdentifierOrKeyword();
|
||||
|
||||
if (this.optionalCharacter($LPAREN)) {
|
||||
var args = this.parseCallArguments();
|
||||
this.expectCharacter($RPAREN);
|
||||
var fn = this.reflector.method(id);
|
||||
return new MethodCall(receiver, id, fn, args);
|
||||
return isSafe ? new SafeMethodCall(receiver, id, fn, args) :
|
||||
new MethodCall(receiver, id, fn, args);
|
||||
|
||||
} else {
|
||||
var getter = this.reflector.getter(id);
|
||||
var setter = this.reflector.setter(id);
|
||||
var am = new AccessMember(receiver, id, getter, setter);
|
||||
var am = isSafe ? new SafeAccessMember(receiver, id, getter, setter) :
|
||||
new AccessMember(receiver, id, getter, setter);
|
||||
|
||||
if (this.optionalOperator("|")) {
|
||||
return this.parseInlinedPipe(am);
|
||||
|
Reference in New Issue
Block a user