Repository: sveltejs/eslint-plugin-svelte
Pull request #1135 (2025-03-17 13:02:21)

fix: correct detection of externally defined types in `no-unused-props` rule
close: https://github.com/sveltejs/eslint-plugin-svelte/issues/1133
close: https://github.com/sveltejs/eslint-plugin-svelte/issues/1136

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: bc2cbf4b7ed74945c0348b63a7e926707e4d2a11

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                 | Type  |
| -------------------- | ----- |
| eslint-plugin-svelte | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/sveltejs/eslint-plugin-svelte/new/fix/no-unused-props-2?filename=.changeset/curvy-cups-happen.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0Afix%3A%20correct%20detection%20of%20externally%20defined%20types%20in%20%60no-unused-props%60%20rule%0A)


github-actions[bot]: <!-- posted by pkg.pr.new-comment.mjs -->

## Try the Instant Preview in Online Playground

[ESLint Online Playground](https://eslint-online-playground.netlify.app/?overrideDeps=%7B%22eslint-plugin-svelte%22%3A%22https%3A%2F%2Fpkg.pr.new%2Feslint-plugin-svelte%401135%22%7D#eslint-plugin-svelte%20with%20typescript)

## Install the Instant Preview to Your Local

```
npm i https://pkg.pr.new/eslint-plugin-svelte@1135
```

## Published Instant Preview Packages:

- eslint-plugin-svelte: https://pkg.pr.new/eslint-plugin-svelte@1135

[View Commit](https://github.com/sveltejs/eslint-plugin-svelte/commit/bc2cbf4b7ed74945c0348b63a7e926707e4d2a11)

Patch:
diff --git a/.changeset/plain-chairs-tie.md b/.changeset/plain-chairs-tie.md
new file mode 100644
index 0000000..5b5c46d
--- /dev/null
+++ b/.changeset/plain-chairs-tie.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix: correct detection of externally defined types in `no-unused-props` rule
diff --git a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
index 0d057b3..d425026 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
@@ -80,15 +80,11 @@ export default createRule('no-unused-props', {
 			return shouldIgnore(typeStr) || (symbolName ? shouldIgnore(symbolName) : false);
 		}
 
-		function isExternalType(type: ts.Type): boolean {
-			const symbol = type.getSymbol();
-			if (!symbol) return false;
-
+		function isInternalProperty(symbol: ts.Symbol): boolean {
 			const declarations = symbol.getDeclarations();
 			if (!declarations || declarations.length === 0) return false;
 
-			const sourceFile = declarations[0].getSourceFile();
-			return sourceFile.fileName !== fileName;
+			return declarations.every((decl) => decl.getSourceFile().fileName === fileName);
 		}
 
 		/**
@@ -200,7 +196,6 @@ export default createRule('no-unused-props', {
 			if (checkedTypes.has(typeStr)) return;
 			checkedTypes.add(typeStr);
 			if (shouldIgnoreType(type)) return;
-			if (!checkImportedTypes && isExternalType(type)) return;
 
 			const properties = typeChecker.getPropertiesOfType(type);
 			const baseTypes = type.getBaseTypes();
@@ -225,6 +220,7 @@ export default createRule('no-unused-props', {
 
 			for (const prop of properties) {
 				if (isBuiltInProperty(prop)) continue;
+				if (!checkImportedTypes && !isInternalProperty(prop)) continue;
 
 				const propName = prop.getName();
 				const currentPath = [...parentPath, propName];


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-errors.yaml
new file mode 100644
index 0000000..502c154
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-errors.yaml
@@ -0,0 +1,4 @@
+- message: "'child2' is an unused Props property."
+  line: 9
+  column: 6
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-input.svelte
new file mode 100644
index 0000000..2d26bd2
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/ignore-external-type-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	import type { FooDTO } from './shared-types';
+
+	interface Props extends FooDTO {
+		child1: string;
+		child2: number;
+	}
+
+	let { foo, child1 }: Props = $props();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/imported-type-check-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/imported-type-check-errors.yaml
index 2f34147..278805b 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/imported-type-check-errors.yaml
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/imported-type-check-errors.yaml
@@ -1,6 +1,4 @@
 - message: "'name' is an unused Props property."
   line: 6
   column: 6
-  endLine: 6
-  endColumn: 20
   suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/shared-types.ts b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/shared-types.ts
index a60e61e..e33a3a2 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/shared-types.ts
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/invalid/shared-types.ts
@@ -1,3 +1,14 @@
 export interface BaseProps {
 	name: string;
 }
+
+export interface FooDTO {
+	foo: string;
+	bar: number;
+	baz: BazDTO;
+}
+
+interface BazDTO {
+	qux: string;
+	quux: number;
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/ignore-external-type-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/ignore-external-type-input.svelte
new file mode 100644
index 0000000..920dece
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/ignore-external-type-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	import type { FooDTO } from './shared-types';
+
+	interface Props extends FooDTO {
+		child1: string;
+		child2: number;
+	}
+
+	let { foo, child1, child2 }: Props = $props();
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/shared-types.ts b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/shared-types.ts
index a60e61e..e33a3a2 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/shared-types.ts
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/shared-types.ts
@@ -1,3 +1,14 @@
 export interface BaseProps {
 	name: string;
 }
+
+export interface FooDTO {
+	foo: string;
+	bar: number;
+	baz: BazDTO;
+}
+
+interface BazDTO {
+	qux: string;
+	quux: number;
+}