Skip to content

Commit 3e35e1f

Browse files
committed
feat: add no-unused-props rule
1 parent db39572 commit 3e35e1f

File tree

52 files changed

+666
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+666
-0
lines changed

.changeset/twelve-beers-talk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: add `no-unused-props` rule

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ These rules relate to better ways of doing things to help you avoid problems:
362362
| [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :star::bulb: |
363363
| [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. | :star: |
364364
| [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | |
365+
| [svelte/no-unused-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-props/) | Warns about defined Props properties that are unused | :star: |
365366
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
366367
| [svelte/no-useless-children-snippet](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-children-snippet/) | disallow explicit children snippet where it's not needed | :star: |
367368
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :star::wrench: |

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ These rules relate to better ways of doing things to help you avoid problems:
5959
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :star::bulb: |
6060
| [svelte/no-svelte-internal](./rules/no-svelte-internal.md) | svelte/internal will be removed in Svelte 6. | :star: |
6161
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
62+
| [svelte/no-unused-props](./rules/no-unused-props.md) | Warns about defined Props properties that are unused | :star: |
6263
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
6364
| [svelte/no-useless-children-snippet](./rules/no-useless-children-snippet.md) | disallow explicit children snippet where it's not needed | :star: |
6465
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :star::wrench: |

docs/rules/no-unused-props.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/no-unused-props'
5+
description: 'Warns about defined Props properties that are unused'
6+
---
7+
8+
# svelte/no-unused-props
9+
10+
> Warns about defined Props properties that are unused
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :gear: This rule is included in `"plugin:svelte/recommended"`.
14+
15+
## :book: Rule Details
16+
17+
This rule reports properties that are defined in Props but never used in the component code.
18+
It helps to detect dead code and improve component clarity by ensuring that every declared prop is utilized.
19+
20+
This rule checks various usage patterns of props:
21+
22+
- Direct property access
23+
- Destructuring assignment
24+
- Method calls
25+
- Computed property access
26+
- Object spread
27+
- Constructor calls (new expressions)
28+
- Assignment to other variables
29+
- Index signatures (e.g. `[key: string]: unknown`)
30+
31+
Additionally, this rule checks if index signatures are properly used. When an index signature is defined but not captured using the rest operator (`...`), the rule will suggest using it.
32+
33+
:warning: This rule requires `@typescript-eslint/parser` to work. Make sure you have installed `@typescript-eslint/parser` and configured it in your ESLint configuration. Therefore, the rule violations cannot be seen in the examples on this page because this documentation does not use `@typescript-eslint/parser`.
34+
35+
<!--eslint-skip-->
36+
37+
```svelte
38+
<!-- ✓ Good Examples -->
39+
<script lang="ts">
40+
/* eslint svelte/no-unused-props: "error" */
41+
// Direct property access
42+
const props: { value: string } = $props();
43+
console.log(props.value);
44+
</script>
45+
```
46+
47+
```svelte
48+
<!-- ✓ Good Examples -->
49+
<script lang="ts">
50+
/* eslint svelte/no-unused-props: "error" */
51+
// Destructuring assignment
52+
const { width, height }: { width: number; height: number } = $props();
53+
console.log(width, height);
54+
</script>
55+
```
56+
57+
```svelte
58+
<!-- ✓ Good Examples -->
59+
<script lang="ts">
60+
/* eslint svelte/no-unused-props: "error" */
61+
// Method calls
62+
const props2: { callback: () => void } = $props();
63+
props2.callback();
64+
</script>
65+
```
66+
67+
```svelte
68+
<!-- ✓ Good Examples -->
69+
<script lang="ts">
70+
/* eslint svelte/no-unused-props: "error" */
71+
// Computed property access
72+
const props3: { 'data-value': string } = $props();
73+
const value = props3['data-value'];
74+
</script>
75+
```
76+
77+
```svelte
78+
<!-- ✓ Good Examples -->
79+
<script lang="ts">
80+
/* eslint svelte/no-unused-props: "error" */
81+
// Constructor calls
82+
const props4: { config: { new (): any } } = $props();
83+
new props4.config();
84+
</script>
85+
```
86+
87+
```svelte
88+
<!-- ✓ Good Examples -->
89+
<script lang="ts">
90+
/* eslint svelte/no-unused-props: "error" */
91+
// Using index signature with rest operator
92+
interface Props {
93+
a: number;
94+
[key: string]: unknown;
95+
}
96+
let { a, ...rest }: Props = $props();
97+
console.log(rest);
98+
</script>
99+
```
100+
101+
```svelte
102+
<!-- ✗ Bad Examples -->
103+
<script lang="ts">
104+
/* eslint svelte/no-unused-props: "error" */
105+
// Unused property 'b'
106+
const props: { a: string; b: number } = $props();
107+
console.log(props.a);
108+
</script>
109+
```
110+
111+
```svelte
112+
<!-- ✗ Bad Examples -->
113+
<script lang="ts">
114+
/* eslint svelte/no-unused-props: "error" */
115+
// Unused property in destructuring
116+
const { x }: { x: number; y: number } = $props();
117+
console.log(x);
118+
</script>
119+
```
120+
121+
```svelte
122+
<!-- ✗ Bad Examples -->
123+
<script lang="ts">
124+
/* eslint svelte/no-unused-props: "error" */
125+
// Unused index signature
126+
interface Props {
127+
a: number;
128+
[key: string]: unknown; // This will be reported
129+
}
130+
let { a }: Props = $props();
131+
</script>
132+
```
133+
134+
## :wrench: Options
135+
136+
Nothing.
137+
138+
## :gear: Required Configuration
139+
140+
This rule requires `@typescript-eslint/parser` to work. Here's an example configuration:
141+
142+
```json
143+
{
144+
"parser": "@typescript-eslint/parser",
145+
"parserOptions": {
146+
"project": "./tsconfig.json"
147+
},
148+
"rules": {
149+
"svelte/no-unused-props": "error"
150+
}
151+
}
152+
```
153+
154+
## :mag: Implementation
155+
156+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts)
157+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-unused-props.ts)

packages/eslint-plugin-svelte/src/configs/flat/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const config: Linter.Config[] = [
3232
'svelte/no-store-async': 'error',
3333
'svelte/no-svelte-internal': 'error',
3434
'svelte/no-unknown-style-directive-property': 'error',
35+
'svelte/no-unused-props': 'error',
3536
'svelte/no-unused-svelte-ignore': 'error',
3637
'svelte/no-useless-children-snippet': 'error',
3738
'svelte/no-useless-mustaches': 'error',

packages/eslint-plugin-svelte/src/rule-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ export interface RuleOptions {
261261
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/
262262
*/
263263
'svelte/no-unused-class-name'?: Linter.RuleEntry<SvelteNoUnusedClassName>
264+
/**
265+
* Warns about defined Props properties that are unused
266+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-props/
267+
*/
268+
'svelte/no-unused-props'?: Linter.RuleEntry<[]>
264269
/**
265270
* disallow unused svelte-ignore comments
266271
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { createRule } from '../utils/index.js';
2+
import { getTypeScriptTools } from '../utils/ts-utils/index.js';
3+
import type { TSESTree } from '@typescript-eslint/types';
4+
import type ts from 'typescript';
5+
import { findVariable } from '../utils/ast-utils.js';
6+
7+
const unknown = Symbol('unknown');
8+
9+
export default createRule('no-unused-props', {
10+
meta: {
11+
docs: {
12+
description: 'Warns about defined Props properties that are unused',
13+
category: 'Best Practices',
14+
recommended: true
15+
},
16+
schema: [],
17+
messages: {
18+
unusedProp: "'{{name}}' is an unused Props property.",
19+
unusedIndexSignature:
20+
'Index signature is unused. Consider using rest operator (...) to capture remaining properties.'
21+
},
22+
type: 'suggestion',
23+
conditions: [
24+
{
25+
svelteVersions: ['5'],
26+
runes: [true, 'undetermined']
27+
}
28+
]
29+
},
30+
create(context) {
31+
const tools = getTypeScriptTools(context);
32+
if (!tools) {
33+
return {};
34+
}
35+
36+
const typeChecker = tools.service.program.getTypeChecker();
37+
if (!typeChecker) {
38+
return {};
39+
}
40+
41+
function getUsedPropertyNames(node: TSESTree.Identifier): (string | typeof unknown)[] {
42+
const variable = findVariable(context, node);
43+
if (!variable) {
44+
return [unknown];
45+
}
46+
47+
const usedProps = new Set<string | typeof unknown>();
48+
49+
for (const reference of variable.references) {
50+
const parent = reference.identifier.parent;
51+
if (!parent) continue;
52+
53+
if (parent.type === 'MemberExpression' && parent.object === reference.identifier) {
54+
if (parent.property.type === 'Identifier' && !parent.computed) {
55+
usedProps.add(parent.property.name);
56+
} else {
57+
usedProps.add(unknown);
58+
}
59+
} else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
60+
if (
61+
'arguments' in parent &&
62+
Array.isArray(parent.arguments) &&
63+
parent.arguments.some((arg): arg is TSESTree.Identifier => arg === reference.identifier)
64+
) {
65+
usedProps.add(unknown);
66+
}
67+
} else if (parent.type === 'AssignmentExpression' || parent.type === 'AssignmentPattern') {
68+
usedProps.add(unknown);
69+
} else if (parent.type === 'SpreadElement') {
70+
usedProps.add(unknown);
71+
}
72+
}
73+
74+
return Array.from(usedProps);
75+
}
76+
77+
return {
78+
'VariableDeclaration > VariableDeclarator': (node: TSESTree.VariableDeclarator) => {
79+
if (
80+
node.init?.type !== 'CallExpression' ||
81+
node.init.callee.type !== 'Identifier' ||
82+
node.init.callee.name !== '$props'
83+
) {
84+
return;
85+
}
86+
87+
const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node) as ts.VariableDeclaration;
88+
if (!tsNode || !tsNode.type) return;
89+
const checker = tools.service.program.getTypeChecker();
90+
const propType = checker.getTypeFromTypeNode(tsNode.type);
91+
const properties = checker.getPropertiesOfType(propType);
92+
const propNames = properties.map((p) => p.getName());
93+
94+
const usedNames: (string | typeof unknown)[] = [];
95+
let hasRestElement = false;
96+
97+
if (node.id.type === 'ObjectPattern') {
98+
for (const prop of node.id.properties) {
99+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
100+
usedNames.push(prop.key.name);
101+
} else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
102+
hasRestElement = true;
103+
usedNames.push(...getUsedPropertyNames(prop.argument));
104+
}
105+
}
106+
} else if (node.id.type === 'Identifier' && node.id.typeAnnotation) {
107+
usedNames.push(...getUsedPropertyNames(node.id));
108+
}
109+
110+
if (usedNames.includes(unknown)) {
111+
return;
112+
}
113+
114+
const indexType = propType.getStringIndexType();
115+
const numberIndexType = propType.getNumberIndexType();
116+
const hasIndexSignature = indexType || numberIndexType;
117+
118+
for (const propName of propNames) {
119+
if (!usedNames.includes(propName)) {
120+
context.report({
121+
node: node.id,
122+
messageId: 'unusedProp',
123+
data: {
124+
name: propName
125+
}
126+
});
127+
}
128+
}
129+
130+
if (hasIndexSignature && !hasRestElement) {
131+
context.report({
132+
node: node.id,
133+
messageId: 'unusedIndexSignature'
134+
});
135+
}
136+
}
137+
};
138+
}
139+
});

packages/eslint-plugin-svelte/src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import noTargetBlank from '../rules/no-target-blank.js';
5151
import noTrailingSpaces from '../rules/no-trailing-spaces.js';
5252
import noUnknownStyleDirectiveProperty from '../rules/no-unknown-style-directive-property.js';
5353
import noUnusedClassName from '../rules/no-unused-class-name.js';
54+
import noUnusedProps from '../rules/no-unused-props.js';
5455
import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
5556
import noUselessChildrenSnippet from '../rules/no-useless-children-snippet.js';
5657
import noUselessMustaches from '../rules/no-useless-mustaches.js';
@@ -123,6 +124,7 @@ export const rules = [
123124
noTrailingSpaces,
124125
noUnknownStyleDirectiveProperty,
125126
noUnusedClassName,
127+
noUnusedProps,
126128
noUnusedSvelteIgnore,
127129
noUselessChildrenSnippet,
128130
noUselessMustaches,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0-0"
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: "'email' is an unused Props property."
2+
line: 13
3+
column: 6
4+
suggestions: null
5+
- message: "'role' is an unused Props property."
6+
line: 13
7+
column: 6
8+
suggestions: null

0 commit comments

Comments
 (0)