Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3fa632b

Browse files
pkozlowski-opensourcematsko
authored andcommitted
fix(core): don't re-invoke pure pipes that throw and arguments are the same (#35827)
Pure pipes are not invoked again until their arguments are modified. The same rule should apply to pure pipes that throw an exception. This fix ensures that a pure pipe is not re-invoked if it throws an exception and arguments are not changed. PR Close #35827
1 parent 5b39a36 commit 3fa632b

File tree

2 files changed

+125
-5
lines changed

2 files changed

+125
-5
lines changed

‎packages/core/src/render3/pure_function.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {assertDataInRange} from '../util/assert';
910
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, getBinding, updateBinding} from './bindings';
1011
import {LView} from './interfaces/view';
1112
import {getBindingRoot, getLView} from './state';
@@ -277,6 +278,18 @@ export function ɵɵpureFunctionV(
277278
return pureFunctionVInternal(getLView(), getBindingRoot(), slotOffset, pureFn, exps, thisArg);
278279
}
279280

281+
/**
282+
* Results of a pure function invocation are stored in LView in a dedicated slot that is initialized
283+
* to NO_CHANGE. In rare situations a pure pipe might throw an exception on the very first
284+
* invocation and not produce any valid results. In this case LView would keep holding the NO_CHANGE
285+
* value. The NO_CHANGE is not something that we can use in expressions / bindings thus we convert
286+
* it to `undefined`.
287+
*/
288+
function getPureFunctionReturnValue(lView: LView, returnValueIndex: number) {
289+
ngDevMode && assertDataInRange(lView, returnValueIndex);
290+
const lastReturnValue = lView[returnValueIndex];
291+
return lastReturnValue === NO_CHANGE ? undefined : lastReturnValue;
292+
}
280293

