Skip to content

Commit 21d8c1c

Browse files
authored
feat: improved event handler type (#296)
* feat: improved event handler type * fix * Create .changeset/violet-buses-tickle.md * fix * update fixtures * fix
1 parent cb544cf commit 21d8c1c

24 files changed

+32952
-47
lines changed

.changeset/violet-buses-tickle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: improved event handler type

explorer-v2/src/lib/VirtualScriptCode.svelte

+41-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
/* eslint-disable no-useless-escape -- ignore */
44
import MonacoEditor from './MonacoEditor.svelte';
55
import * as svelteEslintParser from 'svelte-eslint-parser';
6+
import { deserializeState, serializeState } from './scripts/state';
7+
import { onDestroy, onMount } from 'svelte';
68
79
let tsParser = undefined;
810
let loaded = false;
@@ -22,7 +24,7 @@
2224
loaded = true;
2325
});
2426
25-
let svelteValue = `<script lang="ts">
27+
const DEFAULT_CODE = `<script lang="ts">
2628
const array = [1, 2, 3]
2729
2830
function inputHandler () {
@@ -37,19 +39,53 @@
3739
{ee}
3840
{/each}
3941
`;
42+
const state = deserializeState(
43+
(typeof window !== 'undefined' && window.location.hash.slice(1)) || ''
44+
);
45+
let code = state.code || DEFAULT_CODE;
4046
let virtualScriptCode = '';
4147
let time = '';
4248
4349
let vscriptEditor, sourceEditor;
4450
$: {
4551
if (loaded) {
46-
refresh(svelteValue);
52+
refresh(code);
4753
}
4854
}
49-
function refresh(svelteValue) {
55+
// eslint-disable-next-line no-use-before-define -- false positive
56+
$: serializedString = (() => {
57+
const serializeCode = DEFAULT_CODE === code ? undefined : code;
58+
return serializeState({
59+
code: serializeCode
60+
});
61+
})();
62+
$: {
63+
if (typeof window !== 'undefined') {
64+
window.location.replace(`#${serializedString}`);
65+
}
66+
}
67+
onMount(() => {
68+
if (typeof window !== 'undefined') {
69+
window.addEventListener('hashchange', onUrlHashChange);
70+
}
71+
});
72+
onDestroy(() => {
73+
if (typeof window !== 'undefined') {
74+
window.removeEventListener('hashchange', onUrlHashChange);
75+
}
76+
});
77+
function onUrlHashChange() {
78+
const newSerializedString =
79+
(typeof window !== 'undefined' && window.location.hash.slice(1)) || '';
80+
if (newSerializedString !== serializedString) {
81+
const state = deserializeState(newSerializedString);
82+
code = state.code || DEFAULT_CODE;
83+
}
84+
}
85+
function refresh(svelteCodeValue) {
5086
const start = Date.now();
5187
try {
52-
virtualScriptCode = svelteEslintParser.parseForESLint(svelteValue, {
88+
virtualScriptCode = svelteEslintParser.parseForESLint(svelteCodeValue, {
5389
parser: tsParser
5490
})._virtualScriptCode;
5591
} catch (e) {
@@ -66,7 +102,7 @@
66102
<div class="ast-explorer-root">
67103
<div class="ast-tools">{time}</div>
68104
<div class="ast-explorer">
69-
<MonacoEditor bind:this={sourceEditor} bind:code={svelteValue} language="html" />
105+
<MonacoEditor bind:this={sourceEditor} bind:code language="html" />
70106
<MonacoEditor
71107
bind:this={vscriptEditor}
72108
code={virtualScriptCode}

src/context/script-let.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ScopeManager, Scope } from "eslint-scope";
22
import type * as ESTree from "estree";
3+
import type { TSESTree } from "@typescript-eslint/types";
34
import type { Context, ScriptsSourceCode } from ".";
45
import type {
56
Comment,
@@ -166,14 +167,32 @@ export class ScriptLetContext {
166167
}
167168

168169
if (isTS) {
169-
removeScope(
170-
result.scopeManager,
171-
result.getScope(
172-
tsAs!.typeAnnotation.type === "TSParenthesizedType"
173-
? tsAs!.typeAnnotation.typeAnnotation
174-
: tsAs!.typeAnnotation
175-
)
176-
);
170+
const blockNode =
171+
tsAs!.typeAnnotation.type === "TSParenthesizedType"
172+
? tsAs!.typeAnnotation.typeAnnotation
173+
: tsAs!.typeAnnotation;
174+
const targetScopes = [result.getScope(blockNode)];
175+
let targetBlockNode: TSESTree.Node | TSParenthesizedType =
176+
blockNode as any;
177+
while (
178+
targetBlockNode.type === "TSConditionalType" ||
179+
targetBlockNode.type === "TSParenthesizedType"
180+
) {
181+
if (targetBlockNode.type === "TSParenthesizedType") {
182+
targetBlockNode = targetBlockNode.typeAnnotation as any;
183+
continue;
184+
}
185+
// TSConditionalType's `falseType` may not be a child scope.
186+
const falseType: TSESTree.TypeNode = targetBlockNode.falseType;
187+
const falseTypeScope = result.getScope(falseType as any);
188+
if (!targetScopes.includes(falseTypeScope)) {
189+
targetScopes.push(falseTypeScope);
190+
}
191+
targetBlockNode = falseType;
192+
}
193+
for (const scope of targetScopes) {
194+
removeScope(result.scopeManager, scope);
195+
}
177196
this.remapNodes(
178197
[
179198
{

src/parser/converts/attr.ts

+62-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import type {
1515
SvelteStyleDirective,
1616
SvelteStyleDirectiveLongform,
1717
SvelteStyleDirectiveShorthand,
18+
SvelteElement,
19+
SvelteScriptElement,
20+
SvelteStyleElement,
1821
} from "../../ast";
1922
import type ESTree from "estree";
2023
import type { Context } from "../../context";
@@ -33,6 +36,7 @@ import type { AttributeToken } from "../html";
3336
export function* convertAttributes(
3437
attributes: SvAST.AttributeOrDirective[],
3538
parent: SvelteStartTag,
39+
elementName: string,
3640
ctx: Context
3741
): IterableIterator<
3842
| SvelteAttribute
@@ -55,7 +59,7 @@ export function* convertAttributes(
5559
continue;
5660
}
5761
if (attr.type === "EventHandler") {
58-
yield convertEventHandlerDirective(attr, parent, ctx);
62+
yield convertEventHandlerDirective(attr, parent, elementName, ctx);
5963
continue;
6064
}
6165
if (attr.type === "Class") {
@@ -314,6 +318,7 @@ function convertBindingDirective(
314318
function convertEventHandlerDirective(
315319
node: SvAST.DirectiveForExpression,
316320
parent: SvelteDirective["parent"],
321+
elementName: string,
317322
ctx: Context
318323
): SvelteEventHandlerDirective {
319324
const directive: SvelteEventHandlerDirective = {
@@ -324,21 +329,71 @@ function convertEventHandlerDirective(
324329
parent,
325330
...ctx.getConvertLocation(node),
326331
};
327-
const isCustomEvent =
328-
parent.parent.type === "SvelteElement" &&
329-
(parent.parent.kind === "component" || parent.parent.kind === "special");
332+
const typing = buildEventHandlerType(parent.parent, elementName, node.name);
330333
processDirective(node, directive, ctx, {
331334
processExpression: buildProcessExpressionForExpression(
332335
directive,
333336
ctx,
334-
isCustomEvent
335-
? "(e:CustomEvent<any>)=>void"
336-
: `(e:'${node.name}' extends infer U?U extends keyof HTMLElementEventMap?HTMLElementEventMap[U]:CustomEvent<any>:never)=>void`
337+
typing
337338
),
338339
});
339340
return directive;
340341
}
341342

343+
/** Build event handler type */
344+
function buildEventHandlerType(
345+
element: SvelteElement | SvelteScriptElement | SvelteStyleElement,
346+
elementName: string,
347+
eventName: string
348+
) {
349+
const nativeEventHandlerType = [
350+
`(e:`,
351+
/**/ `'${eventName}' extends infer EVT`,
352+
/**/ /**/ `?EVT extends keyof HTMLElementEventMap`,
353+
/**/ /**/ /**/ `?HTMLElementEventMap[EVT]`,
354+
/**/ /**/ /**/ `:CustomEvent<any>`,
355+
/**/ /**/ `:never`,
356+
`)=>void`,
357+
].join("");
358+
if (element.type !== "SvelteElement") {
359+
return nativeEventHandlerType;
360+
}
361+
if (element.kind === "component") {
362+
// `@typescript-eslint/parser` currently cannot parse `*.svelte` import types correctly.
363+
// So if we try to do a correct type parsing, it's argument type will be `any`.
364+
// A workaround is to inject the type directly, as `CustomEvent<any>` is better than `any`.
365+
366+
// const componentEvents = `import('svelte').ComponentEvents<${elementName}>`;
367+
// return `(e:'${eventName}' extends keyof ${componentEvents}?${componentEvents}['${eventName}']:CustomEvent<any>)=>void`;
368+
369+
return `(e:CustomEvent<any>)=>void`;
370+
}
371+
if (element.kind === "special") {
372+
if (elementName === "svelte:component") return `(e:CustomEvent<any>)=>void`;
373+
return nativeEventHandlerType;
374+
}
375+
const attrName = `on:${eventName}`;
376+
const importSvelteHTMLElements =
377+
"import('svelte/elements').SvelteHTMLElements";
378+
return [
379+
`'${elementName}' extends infer EL`,
380+
/**/ `?(`,
381+
/**/ /**/ `EL extends keyof ${importSvelteHTMLElements}`,
382+
/**/ /**/ `?(`,
383+
/**/ /**/ /**/ `'${attrName}' extends infer ATTR`,
384+
/**/ /**/ /**/ `?(`,
385+
/**/ /**/ /**/ /**/ `ATTR extends keyof ${importSvelteHTMLElements}[EL]`,
386+
/**/ /**/ /**/ /**/ /**/ `?${importSvelteHTMLElements}[EL][ATTR]`,
387+
/**/ /**/ /**/ /**/ /**/ `:${nativeEventHandlerType}`,
388+
/**/ /**/ /**/ `)`,
389+
/**/ /**/ /**/ `:never`,
390+
/**/ /**/ `)`,
391+
/**/ /**/ `:${nativeEventHandlerType}`,
392+
/**/ `)`,
393+
/**/ `:never`,
394+
].join("");
395+
}
396+
342397
/** Convert for Class Directive */
343398
function convertClassDirective(
344399
node: SvAST.DirectiveForExpression,

src/parser/converts/element.ts

+17-14
Original file line numberDiff line numberDiff line change
@@ -252,25 +252,26 @@ function convertHTMLElement(
252252
...locs,
253253
};
254254
element.startTag.parent = element;
255+
const elementName = node.name;
255256

256257
const { letDirectives, attributes } = extractLetDirectives(node);
257258
const letParams: ScriptLetBlockParam[] = [];
258259
if (letDirectives.length) {
259260
ctx.letDirCollections.beginExtract();
260261
element.startTag.attributes.push(
261-
...convertAttributes(letDirectives, element.startTag, ctx)
262+
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
262263
);
263264
letParams.push(...ctx.letDirCollections.extract().getLetParams());
264265
}
265266
if (!letParams.length && !needScopeByChildren(node)) {
266267
element.startTag.attributes.push(
267-
...convertAttributes(attributes, element.startTag, ctx)
268+
...convertAttributes(attributes, element.startTag, elementName, ctx)
268269
);
269270
element.children.push(...convertChildren(node, element, ctx));
270271
} else {
271272
ctx.scriptLet.nestBlock(element, letParams);
272273
element.startTag.attributes.push(
273-
...convertAttributes(attributes, element.startTag, ctx)
274+
...convertAttributes(attributes, element.startTag, elementName, ctx)
274275
);
275276
sortNodes(element.startTag.attributes);
276277
element.children.push(...convertChildren(node, element, ctx));
@@ -282,7 +283,7 @@ function convertHTMLElement(
282283
ctx.addToken("HTMLIdentifier", openTokenRange);
283284
const name: SvelteName = {
284285
type: "SvelteName",
285-
name: node.name,
286+
name: elementName,
286287
parent: element,
287288
...ctx.getConvertLocation(openTokenRange),
288289
};
@@ -359,25 +360,26 @@ function convertSpecialElement(
359360
...locs,
360361
};
361362
element.startTag.parent = element;
363+
const elementName = node.name;
362364

363365
const { letDirectives, attributes } = extractLetDirectives(node);
364366
const letParams: ScriptLetBlockParam[] = [];
365367
if (letDirectives.length) {
366368
ctx.letDirCollections.beginExtract();
367369
element.startTag.attributes.push(
368-
...convertAttributes(letDirectives, element.startTag, ctx)
370+
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
369371
);
370372
letParams.push(...ctx.letDirCollections.extract().getLetParams());
371373
}
372374
if (!letParams.length && !needScopeByChildren(node)) {
373375
element.startTag.attributes.push(
374-
...convertAttributes(attributes, element.startTag, ctx)
376+
...convertAttributes(attributes, element.startTag, elementName, ctx)
375377
);
376378
element.children.push(...convertChildren(node, element, ctx));
377379
} else {
378380
ctx.scriptLet.nestBlock(element, letParams);
379381
element.startTag.attributes.push(
380-
...convertAttributes(attributes, element.startTag, ctx)
382+
...convertAttributes(attributes, element.startTag, elementName, ctx)
381383
);
382384
sortNodes(element.startTag.attributes);
383385
element.children.push(...convertChildren(node, element, ctx));
@@ -386,9 +388,9 @@ function convertSpecialElement(
386388

387389
const thisExpression =
388390
(node.type === "InlineComponent" &&
389-
node.name === "svelte:component" &&
391+
elementName === "svelte:component" &&
390392
node.expression) ||
391-
(node.type === "Element" && node.name === "svelte:element" && node.tag);
393+
(node.type === "Element" && elementName === "svelte:element" && node.tag);
392394
if (thisExpression) {
393395
const eqIndex = ctx.code.lastIndexOf("=", getWithLoc(thisExpression).start);
394396
const startIndex = ctx.code.lastIndexOf("this", eqIndex);
@@ -434,7 +436,7 @@ function convertSpecialElement(
434436
ctx.addToken("HTMLIdentifier", openTokenRange);
435437
const name: SvelteName = {
436438
type: "SvelteName",
437-
name: node.name,
439+
name: elementName,
438440
parent: element,
439441
...ctx.getConvertLocation(openTokenRange),
440442
};
@@ -476,25 +478,26 @@ function convertComponentElement(
476478
...locs,
477479
};
478480
element.startTag.parent = element;
481+
const elementName = node.name;
479482

480483
const { letDirectives, attributes } = extractLetDirectives(node);
481484
const letParams: ScriptLetBlockParam[] = [];
482485
if (letDirectives.length) {
483486
ctx.letDirCollections.beginExtract();
484487
element.startTag.attributes.push(
485-
...convertAttributes(letDirectives, element.startTag, ctx)
488+
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
486489
);
487490
letParams.push(...ctx.letDirCollections.extract().getLetParams());
488491
}
489492
if (!letParams.length && !needScopeByChildren(node)) {
490493
element.startTag.attributes.push(
491-
...convertAttributes(attributes, element.startTag, ctx)
494+
...convertAttributes(attributes, element.startTag, elementName, ctx)
492495
);
493496
element.children.push(...convertChildren(node, element, ctx));
494497
} else {
495498
ctx.scriptLet.nestBlock(element, letParams);
496499
element.startTag.attributes.push(
497-
...convertAttributes(attributes, element.startTag, ctx)
500+
...convertAttributes(attributes, element.startTag, elementName, ctx)
498501
);
499502
sortNodes(element.startTag.attributes);
500503
element.children.push(...convertChildren(node, element, ctx));
@@ -503,7 +506,7 @@ function convertComponentElement(
503506

504507
extractElementTags(element, ctx, {
505508
buildNameNode: (openTokenRange) => {
506-
const chains = node.name.split(".");
509+
const chains = elementName.split(".");
507510
const id = chains.shift()!;
508511
const idRange = {
509512
start: openTokenRange.start,

src/scope/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ function referencesToThrough(references: Reference[], baseScope: Scope) {
327327

328328
/** Remove scope */
329329
export function removeScope(scopeManager: ScopeManager, scope: Scope): void {
330-
for (const childScope of scope.childScopes) {
331-
removeScope(scopeManager, childScope);
330+
while (scope.childScopes[0]) {
331+
removeScope(scopeManager, scope.childScopes[0]);
332332
}
333333

334334
while (scope.references[0]) {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="typescript">
22
import Component from 'foo.svelte' // Component: typeof SvelteComponentDev
33
</script>
4-
<button on:click="{e=>{}}"></button> <!-- e: MouseEvent -->
4+
<button on:click="{e=>{}}"></button> <!-- e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement; } -->
55
<Component on:click="{e=>{}}"></Component> <!-- Component: typeof SvelteComponentDev, e: CustomEvent<any> -->

0 commit comments

Comments
 (0)