feat: add support for the safe navigation (aka Elvis) operator
fixes #791
This commit is contained in:
parent
ec2d8cc2c8
commit
a9be2ebf1b
|
@ -17,7 +17,9 @@ import {
|
||||||
RECORD_TYPE_KEYED_ACCESS,
|
RECORD_TYPE_KEYED_ACCESS,
|
||||||
RECORD_TYPE_PIPE,
|
RECORD_TYPE_PIPE,
|
||||||
RECORD_TYPE_BINDING_PIPE,
|
RECORD_TYPE_BINDING_PIPE,
|
||||||
RECORD_TYPE_INTERPOLATE
|
RECORD_TYPE_INTERPOLATE,
|
||||||
|
RECORD_TYPE_SAFE_PROPERTY,
|
||||||
|
RECORD_TYPE_SAFE_INVOKE_METHOD
|
||||||
} from './proto_record';
|
} from './proto_record';
|
||||||
|
|
||||||
|
|
||||||
|
@ -295,6 +297,10 @@ export class ChangeDetectorJITGenerator {
|
||||||
rhs = `${context}.${r.name}`;
|
rhs = `${context}.${r.name}`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RECORD_TYPE_SAFE_PROPERTY:
|
||||||
|
rhs = `${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}`;
|
||||||
|
break;
|
||||||
|
|
||||||
case RECORD_TYPE_LOCAL:
|
case RECORD_TYPE_LOCAL:
|
||||||
rhs = `${LOCALS_ACCESSOR}.get('${r.name}')`;
|
rhs = `${LOCALS_ACCESSOR}.get('${r.name}')`;
|
||||||
break;
|
break;
|
||||||
|
@ -303,6 +309,10 @@ export class ChangeDetectorJITGenerator {
|
||||||
rhs = `${context}.${r.name}(${argString})`;
|
rhs = `${context}.${r.name}(${argString})`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RECORD_TYPE_SAFE_INVOKE_METHOD:
|
||||||
|
rhs = `${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}(${argString})`;
|
||||||
|
break;
|
||||||
|
|
||||||
case RECORD_TYPE_INVOKE_CLOSURE:
|
case RECORD_TYPE_INVOKE_CLOSURE:
|
||||||
rhs = `${context}(${argString})`;
|
rhs = `${context}(${argString})`;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -142,4 +142,6 @@ export class ChangeDetectionUtil {
|
||||||
changes[propertyName] = change;
|
changes[propertyName] = change;
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isValueBlank(value: any): boolean { return isBlank(value); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,9 @@ import {
|
||||||
RECORD_TYPE_KEYED_ACCESS,
|
RECORD_TYPE_KEYED_ACCESS,
|
||||||
RECORD_TYPE_PIPE,
|
RECORD_TYPE_PIPE,
|
||||||
RECORD_TYPE_BINDING_PIPE,
|
RECORD_TYPE_BINDING_PIPE,
|
||||||
RECORD_TYPE_INTERPOLATE
|
RECORD_TYPE_INTERPOLATE,
|
||||||
|
RECORD_TYPE_SAFE_PROPERTY,
|
||||||
|
RECORD_TYPE_SAFE_INVOKE_METHOD
|
||||||
} from './proto_record';
|
} from './proto_record';
|
||||||
|
|
||||||
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
|
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
|
||||||
|
@ -192,6 +194,10 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
|
||||||
var context = this._readContext(proto);
|
var context = this._readContext(proto);
|
||||||
return proto.funcOrValue(context);
|
return proto.funcOrValue(context);
|
||||||
|
|
||||||
|
case RECORD_TYPE_SAFE_PROPERTY:
|
||||||
|
var context = this._readContext(proto);
|
||||||
|
return isBlank(context) ? null : proto.funcOrValue(context);
|
||||||
|
|
||||||
case RECORD_TYPE_LOCAL:
|
case RECORD_TYPE_LOCAL:
|
||||||
return this.locals.get(proto.name);
|
return this.locals.get(proto.name);
|
||||||
|
|
||||||
|
@ -200,6 +206,14 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
|
||||||
var args = this._readArgs(proto);
|
var args = this._readArgs(proto);
|
||||||
return proto.funcOrValue(context, args);
|
return proto.funcOrValue(context, args);
|
||||||
|
|
||||||
|
case RECORD_TYPE_SAFE_INVOKE_METHOD:
|
||||||
|
var context = this._readContext(proto);
|
||||||
|
if (isBlank(context)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var args = this._readArgs(proto);
|
||||||
|
return proto.funcOrValue(context, args);
|
||||||
|
|
||||||
case RECORD_TYPE_KEYED_ACCESS:
|
case RECORD_TYPE_KEYED_ACCESS:
|
||||||
var arg = this._readArgs(proto)[0];
|
var arg = this._readArgs(proto)[0];
|
||||||
return this._readContext(proto)[arg];
|
return this._readContext(proto)[arg];
|
||||||
|
|
|
@ -91,6 +91,20 @@ export class AccessMember extends AST {
|
||||||
visit(visitor) { return visitor.visitAccessMember(this); }
|
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 {
|
export class KeyedAccess extends AST {
|
||||||
constructor(public obj: AST, public key: AST) { super(); }
|
constructor(public obj: AST, public key: AST) { super(); }
|
||||||
|
|
||||||
|
@ -251,6 +265,22 @@ export class MethodCall extends AST {
|
||||||
visit(visitor) { return visitor.visitMethodCall(this); }
|
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 {
|
export class FunctionCall extends AST {
|
||||||
constructor(public target: AST, public args: List<any>) { super(); }
|
constructor(public target: AST, public args: List<any>) { super(); }
|
||||||
|
|
||||||
|
@ -300,6 +330,8 @@ export class AstVisitor {
|
||||||
visitLiteralPrimitive(ast: LiteralPrimitive) {}
|
visitLiteralPrimitive(ast: LiteralPrimitive) {}
|
||||||
visitMethodCall(ast: MethodCall) {}
|
visitMethodCall(ast: MethodCall) {}
|
||||||
visitPrefixNot(ast: PrefixNot) {}
|
visitPrefixNot(ast: PrefixNot) {}
|
||||||
|
visitSafeAccessMember(ast: SafeAccessMember) {}
|
||||||
|
visitSafeMethodCall(ast: SafeMethodCall) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AstTransformer {
|
export class AstTransformer {
|
||||||
|
@ -315,10 +347,18 @@ export class AstTransformer {
|
||||||
return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter);
|
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) {
|
visitMethodCall(ast: MethodCall) {
|
||||||
return new MethodCall(ast.receiver.visit(this), ast.name, ast.fn, this.visitAll(ast.args));
|
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) {
|
visitFunctionCall(ast: FunctionCall) {
|
||||||
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
|
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,15 +219,15 @@ class _Scanner {
|
||||||
case $DQ:
|
case $DQ:
|
||||||
return this.scanString();
|
return this.scanString();
|
||||||
case $HASH:
|
case $HASH:
|
||||||
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
|
||||||
case $PLUS:
|
case $PLUS:
|
||||||
case $MINUS:
|
case $MINUS:
|
||||||
case $STAR:
|
case $STAR:
|
||||||
case $SLASH:
|
case $SLASH:
|
||||||
case $PERCENT:
|
case $PERCENT:
|
||||||
case $CARET:
|
case $CARET:
|
||||||
case $QUESTION:
|
|
||||||
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
||||||
|
case $QUESTION:
|
||||||
|
return this.scanComplexOperator(start, $PERIOD, '?', '.');
|
||||||
case $LT:
|
case $LT:
|
||||||
case $GT:
|
case $GT:
|
||||||
case $BANG:
|
case $BANG:
|
||||||
|
@ -434,7 +434,8 @@ var OPERATORS = SetWrapper.createFromList([
|
||||||
'|',
|
'|',
|
||||||
'!',
|
'!',
|
||||||
'?',
|
'?',
|
||||||
'#'
|
'#',
|
||||||
|
'?.'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
EmptyExpr,
|
EmptyExpr,
|
||||||
ImplicitReceiver,
|
ImplicitReceiver,
|
||||||
AccessMember,
|
AccessMember,
|
||||||
|
SafeAccessMember,
|
||||||
LiteralPrimitive,
|
LiteralPrimitive,
|
||||||
Binary,
|
Binary,
|
||||||
PrefixNot,
|
PrefixNot,
|
||||||
|
@ -40,6 +41,7 @@ import {
|
||||||
LiteralMap,
|
LiteralMap,
|
||||||
Interpolation,
|
Interpolation,
|
||||||
MethodCall,
|
MethodCall,
|
||||||
|
SafeMethodCall,
|
||||||
FunctionCall,
|
FunctionCall,
|
||||||
TemplateBinding,
|
TemplateBinding,
|
||||||
ASTWithSource
|
ASTWithSource
|
||||||
|
@ -360,7 +362,10 @@ class _ParseAST {
|
||||||
var result = this.parsePrimary();
|
var result = this.parsePrimary();
|
||||||
while (true) {
|
while (true) {
|
||||||
if (this.optionalCharacter($PERIOD)) {
|
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)) {
|
} else if (this.optionalCharacter($LBRACKET)) {
|
||||||
var key = this.parseExpression();
|
var key = this.parseExpression();
|
||||||
|
@ -405,7 +410,7 @@ class _ParseAST {
|
||||||
return this.parseLiteralMap();
|
return this.parseLiteralMap();
|
||||||
|
|
||||||
} else if (this.next.isIdentifier()) {
|
} else if (this.next.isIdentifier()) {
|
||||||
return this.parseAccessMemberOrMethodCall(_implicitReceiver);
|
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
|
||||||
|
|
||||||
} else if (this.next.isNumber()) {
|
} else if (this.next.isNumber()) {
|
||||||
var value = this.next.toNumber();
|
var value = this.next.toNumber();
|
||||||
|
@ -451,19 +456,21 @@ class _ParseAST {
|
||||||
return new LiteralMap(keys, values);
|
return new LiteralMap(keys, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseAccessMemberOrMethodCall(receiver): AST {
|
parseAccessMemberOrMethodCall(receiver, isSafe: boolean = false): AST {
|
||||||
var id = this.expectIdentifierOrKeyword();
|
var id = this.expectIdentifierOrKeyword();
|
||||||
|
|
||||||
if (this.optionalCharacter($LPAREN)) {
|
if (this.optionalCharacter($LPAREN)) {
|
||||||
var args = this.parseCallArguments();
|
var args = this.parseCallArguments();
|
||||||
this.expectCharacter($RPAREN);
|
this.expectCharacter($RPAREN);
|
||||||
var fn = this.reflector.method(id);
|
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 {
|
} else {
|
||||||
var getter = this.reflector.getter(id);
|
var getter = this.reflector.getter(id);
|
||||||
var setter = this.reflector.setter(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("|")) {
|
if (this.optionalOperator("|")) {
|
||||||
return this.parseInlinedPipe(am);
|
return this.parseInlinedPipe(am);
|
||||||
|
|
|
@ -19,7 +19,9 @@ import {
|
||||||
LiteralMap,
|
LiteralMap,
|
||||||
LiteralPrimitive,
|
LiteralPrimitive,
|
||||||
MethodCall,
|
MethodCall,
|
||||||
PrefixNot
|
PrefixNot,
|
||||||
|
SafeAccessMember,
|
||||||
|
SafeMethodCall
|
||||||
} from './parser/ast';
|
} from './parser/ast';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -49,7 +51,9 @@ import {
|
||||||
RECORD_TYPE_KEYED_ACCESS,
|
RECORD_TYPE_KEYED_ACCESS,
|
||||||
RECORD_TYPE_PIPE,
|
RECORD_TYPE_PIPE,
|
||||||
RECORD_TYPE_BINDING_PIPE,
|
RECORD_TYPE_BINDING_PIPE,
|
||||||
RECORD_TYPE_INTERPOLATE
|
RECORD_TYPE_INTERPOLATE,
|
||||||
|
RECORD_TYPE_SAFE_PROPERTY,
|
||||||
|
RECORD_TYPE_SAFE_INVOKE_METHOD
|
||||||
} from './proto_record';
|
} from './proto_record';
|
||||||
|
|
||||||
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
|
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
|
||||||
|
@ -149,6 +153,11 @@ class _ConvertAstIntoProtoRecords {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitSafeAccessMember(ast: SafeAccessMember) {
|
||||||
|
var receiver = ast.receiver.visit(this);
|
||||||
|
return this._addRecord(RECORD_TYPE_SAFE_PROPERTY, ast.name, ast.getter, [], null, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
visitMethodCall(ast: MethodCall) {
|
visitMethodCall(ast: MethodCall) {
|
||||||
var receiver = ast.receiver.visit(this);
|
var receiver = ast.receiver.visit(this);
|
||||||
var args = this._visitAll(ast.args);
|
var args = this._visitAll(ast.args);
|
||||||
|
@ -160,6 +169,12 @@ class _ConvertAstIntoProtoRecords {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitSafeMethodCall(ast: SafeMethodCall) {
|
||||||
|
var receiver = ast.receiver.visit(this);
|
||||||
|
var args = this._visitAll(ast.args);
|
||||||
|
return this._addRecord(RECORD_TYPE_SAFE_INVOKE_METHOD, ast.name, ast.fn, args, null, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
visitFunctionCall(ast: FunctionCall) {
|
visitFunctionCall(ast: FunctionCall) {
|
||||||
var target = ast.target.visit(this);
|
var target = ast.target.visit(this);
|
||||||
var args = this._visitAll(ast.args);
|
var args = this._visitAll(ast.args);
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const RECORD_TYPE_KEYED_ACCESS = 7;
|
||||||
export const RECORD_TYPE_PIPE = 8;
|
export const RECORD_TYPE_PIPE = 8;
|
||||||
export const RECORD_TYPE_BINDING_PIPE = 9;
|
export const RECORD_TYPE_BINDING_PIPE = 9;
|
||||||
export const RECORD_TYPE_INTERPOLATE = 10;
|
export const RECORD_TYPE_INTERPOLATE = 10;
|
||||||
|
export const RECORD_TYPE_SAFE_PROPERTY = 11;
|
||||||
|
export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12;
|
||||||
|
|
||||||
export class ProtoRecord {
|
export class ProtoRecord {
|
||||||
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,
|
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,
|
||||||
|
|
|
@ -151,7 +151,7 @@ export function main() {
|
||||||
expect(executeWatch('const', '"a\n\nb"')).toEqual(['const=a\n\nb']);
|
expect(executeWatch('const', '"a\n\nb"')).toEqual(['const=a\n\nb']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple chained property access', () => {
|
it('should support simple chained property access', () => {
|
||||||
var address = new Address('Grenoble');
|
var address = new Address('Grenoble');
|
||||||
var person = new Person('Victor', address);
|
var person = new Person('Victor', address);
|
||||||
|
|
||||||
|
@ -159,6 +159,18 @@ export function main() {
|
||||||
.toEqual(['address.city=Grenoble']);
|
.toEqual(['address.city=Grenoble']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support the safe navigation operator', () => {
|
||||||
|
var person = new Person('Victor', null);
|
||||||
|
|
||||||
|
expect(executeWatch('city', 'address?.city', person)).toEqual(['city=null']);
|
||||||
|
expect(executeWatch('city', 'address?.toString()', person)).toEqual(['city=null']);
|
||||||
|
|
||||||
|
person.address = new Address('MTV');
|
||||||
|
|
||||||
|
expect(executeWatch('city', 'address?.city', person)).toEqual(['city=MTV']);
|
||||||
|
expect(executeWatch('city', 'address?.toString()', person)).toEqual(['city=MTV']);
|
||||||
|
});
|
||||||
|
|
||||||
it("should support method calls", () => {
|
it("should support method calls", () => {
|
||||||
var person = new Person('Victor');
|
var person = new Person('Victor');
|
||||||
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
|
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
|
||||||
|
@ -976,7 +988,7 @@ class Address {
|
||||||
city: string;
|
city: string;
|
||||||
constructor(city: string) { this.city = city; }
|
constructor(city: string) { this.city = city; }
|
||||||
|
|
||||||
toString(): string { return this.city; }
|
toString(): string { return isBlank(this.city) ? '-' : this.city }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uninitialized {
|
class Uninitialized {
|
||||||
|
|
|
@ -126,14 +126,14 @@ export function main() {
|
||||||
expectIdentifierToken(tokens[1], 8, 'b');
|
expectIdentifierToken(tokens[1], 8, 'b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tokenize quoted string', function() {
|
it('should tokenize quoted string', () => {
|
||||||
var str = "['\\'', \"\\\"\"]";
|
var str = "['\\'', \"\\\"\"]";
|
||||||
var tokens: List<Token> = lex(str);
|
var tokens: List<Token> = lex(str);
|
||||||
expectStringToken(tokens[1], 1, "'");
|
expectStringToken(tokens[1], 1, "'");
|
||||||
expectStringToken(tokens[3], 7, '"');
|
expectStringToken(tokens[3], 7, '"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tokenize escaped quoted string', function() {
|
it('should tokenize escaped quoted string', () => {
|
||||||
var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
|
var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
|
||||||
var tokens: List<Token> = lex(str);
|
var tokens: List<Token> = lex(str);
|
||||||
expect(tokens.length).toEqual(1);
|
expect(tokens.length).toEqual(1);
|
||||||
|
@ -203,7 +203,7 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE(deboer): NOT A LEXER TEST
|
// NOTE(deboer): NOT A LEXER TEST
|
||||||
// it('should tokenize negative number', function() {
|
// it('should tokenize negative number', () => {
|
||||||
// var tokens:List<Token> = lex("-0.5");
|
// var tokens:List<Token> = lex("-0.5");
|
||||||
// expectNumberToken(tokens[0], 0, -0.5);
|
// expectNumberToken(tokens[0], 0, -0.5);
|
||||||
// });
|
// });
|
||||||
|
@ -240,6 +240,11 @@ export function main() {
|
||||||
expectOperatorToken(tokens[0], 0, '#');
|
expectOperatorToken(tokens[0], 0, '#');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should tokenize ?. as operator', () => {
|
||||||
|
var tokens: List<Token> = lex('?.');
|
||||||
|
expectOperatorToken(tokens[0], 0, '?.');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'angular2/test_lib';
|
import {ddescribe, describe, it, xit, iit, expect, beforeEach, IS_DARTIUM} from 'angular2/test_lib';
|
||||||
import {BaseException, isBlank, isPresent} from 'angular2/src/facade/lang';
|
import {BaseException, isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||||
import {reflector} from 'angular2/src/reflection/reflection';
|
import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||||
|
@ -202,6 +202,33 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('safe navigation operator', () => {
|
||||||
|
it('should parse field access', () => {
|
||||||
|
expectEval('a?.a', td(td(999))).toEqual(999);
|
||||||
|
expectEval('a.a?.a', td(td(td(999)))).toEqual(999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when accessing a field on null',
|
||||||
|
() => { expect(() => { expectEval('null?.a', td()).toEqual(null); }).not.toThrow(); });
|
||||||
|
|
||||||
|
it('should have the same priority as .', () => {
|
||||||
|
expect(() => { expectEval('null?.a.a', td()).toEqual(null); }).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!IS_DARTIUM) {
|
||||||
|
it('should return null when accessing a field on undefined', () => {
|
||||||
|
expect(() => { expectEval('_undefined?.a', td()).toEqual(null); }).not.toThrow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should evaluate method calls',
|
||||||
|
() => { expectEval('a?.add(1,2)', td(td())).toEqual(3); });
|
||||||
|
|
||||||
|
it('should return null when accessing a method on null', () => {
|
||||||
|
expect(() => { expectEval('null?.add(1, 2)', td()).toEqual(null); }).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("method calls", () => {
|
describe("method calls", () => {
|
||||||
it("should evaluate method calls", () => {
|
it("should evaluate method calls", () => {
|
||||||
expectEval("fn()", td(0, 0, "constant")).toEqual("constant");
|
expectEval("fn()", td(0, 0, "constant")).toEqual("constant");
|
||||||
|
|
Loading…
Reference in New Issue