track assignment locations

gh-13256
Rich Harris 2 months ago
parent 1a5cf6fef4
commit cc2469cb0d

@ -42,7 +42,9 @@
## effect_update_depth_exceeded ## effect_update_depth_exceeded
> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops > Maximum update depth exceeded. This usually indicates state is being updated inside an effect, which you should avoid. Svelte limits the number of nested updates to prevent infinite loops
> Maximum update depth exceeded after assignment at %location%. This usually indicates state is being updated inside an effect that also depends on that state. Svelte limits the number of nested updates to prevent infinite loops
## hydration_failed ## hydration_failed

@ -1,8 +1,9 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js'; import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.js'; import { dev, filename, is_ignored, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js'; import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js'; import { visit_assignment_expression } from '../../shared/assignments.js';
@ -11,10 +12,23 @@ import { visit_assignment_expression } from '../../shared/assignments.js';
* @param {Context} context * @param {Context} context
*/ */
export function AssignmentExpression(node, context) { export function AssignmentExpression(node, context) {
const expression = /** @type {Expression} */ ( let expression = /** @type {Expression} */ (
visit_assignment_expression(node, context, build_assignment) ?? context.next() visit_assignment_expression(node, context, build_assignment) ?? context.next()
); );
const loc =
dev &&
node.left.type === 'MemberExpression' &&
node.left.start !== undefined &&
locator(node.left.start);
if (loc) {
expression = b.sequence([
b.call('$.track_assignment', b.literal(`${filename}:${loc.line}:${loc.column}`)),
expression
]);
}
return is_ignored(node, 'ownership_invalid_mutation') return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(expression)) ? b.call('$.skip_ownership_validation', b.thunk(expression))
: expression; : expression;

@ -1,7 +1,9 @@
/** @import { Identifier } from 'estree' */ /** @import { Expression, Identifier } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { ComponentContext, Context } from '../../types' */ /** @import { ComponentContext, Context } from '../../types' */
import { is_state_source } from '../../utils.js'; import { is_state_source } from '../../utils.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { dev, filename, locator } from '../../../../../state.js';
/** /**
* Turns `foo` into `$.get(foo)` * Turns `foo` into `$.get(foo)`
@ -25,8 +27,18 @@ export function add_state_transformers(context) {
context.state.transform[name] = { context.state.transform[name] = {
read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value, read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value,
assign: (node, value) => { assign: (node, value) => {
/** @type {Expression} */
let call = b.call('$.set', node, value); let call = b.call('$.set', node, value);
const loc = dev && node.start !== undefined && locator(node.start);
if (loc) {
call = b.sequence([
b.call('$.track_assignment', b.literal(`${filename}:${loc.line}:${loc.column}`)),
call
]);
}
if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') { if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') {
call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores')); call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores'));
} }
@ -41,11 +53,23 @@ export function add_state_transformers(context) {
return b.call('$.mutate', node, mutation); return b.call('$.mutate', node, mutation);
}, },
update: (node) => { update: (node) => {
return b.call( /** @type {Expression} */
let call = b.call(
node.prefix ? '$.update_pre' : '$.update', node.prefix ? '$.update_pre' : '$.update',
node.argument, node.argument,
node.operator === '--' && b.literal(-1) node.operator === '--' && b.literal(-1)
); );
const loc = dev && node.start !== undefined && locator(node.start);
if (loc) {
call = b.sequence([
b.call('$.track_assignment', b.literal(`${filename}:${loc.line}:${loc.column}`)),
call
]);
}
return call;
} }
}; };
} }

@ -0,0 +1,9 @@
/** @type {string[]} */
export const assignment_stack = [];
/**
* @param {string} location
*/
export function track_assignment(location) {
assignment_stack.push(location);
}

