From fdcf1940e62d6e1a0b1c427fff503c4ff7e7254a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 3 Oct 2023 18:14:29 +0200 Subject: [PATCH 1/4] Adding a new no-ignored-unsubscribe rule. This rule fails if an "unsubscriber" returned by call to `subscribe()` is neither assigned to a variable or property or passed to a function. One should always unsubscribe from a store when it is no longer needed. Otherwise, the subscription will remain active and constitute a **memory leak**. This rule helps to find such cases by ensuring that the unsubscriber (the return value from the store's `subscribe` method) is not ignored. --- docs/rules/no-ignored-unsubscribe.md | 52 +++++++++++++++++++ src/rules/no-ignored-unsubscribe.ts | 30 +++++++++++ src/utils/rules.ts | 2 + .../invalid/test01-errors.yaml | 4 ++ .../invalid/test01-input.svelte | 9 ++++ .../valid/test01-input.svelte | 9 ++++ .../valid/test02-input.svelte | 10 ++++ tests/src/rules/no-ignored-unsubscribe.ts | 12 +++++ 8 files changed, 128 insertions(+) create mode 100644 docs/rules/no-ignored-unsubscribe.md create mode 100644 src/rules/no-ignored-unsubscribe.ts create mode 100644 tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml create mode 100644 tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte create mode 100644 tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte create mode 100644 tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte create mode 100644 tests/src/rules/no-ignored-unsubscribe.ts diff --git a/docs/rules/no-ignored-unsubscribe.md b/docs/rules/no-ignored-unsubscribe.md new file mode 100644 index 000000000..99196b606 --- /dev/null +++ b/docs/rules/no-ignored-unsubscribe.md @@ -0,0 +1,52 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-ignored-unsubscribe' +description: 'disallow ignoring the return value of `subscribe()`' +since: 'v2.34.0' +--- + +# (svelte/no-ignored-unsubscribe) + +> disallow ignoring the return value of `subscribe()` + +## :book: Rule Details + +This rule fails if an "unsubscriber" returned by call to `subscribe()` is neither assigned to a variable or property or passed to a function. + +One should always unsubscribe from a store when it is no longer needed. Otherwise, the subscription will remain active and constitute a **memory leak**. +This rule helps to find such cases by ensuring that the unsubscriber (the return value from the store's `subscribe` method) is not ignored. + + + + + +```svelte + +``` + + + +## :wrench: Options + +Nothing. + +## :rocket: Version + +This rule was introduced in eslint-plugin-svelte v2.34.0 + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-ignored-unsubscribe.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-ignored-unsubscribe.ts) diff --git a/src/rules/no-ignored-unsubscribe.ts b/src/rules/no-ignored-unsubscribe.ts new file mode 100644 index 000000000..20bff9188 --- /dev/null +++ b/src/rules/no-ignored-unsubscribe.ts @@ -0,0 +1,30 @@ +import type { TSESTree } from '@typescript-eslint/types'; +import { createRule } from "../utils"; + +export default createRule("no-ignored-unsubscribe", { + meta: { + docs: { + description: "Forbids ignoring the unsubscribe method returned by the `subscribe()` os Svelte stores.", + category: 'Best Practices', + recommended: false, + }, + fixable: undefined, + hasSuggestions: false, + messages: { + forbidden: "Ignoring returned value of the subscribe method is forbidden.", + }, + schema: [], + type: "problem", + }, + create: (context) => { + return { + "ExpressionStatement > CallExpression > MemberExpression[property.name='subscribe']": + (node: TSESTree.MemberExpression) => { + context.report({ + messageId: "forbidden", + node: node.property, + }); + }, + }; + }, +}); diff --git a/src/utils/rules.ts b/src/utils/rules.ts index f1181de8d..679f4c371 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -61,6 +61,7 @@ import system from '../rules/system'; import validCompile from '../rules/valid-compile'; import validEachKey from '../rules/valid-each-key'; import validPropNamesInKitPages from '../rules/valid-prop-names-in-kit-pages'; +import noIgnoredUnsubscribe from "../rules/no-ignored-unsubscribe"; export const rules = [ typescriptEslintNoUnnecessaryCondition, @@ -88,6 +89,7 @@ export const rules = [ noDynamicSlotName, noExportLoadInSvelteModuleInKitPages, noExtraReactiveCurlies, + noIgnoredUnsubscribe, noImmutableReactiveStatements, noInnerDeclarations, noNotFunctionHandler, diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml new file mode 100644 index 000000000..159f166ad --- /dev/null +++ b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml @@ -0,0 +1,4 @@ +- message: Ignoring returned value of the subscribe method is forbidden. + line: 6 + column: 9 + suggestions: null diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte new file mode 100644 index 000000000..8a70c7bb6 --- /dev/null +++ b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte new file mode 100644 index 000000000..87ef038f7 --- /dev/null +++ b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte @@ -0,0 +1,9 @@ + diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte new file mode 100644 index 000000000..c6a9c9fbb --- /dev/null +++ b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte @@ -0,0 +1,10 @@ + diff --git a/tests/src/rules/no-ignored-unsubscribe.ts b/tests/src/rules/no-ignored-unsubscribe.ts new file mode 100644 index 000000000..2021ade3a --- /dev/null +++ b/tests/src/rules/no-ignored-unsubscribe.ts @@ -0,0 +1,12 @@ +import { RuleTester } from 'eslint'; +import rule from '../../../src/rules/no-ignored-unsubscribe'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + } +}); + +tester.run('no-ignored-unsubscribe', rule as any, loadTestCases('no-ignored-unsubscribe')); From ab28694769f28fc0bf2224914e888b317cbbe817 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Wed, 4 Oct 2023 02:38:28 +0900 Subject: [PATCH 2/4] Create hip-tips-smell.md --- .changeset/hip-tips-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-tips-smell.md diff --git a/.changeset/hip-tips-smell.md b/.changeset/hip-tips-smell.md new file mode 100644 index 000000000..60dd0cd3b --- /dev/null +++ b/.changeset/hip-tips-smell.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +feat: add new `svelte/no-ignored-unsubscribe` rule. From 399f3d9541af76773e606a7d07f0edf8026d3219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 4 Oct 2023 09:16:59 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Yosuke Ota --- docs/rules/no-ignored-unsubscribe.md | 1 - src/rules/no-ignored-unsubscribe.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/rules/no-ignored-unsubscribe.md b/docs/rules/no-ignored-unsubscribe.md index 99196b606..d511a8509 100644 --- a/docs/rules/no-ignored-unsubscribe.md +++ b/docs/rules/no-ignored-unsubscribe.md @@ -3,7 +3,6 @@ pageClass: 'rule-details' sidebarDepth: 0 title: 'svelte/no-ignored-unsubscribe' description: 'disallow ignoring the return value of `subscribe()`' -since: 'v2.34.0' --- # (svelte/no-ignored-unsubscribe) diff --git a/src/rules/no-ignored-unsubscribe.ts b/src/rules/no-ignored-unsubscribe.ts index 20bff9188..6b415709e 100644 --- a/src/rules/no-ignored-unsubscribe.ts +++ b/src/rules/no-ignored-unsubscribe.ts @@ -4,7 +4,7 @@ import { createRule } from "../utils"; export default createRule("no-ignored-unsubscribe", { meta: { docs: { - description: "Forbids ignoring the unsubscribe method returned by the `subscribe()` os Svelte stores.", + description: "disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores", category: 'Best Practices', recommended: false, }, @@ -18,7 +18,7 @@ export default createRule("no-ignored-unsubscribe", { }, create: (context) => { return { - "ExpressionStatement > CallExpression > MemberExpression[property.name='subscribe']": + "ExpressionStatement > CallExpression > MemberExpression.callee[property.name='subscribe']": (node: TSESTree.MemberExpression) => { context.report({ messageId: "forbidden", From 0c2dcbf590591cbc643d19a56154e2660c93687d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 4 Oct 2023 09:25:46 +0200 Subject: [PATCH 4/4] Running lint and update --- README.md | 1 + docs/rules.md | 1 + docs/rules/no-ignored-unsubscribe.md | 15 ++++------ src/rules/no-ignored-unsubscribe.ts | 30 ++++++++++--------- src/utils/rules.ts | 2 +- .../invalid/test01-errors.yaml | 2 +- .../invalid/test01-input.svelte | 10 +++---- .../valid/test01-input.svelte | 10 +++---- .../valid/test02-input.svelte | 14 +++++---- .../valid/test03-input.svelte | 3 ++ tests/src/rules/no-ignored-unsubscribe.ts | 2 +- 11 files changed, 48 insertions(+), 42 deletions(-) create mode 100644 tests/fixtures/rules/no-ignored-unsubscribe/valid/test03-input.svelte diff --git a/README.md b/README.md index 80f6f903f..7df272aa1 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of ` ``` @@ -41,10 +42,6 @@ This rule helps to find such cases by ensuring that the unsubscriber (the return Nothing. -## :rocket: Version - -This rule was introduced in eslint-plugin-svelte v2.34.0 - ## :mag: Implementation - [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-ignored-unsubscribe.ts) diff --git a/src/rules/no-ignored-unsubscribe.ts b/src/rules/no-ignored-unsubscribe.ts index 6b415709e..471f38a36 100644 --- a/src/rules/no-ignored-unsubscribe.ts +++ b/src/rules/no-ignored-unsubscribe.ts @@ -1,30 +1,32 @@ import type { TSESTree } from '@typescript-eslint/types'; -import { createRule } from "../utils"; +import { createRule } from '../utils'; -export default createRule("no-ignored-unsubscribe", { +export default createRule('no-ignored-unsubscribe', { meta: { docs: { - description: "disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores", + description: + 'disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores.', category: 'Best Practices', - recommended: false, + recommended: false }, fixable: undefined, hasSuggestions: false, messages: { - forbidden: "Ignoring returned value of the subscribe method is forbidden.", + forbidden: 'Ignoring returned value of the subscribe method is forbidden.' }, schema: [], - type: "problem", + type: 'problem' }, create: (context) => { return { - "ExpressionStatement > CallExpression > MemberExpression.callee[property.name='subscribe']": - (node: TSESTree.MemberExpression) => { - context.report({ - messageId: "forbidden", - node: node.property, - }); - }, + "ExpressionStatement > CallExpression > MemberExpression.callee[property.name='subscribe']": ( + node: TSESTree.MemberExpression + ) => { + context.report({ + messageId: 'forbidden', + node: node.property + }); + } }; - }, + } }); diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 679f4c371..c496dec7b 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -27,6 +27,7 @@ import noDupeUseDirectives from '../rules/no-dupe-use-directives'; import noDynamicSlotName from '../rules/no-dynamic-slot-name'; import noExportLoadInSvelteModuleInKitPages from '../rules/no-export-load-in-svelte-module-in-kit-pages'; import noExtraReactiveCurlies from '../rules/no-extra-reactive-curlies'; +import noIgnoredUnsubscribe from '../rules/no-ignored-unsubscribe'; import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statements'; import noInnerDeclarations from '../rules/no-inner-declarations'; import noNotFunctionHandler from '../rules/no-not-function-handler'; @@ -61,7 +62,6 @@ import system from '../rules/system'; import validCompile from '../rules/valid-compile'; import validEachKey from '../rules/valid-each-key'; import validPropNamesInKitPages from '../rules/valid-prop-names-in-kit-pages'; -import noIgnoredUnsubscribe from "../rules/no-ignored-unsubscribe"; export const rules = [ typescriptEslintNoUnnecessaryCondition, diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml index 159f166ad..9b0c1ee4c 100644 --- a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml +++ b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-errors.yaml @@ -1,4 +1,4 @@ - message: Ignoring returned value of the subscribe method is forbidden. line: 6 - column: 9 + column: 6 suggestions: null diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte index 8a70c7bb6..47f230d91 100644 --- a/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte +++ b/tests/fixtures/rules/no-ignored-unsubscribe/invalid/test01-input.svelte @@ -1,9 +1,9 @@ diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte index 87ef038f7..f4f68b2df 100644 --- a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte +++ b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test01-input.svelte @@ -1,9 +1,9 @@ diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte index c6a9c9fbb..e255f63c4 100644 --- a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte +++ b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test02-input.svelte @@ -1,10 +1,12 @@ diff --git a/tests/fixtures/rules/no-ignored-unsubscribe/valid/test03-input.svelte b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test03-input.svelte new file mode 100644 index 000000000..19615225d --- /dev/null +++ b/tests/fixtures/rules/no-ignored-unsubscribe/valid/test03-input.svelte @@ -0,0 +1,3 @@ + diff --git a/tests/src/rules/no-ignored-unsubscribe.ts b/tests/src/rules/no-ignored-unsubscribe.ts index 2021ade3a..2d3bd2d3e 100644 --- a/tests/src/rules/no-ignored-unsubscribe.ts +++ b/tests/src/rules/no-ignored-unsubscribe.ts @@ -5,7 +5,7 @@ import { loadTestCases } from '../../utils/utils'; const tester = new RuleTester({ parserOptions: { ecmaVersion: 2020, - sourceType: 'module', + sourceType: 'module' } });