Skip to content

Commit

Permalink
feat: improved event handler type (#296)
Browse files Browse the repository at this point in the history
* feat: improved event handler type

* fix

* Create .changeset/violet-buses-tickle.md

* fix

* update fixtures

* fix
  • Loading branch information
ota-meshi authored Apr 1, 2023
1 parent cb544cf commit 21d8c1c
Show file tree
Hide file tree
Showing 24 changed files with 32,952 additions and 47 deletions.
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

0 comments on commit 21d8c1c

Please sign in to comment.