@ -0,0 +1,7 @@
/**
* Append zero-width space to '/' characters to prevent devtools trying to make locations clickable
* @param {string} location
*/
export function sanitize_location(location) {
return location?.replace(/\//g, '/\u200b');
}

@ -9,6 +9,7 @@ import { hash } from '../../../../utils.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js'; import { dev_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js'; import { get_first_child, get_next_sibling } from '../operations.js';
import { sanitize_location } from '../../dev/utils.js';
/** /**
* @param {Element} element * @param {Element} element
@ -28,9 +29,7 @@ function check_hash(element, server_hash, value) {
location = `in ${dev_current_component_function[FILENAME]}`; location = `in ${dev_current_component_function[FILENAME]}`;
} }
w.hydration_html_changed( w.hydration_html_changed(location && sanitize_location(location));
location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
);
} }
/** /**

@ -179,12 +179,13 @@ export function effect_orphan(rune) {
} }
/** /**
* Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops * Maximum update depth exceeded after assignment at %location%. This usually indicates state is being updated inside an effect that also depends on that state. Svelte limits the number of nested updates to prevent infinite loops
* @param {string | undefined | null} [location]
* @returns {never} * @returns {never}
*/ */
export function effect_update_depth_exceeded() { export function effect_update_depth_exceeded(location) {
if (DEV) { if (DEV) {
const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops`); const error = new Error(`effect_update_depth_exceeded\n${location ? `Maximum update depth exceeded after assignment at ${location}. This usually indicates state is being updated inside an effect that also depends on that state. Svelte limits the number of nested updates to prevent infinite loops` : "Maximum update depth exceeded. This usually indicates state is being updated inside an effect, which you should avoid. Svelte limits the number of nested updates to prevent infinite loops"}`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;

@ -1,4 +1,5 @@
export { FILENAME, HMR } from '../../constants.js'; export { FILENAME, HMR } from '../../constants.js';
export { track_assignment } from './dev/assignment-stack.js';
export { cleanup_styles } from './dev/css.js'; export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js'; export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js'; export { hmr } from './dev/hmr.js';

@ -31,6 +31,8 @@ import { update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { lifecycle_outside_component } from '../shared/errors.js'; import { lifecycle_outside_component } from '../shared/errors.js';
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { assignment_stack } from './dev/assignment-stack.js';
import { sanitize_location } from './dev/utils.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
@ -62,8 +64,7 @@ export function set_is_destroying_effect(value) {
let queued_root_effects = []; let queued_root_effects = [];
let flush_count = 0; let flush_count = 0;
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions // Handle signal reactivity tree dependencies and reactions
/** @type {null | Reaction} */ /** @type {null | Reaction} */
@ -451,10 +452,6 @@ export function update_effect(effect) {
var teardown = update_reaction(effect); var teardown = update_reaction(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null; effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.version = current_version; effect.version = current_version;
if (DEV) {
dev_effect_stack.push(effect);
}
} catch (error) { } catch (error) {
handle_error(/** @type {Error} */ (error), effect, previous_component_context); handle_error(/** @type {Error} */ (error), effect, previous_component_context);
} finally { } finally {
@ -471,20 +468,12 @@ function infinite_loop_guard() {
if (flush_count > 1000) { if (flush_count > 1000) {
flush_count = 0; flush_count = 0;
if (DEV) { if (DEV) {
let location = assignment_stack.pop();
try { try {
e.effect_update_depth_exceeded(); e.effect_update_depth_exceeded(location && sanitize_location(location));
} catch (error) { } finally {
// stack is garbage, ignore. Instead add a console.error message. assignment_stack.length = 0;
define_property(error, 'stack', {
value: ''
});
// eslint-disable-next-line no-console
console.error(
'Last ten effects were: ',
dev_effect_stack.slice(-10).map((d) => d.fn)
);
dev_effect_stack = [];
throw error;
} }
} else { } else {
e.effect_update_depth_exceeded(); e.effect_update_depth_exceeded();
@ -560,16 +549,13 @@ function flush_queued_effects(effects) {
function process_deferred() { function process_deferred() {
is_micro_task_queued = false; is_micro_task_queued = false;
if (flush_count > 1001) {
return;
}
const previous_queued_root_effects = queued_root_effects; const previous_queued_root_effects = queued_root_effects;
queued_root_effects = []; queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects); flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) { if (!is_micro_task_queued) {
flush_count = 0; flush_count = 0;
if (DEV) { if (DEV) {
dev_effect_stack = []; assignment_stack.length = 0;
} }
} }
} }
@ -701,7 +687,7 @@ export function flush_sync(fn) {
flush_count = 0; flush_count = 0;
if (DEV) { if (DEV) {
dev_effect_stack = []; assignment_stack.length = 0;
} }
return result; return result;

Loading…
Cancel
Save