fix: disallow mixing event-handling syntaxes (#11295)

Closes #11262

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/11369/head
Caique Torres 3 months ago committed by GitHub
parent bda32edb1a
commit 68071f7c06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: disallow mixing on:click and onclick syntax

@ -172,6 +172,10 @@
> `let:` directive at invalid position > `let:` directive at invalid position
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax.
## node_invalid_placement ## node_invalid_placement
> %thing% is invalid inside <%parent%> > %thing% is invalid inside <%parent%>

@ -918,6 +918,16 @@ export function let_directive_invalid_placement(node) {
e(node, "let_directive_invalid_placement", "`let:` directive at invalid position"); e(node, "let_directive_invalid_placement", "`let:` directive at invalid position");
} }
/**
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax.
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function mixed_event_handler_syntaxes(node, name) {
e(node, "mixed_event_handler_syntaxes", `Mixing old (on:${name}) and new syntaxes for event handling is not allowed. Use only the on${name} syntax.`);
}
/** /**
* %thing% is invalid inside <%parent%> * %thing% is invalid inside <%parent%>
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -372,6 +372,8 @@ export function analyze_component(root, source, options) {
uses_render_tags: false, uses_render_tags: false,
needs_context: false, needs_context: false,
needs_props: false, needs_props: false,
event_directive_node: null,
uses_event_attributes: false,
custom_element: options.customElementOptions ?? options.customElement, custom_element: options.customElementOptions ?? options.customElement,
inject_styles: options.css === 'injected' || options.customElement, inject_styles: options.css === 'injected' || options.customElement,
accessors: options.customElement accessors: options.customElement
@ -494,6 +496,13 @@ export function analyze_component(root, source, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
if (analysis.event_directive_node && analysis.uses_event_attributes) {
e.mixed_event_handler_syntaxes(
analysis.event_directive_node,
analysis.event_directive_node.name
);
}
if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) { if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) {
e.slot_snippet_conflict(analysis.slot_names.values().next().value); e.slot_snippet_conflict(analysis.slot_names.values().next().value);
} }
@ -1153,6 +1162,11 @@ const common_visitors = {
}); });
if (is_event_attribute(node)) { if (is_event_attribute(node)) {
const parent = context.path.at(-1);
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
context.state.analysis.uses_event_attributes = true;
}
const expression = node.value[0].expression; const expression = node.value[0].expression;
const delegated_event = get_delegated_event(node.name.slice(2), expression, context); const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
@ -1286,6 +1300,13 @@ const common_visitors = {
context.next(); context.next();
}, },
OnDirective(node, { state, path, next }) {
const parent = path.at(-1);
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node;
}
next();
},
BindDirective(node, context) { BindDirective(node, context) {
let i = context.path.length; let i = context.path.length;
while (i--) { while (i--) {

@ -8,6 +8,7 @@ import * as e from '../../errors.js';
import { import {
extract_identifiers, extract_identifiers,
get_parent, get_parent,
is_event_attribute,
is_expression_attribute, is_expression_attribute,
is_text_attribute, is_text_attribute,
object, object,

@ -2,6 +2,7 @@ import type {
Binding, Binding,
Css, Css,
Fragment, Fragment,
OnDirective,
RegularElement, RegularElement,
SlotElement, SlotElement,
SvelteElement, SvelteElement,
@ -59,6 +60,10 @@ export interface ComponentAnalysis extends Analysis {
uses_render_tags: boolean; uses_render_tags: boolean;
needs_context: boolean; needs_context: boolean;
needs_props: boolean; needs_props: boolean;
/** Set to the first event directive (on:x) found on a DOM element in the code */
event_directive_node: OnDirective | null;
/** true if uses event attributes (onclick) on a DOM element */
uses_event_attributes: boolean;
custom_element: boolean | SvelteOptions['customElement']; custom_element: boolean | SvelteOptions['customElement'];
/** If `true`, should append styles through JavaScript */ /** If `true`, should append styles through JavaScript */
inject_styles: boolean; inject_styles: boolean;

@ -1,4 +1,4 @@
<div on:click={(e) => { console.log('clicked div') }}> <div onclick={(e) => { console.log('clicked div') }}>
<button onclick={(e) => { console.log('clicked button'); e.stopPropagation() }}> <button onclick={(e) => { console.log('clicked button'); e.stopPropagation() }}>
Button Button
</button> </button>

@ -0,0 +1,7 @@
<script>
const { children, ...props } = $props();
</script>
<div {...props} on:click>
{@render children()}
</div>

@ -1,12 +1,13 @@
<script> <script>
import Component from "./Component.svelte";
import Sub from "./sub.svelte"; import Sub from "./sub.svelte";
</script> </script>
<svelte:window onclick="{() => console.log('window main')}" /> <svelte:window onclick="{() => console.log('window main')}" />
<svelte:document onclick="{() => console.log('document main')}" /> <svelte:document onclick="{() => console.log('document main')}" />
<div on:click={() => console.log('div main 1')} on:click={() => console.log('div main 2')}> <Component on:click={() => console.log('div main 1')} on:click={() => console.log('div main 2')}>
<button onclick={() => console.log('button main')}>main</button> <button onclick={() => console.log('button main')}>main</button>
</div> </Component>
<Sub /> <Sub />

@ -0,0 +1,7 @@
<script>
const { children, ...props } = $props();
</script>
<button {...props} on:click>
{@render children()}
</button>

