Skip to content

Commit 3c33117

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

Some content is hidden

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

47 files changed

+534
-0
lines changed

.changeset/twelve-beers-talk.md

+5
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

+1
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 | |
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

+1
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 | |
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

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
14+
## :book: Rule Details
15+
16+
This rule reports properties that are defined in Props but never used in the component code.
17+
It helps to detect dead code and improve component clarity by ensuring that every declared prop is utilized.
18+
19+
This rule checks various usage patterns of props:
20+
21+
- Direct property access
22+
- Destructuring assignment
23+
- Method calls
24+
- Computed property access
25+
- Object spread
26+
- Constructor calls (new expressions)
27+
- Assignment to other variables
28+
29+
<!--eslint-skip-->
30+
31+
```svelte
32+
<!-- ✓ Good Examples -->
33+
<script lang="ts">
34+
// Direct property access
35+
const props = $props<{ value: string }>();
36+
console.log(props.value);
37+
38+
// Destructuring assignment
39+
const { width, height } = $props<{ width: number; height: number }>();
40+
console.log(width, height);
41+
42+
// Method calls
43+
const props2 = $props<{ callback: () => void }>();
44+
props2.callback();
45+
46+
// Computed property access
47+
const props3 = $props<{ 'data-value': string }>();
48+
const value = props3['data-value'];
49+
50+
// Constructor calls
51+
const props4 = $props<{ config: { new(): any } }>();
52+
new props4.config();
53+
</script>
54+
55+
<!-- ✗ Bad Examples -->
56+
<script lang="ts">
57+
// Unused property 'b'
58+
const props = $props<{ a: string; b: number }>();
59+
console.log(props.a);
60+
61+
// Unused property in destructuring
62+
const { x } = $props<{ x: number; y: number }>();
63+
console.log(x);
64+
</script>
65+
```
66+
67+
## :wrench: Options
68+
69+
Nothing.
70+
71+
## :mag: Implementation
72+
73+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts)
74+
- [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/rule-types.ts

+5
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/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
},
20+
type: 'suggestion',
21+
conditions: [
22+
{
23+
svelteVersions: ['5'],
24+
runes: [true, 'undetermined']
25+
}
26+
]
27+
},
28+
create(context) {
29+
const tools = getTypeScriptTools(context);
30+
if (!tools) {
31+
return {};
32+
}
33+
34+
const typeChecker = tools.service.program.getTypeChecker();
35+
if (!typeChecker) {
36+
return {};
37+
}
38+
39+
function getUsedPropertyNames(node: TSESTree.Identifier): (string | typeof unknown)[] {
40+
const variable = findVariable(context, node);
41+
if (!variable) {
42+
return [unknown];
43+
}
44+
45+
const usedProps = new Set<string | typeof unknown>();
46+
47+
for (const reference of variable.references) {
48+
const parent = reference.identifier.parent;
49+
if (!parent) continue;
50+
51+
if (parent.type === 'MemberExpression' && parent.object === reference.identifier) {
52+
if (parent.property.type === 'Identifier' && !parent.computed) {
53+
usedProps.add(parent.property.name);
54+
} else {
55+
usedProps.add(unknown);
56+
}
57+
} else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
58+
if (
59+
'arguments' in parent &&
60+
Array.isArray(parent.arguments) &&
61+
parent.arguments.some((arg): arg is TSESTree.Identifier => arg === reference.identifier)
62+
) {
63+
usedProps.add(unknown);
64+
}
65+
} else if (parent.type === 'AssignmentExpression' || parent.type === 'AssignmentPattern') {
66+
usedProps.add(unknown);
67+
} else if (parent.type === 'SpreadElement') {
68+
usedProps.add(unknown);
69+
}
70+
}
71+
72+
return Array.from(usedProps);
73+
}
74+
75+
return {
76+
'VariableDeclaration > VariableDeclarator': (node: TSESTree.VariableDeclarator) => {
77+
if (
78+
node.init?.type !== 'CallExpression' ||
79+
node.init.callee.type !== 'Identifier' ||
80+
node.init.callee.name !== '$props'
81+
) {
82+
return;
83+
}
84+
85+
const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node) as ts.VariableDeclaration;
86+
if (!tsNode || !tsNode.type) return;
87+
const checker = tools.service.program.getTypeChecker();
88+
const propType = checker.getTypeFromTypeNode(tsNode.type);
89+
const properties = checker.getPropertiesOfType(propType);
90+
const propNames = properties.map((p) => p.getName());
91+
92+
const usedNames: (string | typeof unknown)[] = [];
93+
if (node.id.type === 'ObjectPattern') {
94+
for (const prop of node.id.properties) {
95+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
96+
usedNames.push(prop.key.name);
97+
} else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
98+
usedNames.push(...getUsedPropertyNames(prop.argument));
99+
}
100+
}
101+
} else if (node.id.type === 'Identifier' && node.id.typeAnnotation) {
102+
usedNames.push(...getUsedPropertyNames(node.id));
103+
}
104+
105+
if (usedNames.includes(unknown)) {
106+
return;
107+
}
108+
for (const propName of propNames) {
109+
if (!usedNames.includes(propName)) {
110+
context.report({
111+
node: node.id,
112+
messageId: 'unusedProp',
113+
data: {
114+
name: propName
115+
}
116+
});
117+
}
118+
}
119+
}
120+
};
121+
}
122+
});

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

