Skip to content

Commit c5912aa

Browse files
adigubaRich-Harris
andauthored
fix: null and warnings for local handlers (#15460)
* fix null and warning for local handlers * test * changeset * treat `let handler = () => {...}` the same as `function handler() {...}` --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 2c4d85b commit c5912aa

File tree

6 files changed

+102
-13
lines changed

6 files changed

+102
-13
lines changed

.changeset/shy-mirrors-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: null and warnings for local handlers

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ export function visit_event_attribute(node, context) {
4646

4747
// When we hoist a function we assign an array with the function and all
4848
// hoisted closure params.
49-
const args = [handler, ...hoisted_params];
50-
delegated_assignment = b.array(args);
49+
if (hoisted_params) {
50+
const args = [handler, ...hoisted_params];
51+
delegated_assignment = b.array(args);
52+
} else {
53+
delegated_assignment = handler;
54+
}
5155
} else {
5256
delegated_assignment = handler;
5357
}
@@ -123,11 +127,19 @@ export function build_event_handler(node, metadata, context) {
123127
}
124128

125129
// function declared in the script
126-
if (
127-
handler.type === 'Identifier' &&
128-
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
129-
) {
130-
return handler;
130+
if (handler.type === 'Identifier') {
131+
const binding = context.state.scope.get(handler.name);
132+
133+
if (binding?.is_function()) {
134+
return handler;
135+
}
136+
137+
// local variable can be assigned directly
138+
// except in dev mode where when need $.apply()
139+
// in order to handle warnings.
140+
if (!dev && binding?.declaration_kind !== 'import') {
141+
return handler;
142+
}
131143
}
132144

133145
if (metadata.has_call) {

packages/svelte/src/compiler/phases/scope.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ export class Binding {
7979
get updated() {
8080
return this.mutated || this.reassigned;
8181
}
82+
83+
is_function() {
84+
if (this.reassigned) {
85+
// even if it's reassigned to another function,
86+
// we can't use it directly as e.g. an event handler
87+
return false;
88+
}
89+
90+
if (this.declaration_kind === 'function') {
91+
return true;
92+
}
93+
94+
const type = this.initial?.type;
95+
return type === 'ArrowFunctionExpression' || type === 'FunctionExpression';
96+
}
8297
}
8398

8499
export class Scope {

packages/svelte/src/internal/client/dom/elements/events.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function handle_event_propagation(event) {
238238
var delegated = current_target['__' + event_name];
239239

240240
if (
241-
delegated !== undefined &&
241+
delegated != null &&
242242
(!(/** @type {any} */ (current_target).disabled) ||
243243
// DOM could've been updated already by the time this is reached, so we check this as well
244244
// -> the target could not have been disabled because it emits the event in the first place
@@ -311,13 +311,11 @@ export function apply(
311311
error = e;
312312
}
313313

314-
if (typeof handler === 'function') {
315-
handler.apply(element, args);
316-
} else if (has_side_effects || handler != null || error) {
314+
if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
317315
const filename = component?.[FILENAME];
318316
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
319-
320-
const event_name = args[0].type;
317+
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
318+
const event_name = args[0]?.type + phase;
321319
const description = `\`${event_name}\` handler${location}`;
322320
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
323321

@@ -327,4 +325,5 @@ export function apply(
327325
throw error;
328326
}
329327
}
328+
handler?.apply(element, args);
330329
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { assertType } from 'vitest';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
7+
compileOptions: {
8+
dev: true
9+
},
10+
11+
test({ assert, target, warnings, logs }) {
12+
/** @type {any} */
13+
let error = null;
14+
15+
const handler = (/** @type {any} */ e) => {
16+
error = e.error;
17+
e.stopImmediatePropagation();
18+
};
19+
20+
window.addEventListener('error', handler, true);
21+
22+
const [b1, b2, b3] = target.querySelectorAll('button');
23+
24+
b1.click();
25+
assert.deepEqual(logs, []);
26+
assert.equal(error, null);
27+
28+
error = null;
29+
logs.length = 0;
30+
31+
b2.click();
32+
assert.deepEqual(logs, ['clicked']);
33+
assert.equal(error, null);
34+
35+
error = null;
36+
logs.length = 0;
37+
38+
b3.click();
39+
assert.deepEqual(logs, []);
40+
assert.deepEqual(warnings, [
41+
'`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?'
42+
]);
43+
assert.isNotNull(error);
44+
assert.match(error.message, /is not a function/);
45+
46+
window.removeEventListener('error', handler, true);
47+
}
48+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let ignore = null;
3+
let handler = () => console.log("clicked");
4+
let bad = "invalid";
5+
6+
</script>
7+
8+
<button onclick={ignore}>click</button>
9+
<button onclick={handler}>click</button>
10+
<button onclick={bad}>click</button>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy