Skip to content
Permalink
Browse files

fix($parse): Preserve expensive checks when runnning $eval inside an …

…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 authored and gkalpak committed Jan 26, 2016
1 parent 5cb7d0e commit 96d62cc0fc77248d7e3ec4aa458bac0d3e072629
Showing with 120 additions and 8 deletions.
  1. +37 −2 src/ng/parse.js
  2. +2 −1 src/ng/rootScope.js
  3. +77 −1 test/ng/parseSpec.js
  4. +4 −4 test/ng/rootScopeSpec.js
@@ -1735,10 +1735,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();
@@ -1764,6 +1773,9 @@ function $ParseProvider() {
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
if (expensiveChecks) {
parsedExpression = expensiveChecksInterceptor(parsedExpression);
}
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -1774,7 +1786,30 @@ function $ParseProvider() {
default:
return addInterceptor(noop, interceptorFn);
}
};
}

function expensiveChecksInterceptor(fn) {
if (!fn) return fn;
expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
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]);
}

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) {

@@ -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) {
@@ -1090,6 +1090,7 @@ function $RootScopeProvider() {
$applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
expr = $parse(expr);
scheduleApplyAsync();

function $applyAsyncExpression() {
@@ -1666,7 +1666,6 @@ describe('parser', function() {
$filterProvider = filterProvider;
}]));


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

@@ -2372,6 +2371,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);
});
});
});
});

@@ -2932,6 +2989,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); });
@@ -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);

@@ -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')}
]);
}));

0 comments on commit 96d62cc

Please sign in to comment.
You can’t perform that action at this time.