From 07c34a1a756cdae842b05b7b35e5d193e6a75d77 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Tue, 23 Sep 2025 20:41:17 +0900 Subject: [PATCH] fix --- .changeset/real-books-stare.md | 5 ++ .../src/rules/no-unused-props.ts | 83 +++++++++++++------ .../valid/spread-nested1-input.svelte | 16 ++++ .../valid/spread-nested2-input.svelte | 16 ++++ .../valid/spread-nested3-input.svelte | 15 ++++ .../valid/spread-nested4-config.json | 7 ++ .../valid/spread-nested4-input.svelte | 15 ++++ .../valid/spread-nested5-input.svelte | 18 ++++ .../valid/spread-root1-input.svelte | 11 +++ .../valid/spread-root2-config.json | 7 ++ .../valid/spread-root2-input.svelte | 11 +++ .../valid/spread-root3-input.svelte | 13 +++ 12 files changed, 192 insertions(+), 25 deletions(-) create mode 100644 .changeset/real-books-stare.md create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested1-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested2-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested3-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested5-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root1-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-config.json create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-input.svelte create mode 100644 packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root3-input.svelte diff --git a/.changeset/real-books-stare.md b/.changeset/real-books-stare.md new file mode 100644 index 000000000..3506e315c --- /dev/null +++ b/.changeset/real-books-stare.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': patch +--- + +fix(no-unused-props): validate spread operator properly diff --git a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts index 41e198da4..dd56277a9 100644 --- a/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts +++ b/packages/eslint-plugin-svelte/src/rules/no-unused-props.ts @@ -5,6 +5,7 @@ import type ts from 'typescript'; import { findVariable } from '../utils/ast-utils.js'; import { toRegExp } from '../utils/regexp.js'; import { normalize } from 'path'; +import type { AST as SvAST } from 'svelte-eslint-parser'; type PropertyPathArray = string[]; type DeclaredPropertyNames = Set<{ originalName: string; aliasName: string }>; @@ -130,11 +131,15 @@ export default createRule('no-unused-props', { /** * Extracts property paths from member expressions. */ - function getPropertyPath(node: TSESTree.Identifier): PropertyPathArray { + function getPropertyPath(node: TSESTree.Identifier): { + paths: PropertyPathArray; + isSpread: boolean; + } { const paths: PropertyPathArray = []; - let currentNode: TSESTree.Node = node; - let parentNode: TSESTree.Node | null = currentNode.parent ?? null; - + let isSpread = false; + let currentNode: TSESTree.Node | SvAST.SvelteSpreadAttribute = node; + let parentNode: TSESTree.Node | SvAST.SvelteSpreadAttribute | null = + currentNode.parent ?? null; while (parentNode) { if (parentNode.type === 'MemberExpression' && parentNode.object === currentNode) { const property = parentNode.property; @@ -142,25 +147,33 @@ export default createRule('no-unused-props', { paths.push(property.name); } else if (property.type === 'Literal' && typeof property.value === 'string') { paths.push(property.value); - } else { - break; } + } else { + if (parentNode.type === 'SpreadElement' || parentNode.type === 'SvelteSpreadAttribute') { + isSpread = true; + } + break; } + currentNode = parentNode; - parentNode = currentNode.parent ?? null; + parentNode = (currentNode.parent as TSESTree.Node | SvAST.SvelteSpreadAttribute) ?? null; } - return paths; + return { paths, isSpread }; } /** * Finds all property access paths for a given variable. */ - function getUsedNestedPropertyPathsArray(node: TSESTree.Identifier): PropertyPathArray[] { + function getUsedNestedPropertyPathsArray(node: TSESTree.Identifier): { + paths: PropertyPathArray[]; + spreadPaths: PropertyPathArray[]; + } { const variable = findVariable(context, node); - if (!variable) return []; + if (!variable) return { paths: [], spreadPaths: [] }; const pathsArray: PropertyPathArray[] = []; + const spreadPathsArray: PropertyPathArray[] = []; for (const reference of variable.references) { if ( 'identifier' in reference && @@ -168,11 +181,16 @@ export default createRule('no-unused-props', { (reference.identifier.range[0] !== node.range[0] || reference.identifier.range[1] !== node.range[1]) ) { - const referencePath = getPropertyPath(reference.identifier); - pathsArray.push(referencePath); + const { paths, isSpread } = getPropertyPath(reference.identifier); + if (isSpread) { + spreadPathsArray.push(paths); + } else { + pathsArray.push(paths); + } } } - return pathsArray; + + return { paths: pathsArray, spreadPaths: spreadPathsArray }; } /** @@ -239,6 +257,7 @@ export default createRule('no-unused-props', { function checkUnusedProperties({ propsType, usedPropertyPaths, + usedSpreadPropertyPaths, declaredPropertyNames, reportNode, parentPath, @@ -247,6 +266,7 @@ export default createRule('no-unused-props', { }: { propsType: ts.Type; usedPropertyPaths: string[]; + usedSpreadPropertyPaths: string[]; declaredPropertyNames: DeclaredPropertyNames; reportNode: TSESTree.Node; parentPath: string[]; @@ -273,6 +293,7 @@ export default createRule('no-unused-props', { checkUnusedProperties({ propsType: propsBaseType, usedPropertyPaths, + usedSpreadPropertyPaths, declaredPropertyNames, reportNode, parentPath, @@ -290,13 +311,17 @@ export default createRule('no-unused-props', { if (shouldIgnoreProperty(propName)) continue; const currentPath = [...parentPath, propName]; - const currentPathStr = [...parentPath, propName].join('.'); + const currentPathStr = currentPath.join('.'); if (reportedPropertyPaths.has(currentPathStr)) continue; const propType = typeChecker.getTypeOfSymbol(prop); - const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr); + const isUsedThisInPath = + usedPropertyPaths.includes(currentPathStr) || + usedSpreadPropertyPaths.some((path) => { + return path === '' || path === currentPathStr || path.startsWith(`${currentPathStr}.`); + }); const isUsedInPath = usedPropertyPaths.some((path) => { return path.startsWith(`${currentPathStr}.`); }); @@ -330,6 +355,7 @@ export default createRule('no-unused-props', { checkUnusedProperties({ propsType: propType, usedPropertyPaths, + usedSpreadPropertyPaths, declaredPropertyNames, reportNode, parentPath: currentPath, @@ -370,7 +396,6 @@ export default createRule('no-unused-props', { ): PropertyPathArray[] { const normalized: PropertyPathArray[] = []; for (const path of paths.sort((a, b) => a.length - b.length)) { - if (path.length === 0) continue; if (normalized.some((p) => p.every((part, idx) => part === path[idx]))) { continue; } @@ -398,7 +423,8 @@ export default createRule('no-unused-props', { if (!tsNode || !tsNode.type) return; const propsType = typeChecker.getTypeFromTypeNode(tsNode.type); - let usedPropertyPathsArray: PropertyPathArray[] = []; + const usedPropertyPathsArray: PropertyPathArray[] = []; + const usedSpreadPropertyPathsArray: PropertyPathArray[] = []; let declaredPropertyNames: DeclaredPropertyNames = new Set(); if (node.id.type === 'ObjectPattern') { @@ -416,21 +442,28 @@ export default createRule('no-unused-props', { } } for (const identifier of identifiers) { - const paths = getUsedNestedPropertyPathsArray(identifier); + const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(identifier); usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path])); + usedSpreadPropertyPathsArray.push( + ...spreadPaths.map((path) => [identifier.name, ...path]) + ); } } else if (node.id.type === 'Identifier') { - usedPropertyPathsArray = getUsedNestedPropertyPathsArray(node.id); + const { paths, spreadPaths } = getUsedNestedPropertyPathsArray(node.id); + usedPropertyPathsArray.push(...paths); + usedSpreadPropertyPathsArray.push(...spreadPaths); + } + + function runNormalizeUsedPaths(paths: PropertyPathArray[]) { + return normalizeUsedPaths(paths, options.allowUnusedNestedProperties).map((pathArray) => { + return pathArray.join('.'); + }); } checkUnusedProperties({ propsType, - usedPropertyPaths: normalizeUsedPaths( - usedPropertyPathsArray, - options.allowUnusedNestedProperties - ).map((pathArray) => { - return pathArray.join('.'); - }), + usedPropertyPaths: runNormalizeUsedPaths(usedPropertyPathsArray), + usedSpreadPropertyPaths: runNormalizeUsedPaths(usedSpreadPropertyPathsArray), declaredPropertyNames, reportNode: node.id, parentPath: [], diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested1-input.svelte new file mode 100644 index 000000000..918c10cd8 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested1-input.svelte @@ -0,0 +1,16 @@ + + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested2-input.svelte new file mode 100644 index 000000000..644d4a24e --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested2-input.svelte @@ -0,0 +1,16 @@ + + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested3-input.svelte new file mode 100644 index 000000000..c73b58d53 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested3-input.svelte @@ -0,0 +1,15 @@ + + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-config.json new file mode 100644 index 000000000..57afa3f3f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowUnusedNestedProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-input.svelte new file mode 100644 index 000000000..c73b58d53 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested4-input.svelte @@ -0,0 +1,15 @@ + + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested5-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested5-input.svelte new file mode 100644 index 000000000..521ee375a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-nested5-input.svelte @@ -0,0 +1,18 @@ + + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root1-input.svelte new file mode 100644 index 000000000..553d55b70 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root1-input.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-config.json new file mode 100644 index 000000000..57afa3f3f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowUnusedNestedProperties": true + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-input.svelte new file mode 100644 index 000000000..553d55b70 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root2-input.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root3-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root3-input.svelte new file mode 100644 index 000000000..a1d3dacfe --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-unused-props/valid/spread-root3-input.svelte @@ -0,0 +1,13 @@ + + +