281294
/**
282295
* If the value of the provided exp has changed, calls the pure function to return
@@ -296,7 +309,7 @@ export function pureFunction1Internal(
296309
const bindingIndex = bindingRoot + slotOffset;
297310
return bindingUpdated(lView, bindingIndex, exp) ?
298311
updateBinding(lView, bindingIndex + 1, thisArg ? pureFn.call(thisArg, exp) : pureFn(exp)) :
299-
getBinding(lView, bindingIndex + 1);
312+
getPureFunctionReturnValue(lView, bindingIndex + 1);
300313
}
301314

302315

@@ -321,7 +334,7 @@ export function pureFunction2Internal(
321334
updateBinding(
322335
lView, bindingIndex + 2,
323336
thisArg ? pureFn.call(thisArg, exp1, exp2) : pureFn(exp1, exp2)) :
324-
getBinding(lView, bindingIndex + 2);
337+
getPureFunctionReturnValue(lView, bindingIndex + 2);
325338
}
326339

327340
/**
@@ -347,7 +360,7 @@ export function pureFunction3Internal(
347360
updateBinding(
348361
lView, bindingIndex + 3,
349362
thisArg ? pureFn.call(thisArg, exp1, exp2, exp3) : pureFn(exp1, exp2, exp3)) :
350-
getBinding(lView, bindingIndex + 3);
363+
getPureFunctionReturnValue(lView, bindingIndex + 3);
351364
}
352365

353366

@@ -376,7 +389,7 @@ export function pureFunction4Internal(
376389
updateBinding(
377390
lView, bindingIndex + 4,
378391
thisArg ? pureFn.call(thisArg, exp1, exp2, exp3, exp4) : pureFn(exp1, exp2, exp3, exp4)) :
379-
getBinding(lView, bindingIndex + 4);
392+
getPureFunctionReturnValue(lView, bindingIndex + 4);
380393
}
381394

382395
/**
@@ -403,5 +416,5 @@ export function pureFunctionVInternal(
403416
bindingUpdated(lView, bindingIndex++, exps[i]) && (different = true);
404417
}
405418
return different ? updateBinding(lView, bindingIndex, pureFn.apply(thisArg, exps)) :
406-
getBinding(lView, bindingIndex);
419+
getPureFunctionReturnValue(lView, bindingIndex);
407420
}

‎packages/core/test/acceptance/pipe_spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,4 +582,111 @@ describe('pipe', () => {
582582

583583
});
584584

585+
describe('pure pipe error handling', () => {
586+
587+
it('should not re-invoke pure pipes if it fails initially', () => {
588+
589+
@Pipe({name: 'throwPipe', pure: true})
590+
class ThrowPipe implements PipeTransform {
591+
transform(): never { throw new Error('ThrowPipeError'); }
592+
}
593+
@Component({template: `{{val | throwPipe}}`})
594+
class App {
595+
val = 'anything';
596+
}
597+
598+
const fixture =
599+
TestBed.configureTestingModule({declarations: [App, ThrowPipe]}).createComponent(App);
600+
601+
// first invocation
602+
expect(() => fixture.detectChanges()).toThrowError(/ThrowPipeError/);
603+
604+
// second invocation - should not throw
605+
fixture.detectChanges();
606+
});
607+
608+
609+
it('should display the last known result from a pure pipe when it throws', () => {
610+
611+
@Pipe({name: 'throwPipe', pure: true})
612+
class ThrowPipe implements PipeTransform {
613+
transform(value: string): string {
614+
if (value === 'KO') {
615+
throw new Error('ThrowPipeError');
616+
} else {
617+
return value;
618+
}
619+
}
620+
}
621+
622+
@Component({template: `{{val | throwPipe}}`})
623+
class App {
624+
val = 'anything';
625+
}
626+
627+
const fixture =
628+
TestBed.configureTestingModule({declarations: [App, ThrowPipe]}).createComponent(App);
629+
630+
// first invocation - no error thrown
631+
fixture.detectChanges();
632+
expect(fixture.nativeElement.textContent).toBe('anything');
633+
634+
635+
// second invocation when the error is thrown
636+
fixture.componentInstance.val = 'KO';
637+
expect(() => fixture.detectChanges()).toThrowError(/ThrowPipeError/);
638+
expect(fixture.nativeElement.textContent).toBe('anything');
639+
640+
641+
// third invocation with no changes to input - should not thrown and preserve the last known
642+
// results
643+
fixture.detectChanges();
644+
expect(fixture.nativeElement.textContent).toBe('anything');
645+
});
646+
647+
describe('pure pipe error handling with multiple arguments', () => {
648+
const args: string[] = new Array(10).fill(':0');
649+
for (let numberOfPipeArgs = 0; numberOfPipeArgs < args.length; numberOfPipeArgs++) {
650+
it(`should not invoke ${numberOfPipeArgs} argument pure pipe second time if it throws unless input changes`,
651+
() => {
652+
// https://stackblitz.com/edit/angular-mbx2pg
653+
const log: string[] = [];
654+
@Pipe({name: 'throw', pure: true})
655+
class ThrowPipe implements PipeTransform {
656+
transform(): never {
657+
log.push('throw');
658+
throw new Error('ThrowPipeError');
659+
}
660+
}
661+
@Component({template: `{{val | throw${args.slice(0, numberOfPipeArgs).join('')}}}`})
662+
class App {
663+
val = 'anything';
664+
}
665+
666+
const fixture = TestBed.configureTestingModule({declarations: [App, ThrowPipe]})
667+
.createComponent(App);
668+
// First invocation of detect changes should throw.
669+
expect(() => fixture.detectChanges()).toThrowError(/ThrowPipeError/);
670+
expect(log).toEqual(['throw']);
671+
// Second invocation should not throw as input to the `throw` pipe has not changed and
672+
// the pipe is pure.
673+
log.length = 0;
674+
expect(() => fixture.detectChanges()).not.toThrow();
675+
expect(log).toEqual([]);
676+
fixture.componentInstance.val = 'change';
677+
// First invocation of detect changes should throw because the input changed.
678+
expect(() => fixture.detectChanges()).toThrowError(/ThrowPipeError/);
679+
expect(log).toEqual(['throw']);
680+
// Second invocation should not throw as input to the `throw` pipe has not changed and
681+
// the pipe is pure.
682+
log.length = 0;
683+
expect(() => fixture.detectChanges()).not.toThrow();
684+
expect(log).toEqual([]);
685+
});
686+
}
687+
});
688+
689+
});
690+
691+
585692
});

0 commit comments

Comments
 (0)