Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-books-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': patch
---

fix(no-unused-props): validate spread operator properly
83 changes: 58 additions & 25 deletions packages/eslint-plugin-svelte/src/rules/no-unused-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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 }>;
Expand Down Expand Up @@ -76,11 +77,11 @@

const options = context.options[0] ?? {};

// TODO: Remove in v4

Check warning on line 80 in packages/eslint-plugin-svelte/src/rules/no-unused-props.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO: Remove in v4'
// MEMO: `ignorePatterns` was a property that only existed from v3.2.0 to v3.2.2.
// From v3.3.0, it was replaced with `ignorePropertyPatterns` and `ignoreTypePatterns`.
if (options.ignorePatterns != null && !isRemovedWarningShown) {
console.warn(

Check warning on line 84 in packages/eslint-plugin-svelte/src/rules/no-unused-props.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
'eslint-plugin-svelte: The `ignorePatterns` option in the `no-unused-props` rule has been removed. Please use `ignorePropertyPatterns` or/and `ignoreTypePatterns` instead.'
);
isRemovedWarningShown = true;
Expand Down Expand Up @@ -130,49 +131,66 @@
/**
* 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;
if (property.type === 'Identifier') {
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 &&
reference.identifier.type === 'Identifier' &&
(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 };
}

/**
Expand Down Expand Up @@ -239,6 +257,7 @@
function checkUnusedProperties({
propsType,
usedPropertyPaths,
usedSpreadPropertyPaths,
declaredPropertyNames,
reportNode,
parentPath,
Expand All @@ -247,6 +266,7 @@
}: {
propsType: ts.Type;
usedPropertyPaths: string[];
usedSpreadPropertyPaths: string[];
declaredPropertyNames: DeclaredPropertyNames;
reportNode: TSESTree.Node;
parentPath: string[];
Expand All @@ -273,6 +293,7 @@
checkUnusedProperties({
propsType: propsBaseType,
usedPropertyPaths,
usedSpreadPropertyPaths,
declaredPropertyNames,
reportNode,
parentPath,
Expand All @@ -290,13 +311,17 @@
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}.`);
});
Expand Down Expand Up @@ -330,6 +355,7 @@
checkUnusedProperties({
propsType: propType,
usedPropertyPaths,
usedSpreadPropertyPaths,
declaredPropertyNames,
reportNode,
parentPath: currentPath,
Expand Down Expand Up @@ -370,7 +396,6 @@
): 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;
}
Expand Down Expand Up @@ -398,7 +423,8 @@
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') {
Expand All @@ -416,21 +442,28 @@
}
}
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: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--Test1.svelte-->
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
b: {
c: string;
d: number;
};
}

let props: Props = $props();
</script>

<Test a={props.a} {...props.b} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!--Test1.svelte-->
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
b: {
c: string;
d: number;
};
}

let { a, b }: Props = $props();
</script>

<Test {a} {...b} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!--Test1.svelte-->
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: {
c: string;
d: number;
};
}

let props: Props = $props();
</script>

<Test {...props.a} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"options": [
{
"allowUnusedNestedProperties": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!--Test1.svelte-->
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: {
c: string;
d: number;
};
}

let props: Props = $props();
</script>

<Test {...props.a} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!--Test1.svelte-->
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
b: {
c: string;
d: number;
};
}

let props: Props = $props();

console.log(...props);
</script>

<Test />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
}

let props: Props = $props();
</script>

<Test {...props} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"options": [
{
"allowUnusedNestedProperties": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
}

let props: Props = $props();
</script>

<Test {...props} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import Test from '$lib/Test.svelte';

interface Props {
a: string;
}

let props: Props = $props();

console.log(...props);
</script>

<Test />
Loading