Skip to content

Commit 437c9af

Browse files
committed
fix($parse): always re-evaluate filters within literals when an input is an object
Fixes angular#15964
1 parent a86a319 commit 437c9af

File tree

3 files changed

+246
-28
lines changed

3 files changed

+246
-28
lines changed

src/ng/parse.js

+58-26
Original file line numberDiff line numberDiff line change
@@ -622,15 +622,43 @@ function isStateless($filter, filterName) {
622622
return !fn.$stateful;
623623
}
624624

625-
function findConstantAndWatchExpressions(ast, $filter) {
625+
// Detect nodes which could depend on non-shallow state of objects
626+
function isPure(node, parentPureness) {
627+
switch (node.type) {
628+
// Computed members might invoke a stateful toString()
629+
case AST.MemberExpression:
630+
if (node.computed) {
631+
return false;
632+
}
633+
break;
634+
635+
case AST.UnaryExpression:
636+
return true;
637+
638+
// The binary + operator can invoke a stateful toString().
639+
case AST.BinaryExpression:
640+
return node.operator !== '+';
641+
642+
// Functions / filters probably read state from within objects
643+
case AST.CallExpression:
644+
return false;
645+
}
646+
647+
return parentPureness;
648+
}
649+
650+
function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
626651
var allConstants;
627652
var argsToWatch;
628653
var isStatelessFilter;
654+
655+
ast.isPure = parentIsPure = isPure(ast, (undefined === parentIsPure) || parentIsPure);
656+
629657
switch (ast.type) {
630658
case AST.Program:
631659
allConstants = true;
632660
forEach(ast.body, function(expr) {
633-
findConstantAndWatchExpressions(expr.expression, $filter);
661+
findConstantAndWatchExpressions(expr.expression, $filter, parentIsPure);
634662
allConstants = allConstants && expr.expression.constant;
635663
});
636664
ast.constant = allConstants;
@@ -640,26 +668,26 @@ function findConstantAndWatchExpressions(ast, $filter) {
640668
ast.toWatch = [];
641669
break;
642670
case AST.UnaryExpression:
643-
findConstantAndWatchExpressions(ast.argument, $filter);
671+
findConstantAndWatchExpressions(ast.argument, $filter, parentIsPure);
644672
ast.constant = ast.argument.constant;
645673
ast.toWatch = ast.argument.toWatch;
646674
break;
647675
case AST.BinaryExpression:
648-
findConstantAndWatchExpressions(ast.left, $filter);
649-
findConstantAndWatchExpressions(ast.right, $filter);
676+
findConstantAndWatchExpressions(ast.left, $filter, parentIsPure);
677+
findConstantAndWatchExpressions(ast.right, $filter, parentIsPure);
650678
ast.constant = ast.left.constant && ast.right.constant;
651679
ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
652680
break;
653681
case AST.LogicalExpression:
654-
findConstantAndWatchExpressions(ast.left, $filter);
655-
findConstantAndWatchExpressions(ast.right, $filter);
682+
findConstantAndWatchExpressions(ast.left, $filter, parentIsPure);
683+
findConstantAndWatchExpressions(ast.right, $filter, parentIsPure);
656684
ast.constant = ast.left.constant && ast.right.constant;
657685
ast.toWatch = ast.constant ? [] : [ast];
658686
break;
659687
case AST.ConditionalExpression:
660-
findConstantAndWatchExpressions(ast.test, $filter);
661-
findConstantAndWatchExpressions(ast.alternate, $filter);
662-
findConstantAndWatchExpressions(ast.consequent, $filter);
688+
findConstantAndWatchExpressions(ast.test, $filter, parentIsPure);
689+
findConstantAndWatchExpressions(ast.alternate, $filter, parentIsPure);
690+
findConstantAndWatchExpressions(ast.consequent, $filter, parentIsPure);
663691
ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
664692
ast.toWatch = ast.constant ? [] : [ast];
665693
break;
@@ -668,9 +696,9 @@ function findConstantAndWatchExpressions(ast, $filter) {
668696
ast.toWatch = [ast];
669697
break;
670698
case AST.MemberExpression:
671-
findConstantAndWatchExpressions(ast.object, $filter);
699+
findConstantAndWatchExpressions(ast.object, $filter, parentIsPure);
672700
if (ast.computed) {
673-
findConstantAndWatchExpressions(ast.property, $filter);
701+
findConstantAndWatchExpressions(ast.property, $filter, parentIsPure);
674702
}
675703
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
676704
ast.toWatch = [ast];
@@ -680,7 +708,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
680708
allConstants = isStatelessFilter;
681709
argsToWatch = [];
682710
forEach(ast.arguments, function(expr) {
683-
findConstantAndWatchExpressions(expr, $filter);
711+
findConstantAndWatchExpressions(expr, $filter, parentIsPure);
684712
allConstants = allConstants && expr.constant;
685713
if (!expr.constant) {
686714
argsToWatch.push.apply(argsToWatch, expr.toWatch);
@@ -690,16 +718,16 @@ function findConstantAndWatchExpressions(ast, $filter) {
690718
ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
691719
break;
692720
case AST.AssignmentExpression:
693-
findConstantAndWatchExpressions(ast.left, $filter);
694-
findConstantAndWatchExpressions(ast.right, $filter);
721+
findConstantAndWatchExpressions(ast.left, $filter, parentIsPure);
722+
findConstantAndWatchExpressions(ast.right, $filter, parentIsPure);
695723
ast.constant = ast.left.constant && ast.right.constant;
696724
ast.toWatch = [ast];
697725
break;
698726
case AST.ArrayExpression:
699727
allConstants = true;
700728
argsToWatch = [];
701729
forEach(ast.elements, function(expr) {
702-
findConstantAndWatchExpressions(expr, $filter);
730+
findConstantAndWatchExpressions(expr, $filter, parentIsPure);
703731
allConstants = allConstants && expr.constant;
704732
if (!expr.constant) {
705733
argsToWatch.push.apply(argsToWatch, expr.toWatch);
@@ -712,13 +740,13 @@ function findConstantAndWatchExpressions(ast, $filter) {
712740
allConstants = true;
713741
argsToWatch = [];
714742
forEach(ast.properties, function(property) {
715-
findConstantAndWatchExpressions(property.value, $filter);
743+
findConstantAndWatchExpressions(property.value, $filter, parentIsPure);
716744
allConstants = allConstants && property.value.constant && !property.computed;
717745
if (!property.value.constant) {
718746
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
719747
}
720748
if (property.computed) {
721-
findConstantAndWatchExpressions(property.key, $filter);
749+
findConstantAndWatchExpressions(property.key, $filter, parentIsPure);
722750
if (!property.key.constant) {
723751
argsToWatch.push.apply(argsToWatch, property.key.toWatch);
724752
}
@@ -803,7 +831,7 @@ ASTCompiler.prototype = {
803831
var intoId = self.nextId();
804832
self.recurse(watch, intoId);
805833
self.return_(intoId);
806-
self.state.inputs.push(fnKey);
834+
self.state.inputs.push({name: fnKey, isPure: watch.isPure});
807835
watch.watchId = key;
808836
});
809837
this.state.computing = 'fn';
@@ -839,13 +867,16 @@ ASTCompiler.prototype = {
839867

840868
watchFns: function() {
841869
var result = [];
842-
var fns = this.state.inputs;
870+
var inputs = this.state.inputs;
843871
var self = this;
844-
forEach(fns, function(name) {
845-
result.push('var ' + name + '=' + self.generateFunction(name, 's'));
872+
forEach(inputs, function(input) {
873+
result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's'));
874+
if (input.isPure) {
875+
result.push(input.name, '.isPure=true;');
876+
}
846877
});
847-
if (fns.length) {
848-
result.push('fn.inputs=[' + fns.join(',') + '];');
878+
if (inputs.length) {
879+
result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];');
849880
}
850881
return result.join('');
851882
},
@@ -1251,6 +1282,7 @@ ASTInterpreter.prototype = {
12511282
inputs = [];
12521283
forEach(toWatch, function(watch, key) {
12531284
var input = self.recurse(watch);
1285+
input.isPure = watch.isPure;
12541286
watch.input = input;
12551287
inputs.push(input);
12561288
watch.watchId = key;
@@ -1817,7 +1849,7 @@ function $ParseProvider() {
18171849
inputExpressions = inputExpressions[0];
18181850
return scope.$watch(function expressionInputWatch(scope) {
18191851
var newInputValue = inputExpressions(scope);
1820-
if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) {
1852+
if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) {
18211853
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
18221854
oldInputValueOf = newInputValue && getValueOf(newInputValue);
18231855
}
@@ -1837,7 +1869,7 @@ function $ParseProvider() {
18371869

18381870
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
18391871
var newInputValue = inputExpressions[i](scope);
1840-
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) {
1872+
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) {
18411873
oldInputValues[i] = newInputValue;
18421874
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
18431875
}

test/ng/directive/ngClassSpec.js

+20
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,26 @@ describe('ngClass', function() {
567567
})
568568
);
569569

570+
//https://github.com/angular/angular.js/issues/15960#issuecomment-299109412
571+
it('should always reevaluate filters with non-primitive inputs within literals', function() {
572+
module(function($filterProvider) {
573+
$filterProvider.register('foo', valueFn(function(o) {
574+
return o.a || o.b;
575+
}));
576+
});
577+
578+
inject(function($rootScope, $compile) {
579+
$rootScope.testObj = {};
580+
element = $compile('<div ng-class="{x: (testObj | foo)}">')($rootScope);
581+
582+
$rootScope.$apply();
583+
expect(element).not.toHaveClass('x');
584+
585+
$rootScope.$apply('testObj.a = true');
586+
expect(element).toHaveClass('x');
587+
});
588+
});
589+
570590
describe('large objects', function() {
571591
var getProp;
572592
var veryLargeObj;

0 commit comments

Comments
 (0)