+2
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,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0-0"
3+
}
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
interface BaseProps {
3+
id: string;
4+
type: 'user' | 'admin';
5+
role: string; // unused
6+
}
7+
8+
interface Props extends BaseProps {
9+
name: string;
10+
email: string; // unused
11+
}
12+
13+
let props: Props = $props();
14+
console.log(props.id, props.type, props.name);
15+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'extra' is an unused Props property."
2+
line: 6
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import type { GenericProps } from './types';
3+
interface Props extends GenericProps<string> {
4+
extra: boolean; // unused
5+
}
6+
let { data, loading }: Props = $props();
7+
console.log(data, loading);
8+
// extra is unused
9+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'role' is an unused Props property."
2+
line: 6
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import type { ExtendedProps } from './types';
3+
interface Props extends ExtendedProps {
4+
role: string; // unused
5+
}
6+
let { name, age }: Props = $props();
7+
console.log(name, age);
8+
// role is unused
9+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'role' is an unused Props property."
2+
line: 8
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
interface Props {
3+
name: string;
4+
age: number;
5+
role: string; // unused
6+
[key: string]: string | number;
7+
}
8+
let { name, age }: Props = $props();
9+
console.log(name, age);
10+
// ...rest is missing, so the role and index signature properties are unused.
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: "'permissions' is an unused Props property."
2+
line: 17
3+
column: 6
4+
suggestions: null
5+
- message: "'email' is an unused Props property."
6+
line: 17
7+
column: 6
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
interface BaseProps {
3+
id: string;
4+
type: 'user' | 'admin';
5+
}
6+
7+
interface UserProps extends BaseProps {
8+
name: string;
9+
email: string; // unused
10+
}
11+
12+
interface Props extends UserProps {
13+
role: string;
14+
permissions: string[]; // unused
15+
}
16+
17+
let props: Props = $props();
18+
console.log(props.id, props.type, props.name, props.role);
19+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'age' is an unused Props property."
2+
line: 8
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
interface Props {
3+
name: string;
4+
age: number; // unused
5+
[key: string]: string | number | boolean;
6+
[index: number]: string | number;
7+
}
8+
let { name, ...rest }: Props = $props();
9+
console.log(name);
10+
// rest is unused, but since there is an index signature, the warning is only for age.
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface SharedProps {
2+
id: string;
3+
name: string;
4+
description: string; // 未使用
5+
metadata: {
6+
created: string;
7+
updated: string; // 未使用
8+
};
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'age' is an unused Props property."
2+
line: 6
3+
column: 6
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
interface Props {
3+
name: string;
4+
age: number; // unused
5+
}
6+
let { name }: Props = $props();
7+
console.log(name);
8+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0-0"
3+
}

0 commit comments

Comments
 (0)