Skip to content

Commit af43c87

Browse files
committed
wip
1 parent db39572 commit af43c87

Some content is hidden

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

45 files changed

+574
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { createRule } from '../utils/index.js';
2+
import { getSourceCode } from '../utils/compat.js';
3+
import { getTypeScriptTools } from '../utils/ts-utils/index.js';
4+
import type { TSESTree } from '@typescript-eslint/types';
5+
6+
type TSInterfaceDeclarationWithId = TSESTree.TSInterfaceDeclaration & {
7+
id: TSESTree.Identifier;
8+
body: TSESTree.TSInterfaceBody;
9+
};
10+
11+
export default createRule('no-unused-props', {
12+
meta: {
13+
docs: {
14+
description: 'Warns about defined Props properties that are unused',
15+
category: 'Best Practices',
16+
recommended: false
17+
},
18+
schema: [],
19+
messages: {
20+
unusedProp: "'{{name}}' is an unused Props property."
21+
},
22+
type: 'suggestion',
23+
conditions: [
24+
{
25+
svelteVersions: ['5'],
26+
runes: [true, 'undetermined']
27+
}
28+
]
29+
},
30+
create(context) {
31+
const sourceCode = getSourceCode(context);
32+
const scopeManager = sourceCode.scopeManager;
33+
34+
// Get TypeScript tools using the provided utility.
35+
const _tsTools = getTypeScriptTools(context);
36+
37+
// Property names obtained from the Props interface.
38+
const declaredProps = new Map<string, TSESTree.Node>();
39+
let hasIndexSignature = false;
40+
41+
// Track used properties and rest usage.
42+
const usedProps = new Set<string>();
43+
const restUsage = new Map<string, boolean>(); // variableName -> hasRest
44+
45+
// Track renamed variables.
46+
const renamedVars = new Map<string, string>();
47+
48+
// Track used variables.
49+
const usedVars = new Set<string>();
50+
51+
// Track $props variables by name and its identifier node.
52+
const propsVars = new Set<string>();
53+
const propsNodes = new Map<string, TSESTree.Node>();
54+
55+
// Track processed interfaces to avoid infinite recursion.
56+
const processedInterfaces = new Set<string>();
57+
58+
// Collect declared properties from the Props interface.
59+
function collectPropsFromInterface(node: TSInterfaceDeclarationWithId) {
60+
if (!node.id || node.id.name !== 'Props') return;
61+
processedInterfaces.add(node.id.name);
62+
for (const m of node.body.body) {
63+
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
64+
declaredProps.set(m.key.name, m);
65+
// For nested properties, collect property paths (e.g., "nested.a").
66+
if (m.typeAnnotation?.typeAnnotation.type === 'TSTypeLiteral') {
67+
const nestedProps = m.typeAnnotation.typeAnnotation.members;
68+
for (const nestedProp of nestedProps) {
69+
if (
70+
nestedProp.type === 'TSPropertySignature' &&
71+
nestedProp.key.type === 'Identifier'
72+
) {
73+
const fullPath = `${m.key.name}.${nestedProp.key.name}`;
74+
declaredProps.set(fullPath, nestedProp);
75+
}
76+
}
77+
}
78+
}
79+
if (m.type === 'TSIndexSignature') {
80+
hasIndexSignature = true;
81+
}
82+
}
83+
// Handle extends clause.
84+
if (node.extends) {
85+
for (const heritage of node.extends) {
86+
if (heritage.expression.type === 'Identifier') {
87+
const baseName = heritage.expression.name;
88+
if (baseName === 'Props' || processedInterfaces.has(baseName)) continue;
89+
const program = sourceCode.ast;
90+
for (const scriptElem of program.body) {
91+
if (scriptElem.type === 'SvelteScriptElement') {
92+
const scriptProgram = scriptElem.body;
93+
if (scriptProgram && 'body' in scriptProgram) {
94+
const statements = scriptProgram.body as TSESTree.Statement[];
95+
for (const stmt of statements) {
96+
if (
97+
stmt.type === 'TSInterfaceDeclaration' &&
98+
stmt.id.type === 'Identifier' &&
99+
stmt.id.name === baseName
100+
) {
101+
collectPropsFromInterface(stmt as TSInterfaceDeclarationWithId);
102+
break;
103+
}
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
}
112+
113+
// Collect used property names from an object destructuring pattern.
114+
function markDestructuredProps(pattern: TSESTree.ObjectPattern, variableName: string) {
115+
for (const prop of pattern.properties) {
116+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
117+
const originalName = prop.key.name;
118+
if (prop.value.type === 'Identifier') {
119+
const renamedVar = prop.value.name;
120+
renamedVars.set(renamedVar, originalName);
121+
} else {
122+
usedProps.add(originalName);
123+
}
124+
}
125+
if (prop.type === 'RestElement') {
126+
restUsage.set(variableName, true);
127+
if (hasIndexSignature) {
128+
for (const [propName] of declaredProps) {
129+
usedProps.add(propName);
130+
}
131+
}
132+
}
133+
}
134+
}
135+
136+
function isPropsCall(initNode: TSESTree.Expression): boolean {
137+
return (
138+
initNode.type === 'CallExpression' &&
139+
initNode.callee.type === 'Identifier' &&
140+
initNode.callee.name === '$props'
141+
);
142+
}
143+
144+
// Traverse scopes and record variables assigned from $props().
145+
function analyzePropsUsageInScopes() {
146+
for (const scope of scopeManager.scopes) {
147+
for (const variable of scope.variables) {
148+
const def = variable.defs.find((d) => d.type === 'Variable');
149+
if (!def || !def.node || !def.node.init) continue;
150+
const typeAnn = def.node.id.typeAnnotation?.typeAnnotation;
151+
if (
152+
isPropsCall(def.node.init) &&
153+
typeAnn?.type === 'TSTypeReference' &&
154+
typeAnn.typeName.type === 'Identifier' &&
155+
typeAnn.typeName.name === 'Props'
156+
) {
157+
if (def.node.id.type === 'Identifier') {
158+
propsVars.add(def.node.id.name);
159+
propsNodes.set(def.node.id.name, def.node.id);
160+
}
161+
if (def.node.id.type === 'ObjectPattern') {
162+
markDestructuredProps(def.node.id, variable.name);
163+
}
164+
}
165+
}
166+
}
167+
}
168+
169+
return {
170+
TSInterfaceDeclaration(node: TSESTree.Node) {
171+
if (
172+
node.type === 'TSInterfaceDeclaration' &&
173+
node.id &&
174+
node.id.type === 'Identifier' &&
175+
node.id.name === 'Props'
176+
) {
177+
collectPropsFromInterface(node as TSInterfaceDeclarationWithId);
178+
}
179+
},
180+
Program() {
181+
analyzePropsUsageInScopes();
182+
},
183+
MemberExpression(node: TSESTree.MemberExpression) {
184+
let current: TSESTree.Expression | TSESTree.Super = node.object;
185+
const parts: string[] = [];
186+
if (node.property.type === 'Identifier') {
187+
parts.push(node.property.name);
188+
}
189+
while (current.type === 'MemberExpression') {
190+
if (current.property.type === 'Identifier') {
191+
parts.unshift(current.property.name);
192+
}
193+
current = current.object;
194+
}
195+
if (current.type === 'Identifier' && propsVars.has(current.name)) {
196+
let path = '';
197+
for (const part of parts) {
198+
path = path ? `${path}.${part}` : part;
199+
usedProps.add(path);
200+
}
201+
}
202+
},
203+
Identifier(node: TSESTree.Identifier) {
204+
usedVars.add(node.name);
205+
const originalName = renamedVars.get(node.name);
206+
if (originalName && usedVars.has(node.name)) {
207+
usedProps.add(originalName);
208+
}
209+
},
210+
'Program:exit'() {
211+
let hasRestWithIndexSignature = false;
212+
for (const [_, hasRest] of restUsage) {
213+
if (hasRest && hasIndexSignature) {
214+
hasRestWithIndexSignature = true;
215+
break;
216+
}
217+
}
218+
if (hasRestWithIndexSignature) {
219+
return;
220+
}
221+
for (const [propName, propNode] of declaredProps) {
222+
if (!usedProps.has(propName)) {
223+
context.report({
224+
node: propNode,
225+
messageId: 'unusedProp',
226+
data: { name: propName }
227+
});
228+
}
229+
}
230+
}
231+
};
232+
}
233+
});

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'email' is an unused Props property."
2+
line: 10
3+
column: 3
4+
suggestions: null
Lines changed: 15 additions & 0 deletions
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>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'extra' is an unused Props property."
2+
line: 4
3+
column: 3
4+
suggestions: null
Lines changed: 9 additions & 0 deletions
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>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'role' is an unused Props property."
2+
line: 4
3+
column: 3
4+
suggestions: null
Lines changed: 9 additions & 0 deletions
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>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'role' is an unused Props property."
2+
line: 5
3+
column: 3
4+
suggestions: null

0 commit comments

Comments
 (0)