Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
fix($parse): Preserve expensive checks when runnning $eval inside an …
Browse files Browse the repository at this point in the history
…expression

When running an expression with expensive checks, there is a call to `$eval` or `$evalAsync`
then that expression is also evaluated using expensive checks

Closes: #13850
  • Loading branch information
lgalfaso committed Jan 27, 2016
1 parent 04d4d93 commit acfda10
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 8 deletions.
39 changes: 37 additions & 2 deletions src/ng/parse.js
Expand Up @@ -1757,10 +1757,19 @@ function $ParseProvider() {
csp: noUnsafeEval,
expensiveChecks: true
};
var runningChecksEnabled = false;

return function $parse(exp, interceptorFn, expensiveChecks) {
$parse.$$runningExpensiveChecks = function() {
return runningChecksEnabled;
};

return $parse;

function $parse(exp, interceptorFn, expensiveChecks) {
var parsedExpression, oneTime, cacheKey;

expensiveChecks = expensiveChecks || runningChecksEnabled;

switch (typeof exp) {
case 'string':
exp = exp.trim();
Expand All @@ -1786,6 +1795,9 @@ function $ParseProvider() {
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
if (expensiveChecks) {
parsedExpression = expensiveChecksInterceptor(parsedExpression);
}
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
Expand All @@ -1796,7 +1808,30 @@ function $ParseProvider() {
default:
return addInterceptor(noop, interceptorFn);
}
};
}

function expensiveChecksInterceptor(fn) {
if (!fn) return fn;
expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 27, 2016

Member

Do we really need to pass fn.assign through expesiveChecksInterceptor() ?
Isn't the function already created based on expensiveChecks ?

This comment has been minimized.

Copy link
@lgalfaso

lgalfaso Jan 27, 2016

Author Contributor

expesiveChecksInterceptor adds a try { ... } finally {...}, it is not important if the function was created with or without expensiveChecks. The alternative to this (that I am not sure would be a good idea) is to add the try...finally inside the function, but I find this solution better as it works the same way for ASTCompiler and for ASTInterpreter

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 28, 2016

Member

I mean since fn.assign has already been created (so it's body should contain (or not contain) appropriate checks based on the current value of expensiveChecks), wouldn't it be enough to just copy it over to expensiveCheckFn ?
Can the value of runningChecksEnabled affect the execution of the assign function ?

This comment has been minimized.

Copy link
@lgalfaso

lgalfaso Jan 28, 2016

Author Contributor

expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign) is done so .assign has the try...catch. The execution of the function is fine

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 28, 2016

Member

But do we need the try...finally ?
If there can't be any parsing inside assign then we shouldn't need it.

This comment has been minimized.

Copy link
@lgalfaso

lgalfaso Jan 28, 2016

Author Contributor

the try...finally is only there so we set and reset runningChecksEnabled when there is an exception. Keep in mind that you can have an exception that is not related to expensiveChecks. Eg with the expression foo().bar, as we do not have any control with what is going on inside foo()

expensiveCheckFn.constant = fn.constant;
expensiveCheckFn.literal = fn.literal;
for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 27, 2016

Member

@lgalfaso, I don't really understand under what circumstances (or if at all possible), but could it be that not copying fn.inputs onto expensiveCheckFn could cause trouble in parse.js#L1984 ?

This comment has been minimized.

Copy link
@lgalfaso

lgalfaso Jan 27, 2016

Author Contributor

Given that we copy $$watchDelegate then there is a chance that $$watchDelegate is inputsWatchDelegate and then if inputs is undefined we will throw here

if (inputExpressions.length === 1) {

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 28, 2016

Member

Does this mean that we should also do expensiveCheckFn.inputs = fn.inputs ?

This comment has been minimized.

Copy link
@mattdsteele

mattdsteele Jan 28, 2016

@gkalpak @lgalfaso - I can confirm this is occurring in my application - after updating to 1.5.0-rc.2, there are a few instances where my code is throwing TypeError: 'undefined' is not an object (evaluating 'inputExpressions.length'). rc.1 and 1.4.x are fine.

I haven't tracked it down fully but it seems to happen when we $compile() an element with ng-class on it. I can try to put together a test case to verify, but wanted to let you know sooner rather than later.

Edit - Looks like #13871 solved this - I pulled down the snapshot build and it's resolved the issue.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jan 28, 2016

Member

@mattdsteele, tgx for reporting this and thx for the follow-up. The fix will be included in the next release.

This comment has been minimized.

Copy link
@manast

manast Feb 3, 2016

@lgalfaso regarding "Given that we copy $$watchDelegate then there is a chance that $$watchDelegate is inputsWatchDelegate and then if inputs is undefined we will throw here

if (inputExpressions.length === 1) {
"
Its not only a potenial bug, we have a product based on angular failing all over the place because of this (worked well with angular 1.5RC).
+1 for a fix.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Feb 3, 2016

Member

@manast: This has already been fixed in #13871. The fix is in master and will be included in the upcoming v1.5.0 and v1.4.10 releases.

This comment has been minimized.

Copy link
@lgalfaso

lgalfaso Feb 3, 2016

Author Contributor

@manast sorry for breaking this, as @gkalpak mention, the fix is already in master and a new release should be out this week.

}

return expensiveCheckFn;

function expensiveCheckFn(scope, locals, assign, inputs) {
var expensiveCheckOldValue = runningChecksEnabled;
runningChecksEnabled = true;
try {
return fn(scope, locals, assign, inputs);
} finally {
runningChecksEnabled = expensiveCheckOldValue;
}
}
}

function expressionInputDirtyCheck(newValue, oldValueOfValue) {

Expand Down
3 changes: 2 additions & 1 deletion src/ng/rootScope.js
Expand Up @@ -998,7 +998,7 @@ function $RootScopeProvider() {
});
}

asyncQueue.push({scope: this, expression: expr, locals: locals});
asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
},

$$postDigest: function(fn) {
Expand Down Expand Up @@ -1090,6 +1090,7 @@ function $RootScopeProvider() {
$applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
expr = $parse(expr);
scheduleApplyAsync();

function $applyAsyncExpression() {
Expand Down
78 changes: 77 additions & 1 deletion test/ng/parseSpec.js
Expand Up @@ -1681,7 +1681,6 @@ describe('parser', function() {
$filterProvider = filterProvider;
}]));


forEach([true, false], function(cspEnabled) {
describe('csp: ' + cspEnabled, function() {

Expand Down Expand Up @@ -2400,6 +2399,64 @@ describe('parser', function() {
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
'Expression: foo.w = 1');
}));

they('should propagate expensive checks when calling $prop',
['foo.w && true',
'$eval("foo.w && true")',
'this["$eval"]("foo.w && true")',
'bar;$eval("foo.w && true")',
'$eval("foo.w && true");bar',
'$eval("foo.w && true", null, false)',
'$eval("foo");$eval("foo.w && true")',
'$eval("$eval(\\"foo.w && true\\")")',
'$eval("foo.e()")',
'$evalAsync("foo.w && true")',
'this["$evalAsync"]("foo.w && true")',
'bar;$evalAsync("foo.w && true")',
'$evalAsync("foo.w && true");bar',
'$evalAsync("foo.w && true", null, false)',
'$evalAsync("foo");$evalAsync("foo.w && true")',
'$evalAsync("$evalAsync(\\"foo.w && true\\")")',
'$evalAsync("foo.e()")',
'$evalAsync("$eval(\\"foo.w && true\\")")',
'$eval("$evalAsync(\\"foo.w && true\\")")',
'$watch("foo.w && true")',
'$watchCollection("foo.w && true", foo.f)',
'$watchGroup(["foo.w && true"])',
'$applyAsync("foo.w && true")'], function(expression) {
inject(function($parse, $window) {
scope.foo = {
w: $window,
bar: 'bar',
e: function() { scope.$eval("foo.w && true"); },
f: function() {}
};
expect($parse.$$runningExpensiveChecks()).toEqual(false);
expect(function() {
scope.$eval($parse(expression, null, true));
scope.$digest();
}).toThrowMinErr(
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
'Expression: foo.w && true');
expect($parse.$$runningExpensiveChecks()).toEqual(false);
});
});

they('should restore the state of $$runningExpensiveChecks when the expression $prop throws',
['$eval("foo.t()")',
'$evalAsync("foo.t()", {foo: foo})'], function(expression) {
inject(function($parse, $window) {
scope.foo = {
t: function() { throw new Error(); }
};
expect($parse.$$runningExpensiveChecks()).toEqual(false);
expect(function() {
scope.$eval($parse(expression, null, true));
scope.$digest();
}).toThrow();
expect($parse.$$runningExpensiveChecks()).toEqual(false);
});
});
});
});

