Skip to content

Commit

Permalink
feat: add $inspect.trace rune (#14290)
Browse files Browse the repository at this point in the history
* feat: add $trace rune

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

WIP

* lint

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* more tweaks

* lint

* improve label for derived cached

* improve label for derived cached

* lint

* better stacks

* complete redesign

* fixes

* dead code

* dead code

* improve change detection

* rename rune

* lint

* lint

* fix bug

* tweaks

* Update packages/svelte/src/internal/client/dev/tracing.js

Co-authored-by: Rich Harris <[email protected]>

* Update packages/svelte/src/internal/client/dev/tracing.js

Co-authored-by: Rich Harris <[email protected]>

* Update packages/svelte/src/internal/client/dev/tracing.js

Co-authored-by: Rich Harris <[email protected]>

* Update packages/svelte/src/internal/client/dev/tracing.js

Co-authored-by: Rich Harris <[email protected]>

* todos

* add test + some docs

* changeset

* update messages

* address feedback

* address feedback

* limit to first statement of function

* remove unreachable trace_rune_duplicate error

* tweak message

* remove the expression statement, not the expression

* revert

* make label optional

* relax restriction on label - no longer necessary with new design

* update errors

* newline

* tweak

* add some docs

* fix playground

* fix playground

* tweak message when function runs outside an effect

* unused

* tweak

* handle async functions

* fail on generators

* regenerate, update docs

* better labelling

---------

Co-authored-by: Rich Harris <[email protected]>
  • Loading branch information
trueadm and Rich-Harris authored Dec 15, 2024
1 parent 64a32ce commit 5483495
Show file tree
Hide file tree
Showing 32 changed files with 596 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-ants-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: adds $inspect.trace rune
17 changes: 17 additions & 0 deletions documentation/docs/02-runes/07-$inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,20 @@ A convenient way to find the origin of some change is to pass `console.trace` to
// @errors: 2304
$inspect(stuff).with(console.trace);
```

## $inspect.trace(...)

This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.

```svelte
<script>
import { doSomeWork } from './elsewhere';
$effect(() => {
+++$inspect.trace();+++
doSomeWork();
});
</script>
```

`$inspect.trace` takes an optional first argument which will be used as the label.
12 changes: 12 additions & 0 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,18 @@ Expected whitespace
Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
```

### inspect_trace_generator

```
`$inspect.trace(...)` cannot be used inside a generator function
```

### inspect_trace_invalid_placement

```
`$inspect.trace(...)` must be the first statement of a function body
```

### invalid_arguments_usage

```
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@

> Imports of `svelte/internal/*` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from `svelte/internal/*` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case
## inspect_trace_generator

> `$inspect.trace(...)` cannot be used inside a generator function
## inspect_trace_invalid_placement

> `$inspect.trace(...)` must be the first statement of a function body
## invalid_arguments_usage

> The arguments keyword cannot be used within the template or at the top level of a component
Expand Down
19 changes: 19 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,25 @@ declare function $inspect<T extends any[]>(
...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };

declare namespace $inspect {
/**
* Tracks which reactive state changes caused an effect to re-run. Must be the first
* statement of a function body. Example:
*
* ```svelte
* <script>
* let count = $state(0);
*
* $effect(() => {
* $inspect.trace('my effect');
*
* count;
* });
* </script>
*/
export function trace(name: string): void;
}

/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
Expand Down
18 changes: 18 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,24 @@ export function import_svelte_internal_forbidden(node) {
e(node, "import_svelte_internal_forbidden", `Imports of \`svelte/internal/*\` are forbidden. It contains private runtime code which is subject to change without notice. If you're importing from \`svelte/internal/*\` to work around a limitation of Svelte, please open an issue at https://github.com/sveltejs/svelte and explain your use case\nhttps://svelte.dev/e/import_svelte_internal_forbidden`);
}

/**
* `$inspect.trace(...)` cannot be used inside a generator function
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_generator(node) {
e(node, "inspect_trace_generator", `\`$inspect.trace(...)\` cannot be used inside a generator function\nhttps://svelte.dev/e/inspect_trace_generator`);
}

/**
* `$inspect.trace(...)` must be the first statement of a function body
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function inspect_trace_invalid_placement(node) {
e(node, "inspect_trace_invalid_placement", `\`$inspect.trace(...)\` must be the first statement of a function body\nhttps://svelte.dev/e/inspect_trace_invalid_placement`);
}

/**
* The arguments keyword cannot be used within the template or at the top level of a component
* @param {null | number | NodeLike} node
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/** @import { CallExpression, VariableDeclarator } from 'estree' */
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_pure, is_safe_identifier } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { dev, locate_node, source } from '../../../state.js';
import * as b from '../../../utils/builders.js';

