Skip to content

feat: improved event handler type #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-buses-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-eslint-parser": minor
---

feat: improved event handler type
46 changes: 41 additions & 5 deletions explorer-v2/src/lib/VirtualScriptCode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/* eslint-disable no-useless-escape -- ignore */
import MonacoEditor from './MonacoEditor.svelte';
import * as svelteEslintParser from 'svelte-eslint-parser';
import { deserializeState, serializeState } from './scripts/state';
import { onDestroy, onMount } from 'svelte';

let tsParser = undefined;
let loaded = false;
Expand All @@ -22,7 +24,7 @@
loaded = true;
});

let svelteValue = `<script lang="ts">
const DEFAULT_CODE = `<script lang="ts">
const array = [1, 2, 3]

function inputHandler () {
Expand All @@ -37,19 +39,53 @@
{ee}
{/each}
`;
const state = deserializeState(
(typeof window !== 'undefined' && window.location.hash.slice(1)) || ''
);
let code = state.code || DEFAULT_CODE;
let virtualScriptCode = '';
let time = '';

let vscriptEditor, sourceEditor;
$: {
if (loaded) {
refresh(svelteValue);
refresh(code);
}
}
function refresh(svelteValue) {
// eslint-disable-next-line no-use-before-define -- false positive
$: serializedString = (() => {
const serializeCode = DEFAULT_CODE === code ? undefined : code;
return serializeState({
code: serializeCode
});
})();
$: {
if (typeof window !== 'undefined') {
window.location.replace(`#${serializedString}`);
}
}
onMount(() => {
if (typeof window !== 'undefined') {
window.addEventListener('hashchange', onUrlHashChange);
}
});
onDestroy(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('hashchange', onUrlHashChange);
}
});
function onUrlHashChange() {
const newSerializedString =
(typeof window !== 'undefined' && window.location.hash.slice(1)) || '';
if (newSerializedString !== serializedString) {
const state = deserializeState(newSerializedString);
code = state.code || DEFAULT_CODE;
}
}
function refresh(svelteCodeValue) {
const start = Date.now();
try {
virtualScriptCode = svelteEslintParser.parseForESLint(svelteValue, {
virtualScriptCode = svelteEslintParser.parseForESLint(svelteCodeValue, {
parser: tsParser
})._virtualScriptCode;
} catch (e) {
Expand All @@ -66,7 +102,7 @@
<div class="ast-explorer-root">
<div class="ast-tools">{time}</div>
<div class="ast-explorer">
<MonacoEditor bind:this={sourceEditor} bind:code={svelteValue} language="html" />
<MonacoEditor bind:this={sourceEditor} bind:code language="html" />
<MonacoEditor
bind:this={vscriptEditor}
code={virtualScriptCode}
Expand Down
35 changes: 27 additions & 8 deletions src/context/script-let.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ScopeManager, Scope } from "eslint-scope";
import type * as ESTree from "estree";
import type { TSESTree } from "@typescript-eslint/types";
import type { Context, ScriptsSourceCode } from ".";
import type {
Comment,
Expand Down Expand Up @@ -166,14 +167,32 @@ export class ScriptLetContext {
}

if (isTS) {
removeScope(
result.scopeManager,
result.getScope(
tsAs!.typeAnnotation.type === "TSParenthesizedType"
? tsAs!.typeAnnotation.typeAnnotation
: tsAs!.typeAnnotation
)
);
const blockNode =
tsAs!.typeAnnotation.type === "TSParenthesizedType"
? tsAs!.typeAnnotation.typeAnnotation
: tsAs!.typeAnnotation;
const targetScopes = [result.getScope(blockNode)];
let targetBlockNode: TSESTree.Node | TSParenthesizedType =
blockNode as any;
while (
targetBlockNode.type === "TSConditionalType" ||
targetBlockNode.type === "TSParenthesizedType"
) {
if (targetBlockNode.type === "TSParenthesizedType") {
targetBlockNode = targetBlockNode.typeAnnotation as any;
continue;
}
// TSConditionalType's `falseType` may not be a child scope.
const falseType: TSESTree.TypeNode = targetBlockNode.falseType;
const falseTypeScope = result.getScope(falseType as any);
if (!targetScopes.includes(falseTypeScope)) {
targetScopes.push(falseTypeScope);
}
targetBlockNode = falseType;
}
for (const scope of targetScopes) {
removeScope(result.scopeManager, scope);
}
this.remapNodes(
[
{
Expand Down
69 changes: 62 additions & 7 deletions src/parser/converts/attr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import type {
SvelteStyleDirective,
SvelteStyleDirectiveLongform,
SvelteStyleDirectiveShorthand,
SvelteElement,
SvelteScriptElement,
SvelteStyleElement,
} from "../../ast";
import type ESTree from "estree";
import type { Context } from "../../context";
Expand All @@ -33,6 +36,7 @@ import type { AttributeToken } from "../html";
export function* convertAttributes(
attributes: SvAST.AttributeOrDirective[],
parent: SvelteStartTag,
elementName: string,
ctx: Context
): IterableIterator<
| SvelteAttribute
Expand All @@ -55,7 +59,7 @@ export function* convertAttributes(
continue;
}
if (attr.type === "EventHandler") {
yield convertEventHandlerDirective(attr, parent, ctx);
yield convertEventHandlerDirective(attr, parent, elementName, ctx);
continue;
}
if (attr.type === "Class") {
Expand Down Expand Up @@ -314,6 +318,7 @@ function convertBindingDirective(
function convertEventHandlerDirective(
node: SvAST.DirectiveForExpression,
parent: SvelteDirective["parent"],
elementName: string,
ctx: Context
): SvelteEventHandlerDirective {
const directive: SvelteEventHandlerDirective = {
Expand All @@ -324,21 +329,71 @@ function convertEventHandlerDirective(
parent,
...ctx.getConvertLocation(node),
};
const isCustomEvent =
parent.parent.type === "SvelteElement" &&
(parent.parent.kind === "component" || parent.parent.kind === "special");
const typing = buildEventHandlerType(parent.parent, elementName, node.name);
processDirective(node, directive, ctx, {
processExpression: buildProcessExpressionForExpression(
directive,
ctx,
isCustomEvent
? "(e:CustomEvent<any>)=>void"
: `(e:'${node.name}' extends infer U?U extends keyof HTMLElementEventMap?HTMLElementEventMap[U]:CustomEvent<any>:never)=>void`
typing
),
});
return directive;
}

/** Build event handler type */
function buildEventHandlerType(
element: SvelteElement | SvelteScriptElement | SvelteStyleElement,
elementName: string,
eventName: string
) {
const nativeEventHandlerType = [
`(e:`,
/**/ `'${eventName}' extends infer EVT`,
/**/ /**/ `?EVT extends keyof HTMLElementEventMap`,
/**/ /**/ /**/ `?HTMLElementEventMap[EVT]`,
/**/ /**/ /**/ `:CustomEvent<any>`,
/**/ /**/ `:never`,
`)=>void`,
].join("");
if (element.type !== "SvelteElement") {
return nativeEventHandlerType;
}
if (element.kind === "component") {
// `@typescript-eslint/parser` currently cannot parse `*.svelte` import types correctly.
// So if we try to do a correct type parsing, it's argument type will be `any`.
// A workaround is to inject the type directly, as `CustomEvent<any>` is better than `any`.

// const componentEvents = `import('svelte').ComponentEvents<${elementName}>`;
// return `(e:'${eventName}' extends keyof ${componentEvents}?${componentEvents}['${eventName}']:CustomEvent<any>)=>void`;

return `(e:CustomEvent<any>)=>void`;
}
if (element.kind === "special") {
if (elementName === "svelte:component") return `(e:CustomEvent<any>)=>void`;
return nativeEventHandlerType;
}
const attrName = `on:${eventName}`;
const importSvelteHTMLElements =
"import('svelte/elements').SvelteHTMLElements";
return [
`'${elementName}' extends infer EL`,
/**/ `?(`,
/**/ /**/ `EL extends keyof ${importSvelteHTMLElements}`,
/**/ /**/ `?(`,
/**/ /**/ /**/ `'${attrName}' extends infer ATTR`,
/**/ /**/ /**/ `?(`,
/**/ /**/ /**/ /**/ `ATTR extends keyof ${importSvelteHTMLElements}[EL]`,
/**/ /**/ /**/ /**/ /**/ `?${importSvelteHTMLElements}[EL][ATTR]`,
/**/ /**/ /**/ /**/ /**/ `:${nativeEventHandlerType}`,
/**/ /**/ /**/ `)`,
/**/ /**/ /**/ `:never`,
/**/ /**/ `)`,
/**/ /**/ `:${nativeEventHandlerType}`,
/**/ `)`,
/**/ `:never`,
].join("");
}

/** Convert for Class Directive */
function convertClassDirective(
node: SvAST.DirectiveForExpression,
Expand Down
31 changes: 17 additions & 14 deletions src/parser/converts/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,25 +252,26 @@ function convertHTMLElement(
...locs,
};
element.startTag.parent = element;
const elementName = node.name;

const { letDirectives, attributes } = extractLetDirectives(node);
const letParams: ScriptLetBlockParam[] = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(
...convertAttributes(letDirectives, element.startTag, ctx)
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
);
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
if (!letParams.length && !needScopeByChildren(node)) {
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
element.children.push(...convertChildren(node, element, ctx));
} else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(node, element, ctx));
Expand All @@ -282,7 +283,7 @@ function convertHTMLElement(
ctx.addToken("HTMLIdentifier", openTokenRange);
const name: SvelteName = {
type: "SvelteName",
name: node.name,
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
Expand Down Expand Up @@ -359,25 +360,26 @@ function convertSpecialElement(
...locs,
};
element.startTag.parent = element;
const elementName = node.name;

const { letDirectives, attributes } = extractLetDirectives(node);
const letParams: ScriptLetBlockParam[] = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(
...convertAttributes(letDirectives, element.startTag, ctx)
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
);
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
if (!letParams.length && !needScopeByChildren(node)) {
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
element.children.push(...convertChildren(node, element, ctx));
} else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(node, element, ctx));
Expand All @@ -386,9 +388,9 @@ function convertSpecialElement(

const thisExpression =
(node.type === "InlineComponent" &&
node.name === "svelte:component" &&
elementName === "svelte:component" &&
node.expression) ||
(node.type === "Element" && node.name === "svelte:element" && node.tag);
(node.type === "Element" && elementName === "svelte:element" && node.tag);
if (thisExpression) {
const eqIndex = ctx.code.lastIndexOf("=", getWithLoc(thisExpression).start);
const startIndex = ctx.code.lastIndexOf("this", eqIndex);
Expand Down Expand Up @@ -434,7 +436,7 @@ function convertSpecialElement(
ctx.addToken("HTMLIdentifier", openTokenRange);
const name: SvelteName = {
type: "SvelteName",
name: node.name,
name: elementName,
parent: element,
...ctx.getConvertLocation(openTokenRange),
};
Expand Down Expand Up @@ -476,25 +478,26 @@ function convertComponentElement(
...locs,
};
element.startTag.parent = element;
const elementName = node.name;

const { letDirectives, attributes } = extractLetDirectives(node);
const letParams: ScriptLetBlockParam[] = [];
if (letDirectives.length) {
ctx.letDirCollections.beginExtract();
element.startTag.attributes.push(
...convertAttributes(letDirectives, element.startTag, ctx)
...convertAttributes(letDirectives, element.startTag, elementName, ctx)
);
letParams.push(...ctx.letDirCollections.extract().getLetParams());
}
if (!letParams.length && !needScopeByChildren(node)) {
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
element.children.push(...convertChildren(node, element, ctx));
} else {
ctx.scriptLet.nestBlock(element, letParams);
element.startTag.attributes.push(
...convertAttributes(attributes, element.startTag, ctx)
...convertAttributes(attributes, element.startTag, elementName, ctx)
);
sortNodes(element.startTag.attributes);
element.children.push(...convertChildren(node, element, ctx));
Expand All @@ -503,7 +506,7 @@ function convertComponentElement(

extractElementTags(element, ctx, {
buildNameNode: (openTokenRange) => {
const chains = node.name.split(".");
const chains = elementName.split(".");
const id = chains.shift()!;
const idRange = {
start: openTokenRange.start,
Expand Down
4 changes: 2 additions & 2 deletions src/scope/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ function referencesToThrough(references: Reference[], baseScope: Scope) {

/** Remove scope */
export function removeScope(scopeManager: ScopeManager, scope: Scope): void {
for (const childScope of scope.childScopes) {
removeScope(scopeManager, childScope);
while (scope.childScopes[0]) {
removeScope(scopeManager, scope.childScopes[0]);
}

while (scope.references[0]) {
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/parser/ast/ts-event02-type-output.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="typescript">
import Component from 'foo.svelte' // Component: typeof SvelteComponentDev
</script>
<button on:click="{e=>{}}"></button> <!-- e: MouseEvent -->
<button on:click="{e=>{}}"></button> <!-- e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement; } -->
<Component on:click="{e=>{}}"></Component> <!-- Component: typeof SvelteComponentDev, e: CustomEvent<any> -->
Loading