Skip to content

Commit

Permalink
feat: add support for the safe navigation (aka Elvis) operator
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed May 28, 2015
1 parent 05d66bb commit 65d67ff
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 18 deletions.
Expand Up @@ -17,7 +17,9 @@ import {
RECORD_TYPE_KEYED_ACCESS,
RECORD_TYPE_PIPE,
RECORD_TYPE_BINDING_PIPE,
RECORD_TYPE_INTERPOLATE
RECORD_TYPE_INTERPOLATE,
RECORD_TYPE_SAFE_PROPERTY,
RECORD_TYPE_SAFE_INVOKE_METHOD
} from './proto_record';


Expand Down Expand Up @@ -295,6 +297,10 @@ export class ChangeDetectorJITGenerator {
rhs = `${context}.${r.name}`;
break;

case RECORD_TYPE_SAFE_PROPERTY:
return assignmentTemplate(newValue,
`${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}`);

case RECORD_TYPE_LOCAL:
rhs = `${LOCALS_ACCESSOR}.get('${r.name}')`;
break;
Expand All @@ -303,6 +309,10 @@ export class ChangeDetectorJITGenerator {
rhs = `${context}.${r.name}(${argString})`;
break;

case RECORD_TYPE_SAFE_INVOKE_METHOD:
return assignmentTemplate(
newValue, `${UTIL}.isValueBlank(${context}) ? null : ${context}.${r.name}(${args})`);

case RECORD_TYPE_INVOKE_CLOSURE:
rhs = `${context}(${argString})`;
break;
Expand Down
Expand Up @@ -142,4 +142,6 @@ export class ChangeDetectionUtil {
changes[propertyName] = change;
return changes;
}

static isValueBlank(value: any): boolean { return isBlank(value); }
}
16 changes: 15 additions & 1 deletion modules/angular2/src/change_detection/dynamic_change_detector.ts
Expand Up @@ -19,7 +19,9 @@ import {
RECORD_TYPE_KEYED_ACCESS,
RECORD_TYPE_PIPE,
RECORD_TYPE_BINDING_PIPE,
RECORD_TYPE_INTERPOLATE
RECORD_TYPE_INTERPOLATE,
RECORD_TYPE_SAFE_PROPERTY,
RECORD_TYPE_SAFE_INVOKE_METHOD
} from './proto_record';

import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
Expand Down Expand Up @@ -192,6 +194,10 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var context = this._readContext(proto);
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:
return this.locals.get(proto.name);

Expand All @@ -200,6 +206,14 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var args = this._readArgs(proto);
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:
var arg = this._readArgs(proto)[0];
return this._readContext(proto)[arg];
Expand Down
40 changes: 40 additions & 0 deletions modules/angular2/src/change_detection/parser/ast.ts
Expand Up @@ -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(); }

Expand Down Expand Up @@ -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(); }

Expand Down Expand Up @@ -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 {
Expand All @@ -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));
}
Expand Down
7 changes: 4 additions & 3 deletions modules/angular2/src/change_detection/parser/lexer.ts
Expand Up @@ -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:
Expand Down Expand Up @@ -434,7 +434,8 @@ var OPERATORS = SetWrapper.createFromList([
'|',
'!',
'?',
'#'
'#',
'?.'
]);


Expand Down
17 changes: 12 additions & 5 deletions modules/angular2/src/change_detection/parser/parser.ts
Expand Up @@ -28,6 +28,7 @@ import {
EmptyExpr,
ImplicitReceiver,
AccessMember,
SafeAccessMember,
LiteralPrimitive,
Binary,
PrefixNot,
Expand All @@ -40,6 +41,7 @@ import {
LiteralMap,
Interpolation,
MethodCall,
SafeMethodCall,
FunctionCall,
TemplateBinding,
ASTWithSource
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 17 additions & 2 deletions modules/angular2/src/change_detection/proto_change_detector.ts
Expand Up @@ -19,7 +19,9 @@ import {
LiteralMap,
LiteralPrimitive,
MethodCall,
PrefixNot
PrefixNot,
SafeAccessMember,
SafeMethodCall
} from './parser/ast';

import {
Expand Down Expand Up @@ -49,7 +51,9 @@ import {
RECORD_TYPE_KEYED_ACCESS,
RECORD_TYPE_PIPE,
RECORD_TYPE_BINDING_PIPE,
RECORD_TYPE_INTERPOLATE
RECORD_TYPE_INTERPOLATE,
RECORD_TYPE_SAFE_PROPERTY,
RECORD_TYPE_SAFE_INVOKE_METHOD
} from './proto_record';

export class DynamicProtoChangeDetector extends ProtoChangeDetector {
Expand Down Expand Up @@ -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) {
var receiver = ast.receiver.visit(this);
var args = this._visitAll(ast.args);
Expand All @@ -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) {
var target = ast.target.visit(this);
var args = this._visitAll(ast.args);
Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/src/change_detection/proto_record.ts
Expand Up @@ -13,6 +13,8 @@ export const RECORD_TYPE_KEYED_ACCESS = 7;
export const RECORD_TYPE_PIPE = 8;
export const RECORD_TYPE_BINDING_PIPE = 9;
export const RECORD_TYPE_INTERPOLATE = 10;
export const RECORD_TYPE_SAFE_PROPERTY = 11;
export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12;

export class ProtoRecord {
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,
Expand Down
20 changes: 18 additions & 2 deletions modules/angular2/test/change_detection/change_detector_spec.ts
Expand Up @@ -151,14 +151,30 @@ export function main() {
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 person = new Person('Victor', address);

expect(executeWatch('address.city', 'address.city', person))
.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", () => {
var person = new Person('Victor');
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
Expand Down Expand Up @@ -976,7 +992,7 @@ class Address {
city: string;
constructor(city: string) { this.city = city; }

toString(): string { return this.city; }
toString(): string { return isBlank(this.city) ? '-' : this.city }
}

class Uninitialized {
Expand Down
11 changes: 8 additions & 3 deletions modules/angular2/test/change_detection/parser/lexer_spec.ts
Expand Up @@ -126,14 +126,14 @@ export function main() {
expectIdentifierToken(tokens[1], 8, 'b');
});

it('should tokenize quoted string', function() {
it('should tokenize quoted string', () => {
var str = "['\\'', \"\\\"\"]";
var tokens: List<Token> = lex(str);
expectStringToken(tokens[1], 1, "'");
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 tokens: List<Token> = lex(str);
expect(tokens.length).toEqual(1);
Expand Down Expand Up @@ -203,7 +203,7 @@ export function main() {
});

// NOTE(deboer): NOT A LEXER TEST
// it('should tokenize negative number', function() {
// it('should tokenize negative number', () => {
// var tokens:List<Token> = lex("-0.5");
// expectNumberToken(tokens[0], 0, -0.5);
// });
Expand Down Expand Up @@ -240,6 +240,11 @@ export function main() {
expectOperatorToken(tokens[0], 0, '#');
});

it('should tokenize ?. as operator', () => {
var tokens:List<Token> = lex('?.');
expectOperatorToken(tokens[0], 0, '?.');
});

});
});
}

0 comments on commit 65d67ff

Please sign in to comment.