Expand Down Expand Up @@ -2966,6 +3023,25 @@ describe('parser', function() {
expect(log).toEqual('');
}));

it('should work with expensive checks', inject(function($parse, $rootScope, log) {
var fn = $parse('::foo', null, true);
$rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });

$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);

$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log).toEqual('bar');
log.reset();

$rootScope.foo = 'man';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log).toEqual('');
}));

it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope, log) {
var fn = $parse('::foo');
$rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });
Expand Down
8 changes: 4 additions & 4 deletions test/ng/rootScopeSpec.js
Expand Up @@ -1387,7 +1387,7 @@ describe('Scope', function() {
expect(child.log).toBe('child context');
}));

it('should operate only with a single queue across all child and isolate scopes', inject(function($rootScope) {
it('should operate only with a single queue across all child and isolate scopes', inject(function($rootScope, $parse) {
var childScope = $rootScope.$new();
var isolateScope = $rootScope.$new(true);

Expand All @@ -1398,9 +1398,9 @@ describe('Scope', function() {
expect(childScope.$$asyncQueue).toBe($rootScope.$$asyncQueue);
expect(isolateScope.$$asyncQueue).toBeUndefined();
expect($rootScope.$$asyncQueue).toEqual([
{scope: $rootScope, expression: 'rootExpression'},
{scope: childScope, expression: 'childExpression'},
{scope: isolateScope, expression: 'isolateExpression'}
{scope: $rootScope, expression: $parse('rootExpression')},
{scope: childScope, expression: $parse('childExpression')},
{scope: isolateScope, expression: $parse('isolateExpression')}
]);
}));

Expand Down

0 comments on commit acfda10

Please sign in to comment.