-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($parse): always re-evaluate filters within literals when an input is an object #15990
Conversation
src/ng/parse.js
Outdated
@@ -622,15 +622,51 @@ function isStateless($filter, filterName) { | |||
return !fn.$stateful; | |||
} | |||
|
|||
function findConstantAndWatchExpressions(ast, $filter) { | |||
// Detect nodes which could depend on non-shallow state of objects | |||
function isShallowState(stack) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could probably use a better name/terminology...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isStackPure
?
src/ng/parse.js
Outdated
@@ -781,7 +819,7 @@ ASTCompiler.prototype = { | |||
filters: {}, | |||
fn: {vars: [], body: [], own: {}}, | |||
assign: {vars: [], body: [], own: {}}, | |||
inputs: [] | |||
inputs: {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably a nicer way of doing this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By doing this (and later Object.keys
) you are abusing the fact that Object.keys
returns the keys in the order that were added to assert that expressions as foo.bar();foo.baz()
will be evaluated in the right order. Even when this is somehow true, this is not part of the spec and we should not depend on this out-of-spec fact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot the order even mattered, good point 👍
test/ng/parseSpec.js
Outdated
expect(watcherCalls).toBe(2); | ||
})); | ||
|
||
it('should always reevaluate filters with literal input containing non-primitives', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One way to test this better (which I'll add) would be something like [(obj | filterThatConvertsToPrimitive)]
src/ng/parse.js
Outdated
function findConstantAndWatchExpressions(ast, $filter) { | ||
// Detect nodes which could depend on non-shallow state of objects | ||
function isShallowState(stack) { | ||
for (var i = 0; i < stack.length; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to walk the stack every time? It seems that we are either pushing or popping items on and off this stack and then re-evaluating the shallowStateness of the stack. Can we not cache the state of each stack and then we only need to evaluate the pushed item?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's probably true. Probably store the result of the switch statement in the stack? I'll try it out...
src/ng/parse.js
Outdated
return true; | ||
} | ||
|
||
function findConstantAndWatchExpressions(ast, $filter, stack) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not following completely the logic here, but by just looking at the code it looks like stack
can be replace with a simple boolean (that the first called call with true
and the rest do ast.isShallowState = parentShallowState && isShallowState([ast]);
(and of course this can be simplified).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that's possible. I guess the idea of the stack
was that it could also potentially be used for other purposes, but I'll try your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this is what I was getting at with #15990 (comment)
437c9af
to
3d07e75
Compare
I think I've addressed all the comments. Also added a I'm also open to other ideas around this and similar issues such as #15905 if anyone has alternative ideas. Note that if the $filter solution in this PR is extended to interceptors it will also fix #15905 except one-time literals containing objects. That one failure is caused by this condition which I really wish I could find an alternative solution for, especially since that condition forces workarounds in multiple places where interceports are used. The other thing that would fix that one ng-class test is dropping nested object support in ng-class 😒 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good; a few minor comments.
My main question is about the purity of UnaryExpressions
.
Otherwise happy to land this.
src/ng/parse.js
Outdated
var allConstants; | ||
var argsToWatch; | ||
var isStatelessFilter; | ||
|
||
ast.isPure = parentIsPure = isPure(ast, (undefined === parentIsPure) || parentIsPure); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(undefined === parentIsPure) || parentIsPure
- I would move this logic into the isPure()
function.
src/ng/parse.js
Outdated
break; | ||
|
||
case AST.UnaryExpression: | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case would you not defer to the parentPureness
also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is to allow shallow-watching the input
in things like fn(!input)
where !parentIsPure
, but the child is.
I've tried to exclude case which can invoke toString
like "" + input
because those could be non-pure (when toString
reads inner state).
forEach(fns, function(name) { | ||
result.push('var ' + name + '=' + self.generateFunction(name, 's')); | ||
forEach(inputs, function(input) { | ||
result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any performance/code-size gain in setting and using var name = input.name;
here?
Or does the minification process deal with that automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think performance/code-size has no (noticeable) gain or loss either way, so I'd vote for whatever is more readable/prettier. My preference is how it is (no local var) but I can change it if others would prefer that.
test/ng/parseSpec.js
Outdated
@@ -2973,6 +2973,172 @@ describe('parser', function() { | |||
expect(watcherCalls).toBe(1); | |||
})); | |||
|
|||
it('should not reevaluate filters in literals with non-primitive input that does support valueOf()', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use some describe blocks to simplify the test descriptions here:
describe('containing literals', function() {
describe('with non-primitive inputs', function() {
describe('that support valueOf()', function() {
it('should not reevaluate filters, inputs do not change', ...);
it('should reevaluate filters, when an input valueOf() result changes', ...);
it('should not reevaluate filters, when the input changes but valueOf() does not ', ...);
});
});
describe('with non-primitive inputs', function() {
});
});
you get the idea...
0e2296e
to
fdf7a2a
Compare
Updated based on PR feedback. Also added more tests and added a separate commit to reorganize the tests. How those tests are organized is debatable but I think what I did is similar to the suggestion in the comment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Thanks @jbedard - please squash and merge (assuming Travis is happy).
@lgalfaso have a chance to look at the latest update? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Just one minor comment.
src/ng/parse.js
Outdated
var allConstants; | ||
var argsToWatch; | ||
var isStatelessFilter; | ||
|
||
ast.isPure = parentIsPure = isPure(ast, parentIsPure); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] I find it slightly odd to change the value of the argument. I would just use ast.isPure
below and keep the argument as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I agree with that. Could also change the parentIsPure
param to parentAst
, but I'll go with your suggestion unless someone says otherwise.
… is an object Fixes angular#15964 Closes angular#15990
e056dd9
to
f0df97d
Compare
Made the change and merged into master and v1.6.x |
|
||
// Unary always convert to primative | ||
case AST.UnaryExpression: | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't unary expressions invoke a stateful valueOf()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think you're right.
Seems like any numeric operator (everything except !
, &&
and ||
?) will use valueOf()
. However this will be handled so I don't think it will lead to bugs, but this method is still wrong.
Now that I type that... &&
and ||
also don't convert to primitives and might return an object so those are also incorrect, but we watch the full logical expression so it won't lead to bugs, but this method is still wrong.
So I don't think there's a bug but it might be nice to make this function more correct?
Once again, always wait for @gkalpak to review your PRs...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, I don't know what &&
and ||
have to do with this (since they are not unary operators) 😕
Regarding getValueOf()
, I don't think it is enough to "save" us (in cases where valueOf()
returns non-primitive values). I admit it is very unlikely that this will affect any real app, but from a theoretical point of view, I think expressions such as '+obj'
, where obj
has a valueOf()
method that returns an object will not be handled correctly (e.g. if the returned object stays the same by reference, but one of its properties changes).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm...OK maybe that is not true, because unary operators will always evaluate to the same thing for objects whose valueOf
returns a non-primitve value (!
will return false
, +/-
will return NaN
), so this might actually work correctly 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I forgot that &&
and ||
are not BinaryExpression
s (the next case statement) so never mind about that one!
I do still think that numeric unary/binary operators are non-pure because valueOf
might be stateful. Which is a bug in isPure
, but the use of getValueOf
later on prevents a bug in $parse
. I'll probably still correct the isPure
function...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do still think that numeric unary/binary operators are non-pure because valueOf might be stateful. Which is a bug in isPure, but the use of getValueOf later on prevents a bug in $parse. I'll probably still correct the isPure function...
That's also my conclusion 😃
Fixes #15964
This changes the compareObjectIdentity flag from being per-expression to per-input. This allows inputs to filters to not compare object entity, but other inputs such as
- x
or{x: x}
to only compare object entity. This also expands thiscompareObjectIdentity = true
beyond just object literals.