Skip to content

Duplicated rules for *.svelte.ts files with recommended ts configs. #1382

@brendan-morin

Description

@brendan-morin

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

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.

What version of ESLint are you using?

9.36.0

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

3.12.4

What did you do?

Configuration from README:

Configuration
import js from "@eslint/js";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import ts from "typescript-eslint";
import svelteConfig from "./svelte.config.js";

export default ts.config(
    js.configs.recommended,
    ...ts.configs.recommended,
    ...svelte.configs.recommended,
    {
        languageOptions: {
            globals: {
                ...globals.browser,
                ...globals.node,
            },
        },
    },
    {
        files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
        // See more details at: https://typescript-eslint.io/packages/parser/
        languageOptions: {
            parserOptions: {
                projectService: true,
                extraFileExtensions: [".svelte"], // Add support for additional file extensions, such as .svelte
                parser: ts.parser,
                // Specify a parser for each language, if needed:
                // parser: {
                //   ts: ts.parser,
                //   js: espree,    // Use espree for .js files (add: import espree from 'espree')
                //   typescript: ts.parser
                // },

                // We recommend importing and specifying svelte.config.js.
                // By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
                // While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it,
                // explicitly specifying it ensures better compatibility and functionality.
                //
                // If non-serializable properties are included, running ESLint with the --cache flag will fail.
                // In that case, please remove the non-serializable properties. (e.g. `svelteConfig: { ...svelteConfig, kit: { ...svelteConfig.kit, typescript: undefined }}`)
                svelteConfig,
            },
        },
    },
    {
        rules: {
            // Override or add rule settings here, such as:
            // 'svelte/rule-name': 'error'
        },
    }
);

Below svelte code copied verbatim from shadcn-svelte data table component.

import {
    type RowData,
    type TableOptions,
    type TableOptionsResolved,
    type TableState,
    createTable,
} from "@tanstack/table-core";

/**
 * Creates a reactive TanStack table object for Svelte.
 * @param options Table options to create the table with.
 * @returns A reactive table object.
 * @example
 * ```svelte
 * <script>
 *   const table = createSvelteTable({ ... })
 * </script>
 *
 * <table>
 *   <thead>
 *     {#each table.getHeaderGroups() as headerGroup}
 *       <tr>
 *         {#each headerGroup.headers as header}
 *           <th colspan={header.colSpan}>
 *         	   <FlexRender content={header.column.columnDef.header} context={header.getContext()} />
 *         	 </th>
 *         {/each}
 *       </tr>
 *     {/each}
 *   </thead>
 * 	 <!-- ... -->
 * </table>
 * ```
 */
export function createSvelteTable<TData extends RowData>(options: TableOptions<TData>) {
    const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(
        {
            state: {},
            onStateChange() {},
            renderFallbackValue: null,
            mergeOptions: (
                defaultOptions: TableOptions<TData>,
                options: Partial<TableOptions<TData>>
            ) => {
                return mergeObjects(defaultOptions, options);
            },
        },
        options
    );

    const table = createTable(resolvedOptions);
    let state = $state<Partial<TableState>>(table.initialState);

    function updateOptions() {
        table.setOptions((prev) => {
            return mergeObjects(prev, options, {
                state: mergeObjects(state, options.state || {}),

                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                onStateChange: (updater: any) => {
                    if (updater instanceof Function) state = updater(state);
                    else state = mergeObjects(state, updater);

                    options.onStateChange?.(updater);
                },
            });
        });
    }

    updateOptions();

    $effect.pre(() => {
        updateOptions();
    });

    return table;
}

type MaybeThunk<T extends object> = T | (() => T | null | undefined);
type Intersection<T extends readonly unknown[]> = (T extends [infer H, ...infer R]
    ? H & Intersection<R>
    : unknown) & {};

/**
 * Lazily merges several objects (or thunks) while preserving
 * getter semantics from every source.
 *
 * Proxy-based to avoid known WebKit recursion issue.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mergeObjects<Sources extends readonly MaybeThunk<any>[]>(
    ...sources: Sources
): Intersection<{ [K in keyof Sources]: Sources[K] }> {
    const resolve = <T extends object>(src: MaybeThunk<T>): T | undefined =>
        typeof src === "function" ? (src() ?? undefined) : src;

    const findSourceWithKey = (key: PropertyKey) => {
        for (let i = sources.length - 1; i >= 0; i--) {
            const obj = resolve(sources[i]);
            if (obj && key in obj) return obj;
        }
        return undefined;
    };

    return new Proxy(Object.create(null), {
        get(_, key) {
            const src = findSourceWithKey(key);

            return src?.[key as never];
        },

        has(_, key) {
            return !!findSourceWithKey(key);
        },

        ownKeys(): (string | symbol)[] {
            const all = new Set<string | symbol>();
            for (const s of sources) {
                const obj = resolve(s);
                if (obj) {
                    for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) {
                        all.add(k);
                    }
                }
            }
            return [...all];
        },

        getOwnPropertyDescriptor(_, key) {
            const src = findSourceWithKey(key);
            if (!src) return undefined;
            return {
                configurable: true,
                enumerable: true,
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                value: (src as any)[key],
                writable: true,
            };
        },
    }) as Intersection<{ [K in keyof Sources]: Sources[K] }>;
}

What did you expect to happen?

svelte rules should trigger once only for svelte.ts files

What actually happened?

svelte rules trigger twice:

% npm run lint

> [email protected] lint
> prettier --check . && eslint .

Checking formatting...
All matched files use Prettier code style!

...

/.../src/lib/components/ui/data-table/data-table.svelte.ts
  117:25  error  Found a mutable instance of the built-in Set class. Use SvelteSet instead  svelte/prefer-svelte-reactivity
  117:25  error  Found a mutable instance of the built-in Set class. Use SvelteSet instead  svelte/prefer-svelte-reactivity

✖ 19 problems (19 errors, 0 warnings)

This is on a fresh install where we installed shadcn-svelte to verify

Link to GitHub Repo with Minimal Reproducible Example

eslint online playground does not seem to work. Repro steps:

  1. initialize a svelte project
  2. add the above file
  3. run npm run lint
  4. see duplicate eslint errors

Additional comments

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions