Skip to content

Commit c69ad56

Browse files
committed
fix
1 parent 98fc478 commit c69ad56

12 files changed

+193
-19
lines changed

.changeset/real-books-stare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix(no-unused-props): validate spread operator properly

packages/eslint-plugin-svelte/src/rules/no-unused-props.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type ts from 'typescript';
55
import { findVariable } from '../utils/ast-utils.js';
66
import { toRegExp } from '../utils/regexp.js';
77
import { normalize } from 'path';
8+
import type { AST as SvAST } from 'svelte-eslint-parser';
89

910
type PropertyPathArray = string[];
1011
type DeclaredPropertyNames = Set<{ originalName: string; aliasName: string }>;
@@ -130,49 +131,69 @@ export default createRule('no-unused-props', {
130131
/**
131132
* Extracts property paths from member expressions.
132133
*/
133-
function getPropertyPath(node: TSESTree.Identifier): PropertyPathArray {
134+
function getPropertyPath(node: TSESTree.Identifier): {
135+
paths: PropertyPathArray;
136+
isSpread: boolean;
137+
} {
134138
const paths: PropertyPathArray = [];
135-
let currentNode: TSESTree.Node = node;
136-
let parentNode: TSESTree.Node | null = currentNode.parent ?? null;
137-
139+
let isSpread = false;
140+
let currentNode: TSESTree.Node | SvAST.SvelteSpreadAttribute = node;
141+
let parentNode: TSESTree.Node | SvAST.SvelteSpreadAttribute | null =
142+
currentNode.parent ?? null;
138143
while (parentNode) {
139144
if (parentNode.type === 'MemberExpression' && parentNode.object === currentNode) {
140145
const property = parentNode.property;
141146
if (property.type === 'Identifier') {
142147
paths.push(property.name);
143148
} else if (property.type === 'Literal' && typeof property.value === 'string') {
144149
paths.push(property.value);
145-
} else {
146-
break;
147150
}
151+
} else if (
152+
parentNode.type === 'SpreadElement' ||
153+
parentNode.type === 'SvelteSpreadAttribute'
154+
) {
155+
isSpread = true;
156+
break;
157+
} else {
158+
break;
148159
}
160+
149161
currentNode = parentNode;
150-
parentNode = currentNode.parent ?? null;
162+
parentNode = (currentNode.parent as TSESTree.Node | SvAST.SvelteSpreadAttribute) ?? null;
151163
}
152164

153-
return paths;
165+
return { paths, isSpread };
154166
}
155167

156168
/**
157169
* Finds all property access paths for a given variable.
158170
*/
159-
function getUsedNestedPropertyPathsArray(node: TSESTree.Identifier): PropertyPathArray[] {
171+
function getUsedNestedPropertyPathsArray(node: TSESTree.Identifier): {
172+
paths: PropertyPathArray[];
173+
spreadPaths: PropertyPathArray[];
174+
} {
160175
const variable = findVariable(context, node);
161-
if (!variable) return [];
176+
if (!variable) return { paths: [], spreadPaths: [] };
162177

163178
const pathsArray: PropertyPathArray[] = [];
179+
const spreadPathsArray: PropertyPathArray[] = [];
164180
for (const reference of variable.references) {
165181
if (
166182
'identifier' in reference &&
167183
reference.identifier.type === 'Identifier' &&
168184
(reference.identifier.range[0] !== node.range[0] ||
169185
reference.identifier.range[1] !== node.range[1])
170186
) {
171-
const referencePath = getPropertyPath(reference.identifier);
172-
pathsArray.push(referencePath);
187+
const { paths, isSpread } = getPropertyPath(reference.identifier);
188+
if (isSpread) {
189+
spreadPathsArray.push(paths);
190+
} else {
191+
pathsArray.push(paths);
192+
}
173193
}
174194
}
175-
return pathsArray;
195+
196+
return { paths: pathsArray, spreadPaths: spreadPathsArray };
176197
}
177198

178199
/**
@@ -239,6 +260,7 @@ export default createRule('no-unused-props', {
239260
function checkUnusedProperties({
240261
propsType,
241262
usedPropertyPaths,
263+
usedSpreadPropertyPaths,
242264
declaredPropertyNames,
243265
reportNode,
244266
parentPath,
@@ -247,6 +269,7 @@ export default createRule('no-unused-props', {
247269
}: {
248270
propsType: ts.Type;
249271
usedPropertyPaths: string[];
272+
usedSpreadPropertyPaths: string[];
250273
declaredPropertyNames: DeclaredPropertyNames;
251274
reportNode: TSESTree.Node;
252275
parentPath: string[];
@@ -273,6 +296,7 @@ export default createRule('no-unused-props', {
273296
checkUnusedProperties({
274297
propsType: propsBaseType,
275298
usedPropertyPaths,
299+
usedSpreadPropertyPaths,
276300
declaredPropertyNames,
277301
reportNode,
278302
parentPath,
@@ -290,13 +314,17 @@ export default createRule('no-unused-props', {
290314
if (shouldIgnoreProperty(propName)) continue;
291315

292316
const currentPath = [...parentPath, propName];
293-
const currentPathStr = [...parentPath, propName].join('.');
317+
const currentPathStr = currentPath.join('.');
294318

295319
if (reportedPropertyPaths.has(currentPathStr)) continue;
296320

297321
const propType = typeChecker.getTypeOfSymbol(prop);
298322

299-
const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr);
323+
const isUsedThisInPath =
324+
usedPropertyPaths.includes(currentPathStr) ||
325+
usedSpreadPropertyPaths.some((path) => {
326+
return path === '' || path === currentPathStr || path.startsWith(`${currentPathStr}.`);
327+
});
300328
const isUsedInPath = usedPropertyPaths.some((path) => {
301329
return path.startsWith(`${currentPathStr}.`);
302330
});
@@ -330,6 +358,7 @@ export default createRule('no-unused-props', {
330358
checkUnusedProperties({
331359
propsType: propType,
332360
usedPropertyPaths,
361+
usedSpreadPropertyPaths,
333362
declaredPropertyNames,
334363
reportNode,
335364
parentPath: currentPath,
@@ -370,7 +399,6 @@ export default createRule('no-unused-props', {
370399
): PropertyPathArray[] {
371400
const normalized: PropertyPathArray[] = [];
372401
for (const path of paths.sort((a, b) => a.length - b.length)) {
373-
if (path.length === 0) continue;
374402
if (normalized.some((p) => p.every((part, idx) => part === path[idx]))) {
375403
continue;
376404
}
@@ -398,7 +426,8 @@ export default createRule('no-unused-props', {
398426
if (!tsNode || !tsNode.type) return;
399427

400428
const propsType = typeChecker.getTypeFromTypeNode(tsNode.type);
401-
let usedPropertyPathsArray: PropertyPathArray[] = [];
429+
const usedPropertyPathsArray: PropertyPathArray[] = [];
430+
const usedSpreadPropertyPathsArray: PropertyPathArray[] = [];
402431
let declaredPropertyNames: DeclaredPropertyNames = new Set();
403432

404433
if (node.id.type === 'ObjectPattern') {
@@ -416,11 +445,16 @@ export default createRule('no-unused-props', {
416445
}
417446
}
418447
for (const identifier of identifiers) {
419-
const paths = getUsedNestedPropertyPathsArray(identifier);
448+
const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(identifier);
420449
usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path]));
450+
usedSpreadPropertyPathsArray.push(
451+
...spreadPaths.map((path) => [identifier.name, ...path])
452+
);
421453
}
422454
} else if (node.id.type === 'Identifier') {
423-
usedPropertyPathsArray = getUsedNestedPropertyPathsArray(node.id);
455+
const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(node.id);
456+
usedPropertyPathsArray.push(...paths);
457+
usedSpreadPropertyPathsArray.push(...spreadPaths);
424458
}
425459

426460
checkUnusedProperties({
@@ -431,6 +465,12 @@ export default createRule('no-unused-props', {
431465
).map((pathArray) => {
432466
return pathArray.join('.');
433467
}),
468+
usedSpreadPropertyPaths: normalizeUsedPaths(
469+
usedSpreadPropertyPathsArray,
470+
options.allowUnusedNestedProperties
471+
).map((pathArray) => {
472+
return pathArray.join('.');
473+
}),
434474
declaredPropertyNames,
435475
reportNode: node.id,
436476
parentPath: [],
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--Test1.svelte-->
2+
<script lang="ts">
3+
import Test from '$lib/Test.svelte';
4+
5+
interface Props {
6+
a: string;
7+
b: {
8+
c: string;
9+
d: number;
10+
};
11+
}
12+
13+
let props: Props = $props();
14+
</script>
15+
16+
<Test a={props.a} {...props.b} />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--Test1.svelte-->
2+
<script lang="ts">
3+
import Test from '$lib/Test.svelte';
4+
5+
interface Props {
6+
a: string;
7+
b: {
8+
c: string;
9+
d: number;
10+
};
11+
}
12+
13+
let { a, b }: Props = $props();
14+
</script>
15+
16+
<Test {a} {...b} />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!--Test1.svelte-->
2+
<script lang="ts">
3+
import Test from '$lib/Test.svelte';
4+
5+
interface Props {
6+
a: {
7+
c: string;
8+
d: number;
9+
};
10+
}
11+
12+
let props: Props = $props();
13+
</script>
14+
15+
<Test {...props.a} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"allowUnusedNestedProperties": true
5+
}
6+
]
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!--Test1.svelte-->
2+
<script lang="ts">
3+
import Test from '$lib/Test.svelte';
4+
5+
interface Props {
6+
a: {
7+
c: string;
8+
d: number;
9+
};
10+
}
11+
12+
let props: Props = $props();
13+
</script>
14+
15+
<Test {...props.a} />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!--Test1.svelte-->
2+
<script lang="ts">
3+
import Test from '$lib/Test.svelte';
4+
5+
interface Props {
6+
a: string;
7+
b: {
8+
c: string;
9+
d: number;
10+
};
11+
}
12+
13+
let props: Props = $props();
14+
15+
console.log(...props);
16+
</script>
17+
18+
<Test />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
import Test from '$lib/Test.svelte';
3+
4+
interface Props {
5+
a: string;
6+
}
7+
8+
let props: Props = $props();
9+
</script>
10+
11+
<Test {...props} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"allowUnusedNestedProperties": true
5+
}
6+
]
7+
}

0 commit comments

Comments
 (0)