Skip to content

Commit b18302b

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

File tree

53 files changed

+645
-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.

53 files changed

+645
-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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
30+
: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`.
31+
32+
<!--eslint-skip-->
33+
34+
```svelte
35+
<!-- ✓ Good Examples -->
36+
<script lang="ts">
37+
/* eslint svelte/no-unused-props: "error" */
38+
// Direct property access
39+
const props: { value: string } = $props();
40+
console.log(props.value);
41+
</script>
42+
```
43+
44+
```svelte
45+
<!-- ✓ Good Examples -->
46+
<script lang="ts">
47+
/* eslint svelte/no-unused-props: "error" */
48+
// Destructuring assignment
49+
const { width, height }: { width: number; height: number } = $props();
50+
console.log(width, height);
51+
</script>
52+
```
53+
54+
```svelte
55+
<!-- ✓ Good Examples -->
56+
<script lang="ts">
57+
/* eslint svelte/no-unused-props: "error" */
58+
// Method calls
59+
const props2: { callback: () => void } = $props();
60+
props2.callback();
61+
</script>
62+
```
63+
64+
```svelte
65+
<!-- ✓ Good Examples -->
66+
<script lang="ts">
67+
/* eslint svelte/no-unused-props: "error" */
68+
// Computed property access
69+
const props3: { 'data-value': string } = $props();
70+
const value = props3['data-value'];
71+
</script>
72+
```
73+
74+
```svelte
75+
<!-- ✓ Good Examples -->
76+
<script lang="ts">
77+
/* eslint svelte/no-unused-props: "error" */
78+
// Constructor calls
79+
const props4: { config: { new (): any } } = $props();
80+
new props4.config();
81+
</script>
82+
```
83+
84+
```svelte
85+
<!-- ✗ Bad Examples -->
86+
<script lang="ts">
87+
/* eslint svelte/no-unused-props: "error" */
88+
// Unused property 'b'
89+
const props: { a: string; b: number } = $props();
90+
console.log(props.a);
91+
</script>
92+
```
93+
94+
```svelte
95+
<!-- ✗ Bad Examples -->
96+
<script lang="ts">
97+
/* eslint svelte/no-unused-props: "error" */
98+
// Unused property in destructuring
99+
const { x }: { x: number; y: number } = $props();
100+
console.log(x);
101+
</script>
102+
```
103+
104+
## :wrench: Options
105+
106+
Nothing.
107+
108+
## :gear: Required Configuration
109+
110+
This rule requires `@typescript-eslint/parser` to work. Here's an example configuration:
111+
112+
```json
113+
{
114+
"parser": "@typescript-eslint/parser",
115+
"parserOptions": {
116+
"project": "./tsconfig.json"
117+
},
118+
"rules": {
119+
"svelte/no-unused-props": "error"
120+
}
121+
}
122+
```
123+
124+
## :mag: Implementation
125+
126+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts)
127+
- [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)