diff --git a/modules/angular2/src/change_detection/parser/ast.ts b/modules/angular2/src/change_detection/parser/ast.ts index 7de3a9a5f3..46bdb522eb 100644 --- a/modules/angular2/src/change_detection/parser/ast.ts +++ b/modules/angular2/src/change_detection/parser/ast.ts @@ -8,7 +8,7 @@ export class AST { assign(context, locals, value) { throw new BaseException("Not supported"); } - visit(visitor): any { return null; } + visit(visitor: AstVisitor): any { return null; } toString(): string { return "AST"; } } @@ -16,7 +16,7 @@ export class AST { export class EmptyExpr extends AST { eval(context, locals) { return null; } - visit(visitor) { + visit(visitor: AstVisitor) { // do nothing } } @@ -24,7 +24,7 @@ export class EmptyExpr extends AST { export class ImplicitReceiver extends AST { eval(context, locals) { return context; } - visit(visitor) { return visitor.visitImplicitReceiver(this); } + visit(visitor: AstVisitor) { return visitor.visitImplicitReceiver(this); } } /** @@ -42,7 +42,7 @@ export class Chain extends AST { return result; } - visit(visitor) { return visitor.visitChain(this); } + visit(visitor: AstVisitor) { return visitor.visitChain(this); } } export class Conditional extends AST { @@ -56,7 +56,7 @@ export class Conditional extends AST { } } - visit(visitor) { return visitor.visitConditional(this); } + visit(visitor: AstVisitor) { return visitor.visitConditional(this); } } export class AccessMember extends AST { @@ -88,7 +88,7 @@ export class AccessMember extends AST { } } - visit(visitor) { return visitor.visitAccessMember(this); } + visit(visitor: AstVisitor) { return visitor.visitAccessMember(this); } } export class SafeAccessMember extends AST { @@ -102,7 +102,7 @@ export class SafeAccessMember extends AST { return isBlank(evaluatedReceiver) ? null : this.getter(evaluatedReceiver); } - visit(visitor) { return visitor.visitSafeAccessMember(this); } + visit(visitor: AstVisitor) { return visitor.visitSafeAccessMember(this); } } export class KeyedAccess extends AST { @@ -123,7 +123,7 @@ export class KeyedAccess extends AST { return value; } - visit(visitor) { return visitor.visitKeyedAccess(this); } + visit(visitor: AstVisitor) { return visitor.visitKeyedAccess(this); } } export class Pipe extends AST { @@ -132,7 +132,7 @@ export class Pipe extends AST { super(); } - visit(visitor) { return visitor.visitPipe(this); } + visit(visitor: AstVisitor) { return visitor.visitPipe(this); } } export class LiteralPrimitive extends AST { @@ -140,7 +140,7 @@ export class LiteralPrimitive extends AST { eval(context, locals) { return this.value; } - visit(visitor) { return visitor.visitLiteralPrimitive(this); } + visit(visitor: AstVisitor) { return visitor.visitLiteralPrimitive(this); } } export class LiteralArray extends AST { @@ -150,7 +150,7 @@ export class LiteralArray extends AST { return ListWrapper.map(this.expressions, (e) => e.eval(context, locals)); } - visit(visitor) { return visitor.visitLiteralArray(this); } + visit(visitor: AstVisitor) { return visitor.visitLiteralArray(this); } } export class LiteralMap extends AST { @@ -164,7 +164,7 @@ export class LiteralMap extends AST { return res; } - visit(visitor) { return visitor.visitLiteralMap(this); } + visit(visitor: AstVisitor) { return visitor.visitLiteralMap(this); } } export class Interpolation extends AST { @@ -172,7 +172,7 @@ export class Interpolation extends AST { eval(context, locals) { throw new BaseException("evaluating an Interpolation is not supported"); } - visit(visitor) { visitor.visitInterpolation(this); } + visit(visitor: AstVisitor) { visitor.visitInterpolation(this); } } export class Binary extends AST { @@ -223,7 +223,7 @@ export class Binary extends AST { throw 'Internal error [$operation] not handled'; } - visit(visitor) { return visitor.visitBinary(this); } + visit(visitor: AstVisitor) { return visitor.visitBinary(this); } } export class PrefixNot extends AST { @@ -231,7 +231,7 @@ export class PrefixNot extends AST { eval(context, locals) { return !this.expression.eval(context, locals); } - visit(visitor) { return visitor.visitPrefixNot(this); } + visit(visitor: AstVisitor) { return visitor.visitPrefixNot(this); } } export class Assignment extends AST { @@ -241,7 +241,7 @@ export class Assignment extends AST { return this.target.assign(context, locals, this.value.eval(context, locals)); } - visit(visitor) { return visitor.visitAssignment(this); } + visit(visitor: AstVisitor) { return visitor.visitAssignment(this); } } export class MethodCall extends AST { @@ -262,7 +262,7 @@ export class MethodCall extends AST { } } - visit(visitor) { return visitor.visitMethodCall(this); } + visit(visitor: AstVisitor) { return visitor.visitMethodCall(this); } } export class SafeMethodCall extends AST { @@ -278,7 +278,7 @@ export class SafeMethodCall extends AST { return this.fn(evaluatedReceiver, evaluatedArgs); } - visit(visitor) { return visitor.visitSafeMethodCall(this); } + visit(visitor: AstVisitor) { return visitor.visitSafeMethodCall(this); } } export class FunctionCall extends AST { @@ -292,7 +292,7 @@ export class FunctionCall extends AST { return FunctionWrapper.apply(obj, evalList(context, locals, this.args)); } - visit(visitor) { return visitor.visitFunctionCall(this); } + visit(visitor: AstVisitor) { return visitor.visitFunctionCall(this); } } export class ASTWithSource extends AST { @@ -304,7 +304,7 @@ export class ASTWithSource extends AST { assign(context, locals, value) { return this.ast.assign(context, locals, value); } - visit(visitor) { return this.ast.visit(visitor); } + visit(visitor: AstVisitor) { return this.ast.visit(visitor); } toString(): string { return `${this.source} in ${this.location}`; } } @@ -314,27 +314,27 @@ export class TemplateBinding { public expression: ASTWithSource) {} } -// INTERFACE -export class AstVisitor { - visitAccessMember(ast: AccessMember) {} - visitAssignment(ast: Assignment) {} - visitBinary(ast: Binary) {} - visitChain(ast: Chain) {} - visitConditional(ast: Conditional) {} - visitPipe(ast: Pipe) {} - visitFunctionCall(ast: FunctionCall) {} - visitImplicitReceiver(ast: ImplicitReceiver) {} - visitKeyedAccess(ast: KeyedAccess) {} - visitLiteralArray(ast: LiteralArray) {} - visitLiteralMap(ast: LiteralMap) {} - visitLiteralPrimitive(ast: LiteralPrimitive) {} - visitMethodCall(ast: MethodCall) {} - visitPrefixNot(ast: PrefixNot) {} - visitSafeAccessMember(ast: SafeAccessMember) {} - visitSafeMethodCall(ast: SafeMethodCall) {} +export interface AstVisitor { + visitAccessMember(ast: AccessMember): any; + visitAssignment(ast: Assignment): any; + visitBinary(ast: Binary): any; + visitChain(ast: Chain): any; + visitConditional(ast: Conditional): any; + visitPipe(ast: Pipe): any; + visitFunctionCall(ast: FunctionCall): any; + visitImplicitReceiver(ast: ImplicitReceiver): any; + visitInterpolation(ast: Interpolation): any; + visitKeyedAccess(ast: KeyedAccess): any; + visitLiteralArray(ast: LiteralArray): any; + visitLiteralMap(ast: LiteralMap): any; + visitLiteralPrimitive(ast: LiteralPrimitive): any; + visitMethodCall(ast: MethodCall): any; + visitPrefixNot(ast: PrefixNot): any; + visitSafeAccessMember(ast: SafeAccessMember): any; + visitSafeMethodCall(ast: SafeMethodCall): any; } -export class AstTransformer { +export class AstTransformer implements AstVisitor { visitImplicitReceiver(ast: ImplicitReceiver) { return ast; } visitInterpolation(ast: Interpolation) { @@ -393,6 +393,10 @@ export class AstTransformer { } return res; } + + visitChain(ast: Chain) { throw new BaseException('Not implemented'); } + + visitAssignment(ast: Assignment) { throw new BaseException('Not implemented'); } } var _evalListCache = [ diff --git a/modules/angular2/src/change_detection/proto_change_detector.ts b/modules/angular2/src/change_detection/proto_change_detector.ts index e951df5de9..540594cb3e 100644 --- a/modules/angular2/src/change_detection/proto_change_detector.ts +++ b/modules/angular2/src/change_detection/proto_change_detector.ts @@ -122,7 +122,6 @@ class ProtoRecordBuilder { _appendRecords(b: BindingRecord, variableNames: List) { if (b.isDirectiveLifecycle()) { - ; ListWrapper.push( this.records, new ProtoRecord(RECORD_TYPE_DIRECTIVE_LIFECYCLE, b.lifecycleEvent, null, [], [], -1, null, @@ -133,7 +132,7 @@ class ProtoRecordBuilder { } } -class _ConvertAstIntoProtoRecords { +class _ConvertAstIntoProtoRecords implements AstVisitor { constructor(private _records: List, private _bindingRecord: BindingRecord, private _expressionAsString: string, private _variableNames: List) {} @@ -240,6 +239,10 @@ class _ConvertAstIntoProtoRecords { [key], null, obj); } + visitAssignment(ast: Assignment) { throw new BaseException('Not supported'); } + + visitChain(ast: Chain) { throw new BaseException('Not supported'); } + _visitAll(asts: List) { var res = ListWrapper.createFixedSize(asts.length); for (var i = 0; i < asts.length; ++i) { diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index d94c0b4277..d811f0eeaf 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -88,6 +88,8 @@ class StringWrapper { static bool contains(String s, String substr) { return s.contains(substr); } + + static bool isString(s) => s is String; } class StringJoiner { diff --git a/modules/angular2/src/facade/lang.ts b/modules/angular2/src/facade/lang.ts index 1b00b9ce09..2a97cd752c 100644 --- a/modules/angular2/src/facade/lang.ts +++ b/modules/angular2/src/facade/lang.ts @@ -132,6 +132,8 @@ export class StringWrapper { } static contains(s: string, substr: string): boolean { return s.indexOf(substr) != -1; } + + static isString(s: any): boolean { return typeof s === 'string' || s instanceof String; } } export class StringJoiner { diff --git a/modules/angular2/test/change_detection/parser/unparser.ts b/modules/angular2/test/change_detection/parser/unparser.ts new file mode 100644 index 0000000000..96fab27906 --- /dev/null +++ b/modules/angular2/test/change_detection/parser/unparser.ts @@ -0,0 +1,180 @@ +import { + AST, + AstVisitor, + AccessMember, + Assignment, + Binary, + Chain, + Conditional, + Pipe, + FunctionCall, + ImplicitReceiver, + Interpolation, + KeyedAccess, + LiteralArray, + LiteralMap, + LiteralPrimitive, + MethodCall, + PrefixNot, + SafeAccessMember, + SafeMethodCall +} from 'angular2/src/change_detection/parser/ast'; + + +import {StringWrapper, RegExpWrapper} from 'angular2/src/facade/lang'; + +var quoteRegExp = RegExpWrapper.create('"'); + +export class Unparser implements AstVisitor { + private _expression: string; + + unparse(ast: AST) { + this._expression = ''; + this._visit(ast); + return this._expression; + } + + visitAccessMember(ast: AccessMember) { + this._visit(ast.receiver); + + this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}` : `.${ast.name}`; + } + + visitAssignment(ast: Assignment) { + this._visit(ast.target); + this._expression += ' = '; + this._visit(ast.value); + } + + visitBinary(ast: Binary) { + this._visit(ast.left); + this._expression += ` ${ast.operation} `; + this._visit(ast.right); + } + + visitChain(ast: Chain) { + ast.expressions.forEach(expression => { + this._visit(expression); + this._expression += ';' + }); + } + + visitConditional(ast: Conditional) { + this._visit(ast.condition); + this._expression += ' ? '; + this._visit(ast.trueExp); + this._expression += ' : '; + this._visit(ast.falseExp); + } + + visitPipe(ast: Pipe) { + this._visit(ast.exp); + this._expression += ` | ${ast.name}`; + ast.args.forEach(arg => { + this._expression += ':'; + this._visit(arg); + }) + } + + visitFunctionCall(ast: FunctionCall) { + this._visit(ast.target); + this._expression += '('; + var isFirst = true; + ast.args.forEach(arg => { + if (!isFirst) this._expression += ', '; + isFirst = false; + this._visit(arg); + }); + this._expression += ')'; + } + + visitImplicitReceiver(ast: ImplicitReceiver) {} + + visitInterpolation(ast: Interpolation) { + for (let i = 0; i < ast.strings.length; i++) { + this._expression += ast.strings[i]; + if (i < ast.expressions.length) { + this._expression += '{{ '; + this._visit(ast.expressions[i]); + this._expression += ' }}'; + } + } + } + + visitKeyedAccess(ast: KeyedAccess) { + this._visit(ast.obj); + this._expression += '['; + this._visit(ast.key); + this._expression += ']'; + } + + visitLiteralArray(ast: LiteralArray) { + this._expression += '['; + var isFirst = true; + ast.expressions.forEach(expression => { + if (!isFirst) this._expression += ', '; + isFirst = false; + this._visit(expression); + }); + + this._expression += ']'; + } + + visitLiteralMap(ast: LiteralMap) { + this._expression += '{'; + var isFirst = true; + for (let i = 0; i < ast.keys.length; i++) { + if (!isFirst) this._expression += ', '; + isFirst = false; + this._expression += `${ast.keys[i]}: `; + this._visit(ast.values[i]); + } + + this._expression += '}'; + } + + visitLiteralPrimitive(ast: LiteralPrimitive) { + if (StringWrapper.isString(ast.value)) { + this._expression += `"${StringWrapper.replaceAll(ast.value, quoteRegExp, '\"')}"`; + } else { + this._expression += `${ast.value}`; + } + } + + visitMethodCall(ast: MethodCall) { + this._visit(ast.receiver); + this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}(` : + `.${ast.name}(`; + var isFirst = true; + ast.args.forEach(arg => { + if (!isFirst) this._expression += ', '; + isFirst = false; + this._visit(arg); + }); + this._expression += ')'; + } + + visitPrefixNot(ast: PrefixNot) { + this._expression += '!'; + this._visit(ast.expression); + } + + visitSafeAccessMember(ast: SafeAccessMember) { + this._visit(ast.receiver); + this._expression += `?.${ast.name}`; + } + + visitSafeMethodCall(ast: SafeMethodCall) { + this._visit(ast.receiver); + this._expression += `?.${ast.name}(`; + var isFirst = true; + ast.args.forEach(arg => { + if (!isFirst) this._expression += ', '; + isFirst = false; + this._visit(arg); + }); + this._expression += ')'; + } + + private _visit(ast: AST) { ast.visit(this); } +} diff --git a/modules/angular2/test/change_detection/parser/unparser_spec.ts b/modules/angular2/test/change_detection/parser/unparser_spec.ts new file mode 100644 index 0000000000..7ff995364a --- /dev/null +++ b/modules/angular2/test/change_detection/parser/unparser_spec.ts @@ -0,0 +1,112 @@ +import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'angular2/test_lib'; + +import { + AST, + ASTWithSource, + AccessMember, + Assignment, + Binary, + Chain, + Conditional, + Pipe, + ImplicitReceiver, + Interpolation, + KeyedAccess, + LiteralArray, + LiteralMap, + LiteralPrimitive, + MethodCall, + PrefixNot, + SafeAccessMember, + SafeMethodCall +} from 'angular2/src/change_detection/parser/ast'; + +import {Parser} from 'angular2/src/change_detection/parser/parser'; +import {Lexer} from 'angular2/src/change_detection/parser/lexer'; +import {Unparser} from './unparser'; + +import {reflector} from 'angular2/src/reflection/reflection'; + +import {isPresent, Type} from 'angular2/src/facade/lang'; + +export function main() { + let parser: Parser = new Parser(new Lexer(), reflector); + let unparser: Unparser = new Unparser(); + + function parseAction(text, location = null): ASTWithSource { + return parser.parseAction(text, location); + } + + function parseBinding(text, location = null): ASTWithSource { + return parser.parseBinding(text, location); + } + + function check(expression: string, type: Type): void { + var ast = parseAction(expression).ast; + if (isPresent(type)) { + expect(ast).toBeAnInstanceOf(type); + } + expect(unparser.unparse(ast)).toEqual(expression); + } + + describe('Unparser', () => { + it('should support AccessMember', () => { + check('a', AccessMember); + check('a.b', AccessMember); + }); + + it('should support Assignment', () => { check('a = b', Assignment); }); + + it('should support Binary', () => { check('a && b', Binary); }); + + it('should support Chain', () => { check('a;b;', Chain); }); + + it('should support Conditional', () => { check('a ? b : c', Conditional); }); + + it('should support Pipe', () => { + var originalExp = 'a | b'; + var ast = parseBinding(originalExp).ast; + expect(ast).toBeAnInstanceOf(Pipe); + expect(unparser.unparse(ast)).toEqual(originalExp); + }); + + it('should support KeyedAccess', () => { check('a[b]', KeyedAccess); }); + + it('should support LiteralArray', () => { check('[a, b]', LiteralArray); }); + + it('should support LiteralMap', () => { check('{a: b, c: d}', LiteralMap); }); + + it('should support LiteralPrimitive', () => { + check('true', LiteralPrimitive); + check('"a"', LiteralPrimitive); + check('1.234', LiteralPrimitive); + }); + + it('should support MethodCall', () => { + check('a(b, c)', MethodCall); + check('a.b(c, d)', MethodCall); + }); + + it('should support PrefixNot', () => { check('!a', PrefixNot); }); + + it('should support SafeAccessMember', () => { check('a?.b', SafeAccessMember); }); + + it('should support SafeMethodCall', () => { check('a?.b(c, d)', SafeMethodCall); }); + + it('should support complex expression', () => { + var originalExp = 'a + 3 * fn([c + d | e.f], {a: 3})[g].h && i'; + var ast = parseBinding(originalExp).ast; + expect(unparser.unparse(ast)).toEqual(originalExp); + }); + + it('should support Interpolation', () => { + var ast = parser.parseInterpolation('a {{ b }}', null).ast; + expect(ast).toBeAnInstanceOf(Interpolation); + expect(unparser.unparse(ast)).toEqual('a {{ b }}'); + + ast = parser.parseInterpolation('a {{ b }} c', null).ast; + expect(ast).toBeAnInstanceOf(Interpolation); + expect(unparser.unparse(ast)).toEqual('a {{ b }} c'); + }); + }); +}