/**
* @param {CallExpression} node
Expand Down Expand Up @@ -136,6 +137,45 @@ export function CallExpression(node, context) {

break;

case '$inspect.trace': {
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}

const grand_parent = context.path.at(-2);
const fn = context.path.at(-3);

if (
parent.type !== 'ExpressionStatement' ||
grand_parent?.type !== 'BlockStatement' ||
!(
fn?.type === 'FunctionDeclaration' ||
fn?.type === 'FunctionExpression' ||
fn?.type === 'ArrowFunctionExpression'
) ||
grand_parent.body[0] !== parent
) {
e.inspect_trace_invalid_placement(node);
}

if (fn.generator) {
e.inspect_trace_generator(node);
}

if (dev) {
if (node.arguments[0]) {
context.state.scope.tracing = b.thunk(/** @type {Expression} */ (node.arguments[0]));
} else {
const label = get_function_label(context.path.slice(0, -2)) ?? 'trace';
const loc = `(${locate_node(fn)})`;

context.state.scope.tracing = b.thunk(b.literal(label + ' ' + loc));
}
}

break;
}

case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
Expand Down Expand Up @@ -182,3 +222,31 @@ export function CallExpression(node, context) {
}
}
}

/**
* @param {AST.SvelteNode[]} nodes
*/
function get_function_label(nodes) {
const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ (
nodes.at(-1)
);

if ((fn.type === 'FunctionDeclaration' || fn.type === 'FunctionExpression') && fn.id != null) {
return fn.id.name;
}

const parent = nodes.at(-2);
if (!parent) return;

if (parent.type === 'CallExpression') {
return source.slice(parent.callee.start, parent.callee.end) + '(...)';
}

if (parent.type === 'Property' && !parent.computed) {
return /** @type {Identifier} */ (parent.key).name;
}

if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return parent.id.name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
get_attribute_expression,
is_event_attribute
} from '../../../../utils/ast.js';
import { dev, filename, is_ignored, locator } from '../../../../state.js';
import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';

Expand Down Expand Up @@ -183,9 +183,6 @@ function build_assignment(operator, left, right, context) {
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];

const loc = /** @type {Location} */ (locator(/** @type {number} */ (left.start)));
const location = `${filename}:${loc.line}:${loc.column}`;

return /** @type {Expression} */ (
context.visit(
b.call(
Expand All @@ -197,7 +194,7 @@ function build_assignment(operator, left, right, context) {
: b.literal(/** @type {Identifier} */ (left.property).name)
),
right,
b.literal(location)
b.literal(locate_node(left))
)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
/** @import { BlockStatement } from 'estree' */
/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
import * as b from '../../../../utils/builders.js';

/**
* @param {BlockStatement} node
* @param {ComponentContext} context
*/
export function BlockStatement(node, context) {
add_state_transformers(context);
const tracing = context.state.scope.tracing;

if (tracing !== null) {
const parent =
/** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ (
context.path.at(-1)
);

const is_async = parent.async;

const call = b.call(
'$.trace',
/** @type {Expression} */ (tracing),
b.thunk(b.block(node.body.map((n) => /** @type {Statement} */ (context.visit(n)))), is_async)
);

return b.block([b.return(is_async ? b.await(call) : call)]);
}

context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export function ExpressionStatement(node, context) {

return b.stmt(expr);
}

if (rune === '$inspect.trace') {
return b.empty;
}
}

context.next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function VariableDeclaration(node, context) {
rune === '$effect.tracking' ||
rune === '$effect.root' ||
rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' ||
rune === '$host'
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { get_rune } from '../../../scope.js';
export function ExpressionStatement(node, context) {
const rune = get_rune(node.expression, context.state.scope);

if (rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root') {
if (
rune === '$effect' ||
rune === '$effect.pre' ||
rune === '$effect.root' ||
rune === '$inspect.trace'
) {
return b.empty;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export class Scope {
*/
function_depth = 0;

/**
* If tracing of reactive dependencies is enabled for this scope
* @type {null | Expression}
*/
tracing = null;

/**
*
* @param {ScopeRoot} root
Expand Down
10 changes: 10 additions & 0 deletions packages/svelte/src/compiler/state.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @import { Location } from 'locate-character' */
/** @import { CompileOptions } from './types' */
/** @import { AST, Warning } from '#compiler' */
import { getLocator } from 'locate-character';
import { sanitize_location } from '../utils.js';

/** @typedef {{ start?: number, end?: number }} NodeLike */

Expand Down Expand Up @@ -28,6 +30,14 @@ export let dev;

export let locator = getLocator('', { offsetLine: 1 });

/**
* @param {AST.SvelteNode & { start?: number | undefined }} node
*/
export function locate_node(node) {
const loc = /** @type {Location} */ (locator(/** @type {number} */ (node.start)));
return `${sanitize_location(filename)}:${loc?.line}:${loc.column}`;
}

/** @type {NonNullable<CompileOptions['warningFilter']>} */
export let warning_filter;

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/dev/assign.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sanitize_location } from '../../../utils.js';
import { untrack } from '../runtime.js';
import * as w from '../warnings.js';
import { sanitize_location } from './location.js';

/**
*
Expand Down
25 changes: 0 additions & 25 deletions packages/svelte/src/internal/client/dev/location.js

This file was deleted.

Loading

0 comments on commit 5483495

Please sign in to comment.