feat: add support for the safe navigation (aka Elvis) operator

fixes #791
This commit is contained in:
Victor Berchet
2015-05-26 10:19:47 +02:00
parent ec2d8cc2c8
commit a9be2ebf1b
11 changed files with 153 additions and 18 deletions

View File

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

View File

@ -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([
'|',
'!',
'?',
'#'
'#',
'?.'
]);

View File

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