diff --git a/.changeset/lovely-moments-kneel.md b/.changeset/lovely-moments-kneel.md new file mode 100644 index 000000000..b05d01385 --- /dev/null +++ b/.changeset/lovely-moments-kneel.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat(prefer-svelte-reactivity): reporting returned variables diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts b/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts index 7118b054d..eaece102a 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts @@ -31,6 +31,10 @@ export default createRule('prefer-svelte-reactivity', { ] }, create(context) { + const returnedVariables: Map< + TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration, + TSESTree.VariableDeclarator[] + > = new Map(); const exportedVars: TSESTree.Node[] = []; return { ...(getSvelteContext(context)?.svelteFileType === '.svelte.[js|ts]' && { @@ -59,6 +63,28 @@ export default createRule('prefer-svelte-reactivity', { } } }), + Identifier(node) { + const enclosingReturn = findEnclosingReturn(node); + if (enclosingReturn === null) { + return; + } + const enclosingFunction = findEnclosingFunction(enclosingReturn); + if (enclosingFunction === null) { + return; + } + const variable = findVariable(context, node); + if ( + variable === null || + variable.identifiers.length < 1 || + variable.identifiers[0].parent.type !== 'VariableDeclarator' + ) { + return; + } + if (!returnedVariables.has(enclosingFunction)) { + returnedVariables.set(enclosingFunction, []); + } + returnedVariables.get(enclosingFunction)?.push(variable.identifiers[0].parent); + }, 'Program:exit'() { const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!); for (const { node, path } of referenceTracker.iterateGlobalReferences({ @@ -96,6 +122,20 @@ export default createRule('prefer-svelte-reactivity', { }); } } + for (const returnedVar of Array.from(returnedVariables.values()).flat()) { + if (isIn(node, returnedVar)) { + context.report({ + messageId, + node + }); + } + } + if (findEnclosingReturn(node) !== null) { + context.report({ + messageId, + node + }); + } if (path[0] === 'Date' && isDateMutable(referenceTracker, node as TSESTree.Expression)) { context.report({ messageId: 'mutableDateUsed', @@ -135,6 +175,33 @@ export default createRule('prefer-svelte-reactivity', { } }); +function findAncestorOfTypes( + node: TSESTree.Node, + types: string[] +): (TSESTree.Node & { type: T }) | null { + if (types.includes(node.type)) { + return node as TSESTree.Node & { type: T }; + } + if (node.parent === undefined || node.parent === null) { + return null; + } + return findAncestorOfTypes(node.parent, types); +} + +function findEnclosingReturn(node: TSESTree.Node): TSESTree.ReturnStatement | null { + return findAncestorOfTypes(node, ['ReturnStatement']); +} + +function findEnclosingFunction( + node: TSESTree.Node +): TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | null { + return findAncestorOfTypes(node, [ + 'ArrowFunctionExpression', + 'FunctionDeclaration', + 'MethodDefinition' + ]); +} + function isDateMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean { return !referenceTracker .iteratePropertyReferences(ctorNode, { diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-errors.yaml new file mode 100644 index 000000000..261288a54 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 3 + column: 20 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-input.svelte new file mode 100644 index 000000000..0dafc5973 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return01-input.svelte @@ -0,0 +1,8 @@ + + +{fn().has(42)} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-errors.yaml new file mode 100644 index 000000000..83a23dffc --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 3 + column: 10 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-input.svelte new file mode 100644 index 000000000..9d43ca045 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/arrow-function-return02-input.svelte @@ -0,0 +1,7 @@ + + +{fn().has(42)} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-errors.yaml new file mode 100644 index 000000000..261288a54 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 3 + column: 20 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-input.svelte new file mode 100644 index 000000000..2bb763a44 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return01-input.svelte @@ -0,0 +1,8 @@ + + +{fn().has(42)} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-errors.yaml new file mode 100644 index 000000000..83a23dffc --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 3 + column: 10 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-input.svelte new file mode 100644 index 000000000..5601de190 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/function-return02-input.svelte @@ -0,0 +1,7 @@ + + +{fn().has(42)} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-errors.yaml new file mode 100644 index 000000000..9cae9244d --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 4 + column: 21 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-input.svelte new file mode 100644 index 000000000..c991457fe --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return01-input.svelte @@ -0,0 +1,12 @@ + + +{a.fn().has(42)} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-errors.yaml new file mode 100644 index 000000000..f01de6b75 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Set class. Use SvelteSet instead. + line: 4 + column: 11 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-input.svelte new file mode 100644 index 000000000..90186aa3a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/method-return02-input.svelte @@ -0,0 +1,11 @@ + + +{a.fn().has(42)}