@ -0,0 +1,7 @@
<script>
const { children, ...props } = $props();
</script>
<div {...props} on:click>
{@render children()}
</div>

@ -1,5 +1,10 @@
<div onclick={() => console.log('outer div onclick')}> <script>
<div on:click={() => console.log('inner div on:click')}> import Component from "./Component.svelte";
<button onclick={() => console.log('button onclick')} on:click={() => console.log('button on:click')}>main</button> import Button from "./Button.svelte";
</div> </script>
</div>
<Component onclick={() => console.log('outer div onclick')}>
<Component on:click={() => console.log('inner div on:click')}>
<Button onclick={() => console.log('button onclick')} on:click={() => console.log('button on:click')}>main</Button>
</Component>
</Component>

@ -0,0 +1,7 @@
<script>
const { children, ...props } = $props();
</script>
<button {...props} on:click>
{@render children()}
</button>

@ -1,21 +1,22 @@
<script> <script>
import Button from './Button.svelte';
let text = $state('click me'); let text = $state('click me');
let text2 = $state(''); let text2 = $state('');
let spread = { onclick: () => text = 'click spread' }; let spread = { onclick: () => text = 'click spread' };
</script> </script>
<button onclick={() => text = 'click onclick'} {...spread}> <Button onclick={() => text = 'click onclick'} {...spread}>
{text} {text}
</button> </Button>
<button {...spread} onclick={() => text = 'click onclick'}> <Button {...spread} onclick={() => text = 'click onclick'}>
{text} {text}
</button> </Button>
<button onclick={() => text = 'click onclick'} {...spread} on:click={() => text2 = '!'}> <Button onclick={() => text = 'click onclick'} {...spread} on:click={() => text2 = '!'}>
{text}{text2} {text}{text2}
</button> </Button>
<button on:click={() => text2 = '?'} {...spread} onclick={() => text = 'click onclick'}> <Button on:click={() => text2 = '?'} {...spread} onclick={() => text = 'click onclick'}>
{text}{text2} {text}{text2}
</button> </Button>

@ -25,7 +25,7 @@
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<footer on:click={noop}></footer> <footer on:click={noop}></footer>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<footer onclick={noop}></footer> <footer on:click={noop}></footer>
<!-- should not warn --> <!-- should not warn -->
<div class="foo"></div> <div class="foo"></div>
@ -68,7 +68,7 @@
<div on:click={noop} role="presentation"></div> <div on:click={noop} role="presentation"></div>
<div on:click={noop} role="none"></div> <div on:click={noop} role="none"></div>
<div on:click={noop} role={dynamicRole}></div> <div on:click={noop} role={dynamicRole}></div>
<div onclick={noop} role={dynamicRole}></div> <div on:click={noop} role={dynamicRole}></div>
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<svelte:element this={Math.random() ? 'button' : 'div'} on:click={noop} /> <svelte:element this={Math.random() ? 'button' : 'div'} on:click={noop} />

@ -92,7 +92,7 @@
}, },
"end": { "end": {
"line": 28, "line": 28,
"column": 32 "column": 33
} }
} }
] ]

@ -0,0 +1,14 @@
[
{
"code": "mixed_event_handler_syntaxes",
"message": "Mixing old (on:click) and new syntaxes for event handling is not allowed. Use only the onclick syntax.",
"start": {
"line": 11,
"column": 8
},
"end": {
"line": 11,
"column": 22
}
}
]

@ -0,0 +1,11 @@
<script>
let { foo } = $props();
</script>
<!-- ok -->
<button onclick={foo}>click me</button>
<Button on:click={foo}>click me</Button>
<Button on:click={foo}>click me</Button>
<!-- error -->
<button on:click={foo}>click me</button>

@ -3,8 +3,7 @@
</script> </script>
<!-- ok --> <!-- ok -->
<button onclick={foo}>click me</button> <Button on:click={foo}>click me</Button>
<Button onclick={foo}>click me</Button>
<Button on:click={foo}>click me</Button> <Button on:click={foo}>click me</Button>
<!-- warn --> <!-- warn -->

@ -3,36 +3,36 @@
"code": "slot_element_deprecated", "code": "slot_element_deprecated",
"end": { "end": {
"column": 13, "column": 13,
"line": 11 "line": 10
}, },
"message": "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.", "message": "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.",
"start": { "start": {
"column": 0, "column": 0,
"line": 11 "line": 10
} }
}, },
{ {
"code": "slot_element_deprecated", "code": "slot_element_deprecated",
"end": { "end": {
"column": 24, "column": 24,
"line": 12 "line": 11
}, },
"message": "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.", "message": "Using `<slot>` to render parent content is deprecated. Use `{@render ...}` tags instead.",
"start": { "start": {
"column": 0, "column": 0,
"line": 12 "line": 11
} }
}, },
{ {
"code": "event_directive_deprecated", "code": "event_directive_deprecated",
"end": { "end": {
"column": 22, "column": 22,
"line": 13 "line": 12
}, },
"message": "Using `on:click` to listen to the click event is deprecated. Use the event attribute `onclick` instead.", "message": "Using `on:click` to listen to the click event is deprecated. Use the event attribute `onclick` instead.",
"start": { "start": {
"column": 8, "column": 8,
"line": 13 "line": 12
} }
} }
] ]

Loading…
Cancel
Save