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>
pull/15467/head
adiGuba 6 months ago committed by GitHub
parent 2c4d85bcec
commit c5912aad71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: null and warnings for local handlers

@ -46,11 +46,15 @@ export function visit_event_attribute(node, context) {
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
if (hoisted_params) {
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
@ -123,13 +127,21 @@ export function build_event_handler(node, metadata, context) {
}
// function declared in the script
if (
handler.type === 'Identifier' &&
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
) {
if (handler.type === 'Identifier') {
const binding = context.state.scope.get(handler.name);
if (binding?.is_function()) {
return handler;
}
// local variable can be assigned directly
// except in dev mode where when need $.apply()
// in order to handle warnings.
if (!dev && binding?.declaration_kind !== 'import') {
return handler;
}
}
if (metadata.has_call) {
// memoize where necessary
const id = b.id(context.state.scope.generate('event_handler'));

@ -79,6 +79,21 @@ export class Binding {
get updated() {
return this.mutated || this.reassigned;
}
is_function() {
if (this.reassigned) {
// even if it's reassigned to another function,
// we can't use it directly as e.g. an event handler
return false;
}
if (this.declaration_kind === 'function') {
return true;
}
const type = this.initial?.type;
return type === 'ArrowFunctionExpression' || type === 'FunctionExpression';
}
}
export class Scope {

@ -238,7 +238,7 @@ export function handle_event_propagation(event) {
var delegated = current_target['__' + event_name];
if (
delegated !== undefined &&
delegated != null &&
(!(/** @type {any} */ (current_target).disabled) ||
// DOM could've been updated already by the time this is reached, so we check this as well
// -> the target could not have been disabled because it emits the event in the first place
@ -311,13 +311,11 @@ export function apply(
error = e;
}
if (typeof handler === 'function') {
handler.apply(element, args);
} else if (has_side_effects || handler != null || error) {
if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) {
const filename = component?.[FILENAME];
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const event_name = args[0].type;
const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : '';
const event_name = args[0]?.type + phase;
const description = `\`${event_name}\` handler${location}`;
const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`';
@ -327,4 +325,5 @@ export function apply(
throw error;
}
}
handler?.apply(element, args);
}

@ -0,0 +1,48 @@
import { assertType } from 'vitest';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
test({ assert, target, warnings, logs }) {
/** @type {any} */
let error = null;
const handler = (/** @type {any} */ e) => {
error = e.error;
e.stopImmediatePropagation();
};
window.addEventListener('error', handler, true);
const [b1, b2, b3] = target.querySelectorAll('button');
b1.click();
assert.deepEqual(logs, []);
assert.equal(error, null);
error = null;
logs.length = 0;
b2.click();
assert.deepEqual(logs, ['clicked']);
assert.equal(error, null);
error = null;
logs.length = 0;
b3.click();
assert.deepEqual(logs, []);
assert.deepEqual(warnings, [
'`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?'
]);
assert.isNotNull(error);
assert.match(error.message, /is not a function/);
window.removeEventListener('error', handler, true);
}
});

@ -0,0 +1,10 @@
<script>
let ignore = null;
let handler = () => console.log("clicked");
let bad = "invalid";
</script>
<button onclick={ignore}>click</button>
<button onclick={handler}>click</button>
<button onclick={bad}>click</button>
Loading…
Cancel
Save