Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-jeans-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

fix: add snippet argument validation in dev
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/shared-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ This error would be thrown in a setup like this:

Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.

### invalid_snippet_arguments

```
A snippet function was passed invalid arguments. A snippet function should only be called via `{@render ...}`
```

### lifecycle_outside_component

```
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/shared-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ This error would be thrown in a setup like this:

Here, `List.svelte` is using `{@render children(item)` which means it expects `Parent.svelte` to use snippets. Instead, `Parent.svelte` uses the deprecated `let:` directive. This combination of APIs is incompatible, hence the error.

## invalid_snippet_arguments

> A snippet function was passed invalid arguments. A snippet function should only be called via `{@render ...}`

## lifecycle_outside_component

> `%name%(...)` can only be used during component initialisation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
Expand All @@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js';
*/
export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {Pattern[]} */
/** @type {(Identifier | AssignmentPattern)[]} */
const args = [b.id('$$anchor')];

/** @type {BlockStatement} */
Expand Down Expand Up @@ -66,7 +66,18 @@ export function SnippetBlock(node, context) {
}
}
}

if (dev) {
declarations.unshift(
b.stmt(
b.call(
'$.validate_snippet_args',
.../** @type {Identifier[]} */ (
args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left))
)
)
)
);
}
body = b.block([
...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';

/**
Expand All @@ -13,7 +14,9 @@ export function SnippetBlock(node, context) {
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);

if (dev) {
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
}
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;

Expand Down
15 changes: 15 additions & 0 deletions packages/svelte/src/internal/client/dev/validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { invalid_snippet_arguments } from '../../shared/errors.js';
/**
* @param {Node} anchor
* @param {...(()=>any)[]} args
*/
export function validate_snippet_args(anchor, ...args) {
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
invalid_snippet_arguments();
}
for (let arg of args) {
if (typeof arg !== 'function') {
invalid_snippet_arguments();
}
}
}
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
Expand Down
10 changes: 10 additions & 0 deletions packages/svelte/src/internal/server/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
is_tag_valid_with_parent
} from '../../html-tree-validation.js';
import { current_component } from './context.js';
import { invalid_snippet_arguments } from '../shared/errors.js';

/**
* @typedef {{
Expand Down Expand Up @@ -98,3 +99,12 @@ export function push_element(payload, tag, line, column) {
export function pop_element() {
parent = /** @type {Element} */ (parent).parent;
}

/**
* @param {Payload} payload
*/
export function validate_snippet_args(payload) {
if (typeof payload !== 'object' || !('out' in payload)) {
invalid_snippet_arguments();
}
}
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ export { html } from './blocks/html.js';

export { push, pop } from './context.js';

export { push_element, pop_element } from './dev.js';
export { push_element, pop_element, validate_snippet_args } from './dev.js';

export { snapshot } from '../shared/clone.js';

Expand Down
15 changes: 15 additions & 0 deletions packages/svelte/src/internal/shared/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ export function invalid_default_snippet() {
}
}

/**
* A snippet function was passed invalid arguments. A snippet function should only be called via `{@render ...}`
* @returns {never}
*/
export function invalid_snippet_arguments() {
if (DEV) {
const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. A snippet function should only be called via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);

error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
}
}

/**
* `%name%(...)` can only be used during component initialisation
* @param {string} name
Expand Down