Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): replace assertion with more intentional error #52427

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion goldens/size-tracking/aio-payloads.json
Expand Up @@ -12,7 +12,7 @@
"aio-local": {
"uncompressed": {
"runtime": 4252,
"main": 501754,
"main": 509172,
"polyfills": 33862,
"styles": 60209,
"light-theme": 34317,
Expand Down
28 changes: 17 additions & 11 deletions packages/core/src/render3/instructions/change_detection.ts
Expand Up @@ -13,7 +13,7 @@ import {getComponentViewByInstance} from '../context_discovery';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
import {CONTAINER_HEADER_OFFSET, HAS_CHILD_VIEWS_TO_REFRESH, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentTemplate, RenderFlags} from '../interfaces/definition';
import {CONTEXT, ENVIRONMENT, FLAGS, InitPhaseState, LView, LViewFlags, PARENT, TVIEW, TView} from '../interfaces/view';
import {CONTEXT, ENVIRONMENT, FLAGS, InitPhaseState, LView, LViewFlags, PARENT, REACTIVE_TEMPLATE_CONSUMER, TVIEW, TView} from '../interfaces/view';
import {enterView, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
import {getFirstLContainer, getNextLContainer} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, markViewForRefresh, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
Expand Down Expand Up @@ -159,17 +159,23 @@ export function refreshView<T>(
// execute pre-order hooks (OnInit, OnChanges, DoCheck)
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
if (!isInCheckNoChangesPass) {
if (hooksInitPhaseCompleted) {
const preOrderCheckHooks = tView.preOrderCheckHooks;
if (preOrderCheckHooks !== null) {
executeCheckHooks(lView, preOrderCheckHooks, null);
}
} else {
const preOrderHooks = tView.preOrderHooks;
if (preOrderHooks !== null) {
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, null);
const consumer = lView[REACTIVE_TEMPLATE_CONSUMER];
try {
consumer && (consumer.isRunning = true);
if (hooksInitPhaseCompleted) {
const preOrderCheckHooks = tView.preOrderCheckHooks;
if (preOrderCheckHooks !== null) {
executeCheckHooks(lView, preOrderCheckHooks, null);
}
} else {
const preOrderHooks = tView.preOrderHooks;
if (preOrderHooks !== null) {
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, null);
}
incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);
}
incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);
} finally {
consumer && (consumer.isRunning = false);
}
}

Expand Down
12 changes: 5 additions & 7 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -43,7 +43,7 @@ import {assertPureTNodeType, assertTNodeType} from '../node_assert';
import {clearElementContents, updateTextNode} from '../node_manipulation';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
import {profiler, ProfilerEvent} from '../profiler';
import {commitLViewConsumerIfHasProducers, getReactiveLViewConsumer} from '../reactive_lview_consumer';
import {getReactiveLViewConsumer} from '../reactive_lview_consumer';
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {mergeHostAttrs} from '../util/attrs_utils';
Expand Down Expand Up @@ -82,18 +82,17 @@ export function processHostBindingOpCodes(tView: TView, lView: LView): void {
setBindingRootForHostBindings(bindingRootIndx, directiveIdx);
consumer.dirty = false;
const prevConsumer = consumerBeforeComputation(consumer);
consumer.isRunning = true;
try {
const context = lView[directiveIdx];
hostBindingFn(RenderFlags.Update, context);
} finally {
consumerAfterComputation(consumer, prevConsumer);
consumer.isRunning = false;
}
}
}
} finally {
if (lView[REACTIVE_HOST_BINDING_CONSUMER] === null) {
commitLViewConsumerIfHasProducers(lView, REACTIVE_HOST_BINDING_CONSUMER);
}
setSelectedIndex(-1);
}
}
Expand Down Expand Up @@ -275,15 +274,14 @@ export function executeTemplate<T>(
try {
if (effectiveConsumer !== null) {
effectiveConsumer.dirty = false;
effectiveConsumer.isRunning = true;
}
templateFn(rf, context);
} finally {
consumerAfterComputation(effectiveConsumer, prevConsumer);
effectiveConsumer && (effectiveConsumer.isRunning = false);
}
} finally {
if (isUpdatePhase && lView[REACTIVE_TEMPLATE_CONSUMER] === null) {
commitLViewConsumerIfHasProducers(lView, REACTIVE_TEMPLATE_CONSUMER);
}
setSelectedIndex(prevSelectedIndex);

const postHookType =
Expand Down
61 changes: 27 additions & 34 deletions packages/core/src/render3/reactive_lview_consumer.ts
Expand Up @@ -8,14 +8,17 @@

import {REACTIVE_NODE, ReactiveNode} from '@angular/core/primitives/signals';

import {assertDefined, assertEqual} from '../util/assert';
import {RuntimeError} from '../errors';
import {assertDefined} from '../util/assert';

import {markViewDirty} from './instructions/mark_view_dirty';
import {LView, REACTIVE_HOST_BINDING_CONSUMER, REACTIVE_TEMPLATE_CONSUMER} from './interfaces/view';

let currentConsumer: ReactiveLViewConsumer|null = null;
export interface ReactiveLViewConsumer extends ReactiveNode {
lView: LView|null;
lView: LView;
slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER;
isRunning: boolean;
}

/**
Expand All @@ -26,50 +29,40 @@ export interface ReactiveLViewConsumer extends ReactiveNode {
export function getReactiveLViewConsumer(
lView: LView, slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER):
ReactiveLViewConsumer {
return lView[slot] ?? getOrCreateCurrentLViewConsumer();
return lView[slot] ?? getOrCreateCurrentLViewConsumer(lView, slot);
}

/**
* Assigns the `currentTemplateContext` to its LView's `REACTIVE_CONSUMER` slot if there are tracked
* producers.
*
* The presence of producers means that a signal was read while the consumer was the active
* consumer.
*
* If no producers are present, we do not assign the current template context. This also means we
* can just reuse the template context for the next LView.
*/
export function commitLViewConsumerIfHasProducers(
lView: LView,
slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER): void {
const consumer = getOrCreateCurrentLViewConsumer();
if (!consumer.producerNode?.length) {
return;
}

lView[slot] = currentConsumer;
consumer.lView = lView;
currentConsumer = createLViewConsumer();
}

const REACTIVE_LVIEW_CONSUMER_NODE: ReactiveLViewConsumer = {
const REACTIVE_LVIEW_CONSUMER_NODE: Omit<ReactiveLViewConsumer, 'lView'|'slot'> = {
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
consumerMarkedDirty: (node: ReactiveLViewConsumer) => {
(typeof ngDevMode === 'undefined' || ngDevMode) &&
assertDefined(
node.lView,
'Updating a signal during template or host binding execution is not allowed.');
markViewDirty(node.lView!);
if (ngDevMode && node.isRunning) {
console.warn(
`Angular detected a signal being set which makes the template for this component dirty` +
` while it's being executed, which is not currently supported and will likely result` +
` in ExpressionChangedAfterItHasBeenChecked errors or future updates not working` +
` entirely.`);
}
markViewDirty(node.lView);
},
consumerOnSignalRead(this: ReactiveLViewConsumer): void {
if (currentConsumer !== this) {
return;
}
this.lView[this.slot] = currentConsumer;
currentConsumer = null;
},
lView: null,
isRunning: false,
};

function createLViewConsumer(): ReactiveLViewConsumer {
return Object.create(REACTIVE_LVIEW_CONSUMER_NODE);
}

function getOrCreateCurrentLViewConsumer() {
function getOrCreateCurrentLViewConsumer(
lView: LView, slot: typeof REACTIVE_TEMPLATE_CONSUMER|typeof REACTIVE_HOST_BINDING_CONSUMER) {
currentConsumer ??= createLViewConsumer();
currentConsumer.lView = lView;
currentConsumer.slot = slot;
return currentConsumer;
}
Expand Up @@ -7,7 +7,7 @@
*/

import {NgIf} from '@angular/common';
import {ChangeDetectionStrategy, Component, Input, signal, ViewChild} from '@angular/core';
import {ChangeDetectionStrategy, Component, Directive, Input, signal, ViewChild} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('CheckAlways components', () => {
Expand Down Expand Up @@ -370,4 +370,74 @@ describe('OnPush components with signals', () => {
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).not.toContain('blue');
});

it('should warn when writing to signals during change-detecting a given template, in advance()',
() => {
const counter = signal(0);

@Directive({
standalone: true,
selector: '[misunderstood]',
})
class MisunderstoodDir {
ngOnInit(): void {
counter.update((c) => c + 1);
}
}

@Component({
selector: 'test-component',
standalone: true,
imports: [MisunderstoodDir],
template: `
{{counter()}}<div misunderstood></div>{{ 'force advance()' }}
`,
})
class TestCmp {
counter = counter;
}

const consoleWarnSpy = spyOn(console, 'warn').and.callThrough();

const fixture = TestBed.createComponent(TestCmp);
fixture.detectChanges(false);
expect(consoleWarnSpy)
.toHaveBeenCalledWith(jasmine.stringMatching(
/will likely result in ExpressionChangedAfterItHasBeenChecked/));
});

it('should warn when writing to signals during change-detecting a given template, at the end',
() => {
const counter = signal(0);

@Directive({
standalone: true,
selector: '[misunderstood]',
})
class MisunderstoodDir {
ngOnInit(): void {
counter.update((c) => c + 1);
}
}

@Component({
selector: 'test-component',
standalone: true,
imports: [MisunderstoodDir],
template: `
{{counter()}}<div misunderstood></div>
`,
})
class TestCmp {
counter = counter;
}

const consoleWarnSpy = spyOn(console, 'warn').and.callThrough();

const fixture = TestBed.createComponent(TestCmp);
fixture.detectChanges(false);
expect(consoleWarnSpy)
.toHaveBeenCalledWith(jasmine.stringMatching(
/will likely result in ExpressionChangedAfterItHasBeenChecked/));
});
});
Expand Up @@ -713,9 +713,6 @@
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
{
"name": "computeStaticStyling"
},
Expand Down Expand Up @@ -779,9 +776,6 @@
{
"name": "createLView"
},
{
"name": "createLViewConsumer"
},
{
"name": "createNodeInjector"
},
Expand Down Expand Up @@ -968,9 +962,6 @@
{
"name": "getOrCreateComponentTView"
},
{
"name": "getOrCreateCurrentLViewConsumer"
},
{
"name": "getOrCreateInjectable"
},
Expand Down
Expand Up @@ -773,9 +773,6 @@
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
{
"name": "computeStaticStyling"
},
Expand Down Expand Up @@ -842,9 +839,6 @@
{
"name": "createLView"
},
{
"name": "createLViewConsumer"
},
{
"name": "createNodeInjector"
},
Expand Down Expand Up @@ -1034,9 +1028,6 @@
{
"name": "getOrCreateComponentTView"
},
{
"name": "getOrCreateCurrentLViewConsumer"
},
{
"name": "getOrCreateInjectable"
},
Expand Down
Expand Up @@ -584,9 +584,6 @@
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
{
"name": "computeStaticStyling"
},
Expand Down Expand Up @@ -635,9 +632,6 @@
{
"name": "createLView"
},
{
"name": "createLViewConsumer"
},
{
"name": "createNodeInjector"
},
Expand Down Expand Up @@ -806,9 +800,6 @@
{
"name": "getOrCreateComponentTView"
},
{
"name": "getOrCreateCurrentLViewConsumer"
},
{
"name": "getOrCreateInjectable"
},
Expand Down
9 changes: 0 additions & 9 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Expand Up @@ -662,9 +662,6 @@
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
{
"name": "computeStaticStyling"
},
Expand Down Expand Up @@ -716,9 +713,6 @@
{
"name": "createLView"
},
{
"name": "createLViewConsumer"
},
{
"name": "createNodeInjector"
},
Expand Down Expand Up @@ -908,9 +902,6 @@
{
"name": "getOrCreateComponentTView"
},
{
"name": "getOrCreateCurrentLViewConsumer"
},
{
"name": "getOrCreateInjectable"
},
Expand Down