Skip to content

svelte/no-unused-props misinterprets props as internal properties #1335

@InkedCat

Description

@InkedCat

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.34.0

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

3.12.2

What did you do?

I created a Svelte layout file, defined the props return type, but intentionally left one of the properties unused. The svelte/no-unused-props rule was never triggered on Windows. However, on Linux and macOS, ESLint correctly reported the unused prop as an error when linting the file.

Configuration
import prettier from 'eslint-config-prettier';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';

const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));

export default ts.config(
	includeIgnoreFile(gitignorePath),
	js.configs.recommended,
	...ts.configs.recommended,
	...svelte.configs.recommended,
	prettier,
	...svelte.configs.prettier,
	{
		languageOptions: {
			globals: { ...globals.browser, ...globals.node }
		},
		rules: {
			// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
			// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
			'no-undef': 'off'
		}
	},
	{
		files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
		languageOptions: {
			parserOptions: {
				projectService: true,
				extraFileExtensions: ['.svelte'],
				parser: ts.parser,
				svelteConfig
			}
		}
	}
);
<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { LayoutData } from './$types';

	let { children }: { data: LayoutData; children: Snippet } = $props();
</script>

{@render children?.()}

What did you expect to happen?

I expected this snippet to trigger the rule, since the data field is declared in the type but not used.

What actually happened?

The rule is never applied, ESLint does not report any errors. This was tested on GitHub Runners and locally, and the issue only occurs in Windows environments. Linux and macOS are unaffected.

Link to GitHub Repo with Minimal Reproducible Example

https://github.com/InkedCat/no-unused-props-bug-env

Here is a link to one of my CI run where the bug happened: https://github.com/InkedCat/Vault-Launcher/actions/runs/17535461642

Additional comments

I've done some debugging and believe I've found the source of the bug. In the checkUnusedProperties function (line 240 of no-unused-props.ts), the for (const prop of properties) loop checks whether properties are imported types or from internal sources. The isInternalProperty function in the same file compares the source file path inferred by Typescript declarations with the one passed down by ESLint.

The issue arises from how file paths are handled on Windows: ESLint does normalize the paths (they contains double back slashes, \\), whereas Typescript does not and gets Windows like paths. Therefore the property is considered internal because the paths are differents.

Path format example:
Eslint: C:\\Users\\...\\VaultLauncher\\src\\lib\\components\\navbar\\nav-main.svelte
Typescript: C:/Users/.../VaultLauncher/src/lib/components/navbar/nav-main.svelte

After applying a small local patch to normalize the Typescript path, the rule is now correctly triggered on Windows.

		function isInternalProperty(symbol: ts.Symbol): boolean {
			const declarations = symbol.getDeclarations();
			if (!declarations || declarations.length === 0) return false;

			return declarations.every(
				(decl) => path.normalize(decl.getSourceFile().fileName) === fileName
			);
		}

If needed, I can take some time to open a PR and fix this 👍

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