sveltejs / svelte-eslint-parser

Svelte parser for ESLint
https://sveltejs.github.io/svelte-eslint-parser/
MIT License
90 stars 19 forks source link

If component is generic, events and slot bindings are treated as `any` in ESLint #564

Open ptrxyz opened 2 weeks ago

ptrxyz commented 2 weeks ago

Before You File a Bug Report Please Confirm You Have Done The Following...

What version of ESLint are you using?

9.10.0

What version of eslint-plugin-svelte and svelte-eslint-parser are you using?

What did you do?

Configuration ```js import js from '@eslint/js'; import ts from 'typescript-eslint'; import svelte from 'eslint-plugin-svelte'; import prettier from 'eslint-config-prettier'; import globals from 'globals'; /** @type {import('eslint').Linter.Config[]} */ export default [ js.configs.recommended, ...ts.configs.recommendedTypeChecked, ...svelte.configs['flat/recommended'], prettier, ...svelte.configs['flat/prettier'], { languageOptions: { parserOptions: { parser: ts.parser, project: ['./tsconfig.json'] }, globals: { ...globals.browser, ...globals.node } } }, { files: ['**/*.svelte'], languageOptions: { parser: svelte.parser, parserOptions: { parser: ts.parser, project: ['.svelte-kit/tsconfig.json', './tsconfig.json'], extraFileExtensions: ['.svelte'] } } }, { ignores: ['build/', '.svelte-kit/', 'dist/'] } ]; ```

Consider this code:

<script lang="ts">
    // main.svelte
    import type { ComponentEvents } from 'svelte';
    import Comp from './component.svelte';

    type X = ComponentEvents<Comp>['foo'];
    export const x: X | null = null;
</script>
<script lang="ts">
    // component.svelte 
    import { createEventDispatcher } from 'svelte';

    export const dispatch = createEventDispatcher<{
        foo: { id: number; name: string };
    }>();
</script>

As you can see X should be typed as an object, which is correctly detected by VSCode: image

However when running eslint with type-checking rules enabled, eslint produces an error indicating, that X is simply any: image

Further investigation with more rules that check for types (i.e. @typescript-eslint/no-unsafe-assignment and @typescript-eslint/no-unsafe-member-access) confirm that for eslint, X is simply any

What did you expect to happen?

I wanted eslint to not throw the error since X is properly typed.

What actually happened?

eslint throws an error indicating that X is simply any.

Link to GitHub Repo with Minimal Reproducible Example

https://github.com/ptrxyz/bug-svelte-eslint-componentevents/tree/main

Additional comments

No response

ptrxyz commented 2 weeks ago

I did some research and it turns out that slot bindings are also only typed in VSCode but for ESLint they are simply any.

So if I have this component:

<script lang="ts">
  let foo = { prop: true }
</script>

<slot {foo}></slot>
<Component>
<div let:foo={bar}>{bar.prop}</div>
<Component>

ESLint will let me know that prop is an unsafe member access for example.

ptrxyz commented 2 weeks ago

Did more digging: It seems that the problem with ESLint not seeing the proper types of events can be fixed by using the "typescript-eslint-parser-for-extra-files" as sub-parser for Svelte parser.

However this doesn't seem to work for slot bindings -- those are still broken according to my testing.

ptrxyz commented 2 weeks ago

Even more digging: defining $$Slots in the component won't do it either. I further checked the emitted code that is passed to the typescript parser by the svelte-eslint-parser:

// script part
....
import Comp from './component.svelte'; 
....

code:

// code part
....
 (x);({ id: '1', value: 'something' })as ('Y' extends infer PROP?(PROP extends keyof import('svelte').ComponentProps<Comp>?(import('svelte').ComponentProps<Comp>[PROP]):(never)):(never));
(item: (Comp['$$slot_def']["SLOTNAME"]["PROPNAME"]))=>{if(item){(item.id);}};(Comp);`

So it seems that the type for the slot prop is correctly referred to as Comp['$$slot_def']["SLOTNAME"]["PROPNAME"]), yet Comp is a generic and this definition does not take that into account.

I removed the generic from my component and tried again: now eslint is not complaining anymore about an unsafe member access.

So instead of Comp[$$slot_def], i suppose there should be something like Comp<...GENERIC TYPES HERE...>[$$slot_def]. Relevant code might be this then: https://github.com/sveltejs/svelte-eslint-parser/blob/a7c0d33f82944cee03b01bf08252da15fd0257ea/src/parser/converts/attr.ts#L753C39-L753C47

tl;dr: when a component is generic, slot bindings are not correctly typed.