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 <[email protected]>
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

+5
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

+19-7
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

+15
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

+5-6
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
}
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+
});
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)