Skip to content

Commit 787c71e

Browse files
committed
fix(filterFilter): ignore function properties and account for inherited properties
Closes angular#9984
1 parent 83d7211 commit 787c71e

File tree

2 files changed

+174
-31
lines changed

2 files changed

+174
-31
lines changed

src/ng/filter/filter.js

+31-24
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,15 @@ function filterFilter() {
122122
var predicateFn;
123123

124124
switch (typeof expression) {
125-
case 'object':
126-
// Replace `{$: 'xyz'}` with `'xyz'` and fall through
127-
var keys = Object.keys(expression);
128-
if ((keys.length === 1) && (keys[0] === '$')) expression = expression.$;
129-
// jshint -W086
125+
case 'function':
126+
predicateFn = expression;
127+
break;
130128
case 'boolean':
131129
case 'number':
130+
case 'object':
132131
case 'string':
133-
// jshint +W086
134132
predicateFn = createPredicateFn(expression, comparator);
135133
break;
136-
case 'function':
137-
predicateFn = expression;
138-
break;
139134
default:
140135
return array;
141136
}
@@ -152,8 +147,8 @@ function createPredicateFn(expression, comparator) {
152147
comparator = equals;
153148
} else if (!isFunction(comparator)) {
154149
comparator = function(actual, expected) {
155-
actual = ('' + actual).toLowerCase();
156-
expected = ('' + expected).toLowerCase();
150+
actual = lowercase('' + actual);
151+
expected = lowercase('' + expected);
157152
return actual.indexOf(expected) !== -1;
158153
};
159154
}
@@ -165,32 +160,44 @@ function createPredicateFn(expression, comparator) {
165160
return predicateFn;
166161
}
167162

168-
function deepCompare(actual, expected, comparator) {
163+
function deepCompare(actual, expected, comparator, keyWasDollar) {
169164
var actualType = typeof actual;
170165
var expectedType = typeof expected;
171166

172-
if (expectedType === 'function') {
173-
return expected(actual);
174-
} else if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
167+
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
175168
return !deepCompare(actual, expected.substring(1), comparator);
176169
} else if (actualType === 'array') {
170+
// In case `actual` is an array, consider it a match
171+
// if any of it's items matches `expected`
177172
return actual.some(function(item) {
178173
return deepCompare(item, expected, comparator);
179174
});
180175
}
181176

182177
switch (actualType) {
183178
case 'object':
184-
if (expectedType === 'object') {
185-
return Object.keys(expected).every(function(key) {
186-
var actualVal = (key === '$') ? actual : actual[key];
187-
var expectedVal = expected[key];
188-
return deepCompare(actualVal, expectedVal, comparator);
189-
});
179+
var key;
180+
if (keyWasDollar || (expectedType !== 'object')) {
181+
for (key in actual) {
182+
if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator)) {
183+
return true;
184+
}
185+
}
186+
return false;
190187
} else {
191-
return Object.keys(actual).some(function(key) {
192-
return (key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator);
193-
});
188+
for (key in expected) {
189+
var expectedVal = expected[key];
190+
if (isFunction(expectedVal)) {
191+
continue;
192+
}
193+
194+
var keyIsDollar = key === '$';
195+
var actualVal = keyIsDollar ? actual : actual[key];
196+
if (!deepCompare(actualVal, expectedVal, comparator, keyIsDollar)) {
197+
return false;
198+
}
199+
}
200+
return true;
194201
}
195202
break;
196203
default:

test/ng/filter/filterSpec.js

+143-7
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ describe('Filter: filter', function() {
136136
});
137137

138138

139+
it('should respect the nesting level of "$"', function() {
140+
var items = [{supervisor: 'me', person: {name: 'Annet', email: 'annet@example.com'}},
141+
{supervisor: 'me', person: {name: 'Billy', email: 'me@billy.com'}},
142+
{supervisor: 'me', person: {name: 'Joan', email: 'joan@example.net'}},
143+
{supervisor: 'me', person: {name: 'John', email: 'john@example.com'}},
144+
{supervisor: 'me', person: {name: 'Rita', email: 'rita@example.com'}}];
145+
var expr = {$: {$: 'me'}};
146+
147+
expect(filter(items, expr).length).toBe(1);
148+
expect(filter(items, expr)).toEqual([items[1]]);
149+
});
150+
151+
139152
it('should support boolean properties', function() {
140153
var items = [{name: 'tom', current: true},
141154
{name: 'demi', current: false},
@@ -156,23 +169,146 @@ describe('Filter: filter', function() {
156169
});
157170

158171

159-
it('should not consider the expression\'s inherited properties', function() {
160-
Object.prototype.noop = noop;
172+
it('should ignore function properties in items', function() {
173+
// Own function properties
174+
var items = [
175+
{text: 'hello', func: noop},
176+
{text: 'goodbye'},
177+
{text: 'kittens'},
178+
{text: 'puppies'}
179+
];
180+
var expr = {text: 'hello'};
181+
182+
expect(filter(items, expr).length).toBe(1);
183+
expect(filter(items, expr)[0]).toBe(items[0]);
184+
expect(filter(items, expr, true).length).toBe(1);
185+
expect(filter(items, expr, true)[0]).toBe(items[0]);
186+
187+
// Inherited function proprties
188+
function Item(text) {
189+
this.text = text;
190+
}
191+
Item.prototype.func = noop;
192+
193+
items = [
194+
new Item('hello'),
195+
new Item('goodbye'),
196+
new Item('kittens'),
197+
new Item('puppies')
198+
];
161199

200+
expect(filter(items, expr).length).toBe(1);
201+
expect(filter(items, expr)[0]).toBe(items[0]);
202+
expect(filter(items, expr, true).length).toBe(1);
203+
expect(filter(items, expr, true)[0]).toBe(items[0]);
204+
});
205+
206+
207+
it('should ignore function properties in expression', function() {
208+
// Own function properties
162209
var items = [
163210
{text: 'hello'},
164211
{text: 'goodbye'},
165212
{text: 'kittens'},
166213
{text: 'puppies'}
167214
];
215+
var expr = {text: 'hello', func: noop};
216+
217+
expect(filter(items, expr).length).toBe(1);
218+
expect(filter(items, expr)[0]).toBe(items[0]);
219+
expect(filter(items, expr, true).length).toBe(1);
220+
expect(filter(items, expr, true)[0]).toBe(items[0]);
221+
222+
// Inherited function proprties
223+
function Expr(text) {
224+
this.text = text;
225+
}
226+
Expr.prototype.func = noop;
227+
228+
expr = new Expr('hello');
229+
230+
expect(filter(items, expr).length).toBe(1);
231+
expect(filter(items, expr)[0]).toBe(items[0]);
232+
expect(filter(items, expr, true).length).toBe(1);
233+
expect(filter(items, expr, true)[0]).toBe(items[0]);
234+
});
235+
236+
237+
it('should consider inherited properties in items', function() {
238+
function Item(text) {
239+
this.text = text;
240+
}
241+
Item.prototype.doubleL = 'maybe';
242+
243+
var items = [
244+
new Item('hello'),
245+
new Item('goodbye'),
246+
new Item('kittens'),
247+
new Item('puppies')
248+
];
249+
var expr = {text: 'hello', doubleL: 'perhaps'};
250+
251+
expect(filter(items, expr).length).toBe(0);
252+
expect(filter(items, expr, true).length).toBe(0);
253+
254+
expr = {text: 'hello', doubleL: 'maybe'};
255+
256+
expect(filter(items, expr).length).toBe(1);
257+
expect(filter(items, expr)[0]).toBe(items[0]);
258+
expect(filter(items, expr, true).length).toBe(1);
259+
expect(filter(items, expr, true)[0]).toBe(items[0]);
260+
});
261+
262+
263+
it('should consider inherited properties in expression', function() {
264+
function Expr(text) {
265+
this.text = text;
266+
}
267+
Expr.prototype.doubleL = true;
268+
269+
var items = [
270+
{text: 'hello', doubleL: true},
271+
{text: 'goodbye'},
272+
{text: 'kittens'},
273+
{text: 'puppies'}
274+
];
275+
var expr = new Expr('e');
276+
277+
expect(filter(items, expr).length).toBe(1);
278+
expect(filter(items, expr)[0]).toBe(items[0]);
279+
280+
expr = new Expr('hello');
281+
282+
expect(filter(items, expr, true).length).toBe(1);
283+
expect(filter(items, expr)[0]).toBe(items[0]);
284+
});
285+
286+
287+
it('should not be affected by `Object.prototype` when using a string expression', function() {
288+
Object.prototype.someProp = 'oo';
289+
290+
var items = [
291+
createMap(),
292+
createMap(),
293+
createMap(),
294+
createMap()
295+
];
296+
items[0].someProp = 'hello';
297+
items[1].someProp = 'goodbye';
298+
items[2].someProp = 'kittens';
299+
items[3].someProp = 'puppies';
300+
301+
// Affected by `Object.prototype`
302+
expect(filter(items, {}).length).toBe(1);
303+
expect(filter(items, {})[0]).toBe(items[1]);
168304

169-
expect(filter(items, {text: 'hell'}).length).toBe(1);
170-
expect(filter(items, {text: 'hell'})[0]).toBe(items[0]);
305+
expect(filter(items, {$: 'll'}).length).toBe(0);
171306

172-
expect(filter(items, 'hell').length).toBe(1);
173-
expect(filter(items, 'hell')[0]).toBe(items[0]);
307+
// Not affected by `Object.prototype`
308+
expect(filter(items, 'll').length).toBe(1);
309+
expect(filter(items, 'll')[0]).toBe(items[0]);
174310

175-
delete(Object.prototype.noop);
311+
delete Object.prototype.someProp;
176312
});
177313

178314

0 commit comments

Comments
 (0)