Skip to content

Commit ec1d85c

Browse files
fix: add snippet argument validation in dev (#15521)
* init * fix * make `Payload` a class * doh * lint * tweak changeset * fix * only export things that should be available on $ * tweak message * fix --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 0020e59 commit ec1d85c

File tree

13 files changed

+147
-70
lines changed

13 files changed

+147
-70
lines changed

.changeset/bright-jeans-compare.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: add snippet argument validation in dev

documentation/docs/98-reference/.generated/shared-errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ This error would be thrown in a setup like this:
3030

3131
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.
3232

33+
### invalid_snippet_arguments
34+
35+
```
36+
A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
37+
```
38+
3339
### lifecycle_outside_component
3440

3541
```

packages/svelte/messages/shared-errors/errors.md

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ This error would be thrown in a setup like this:
2626

2727
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.
2828

29+
## invalid_snippet_arguments
30+
31+
> A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
32+
2933
## lifecycle_outside_component
3034

3135
> `%name%(...)` can only be used during component initialisation

packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
1+
/** @import { AssignmentPattern, BlockStatement, Expression, Identifier, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import { dev } from '../../../../state.js';
@@ -12,7 +12,7 @@ import { get_value } from './shared/declarations.js';
1212
*/
1313
export function SnippetBlock(node, context) {
1414
// TODO hoist where possible
15-
/** @type {Pattern[]} */
15+
/** @type {(Identifier | AssignmentPattern)[]} */
1616
const args = [b.id('$$anchor')];
1717

1818
/** @type {BlockStatement} */
@@ -66,7 +66,18 @@ export function SnippetBlock(node, context) {
6666
}
6767
}
6868
}
69-
69+
if (dev) {
70+
declarations.unshift(
71+
b.stmt(
72+
b.call(
73+
'$.validate_snippet_args',
74+
.../** @type {Identifier[]} */ (
75+
args.map((arg) => (arg?.type === 'Identifier' ? arg : arg?.left))
76+
)
77+
)
78+
)
79+
);
80+
}
7081
body = b.block([
7182
...declarations,
7283
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body

packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { BlockStatement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
4+
import { dev } from '../../../../state.js';
45
import * as b from '../../../../utils/builders.js';
56

67
/**
@@ -13,7 +14,9 @@ export function SnippetBlock(node, context) {
1314
[b.id('$$payload'), ...node.parameters],
1415
/** @type {BlockStatement} */ (context.visit(node.body))
1516
);
16-
17+
if (dev) {
18+
fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload'))));
19+
}
1720
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
1821
fn.___snippet = true;
1922

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { invalid_snippet_arguments } from '../../shared/errors.js';
2+
/**
3+
* @param {Node} anchor
4+
* @param {...(()=>any)[]} args
5+
*/
6+
export function validate_snippet_args(anchor, ...args) {
7+
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
8+
invalid_snippet_arguments();
9+
}
10+
for (let arg of args) {
11+
if (typeof arg !== 'function') {
12+
invalid_snippet_arguments();
13+
}
14+
}
15+
}

packages/svelte/src/internal/client/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { create_ownership_validator } from './dev/ownership.js';
88
export { check_target, legacy_api } from './dev/legacy.js';
99
export { trace } from './dev/tracing.js';
1010
export { inspect } from './dev/inspect.js';
11+
export { validate_snippet_args } from './dev/validation.js';
1112
export { await_block as await } from './dom/blocks/await.js';
1213
export { if_block as if } from './dom/blocks/if.js';
1314
export { key_block as key } from './dom/blocks/key.js';

packages/svelte/src/internal/server/blocks/snippet.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @import { Snippet } from 'svelte' */
2-
/** @import { Payload } from '#server' */
2+
/** @import { Payload } from '../payload' */
33
/** @import { Getters } from '#shared' */
44

55
/**

packages/svelte/src/internal/server/dev.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
/** @import { Component, Payload } from '#server' */
1+
/** @import { Component } from '#server' */
22
import { FILENAME } from '../../constants.js';
33
import {
44
is_tag_valid_with_ancestor,
55
is_tag_valid_with_parent
66
} from '../../html-tree-validation.js';
77
import { current_component } from './context.js';
8+
import { invalid_snippet_arguments } from '../shared/errors.js';
9+
import { Payload } from './payload.js';
810

911
/**
1012
* @typedef {{
@@ -98,3 +100,12 @@ export function push_element(payload, tag, line, column) {
98100
export function pop_element() {
99101
parent = /** @type {Element} */ (parent).parent;
100102
}
103+
104+
/**
105+
* @param {Payload} payload
106+
*/
107+
export function validate_snippet_args(payload) {
108+
if (typeof payload !== 'object' || !(payload instanceof Payload)) {
109+
invalid_snippet_arguments();
110+
}
111+
}

packages/svelte/src/internal/server/index.js

+6-51
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @import { ComponentType, SvelteComponent } from 'svelte' */
2-
/** @import { Component, Payload, RenderOutput } from '#server' */
2+
/** @import { Component, RenderOutput } from '#server' */
33
/** @import { Store } from '#shared' */
44
export { FILENAME, HMR } from '../../constants.js';
55
import { attr, clsx, to_class, to_style } from '../shared/attributes.js';
@@ -17,43 +17,13 @@ import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydra
1717
import { validate_store } from '../shared/validate.js';
1818
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
1919
import { reset_elements } from './dev.js';
20+
import { Payload } from './payload.js';
2021

2122
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
2223
// https://infra.spec.whatwg.org/#noncharacter
2324
const INVALID_ATTR_NAME_CHAR_REGEX =
2425
/[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
2526

26-
/**
27-
* @param {Payload} to_copy
28-
* @returns {Payload}
29-
*/
30-
export function copy_payload({ out, css, head, uid }) {
31-
return {
32-
out,
33-
css: new Set(css),
34-
head: {
35-
title: head.title,
36-
out: head.out,
37-
css: new Set(head.css),
38-
uid: head.uid
39-
},
40-
uid
41-
};
42-
}
43-
44-
/**
45-
* Assigns second payload to first
46-
* @param {Payload} p1
47-
* @param {Payload} p2
48-
* @returns {void}
49-
*/
50-
export function assign_payload(p1, p2) {
51-
p1.out = p2.out;
52-
p1.css = p2.css;
53-
p1.head = p2.head;
54-
p1.uid = p2.uid;
55-
}
56-
5727
/**
5828
* @param {Payload} payload
5929
* @param {string} tag
@@ -87,16 +57,6 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
8757
*/
8858
export let on_destroy = [];
8959

90-
/**
91-
* Creates an ID generator
92-
* @param {string} prefix
93-
* @returns {() => string}
94-
*/
95-
function props_id_generator(prefix) {
96-
let uid = 1;
97-
return () => `${prefix}s${uid++}`;
98-
}
99-
10060
/**
10161
* Only available on the server and when compiling with the `server` option.
10262
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
@@ -106,14 +66,7 @@ function props_id_generator(prefix) {
10666
* @returns {RenderOutput}
10767
*/
10868
export function render(component, options = {}) {
109-
const uid = props_id_generator(options.idPrefix ? options.idPrefix + '-' : '');
110-
/** @type {Payload} */
111-
const payload = {
112-
out: '',
113-
css: new Set(),
114-
head: { title: '', out: '', css: new Set(), uid },
115-
uid
116-
};
69+
const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : '');
11770

11871
const prev_on_destroy = on_destroy;
11972
on_destroy = [];
@@ -545,7 +498,9 @@ export { html } from './blocks/html.js';
545498

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

548-
export { push_element, pop_element } from './dev.js';
501+
export { push_element, pop_element, validate_snippet_args } from './dev.js';
502+
503+
export { assign_payload, copy_payload } from './payload.js';
549504

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export class Payload {
2+
/** @type {Set<{ hash: string; code: string }>} */
3+
css = new Set();
4+
out = '';
5+
uid = () => '';
6+
7+
head = {
8+
/** @type {Set<{ hash: string; code: string }>} */
9+
css: new Set(),
10+
title: '',
11+
out: '',
12+
uid: () => ''
13+
};
14+
15+
constructor(id_prefix = '') {
16+
this.uid = props_id_generator(id_prefix);
17+
this.head.uid = this.uid;
18+
}
19+
}
20+
21+
/**
22+
* Used in legacy mode to handle bindings
23+
* @param {Payload} to_copy
24+
* @returns {Payload}
25+
*/
26+
export function copy_payload({ out, css, head, uid }) {
27+
const payload = new Payload();
28+
29+
payload.out = out;
30+
payload.css = new Set(css);
31+
payload.uid = uid;
32+
33+
payload.head = {
34+
title: head.title,
35+
out: head.out,
36+
css: new Set(head.css),
37+
uid: head.uid
38+
};
39+
40+
return payload;
41+
}
42+
43+
/**
44+
* Assigns second payload to first
45+
* @param {Payload} p1
46+
* @param {Payload} p2
47+
* @returns {void}
48+
*/
49+
export function assign_payload(p1, p2) {
50+
p1.out = p2.out;
51+
p1.css = p2.css;
52+
p1.head = p2.head;
53+
p1.uid = p2.uid;
54+
}
55+
56+
/**
57+
* Creates an ID generator
58+
* @param {string} prefix
59+
* @returns {() => string}
60+
*/
61+
function props_id_generator(prefix) {
62+
let uid = 1;
63+
return () => `${prefix}s${uid++}`;
64+
}

packages/svelte/src/internal/server/types.d.ts

-13
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,6 @@ export interface Component {
1111
function?: any;
1212
}
1313

14-
export interface Payload {
15-
out: string;
16-
css: Set<{ hash: string; code: string }>;
17-
head: {
18-
title: string;
19-
out: string;
20-
uid: () => string;
21-
css: Set<{ hash: string; code: string }>;
22-
};
23-
/** Function that generates a unique ID */
24-
uid: () => string;
25-
}
26-
2714
export interface RenderOutput {
2815
/** HTML that goes into the `<head>` */
2916
head: string;

packages/svelte/src/internal/shared/errors.js

+15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ export function invalid_default_snippet() {
1717
}
1818
}
1919

20+
/**
21+
* A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}`
22+
* @returns {never}
23+
*/
24+
export function invalid_snippet_arguments() {
25+
if (DEV) {
26+
const error = new Error(`invalid_snippet_arguments\nA snippet function was passed invalid arguments. Snippets should only be instantiated via \`{@render ...}\`\nhttps://svelte.dev/e/invalid_snippet_arguments`);
27+
28+
error.name = 'Svelte error';
29+
throw error;
30+
} else {
31+
throw new Error(`https://svelte.dev/e/invalid_snippet_arguments`);
32+
}
33+
}
34+
2035
/**
2136
* `%name%(...)` can only be used during component initialisation
2237
* @param {string} name

0 commit comments

Comments
 (0)