From a38a9bff1fc89456133ecf29a0776d7c2a6772b9 Mon Sep 17 00:00:00 2001 From: James deBoer Date: Mon, 17 Jun 2013 11:09:43 -0700 Subject: [PATCH] feat(parser): Add support for member field setters --- lib/angular.dart | 15 ++++---- lib/interface_typing.dart | 64 +++++++++++++++++++++++++++++++++ lib/parser.dart | 43 ++++++++++++++-------- lib/scope.dart | 6 ++-- test/angular_spec.dart | 31 ++++++++++++++++ test/interface_typing_spec.dart | 63 ++++++++++++++++++++++++++++++++ test/parser_spec.dart | 37 +++++++++++++++++++ 7 files changed, 235 insertions(+), 24 deletions(-) create mode 100644 lib/interface_typing.dart create mode 100644 test/angular_spec.dart create mode 100644 test/interface_typing_spec.dart diff --git a/lib/angular.dart b/lib/angular.dart index 133694169..aa264f558 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -18,6 +18,7 @@ part 'directives/ng_repeat.dart'; part 'dom_utilities.dart'; part 'exception_handler.dart'; part 'http.dart'; +part 'interface_typing.dart'; part 'interpolate.dart'; part 'mirrors.dart'; part 'node_cursor.dart'; @@ -55,17 +56,19 @@ typedef FnWith3Args(a0, a1, a2); typedef FnWith4Args(a0, a1, a2, a3); typedef FnWith5Args(a0, a1, a2, a3, a4); -_relaxFnApply(Function fn, List args) { +relaxFnApply(Function fn, List args) { + // Check the args.length to support functions with optional parameters. + var argsLen = args.length; if (fn is Function && fn != null) { - if (fn is FnWith5Args) { + if (fn is FnWith5Args && argsLen > 4) { return fn(args[0], args[1], args[2], args[3], args[4]); - } else if (fn is FnWith4Args) { + } else if (fn is FnWith4Args && argsLen > 3) { return fn(args[0], args[1], args[2], args[3]); - } else if (fn is FnWith3Args) { + } else if (fn is FnWith3Args&& argsLen > 2 ) { return fn(args[0], args[1], args[2]); - } else if (fn is FnWith2Args) { + } else if (fn is FnWith2Args && argsLen > 1 ) { return fn(args[0], args[1]); - } else if (fn is FnWith1Args) { + } else if (fn is FnWith1Args && argsLen > 0) { return fn(args[0]); } else if (fn is FnWith0Args) { return fn(); diff --git a/lib/interface_typing.dart b/lib/interface_typing.dart new file mode 100644 index 000000000..9471fad31 --- /dev/null +++ b/lib/interface_typing.dart @@ -0,0 +1,64 @@ +part of angular; + +var OBJECT_QUAL_NAME = fastReflectClass(Object).qualifiedName; + +_equalTypes(ClassMirror a, ClassMirror b) => a.qualifiedName == b.qualifiedName; + +_isType(obj, type) { + InstanceMirror instanceMirror = reflect(obj); + ClassMirror classM = instanceMirror.type; + + if (type is Type) { + type = fastReflectClass(type); + } + + if (classM.superinterfaces.any((si) {return _equalTypes(si, type);})) return true; + + while (classM != null) { + if (_equalTypes(classM, type)) return true; + if (classM.qualifiedName == OBJECT_QUAL_NAME) classM = null; // dart bug 5794 + else classM = classM.superclass; + + } + return false; +} + +isInterface(obj, Type type) { + if (_isType(obj, type)) return true; + + var objMembers = reflect(obj).type.members; + + bool allPresent = true; + fastReflectClass(type).members.forEach((symbol, mirror) { + if (!objMembers.containsKey(symbol)) allPresent = false; + var objMirror = objMembers[symbol]; + if (!_isType(objMirror, reflect(mirror).type)) allPresent = false; + + // TODO(deboer): Check that the method signatures match. Waiting on dartbug 11334 + /*if (mirror is MethodMirror) { + // are the paremeters the same? + var sameParameters = true; + var interfaceParams = mirror.parameters; + var objParams = objMirror.parameters; + + Map namedParams; + int minParams = 0; + int maxParams = 0; + mirror.parameters.forEach((ParameterMirror param) { + if (param.isNamed) namedParams[param.qualifiedName] = param; + if (param.isOptional) maxParams++; + else { minParams++; minParams++; } + }); + + objMirror.parameters.forEach((param) { + if (param.isNamed) namedParams.remove(param.qualifiedName); + if (param.isOptional) maxParams--; + else { minParams--; maxParams--; } + }); + + if (minParams > 0) return false; + if (maxParams < 0) return false; + }*/ + }); + return allPresent; +} diff --git a/lib/parser.dart b/lib/parser.dart index 72e3c54e9..3b6e10b84 100644 --- a/lib/parser.dart +++ b/lib/parser.dart @@ -125,17 +125,13 @@ ParsedFn ZERO = new ParsedFn((_, _x) => 0); class BreakException {} -// returns a function that calls fn with numArgs args as an array -varArgs(numArgs, fn) { - switch (numArgs) { - case 0: return () => fn([]); - case 1: return (p0) => fn([p0]); - case 2: return (p0, p1) => fn([p0, p1]); - case 3: return (p0, p1, p2) => fn([p0, p1, p3]); - case 4: return (p0, p1, p2, p3) => fn([p0, p1, p2, p3]); - case 5: return (p0, p1, p2, p3) => fn([p0, p1, p2, p3, p4]); - } - throw "varArgs with $numArgs is not supported."; +class Setter { + operator[]=(name, value){} +} + +abstract class Getter { + bool containsKey(name); + operator[](name); } // Returns a tuple [found, value] @@ -146,7 +142,9 @@ getterChild(value, childKey) { } else { return [false, null]; } - } else if (value is Map && value.containsKey(childKey)) { + } + + if (isInterface(value, Getter) && value.containsKey(childKey)) { return [true, value[childKey]]; } else { InstanceMirror instanceMirror = reflect(value); @@ -159,6 +157,7 @@ getterChild(value, childKey) { if (instanceMirror.type.members.containsKey(curSym)) { MethodMirror methodMirror = instanceMirror.type.members[curSym]; return [true, _relaxFnArgs((args) { + if (args == null) args = []; try { return instanceMirror.invoke(curSym, args).reflectee; } catch (e) { @@ -195,6 +194,21 @@ getter(scope, locals, path) { return currentValue; } +setterChild(obj, childKey, value) { + if (isInterface(obj, Setter)) { + obj[childKey] = value; + return value; + } + InstanceMirror instanceMirror = reflect(obj); + Symbol curSym = new Symbol(childKey); + try { + // maybe it is a member field? + return instanceMirror.setField(curSym, value).reflectee; + } catch (e) { + throw "$e \n\n${e.stacktrace}"; + } +} + setter(obj, path, setValue) { var element = path.split('.'); for (var i = 0; element.length > 1; i++) { @@ -206,8 +220,7 @@ setter(obj, path, setValue) { } obj = propertyObj; } - obj[element.removeAt(0)] = setValue; - return setValue; + return setterChild(obj, element.removeAt(0), setValue); } class Parser { @@ -698,7 +711,7 @@ class Parser { args.add(argsFn[i](self, locals)); } var userFn = fn(self, locals); - return _relaxFnApply(userFn, args); + return relaxFnApply(userFn, args); }); }; diff --git a/lib/scope.dart b/lib/scope.dart index 784829504..e5b86afca 100644 --- a/lib/scope.dart +++ b/lib/scope.dart @@ -158,7 +158,7 @@ class Scope implements Map { }; var $watchCollectionAction = () { - _relaxFnApply(listener, [newValue, oldValue, self]); + relaxFnApply(listener, [newValue, oldValue, self]); }; return this.$watch($watchCollectionWatch, $watchCollectionAction); @@ -324,7 +324,7 @@ class Scope implements Map { i = 0; for (var length = namedListeners.length; i "none", sixArgs)).toEqual("none"); + expect(relaxFnApply((a) => a, sixArgs)).toEqual(1); + expect(relaxFnApply((a, b) => a + b, sixArgs)).toEqual(2); + expect(relaxFnApply((a, b, c) => a + b + c, sixArgs)).toEqual(4); + expect(relaxFnApply((a, b, c, d) => a + b + c + d, sixArgs)).toEqual(7); + expect(relaxFnApply((a, b, c, d, e) => a + b + c + d + e, sixArgs)).toEqual(12); + }); + + it('should work with 0 arguments', () { + var noArgs = []; + expect(relaxFnApply(() => "none", noArgs)).toEqual("none"); + expect(relaxFnApply(([a]) => a, noArgs)).toEqual(null); + expect(relaxFnApply(([a, b]) => b, noArgs)).toEqual(null); + expect(relaxFnApply(([a, b, c]) => c, noArgs)).toEqual(null); + expect(relaxFnApply(([a, b, c, d]) => d, noArgs)).toEqual(null); + expect(relaxFnApply(([a, b, c, d, e]) => e, noArgs)).toEqual(null); + }); + + it('should fail with not enough arguments', () { + expect(() { + relaxFnApply((required, alsoRequired) => "happy", [1]); + }).toThrow('Unknown function type, expecting 0 to 5 args.'); + }); + }); +} diff --git a/test/interface_typing_spec.dart b/test/interface_typing_spec.dart new file mode 100644 index 000000000..5ad95d4c3 --- /dev/null +++ b/test/interface_typing_spec.dart @@ -0,0 +1,63 @@ +import '_specs.dart'; +import "dart:mirrors"; + +class InterfaceWithFields { + int aNumber; + String aString; +} + +class ClassWithFields { + int aNumber; + String aString; + bool somethingElse; +} + +class ClassWithDifferentFields { + int aDifferentNumber; +} + +class ClassWithNotFields { + aNumber(){} + aString(){} +} + +class InterfaceWithMethods { + method({b}) {} +} + +class ClassWithMethods { + method({b, c}) {} +} + +class ClassWithDifferentMethods { + method({c, d}) {} +} + +main() { + describe('Interface Typing', () { + it('should recognize built-in objects as an object', () { + var im = reflect(new Object()); + var cm = reflectClass(Object); + expect(im.type.qualifiedName).toEqual(cm.qualifiedName); + + expect(isInterface(new Object(), Object)).toBeTruthy(); + + expect(isInterface([], Object)).toBeTruthy(); + expect(isInterface({}, Object)).toBeTruthy(); + expect(isInterface(6, Object)).toBeTruthy(); + expect(isInterface('s', Object)).toBeTruthy(); + }); + + it('should recognize interfaces with fields', () { + expect(isInterface(new ClassWithFields(), InterfaceWithFields)).toBeTruthy(); + expect(isInterface(new ClassWithDifferentFields(), InterfaceWithFields)).toBeFalsy(); + expect(isInterface(new ClassWithNotFields(), InterfaceWithFields)).toBeFalsy(); + }); + + // waiting on dartbug 11334 + xit('should recognize interfaces with methods', () { + expect(isInterface(new ClassWithMethods(), InterfaceWithMethods)).toBeTruthy(); + expect(isInterface(new ClassWithDifferentMethods(), InterfaceWithMethods)).toBeFalsy(); + }); + }); +} diff --git a/test/parser_spec.dart b/test/parser_spec.dart index 18a5c9945..fd794cec9 100644 --- a/test/parser_spec.dart +++ b/test/parser_spec.dart @@ -1,5 +1,14 @@ import '_specs.dart'; +// Used to test getter / setter logic. +class TestData { + String _str = "testString"; + get str => _str; + set str(x) => _str = x; + + method() => "testMethod"; +} + class LexerExpect extends Expect { LexerExpect(actual) : super(actual); toBeToken(int index, String text) { @@ -528,6 +537,34 @@ main() { // DOES NOT WORK. bool.toString is not reflected // expect(eval('bool.toString()')).toEqual('false'); }); + + it('should support map getters', () { + expect(Parser.parse('a')({'a': 4})).toEqual(4); + }); + + it('should support member getters', () { + expect(Parser.parse('str')(new TestData())).toEqual('testString'); + }); + + it('should support returning member functions', () { + expect(Parser.parse('method')(new TestData())()).toEqual('testMethod'); + }); + + it('should support calling member functions', () { + expect(Parser.parse('method()')(new TestData())).toEqual('testMethod'); + }); + + it('should support array setters', () { + var data = {'a': [1,3]}; + expect(Parser.parse('a[1]=2')(data)).toEqual(2); + expect(data['a'][1]).toEqual(2); + }); + + it('should support member field setters', () { + TestData data = new TestData(); + expect(Parser.parse('str="bob"')(data)).toEqual('bob'); + expect(data.str).toEqual("bob"); + }); }); describe('assignable', () {