From 6472c63fbadc5b2b4bbd56ea3a8cf4d28663471a Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Fri, 22 Nov 2024 16:16:38 -0600 Subject: [PATCH 01/13] docs(no-ignored-observable): tips for this rule --- docs/rules/no-ignored-observable.md | 11 +++++++++++ tests/rules/no-ignored-observable.test.ts | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/docs/rules/no-ignored-observable.md b/docs/rules/no-ignored-observable.md index 2b113752..ef5c3436 100644 --- a/docs/rules/no-ignored-observable.md +++ b/docs/rules/no-ignored-observable.md @@ -7,6 +7,17 @@ The effects failures if an observable returned by a function is neither assigned to a variable or property or passed to a function. +This rule is like [no-floating-promises](https://typescript-eslint.io/rules/no-floating-promises/) but for Observables. + +This rule will report Observable-valued statements that are not treated in one of the following ways: + +- Calling its `.subscribe()` +- `return`ing it +- Wrapping it in `lastValueFrom` or `firstValueFrom` and `await`ing it +- [`void`ing it](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void) + +> [!TIP] +> `no-ignored-observable` only detects apparently unhandled Observable _statements_. ## Rule details diff --git a/tests/rules/no-ignored-observable.test.ts b/tests/rules/no-ignored-observable.test.ts index 8c6b8b6a..3c79ca56 100644 --- a/tests/rules/no-ignored-observable.test.ts +++ b/tests/rules/no-ignored-observable.test.ts @@ -31,6 +31,16 @@ ruleTester({ types: true }).run('no-ignored-observable', noIgnoredObservableRule const a = arrowSource(); sink(arrowSource()); `, + stripIndent` + // void operator + import { of } from "rxjs"; + + function functionSource() { + return of(42); + } + + void functionSource(); + `, ], invalid: [ fromFixture( From fa3f2c5665997bc346c69c574c5c01fba8cb15ca Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Sun, 24 Nov 2024 12:43:22 -0600 Subject: [PATCH 02/13] feat(no-floating-observables)!: rename rule --- README.md | 2 +- ...nored-observable.md => no-floating-observables.md} | 11 ++++++----- src/configs/strict.ts | 2 +- src/index.ts | 4 ++-- ...nored-observable.ts => no-floating-observables.ts} | 10 ++++++---- ...rvable.test.ts => no-floating-observables.test.ts} | 4 ++-- 6 files changed, 18 insertions(+), 15 deletions(-) rename docs/rules/{no-ignored-observable.md => no-floating-observables.md} (64%) rename src/rules/{no-ignored-observable.ts => no-floating-observables.ts} (65%) rename tests/rules/{no-ignored-observable.test.ts => no-floating-observables.test.ts} (88%) diff --git a/README.md b/README.md index 5e1bd97e..e96fa26d 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,10 @@ The package includes the following rules. | [no-explicit-generics](docs/rules/no-explicit-generics.md) | Disallow unnecessary explicit generic type arguments. | 🔒 | | | | | | [no-exposed-subjects](docs/rules/no-exposed-subjects.md) | Disallow public and protected subjects. | 🔒 | | | 💭 | | | [no-finnish](docs/rules/no-finnish.md) | Disallow Finnish notation. | | | | 💭 | | +| [no-floating-observables](docs/rules/no-floating-observables.md) | Require Observables to be handled appropriately. | 🔒 | | | 💭 | | | [no-ignored-default-value](docs/rules/no-ignored-default-value.md) | Disallow using `firstValueFrom`, `lastValueFrom`, `first`, and `last` without specifying a default value. | 🔒 | | | 💭 | | | [no-ignored-error](docs/rules/no-ignored-error.md) | Disallow calling `subscribe` without specifying an error handler. | 🔒 | | | 💭 | | | [no-ignored-notifier](docs/rules/no-ignored-notifier.md) | Disallow observables not composed from the `repeatWhen` or `retryWhen` notifier. | ✅ 🔒 | | | 💭 | | -| [no-ignored-observable](docs/rules/no-ignored-observable.md) | Disallow ignoring observables returned by functions. | 🔒 | | | 💭 | | | [no-ignored-replay-buffer](docs/rules/no-ignored-replay-buffer.md) | Disallow using `ReplaySubject`, `publishReplay` or `shareReplay` without specifying the buffer size. | ✅ 🔒 | | | | | | [no-ignored-subscribe](docs/rules/no-ignored-subscribe.md) | Disallow calling `subscribe` without specifying arguments. | | | | 💭 | | | [no-ignored-subscription](docs/rules/no-ignored-subscription.md) | Disallow ignoring the subscription returned by `subscribe`. | | | | 💭 | | diff --git a/docs/rules/no-ignored-observable.md b/docs/rules/no-floating-observables.md similarity index 64% rename from docs/rules/no-ignored-observable.md rename to docs/rules/no-floating-observables.md index ef5c3436..1e635764 100644 --- a/docs/rules/no-ignored-observable.md +++ b/docs/rules/no-floating-observables.md @@ -1,4 +1,4 @@ -# Disallow ignoring observables returned by functions (`rxjs-x/no-ignored-observable`) +# Require Observables to be handled appropriately (`rxjs-x/no-floating-observables`) 💼 This rule is enabled in the 🔒 `strict` config. @@ -6,10 +6,11 @@ -The effects failures if an observable returned by a function is neither assigned to a variable or property or passed to a function. -This rule is like [no-floating-promises](https://typescript-eslint.io/rules/no-floating-promises/) but for Observables. +A "floating" observable is one that is created without any code set up to handle any errors it might emit. +Like a floating Promise, floating observables can cause several issues, such as ignored errors, unhandled cold observables, and more. -This rule will report Observable-valued statements that are not treated in one of the following ways: +This rule is like [no-floating-promises](https://typescript-eslint.io/rules/no-floating-promises/) but for Observables. +This rule will report observable-valued statements that are not treated in one of the following ways: - Calling its `.subscribe()` - `return`ing it @@ -17,7 +18,7 @@ This rule will report Observable-valued statements that are not treated in one o - [`void`ing it](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void) > [!TIP] -> `no-ignored-observable` only detects apparently unhandled Observable _statements_. +> `no-ignored-observable` only detects apparently unhandled observable _statements_. ## Rule details diff --git a/src/configs/strict.ts b/src/configs/strict.ts index 9fcfef6b..b10efa99 100644 --- a/src/configs/strict.ts +++ b/src/configs/strict.ts @@ -11,10 +11,10 @@ export const createStrictConfig = ( 'rxjs-x/no-create': 'error', 'rxjs-x/no-explicit-generics': 'error', 'rxjs-x/no-exposed-subjects': 'error', + 'rxjs-x/no-floating-observables': 'error', 'rxjs-x/no-ignored-default-value': 'error', 'rxjs-x/no-ignored-error': 'error', 'rxjs-x/no-ignored-notifier': 'error', - 'rxjs-x/no-ignored-observable': 'error', 'rxjs-x/no-ignored-replay-buffer': 'error', 'rxjs-x/no-ignored-takewhile-value': 'error', 'rxjs-x/no-implicit-any-catch': ['error', { diff --git a/src/index.ts b/src/index.ts index d83cef9c..b0afe264 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,10 +16,10 @@ import { noCyclicActionRule } from './rules/no-cyclic-action'; import { noExplicitGenericsRule } from './rules/no-explicit-generics'; import { noExposedSubjectsRule } from './rules/no-exposed-subjects'; import { noFinnishRule } from './rules/no-finnish'; +import { noFloatingObservablesRule } from './rules/no-floating-observables'; import { noIgnoredDefaultValueRule } from './rules/no-ignored-default-value'; import { noIgnoredErrorRule } from './rules/no-ignored-error'; import { noIgnoredNotifierRule } from './rules/no-ignored-notifier'; -import { noIgnoredObservableRule } from './rules/no-ignored-observable'; import { noIgnoredReplayBufferRule } from './rules/no-ignored-replay-buffer'; import { noIgnoredSubscribeRule } from './rules/no-ignored-subscribe'; import { noIgnoredSubscriptionRule } from './rules/no-ignored-subscription'; @@ -63,10 +63,10 @@ const plugin = { 'no-explicit-generics': noExplicitGenericsRule, 'no-exposed-subjects': noExposedSubjectsRule, 'no-finnish': noFinnishRule, + 'no-floating-observables': noFloatingObservablesRule, 'no-ignored-default-value': noIgnoredDefaultValueRule, 'no-ignored-error': noIgnoredErrorRule, 'no-ignored-notifier': noIgnoredNotifierRule, - 'no-ignored-observable': noIgnoredObservableRule, 'no-ignored-replay-buffer': noIgnoredReplayBufferRule, 'no-ignored-subscribe': noIgnoredSubscribeRule, 'no-ignored-subscription': noIgnoredSubscriptionRule, diff --git a/src/rules/no-ignored-observable.ts b/src/rules/no-floating-observables.ts similarity index 65% rename from src/rules/no-ignored-observable.ts rename to src/rules/no-floating-observables.ts index e1b9e46b..0bd84c0d 100644 --- a/src/rules/no-ignored-observable.ts +++ b/src/rules/no-floating-observables.ts @@ -2,21 +2,23 @@ import { TSESTree as es } from '@typescript-eslint/utils'; import { getTypeServices } from '../etc'; import { ruleCreator } from '../utils'; -export const noIgnoredObservableRule = ruleCreator({ +export const noFloatingObservablesRule = ruleCreator({ defaultOptions: [], meta: { docs: { - description: 'Disallow ignoring observables returned by functions.', + description: 'Require Observables to be handled appropriately.', recommended: 'strict', requiresTypeChecking: true, }, messages: { - forbidden: 'Ignoring a returned Observable is forbidden.', + forbidden: + 'Observables must be subscribed to, returned, converted to a promise and awaited, ' + + 'or be explicitly marked as ignored with the `void` operator.', }, schema: [], type: 'problem', }, - name: 'no-ignored-observable', + name: 'no-floating-observables', create: (context) => { const { couldBeObservable } = getTypeServices(context); diff --git a/tests/rules/no-ignored-observable.test.ts b/tests/rules/no-floating-observables.test.ts similarity index 88% rename from tests/rules/no-ignored-observable.test.ts rename to tests/rules/no-floating-observables.test.ts index 3c79ca56..32344a36 100644 --- a/tests/rules/no-ignored-observable.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -1,9 +1,9 @@ import { stripIndent } from 'common-tags'; -import { noIgnoredObservableRule } from '../../src/rules/no-ignored-observable'; +import { noFloatingObservablesRule } from '../../src/rules/no-floating-observables'; import { fromFixture } from '../etc'; import { ruleTester } from '../rule-tester'; -ruleTester({ types: true }).run('no-ignored-observable', noIgnoredObservableRule, { +ruleTester({ types: true }).run('no-floating-observables', noFloatingObservablesRule, { valid: [ stripIndent` // not ignored From eca0cba801a132d4d1acfa4a642855bbf0251b99 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Sun, 24 Nov 2024 13:22:37 -0600 Subject: [PATCH 03/13] feat(no-floating-observables): add option to ignore void expression --- docs/rules/no-floating-observables.md | 10 +++++ src/etc/is.ts | 4 ++ src/rules/no-floating-observables.ts | 50 +++++++++++++++++---- tests/rules/no-floating-observables.test.ts | 32 ++++++++----- 4 files changed, 76 insertions(+), 20 deletions(-) diff --git a/docs/rules/no-floating-observables.md b/docs/rules/no-floating-observables.md index 1e635764..2e3b0eb0 100644 --- a/docs/rules/no-floating-observables.md +++ b/docs/rules/no-floating-observables.md @@ -35,3 +35,13 @@ Examples of **correct** code for this rule: import { of } from "rxjs"; const answers = of(42, 54); ``` + +## Options + + + +| Name | Description | Type | Default | +| :----------- | :------------------------------------ | :------ | :------ | +| `ignoreVoid` | Whether to ignore `void` expressions. | Boolean | `true` | + + diff --git a/src/etc/is.ts b/src/etc/is.ts index 7e3f4d6b..7c6cc415 100644 --- a/src/etc/is.ts +++ b/src/etc/is.ts @@ -124,6 +124,10 @@ export function isTSTypeReference(node: TSESTree.Node): node is TSESTree.TSTypeR return node.type === AST_NODE_TYPES.TSTypeReference; } +export function isUnaryExpression(node: TSESTree.Node): node is TSESTree.UnaryExpression { + return node.type === AST_NODE_TYPES.UnaryExpression; +} + export function isVariableDeclarator( node: TSESTree.Node, ): node is TSESTree.VariableDeclarator { diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 0bd84c0d..52d97bc3 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -1,9 +1,20 @@ import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices } from '../etc'; +import { getTypeServices, isUnaryExpression } from '../etc'; import { ruleCreator } from '../utils'; +const defaultOptions: readonly { + ignoreVoid?: boolean; +}[] = []; + +const messageBase + = 'Observables must be subscribed to, returned, converted to a promise and awaited, ' + + 'or be explicitly marked as ignored with the `void` operator.'; + +const messageBaseNoVoid + = 'Observables must be subscribed to, returned, or converted to a promise and awaited.'; + export const noFloatingObservablesRule = ruleCreator({ - defaultOptions: [], + defaultOptions, meta: { docs: { description: 'Require Observables to be handled appropriately.', @@ -11,24 +22,45 @@ export const noFloatingObservablesRule = ruleCreator({ requiresTypeChecking: true, }, messages: { - forbidden: - 'Observables must be subscribed to, returned, converted to a promise and awaited, ' - + 'or be explicitly marked as ignored with the `void` operator.', + forbidden: messageBase, + forbiddenNoVoid: messageBaseNoVoid, }, - schema: [], + schema: [ + { + properties: { + ignoreVoid: { type: 'boolean', default: true, description: 'Whether to ignore `void` expressions.' }, + }, + type: 'object', + }, + ], type: 'problem', }, name: 'no-floating-observables', create: (context) => { const { couldBeObservable } = getTypeServices(context); + const [config = {}] = context.options; + const { ignoreVoid = true } = config; return { - 'ExpressionStatement > CallExpression': (node: es.CallExpression) => { - if (couldBeObservable(node)) { + ExpressionStatement: (node: es.ExpressionStatement) => { + const { expression } = node; + if (couldBeObservable(expression)) { context.report({ - messageId: 'forbidden', + messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid', node, }); + return; + } + + if (!ignoreVoid && isUnaryExpression(expression)) { + const { operator, argument } = expression; + if (operator === 'void' && couldBeObservable(argument)) { + context.report({ + messageId: 'forbiddenNoVoid', + node: argument, + }); + return; + } } }, }; diff --git a/tests/rules/no-floating-observables.test.ts b/tests/rules/no-floating-observables.test.ts index 32344a36..243a4048 100644 --- a/tests/rules/no-floating-observables.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -16,8 +16,10 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables function sink(source: Observable) { } + functionSource().subscribe(); const a = functionSource(); sink(functionSource()); + void functionSource(); `, stripIndent` // not ignored arrow @@ -28,17 +30,9 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables function sink(source: Observable) { } + functionSource().subscribe(); const a = arrowSource(); sink(arrowSource()); - `, - stripIndent` - // void operator - import { of } from "rxjs"; - - function functionSource() { - return of(42); - } - void functionSource(); `, ], @@ -53,7 +47,7 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables } functionSource(); - ~~~~~~~~~~~~~~~~ [forbidden] + ~~~~~~~~~~~~~~~~~ [forbidden] `, ), fromFixture( @@ -64,8 +58,24 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables const arrowSource = () => of(42); arrowSource(); - ~~~~~~~~~~~~~ [forbidden] + ~~~~~~~~~~~~~~ [forbidden] + `, + ), + fromFixture( + stripIndent` + // ignoreVoid false + import { Observable, of } from "rxjs"; + + function functionSource() { + return of(42); + } + + void functionSource(); + ~~~~~~~~~~~~~~~~ [forbiddenNoVoid] `, + { + options: [{ ignoreVoid: false }], + }, ), ], }); From d0996989f78bfca72521de0eceee641c483c3d11 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 11:09:30 -0600 Subject: [PATCH 04/13] fix(no-floating-observables): re-add call expression check for performance Previously the selector was `ExpressionStatement > CallExpression`, but in order to support `ignoreVoid`, the selector was broadened to just `ExpressionStatement`. This could have a performance impact, but re-adding this call expression check should revert to the original performance. --- src/rules/no-floating-observables.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 52d97bc3..96e6f17b 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -1,5 +1,5 @@ import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices, isUnaryExpression } from '../etc'; +import { getTypeServices, isCallExpression, isUnaryExpression } from '../etc'; import { ruleCreator } from '../utils'; const defaultOptions: readonly { @@ -44,7 +44,8 @@ export const noFloatingObservablesRule = ruleCreator({ return { ExpressionStatement: (node: es.ExpressionStatement) => { const { expression } = node; - if (couldBeObservable(expression)) { + + if (isCallExpression(expression) && couldBeObservable(expression)) { context.report({ messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid', node, @@ -54,7 +55,7 @@ export const noFloatingObservablesRule = ruleCreator({ if (!ignoreVoid && isUnaryExpression(expression)) { const { operator, argument } = expression; - if (operator === 'void' && couldBeObservable(argument)) { + if (operator === 'void' && isCallExpression(argument) && couldBeObservable(argument)) { context.report({ messageId: 'forbiddenNoVoid', node: argument, From f1b79fe1a5fdc1bf880a4737e2cffab2cab879f5 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 11:20:06 -0600 Subject: [PATCH 05/13] fix(no-floating-observables): reduce selectors for perf Go back to more limited selectors for performance. Still checking the expression's type (call or unary) so we don't type-check too early. --- src/rules/no-floating-observables.ts | 37 ++++++++++----------- tests/rules/no-floating-observables.test.ts | 4 +-- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 96e6f17b..47338b79 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -41,28 +41,25 @@ export const noFloatingObservablesRule = ruleCreator({ const [config = {}] = context.options; const { ignoreVoid = true } = config; - return { - ExpressionStatement: (node: es.ExpressionStatement) => { - const { expression } = node; + function checkNode(node: es.Expression) { + if (!ignoreVoid && isUnaryExpression(node) && node.operator === 'void') { + node = node.argument; + } - if (isCallExpression(expression) && couldBeObservable(expression)) { - context.report({ - messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid', - node, - }); - return; - } + if (isCallExpression(node) && couldBeObservable(node)) { + context.report({ + messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid', + node, + }); + } + } - if (!ignoreVoid && isUnaryExpression(expression)) { - const { operator, argument } = expression; - if (operator === 'void' && isCallExpression(argument) && couldBeObservable(argument)) { - context.report({ - messageId: 'forbiddenNoVoid', - node: argument, - }); - return; - } - } + return { + 'ExpressionStatement > CallExpression': (node: es.CallExpression) => { + checkNode(node); + }, + 'ExpressionStatement > UnaryExpression': (node: es.UnaryExpression) => { + checkNode(node); }, }; }, diff --git a/tests/rules/no-floating-observables.test.ts b/tests/rules/no-floating-observables.test.ts index 243a4048..7b05bc77 100644 --- a/tests/rules/no-floating-observables.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -47,7 +47,7 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables } functionSource(); - ~~~~~~~~~~~~~~~~~ [forbidden] + ~~~~~~~~~~~~~~~~ [forbidden] `, ), fromFixture( @@ -58,7 +58,7 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables const arrowSource = () => of(42); arrowSource(); - ~~~~~~~~~~~~~~ [forbidden] + ~~~~~~~~~~~~~ [forbidden] `, ), fromFixture( From d021217485a460e4ad8938507f43a7b3f20820cb Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 11:21:44 -0600 Subject: [PATCH 06/13] fix(no-floating-observables): don't call isCallExpression unnecessarily --- src/rules/no-floating-observables.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 47338b79..06d168f2 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -1,5 +1,5 @@ import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices, isCallExpression, isUnaryExpression } from '../etc'; +import { getTypeServices, isCallExpression } from '../etc'; import { ruleCreator } from '../utils'; const defaultOptions: readonly { @@ -41,12 +41,8 @@ export const noFloatingObservablesRule = ruleCreator({ const [config = {}] = context.options; const { ignoreVoid = true } = config; - function checkNode(node: es.Expression) { - if (!ignoreVoid && isUnaryExpression(node) && node.operator === 'void') { - node = node.argument; - } - - if (isCallExpression(node) && couldBeObservable(node)) { + function checkNode(node: es.CallExpression) { + if (couldBeObservable(node)) { context.report({ messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid', node, @@ -59,7 +55,11 @@ export const noFloatingObservablesRule = ruleCreator({ checkNode(node); }, 'ExpressionStatement > UnaryExpression': (node: es.UnaryExpression) => { - checkNode(node); + if (ignoreVoid) return; + if (node.operator !== 'void') return; + if (!isCallExpression(node.argument)) return; + + checkNode(node.argument); }, }; }, From 04f3bafebda5c442c99acae8554aedbf122940ba Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 11:46:49 -0600 Subject: [PATCH 07/13] fix(no-floating-observables): handle chain expressions --- src/etc/is.ts | 4 ++++ src/rules/no-floating-observables.ts | 16 +++++++++++++--- tests/rules/no-floating-observables.test.ts | 13 +++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/etc/is.ts b/src/etc/is.ts index 7c6cc415..1171d708 100644 --- a/src/etc/is.ts +++ b/src/etc/is.ts @@ -34,6 +34,10 @@ export function isCallExpression(node: TSESTree.Node): node is TSESTree.CallExpr return node.type === AST_NODE_TYPES.CallExpression; } +export function isChainExpression(node: TSESTree.Node): node is TSESTree.ChainExpression { + return node.type === AST_NODE_TYPES.ChainExpression; +} + export function isExportNamedDeclaration( node: TSESTree.Node, ): node is TSESTree.ExportNamedDeclaration { diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 06d168f2..712a937c 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -1,5 +1,5 @@ import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices, isCallExpression } from '../etc'; +import { getTypeServices, isCallExpression, isChainExpression } from '../etc'; import { ruleCreator } from '../utils'; const defaultOptions: readonly { @@ -57,9 +57,19 @@ export const noFloatingObservablesRule = ruleCreator({ 'ExpressionStatement > UnaryExpression': (node: es.UnaryExpression) => { if (ignoreVoid) return; if (node.operator !== 'void') return; - if (!isCallExpression(node.argument)) return; - checkNode(node.argument); + let expression = node.argument; + if (isChainExpression(expression)) { + expression = expression.expression; + } + + if (!isCallExpression(expression)) return; + checkNode(expression); + }, + 'ExpressionStatement > ChainExpression': (node: es.ChainExpression) => { + if (!isCallExpression(node.expression)) return; + + checkNode(node.expression); }, }; }, diff --git a/tests/rules/no-floating-observables.test.ts b/tests/rules/no-floating-observables.test.ts index 7b05bc77..13836359 100644 --- a/tests/rules/no-floating-observables.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -61,6 +61,17 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables ~~~~~~~~~~~~~ [forbidden] `, ), + fromFixture( + stripIndent` + // chain expression + import { Observable, of } from "rxjs"; + + const arrowSource: null | (() => Observable) = () => of(42); + + arrowSource?.(); + ~~~~~~~~~~~~~~~ [forbidden] + `, + ), fromFixture( stripIndent` // ignoreVoid false @@ -72,6 +83,8 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables void functionSource(); ~~~~~~~~~~~~~~~~ [forbiddenNoVoid] + void functionSource?.(); + ~~~~~~~~~~~~~~~~~~ [forbiddenNoVoid] `, { options: [{ ignoreVoid: false }], From 6f75e1abbb0f41b846e0846b1df74d90d14f3abb Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 11:56:00 -0600 Subject: [PATCH 08/13] fix(no-floating-observables): handle sequence expression --- src/rules/no-floating-observables.ts | 7 +++++++ tests/rules/no-floating-observables.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index 712a937c..cf0596b1 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -71,6 +71,13 @@ export const noFloatingObservablesRule = ruleCreator({ checkNode(node.expression); }, + 'ExpressionStatement > SequenceExpression': (node: es.SequenceExpression) => { + node.expressions.forEach(expression => { + if (isCallExpression(expression)) { + checkNode(expression); + } + }); + }, }; }, }); diff --git a/tests/rules/no-floating-observables.test.ts b/tests/rules/no-floating-observables.test.ts index 13836359..b4ca3be4 100644 --- a/tests/rules/no-floating-observables.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -90,5 +90,15 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables options: [{ ignoreVoid: false }], }, ), + fromFixture( + stripIndent` + // sequence expression + import { of } from "rxjs"; + + of(42), of(42), void of(42); + ~~~~~~ [forbidden] + ~~~~~~ [forbidden] + `, + ), ], }); From 35bf86647b8ce7fa77ebc55bbe90ebe4bbb737da Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 12:30:43 -0600 Subject: [PATCH 09/13] fix(no-floating-observables): handle array expressions --- src/rules/no-floating-observables.ts | 36 ++++++++++++++------- tests/rules/no-floating-observables.test.ts | 27 ++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/rules/no-floating-observables.ts b/src/rules/no-floating-observables.ts index cf0596b1..569354ec 100644 --- a/src/rules/no-floating-observables.ts +++ b/src/rules/no-floating-observables.ts @@ -1,5 +1,5 @@ import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices, isCallExpression, isChainExpression } from '../etc'; +import { getTypeServices, isCallExpression, isChainExpression, isUnaryExpression } from '../etc'; import { ruleCreator } from '../utils'; const defaultOptions: readonly { @@ -50,21 +50,25 @@ export const noFloatingObservablesRule = ruleCreator({ } } + function checkVoid(node: es.UnaryExpression) { + if (ignoreVoid) return; + if (node.operator !== 'void') return; + + let expression = node.argument; + if (isChainExpression(expression)) { + expression = expression.expression; + } + + if (!isCallExpression(expression)) return; + checkNode(expression); + } + return { 'ExpressionStatement > CallExpression': (node: es.CallExpression) => { checkNode(node); }, 'ExpressionStatement > UnaryExpression': (node: es.UnaryExpression) => { - if (ignoreVoid) return; - if (node.operator !== 'void') return; - - let expression = node.argument; - if (isChainExpression(expression)) { - expression = expression.expression; - } - - if (!isCallExpression(expression)) return; - checkNode(expression); + checkVoid(node); }, 'ExpressionStatement > ChainExpression': (node: es.ChainExpression) => { if (!isCallExpression(node.expression)) return; @@ -78,6 +82,16 @@ export const noFloatingObservablesRule = ruleCreator({ } }); }, + 'ExpressionStatement > ArrayExpression': (node: es.ArrayExpression) => { + node.elements.forEach(expression => { + if (!expression) return; + if (isCallExpression(expression)) { + checkNode(expression); + } else if (isUnaryExpression(expression)) { + checkVoid(expression); + } + }); + }, }; }, }); diff --git a/tests/rules/no-floating-observables.test.ts b/tests/rules/no-floating-observables.test.ts index b4ca3be4..da588408 100644 --- a/tests/rules/no-floating-observables.test.ts +++ b/tests/rules/no-floating-observables.test.ts @@ -18,6 +18,8 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables functionSource().subscribe(); const a = functionSource(); + const b = [functionSource()]; + [void functionSource()]; sink(functionSource()); void functionSource(); `, @@ -32,9 +34,21 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables functionSource().subscribe(); const a = arrowSource(); + const b = [arrowSource()]; + [void arrowSource()]; sink(arrowSource()); void functionSource(); `, + stripIndent` + // unrelated + function foo() {} + + [1, 2, 3, 'foo']; + const a = [1]; + foo(); + [foo()]; + void foo(); + `, ], invalid: [ fromFixture( @@ -72,6 +86,15 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables ~~~~~~~~~~~~~~~ [forbidden] `, ), + fromFixture( + stripIndent` + // array + import { of } from "rxjs"; + + [of(42)]; + ~~~~~~ [forbidden] + `, + ), fromFixture( stripIndent` // ignoreVoid false @@ -85,6 +108,10 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables ~~~~~~~~~~~~~~~~ [forbiddenNoVoid] void functionSource?.(); ~~~~~~~~~~~~~~~~~~ [forbiddenNoVoid] + [void functionSource()]; + ~~~~~~~~~~~~~~~~ [forbiddenNoVoid] + [void functionSource?.()]; + ~~~~~~~~~~~~~~~~~~ [forbiddenNoVoid] `, { options: [{ ignoreVoid: false }], From c6da3f6324b6741a76912f05e829b85f02d635c4 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 13:56:05 -0600 Subject: [PATCH 10/13] docs(no-floating-observables): fix tip rule naming --- docs/rules/no-floating-observables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-floating-observables.md b/docs/rules/no-floating-observables.md index 2e3b0eb0..078e2afe 100644 --- a/docs/rules/no-floating-observables.md +++ b/docs/rules/no-floating-observables.md @@ -18,7 +18,7 @@ This rule will report observable-valued statements that are not treated in one o - [`void`ing it](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void) > [!TIP] -> `no-ignored-observable` only detects apparently unhandled observable _statements_. +> `no-floating-observables` only detects apparently unhandled observable _statements_. ## Rule details From 47e12716f2e40561528a387d6502666cd3a0f019 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 14:02:51 -0600 Subject: [PATCH 11/13] feat(no-ignored-observable): put back old rule Too many existing projects manually enable this rule, so removing it is more impactful than the other proposed changes during prerelease. This rule will be kept as-is and maybe removed in v2. --- README.md | 1 + docs/rules/no-ignored-observable.md | 31 ++++++++++++ src/index.ts | 2 + src/rules/no-ignored-observable.ts | 35 +++++++++++++ tests/rules/no-ignored-observable.test.ts | 61 +++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 docs/rules/no-ignored-observable.md create mode 100644 src/rules/no-ignored-observable.ts create mode 100644 tests/rules/no-ignored-observable.test.ts diff --git a/README.md b/README.md index e96fa26d..1b9a5714 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ The package includes the following rules. | [no-ignored-default-value](docs/rules/no-ignored-default-value.md) | Disallow using `firstValueFrom`, `lastValueFrom`, `first`, and `last` without specifying a default value. | 🔒 | | | 💭 | | | [no-ignored-error](docs/rules/no-ignored-error.md) | Disallow calling `subscribe` without specifying an error handler. | 🔒 | | | 💭 | | | [no-ignored-notifier](docs/rules/no-ignored-notifier.md) | Disallow observables not composed from the `repeatWhen` or `retryWhen` notifier. | ✅ 🔒 | | | 💭 | | +| [no-ignored-observable](docs/rules/no-ignored-observable.md) | Disallow ignoring observables returned by functions. | | | | 💭 | ❌ | | [no-ignored-replay-buffer](docs/rules/no-ignored-replay-buffer.md) | Disallow using `ReplaySubject`, `publishReplay` or `shareReplay` without specifying the buffer size. | ✅ 🔒 | | | | | | [no-ignored-subscribe](docs/rules/no-ignored-subscribe.md) | Disallow calling `subscribe` without specifying arguments. | | | | 💭 | | | [no-ignored-subscription](docs/rules/no-ignored-subscription.md) | Disallow ignoring the subscription returned by `subscribe`. | | | | 💭 | | diff --git a/docs/rules/no-ignored-observable.md b/docs/rules/no-ignored-observable.md new file mode 100644 index 00000000..135acf99 --- /dev/null +++ b/docs/rules/no-ignored-observable.md @@ -0,0 +1,31 @@ +# Disallow ignoring observables returned by functions (`rxjs-x/no-ignored-observable`) + +❌ This rule is deprecated. It was replaced by [`rxjs-x/no-floating-observables`](no-floating-observables.md). + +💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). + + + +The effects failures if an observable returned by a function is neither assigned to a variable or property or passed to a function. + +> [!WARNING] +> This rule is being replaced by `no-floating-observables`. +> The new rule has been expanded to handle more expression types. +> +> The current rule `no-ignored-observable` will be removed in a future major version. + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +import { of } from "rxjs"; +of(42, 54); +``` + +Examples of **correct** code for this rule: + +```ts +import { of } from "rxjs"; +const answers = of(42, 54); +``` diff --git a/src/index.ts b/src/index.ts index b0afe264..6fc21966 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { noFloatingObservablesRule } from './rules/no-floating-observables'; import { noIgnoredDefaultValueRule } from './rules/no-ignored-default-value'; import { noIgnoredErrorRule } from './rules/no-ignored-error'; import { noIgnoredNotifierRule } from './rules/no-ignored-notifier'; +import { noIgnoredObservableRule } from './rules/no-ignored-observable'; import { noIgnoredReplayBufferRule } from './rules/no-ignored-replay-buffer'; import { noIgnoredSubscribeRule } from './rules/no-ignored-subscribe'; import { noIgnoredSubscriptionRule } from './rules/no-ignored-subscription'; @@ -67,6 +68,7 @@ const plugin = { 'no-ignored-default-value': noIgnoredDefaultValueRule, 'no-ignored-error': noIgnoredErrorRule, 'no-ignored-notifier': noIgnoredNotifierRule, + 'no-ignored-observable': noIgnoredObservableRule, 'no-ignored-replay-buffer': noIgnoredReplayBufferRule, 'no-ignored-subscribe': noIgnoredSubscribeRule, 'no-ignored-subscription': noIgnoredSubscriptionRule, diff --git a/src/rules/no-ignored-observable.ts b/src/rules/no-ignored-observable.ts new file mode 100644 index 00000000..afc04444 --- /dev/null +++ b/src/rules/no-ignored-observable.ts @@ -0,0 +1,35 @@ +import { TSESTree as es } from '@typescript-eslint/utils'; +import { getTypeServices } from '../etc'; +import { ruleCreator } from '../utils'; + +export const noIgnoredObservableRule = ruleCreator({ + defaultOptions: [], + meta: { + deprecated: true, + replacedBy: ['no-floating-observables'], + docs: { + description: 'Disallow ignoring observables returned by functions.', + requiresTypeChecking: true, + }, + messages: { + forbidden: 'Ignoring a returned Observable is forbidden.', + }, + schema: [], + type: 'problem', + }, + name: 'no-ignored-observable', + create: (context) => { + const { couldBeObservable } = getTypeServices(context); + + return { + 'ExpressionStatement > CallExpression': (node: es.CallExpression) => { + if (couldBeObservable(node)) { + context.report({ + messageId: 'forbidden', + node, + }); + } + }, + }; + }, +}); diff --git a/tests/rules/no-ignored-observable.test.ts b/tests/rules/no-ignored-observable.test.ts new file mode 100644 index 00000000..8c6b8b6a --- /dev/null +++ b/tests/rules/no-ignored-observable.test.ts @@ -0,0 +1,61 @@ +import { stripIndent } from 'common-tags'; +import { noIgnoredObservableRule } from '../../src/rules/no-ignored-observable'; +import { fromFixture } from '../etc'; +import { ruleTester } from '../rule-tester'; + +ruleTester({ types: true }).run('no-ignored-observable', noIgnoredObservableRule, { + valid: [ + stripIndent` + // not ignored + import { Observable, of } from "rxjs"; + + function functionSource() { + return of(42); + } + + function sink(source: Observable) { + } + + const a = functionSource(); + sink(functionSource()); + `, + stripIndent` + // not ignored arrow + import { Observable, of } from "rxjs"; + + const arrowSource = () => of(42); + + function sink(source: Observable) { + } + + const a = arrowSource(); + sink(arrowSource()); + `, + ], + invalid: [ + fromFixture( + stripIndent` + // ignored + import { Observable, of } from "rxjs"; + + function functionSource() { + return of(42); + } + + functionSource(); + ~~~~~~~~~~~~~~~~ [forbidden] + `, + ), + fromFixture( + stripIndent` + // ignored arrow + import { Observable, of } from "rxjs"; + + const arrowSource = () => of(42); + + arrowSource(); + ~~~~~~~~~~~~~ [forbidden] + `, + ), + ], +}); From b7ed2829ef8719d1b1706d10de218411431f34a6 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 14:07:29 -0600 Subject: [PATCH 12/13] Revert "feat(no-ignored-observable): put back old rule" This reverts commit 47e12716f2e40561528a387d6502666cd3a0f019. --- README.md | 1 - docs/rules/no-ignored-observable.md | 31 ------------ src/index.ts | 2 - src/rules/no-ignored-observable.ts | 35 ------------- tests/rules/no-ignored-observable.test.ts | 61 ----------------------- 5 files changed, 130 deletions(-) delete mode 100644 docs/rules/no-ignored-observable.md delete mode 100644 src/rules/no-ignored-observable.ts delete mode 100644 tests/rules/no-ignored-observable.test.ts diff --git a/README.md b/README.md index 1b9a5714..e96fa26d 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ The package includes the following rules. | [no-ignored-default-value](docs/rules/no-ignored-default-value.md) | Disallow using `firstValueFrom`, `lastValueFrom`, `first`, and `last` without specifying a default value. | 🔒 | | | 💭 | | | [no-ignored-error](docs/rules/no-ignored-error.md) | Disallow calling `subscribe` without specifying an error handler. | 🔒 | | | 💭 | | | [no-ignored-notifier](docs/rules/no-ignored-notifier.md) | Disallow observables not composed from the `repeatWhen` or `retryWhen` notifier. | ✅ 🔒 | | | 💭 | | -| [no-ignored-observable](docs/rules/no-ignored-observable.md) | Disallow ignoring observables returned by functions. | | | | 💭 | ❌ | | [no-ignored-replay-buffer](docs/rules/no-ignored-replay-buffer.md) | Disallow using `ReplaySubject`, `publishReplay` or `shareReplay` without specifying the buffer size. | ✅ 🔒 | | | | | | [no-ignored-subscribe](docs/rules/no-ignored-subscribe.md) | Disallow calling `subscribe` without specifying arguments. | | | | 💭 | | | [no-ignored-subscription](docs/rules/no-ignored-subscription.md) | Disallow ignoring the subscription returned by `subscribe`. | | | | 💭 | | diff --git a/docs/rules/no-ignored-observable.md b/docs/rules/no-ignored-observable.md deleted file mode 100644 index 135acf99..00000000 --- a/docs/rules/no-ignored-observable.md +++ /dev/null @@ -1,31 +0,0 @@ -# Disallow ignoring observables returned by functions (`rxjs-x/no-ignored-observable`) - -❌ This rule is deprecated. It was replaced by [`rxjs-x/no-floating-observables`](no-floating-observables.md). - -💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). - - - -The effects failures if an observable returned by a function is neither assigned to a variable or property or passed to a function. - -> [!WARNING] -> This rule is being replaced by `no-floating-observables`. -> The new rule has been expanded to handle more expression types. -> -> The current rule `no-ignored-observable` will be removed in a future major version. - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -import { of } from "rxjs"; -of(42, 54); -``` - -Examples of **correct** code for this rule: - -```ts -import { of } from "rxjs"; -const answers = of(42, 54); -``` diff --git a/src/index.ts b/src/index.ts index 6fc21966..b0afe264 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,6 @@ import { noFloatingObservablesRule } from './rules/no-floating-observables'; import { noIgnoredDefaultValueRule } from './rules/no-ignored-default-value'; import { noIgnoredErrorRule } from './rules/no-ignored-error'; import { noIgnoredNotifierRule } from './rules/no-ignored-notifier'; -import { noIgnoredObservableRule } from './rules/no-ignored-observable'; import { noIgnoredReplayBufferRule } from './rules/no-ignored-replay-buffer'; import { noIgnoredSubscribeRule } from './rules/no-ignored-subscribe'; import { noIgnoredSubscriptionRule } from './rules/no-ignored-subscription'; @@ -68,7 +67,6 @@ const plugin = { 'no-ignored-default-value': noIgnoredDefaultValueRule, 'no-ignored-error': noIgnoredErrorRule, 'no-ignored-notifier': noIgnoredNotifierRule, - 'no-ignored-observable': noIgnoredObservableRule, 'no-ignored-replay-buffer': noIgnoredReplayBufferRule, 'no-ignored-subscribe': noIgnoredSubscribeRule, 'no-ignored-subscription': noIgnoredSubscriptionRule, diff --git a/src/rules/no-ignored-observable.ts b/src/rules/no-ignored-observable.ts deleted file mode 100644 index afc04444..00000000 --- a/src/rules/no-ignored-observable.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TSESTree as es } from '@typescript-eslint/utils'; -import { getTypeServices } from '../etc'; -import { ruleCreator } from '../utils'; - -export const noIgnoredObservableRule = ruleCreator({ - defaultOptions: [], - meta: { - deprecated: true, - replacedBy: ['no-floating-observables'], - docs: { - description: 'Disallow ignoring observables returned by functions.', - requiresTypeChecking: true, - }, - messages: { - forbidden: 'Ignoring a returned Observable is forbidden.', - }, - schema: [], - type: 'problem', - }, - name: 'no-ignored-observable', - create: (context) => { - const { couldBeObservable } = getTypeServices(context); - - return { - 'ExpressionStatement > CallExpression': (node: es.CallExpression) => { - if (couldBeObservable(node)) { - context.report({ - messageId: 'forbidden', - node, - }); - } - }, - }; - }, -}); diff --git a/tests/rules/no-ignored-observable.test.ts b/tests/rules/no-ignored-observable.test.ts deleted file mode 100644 index 8c6b8b6a..00000000 --- a/tests/rules/no-ignored-observable.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { stripIndent } from 'common-tags'; -import { noIgnoredObservableRule } from '../../src/rules/no-ignored-observable'; -import { fromFixture } from '../etc'; -import { ruleTester } from '../rule-tester'; - -ruleTester({ types: true }).run('no-ignored-observable', noIgnoredObservableRule, { - valid: [ - stripIndent` - // not ignored - import { Observable, of } from "rxjs"; - - function functionSource() { - return of(42); - } - - function sink(source: Observable) { - } - - const a = functionSource(); - sink(functionSource()); - `, - stripIndent` - // not ignored arrow - import { Observable, of } from "rxjs"; - - const arrowSource = () => of(42); - - function sink(source: Observable) { - } - - const a = arrowSource(); - sink(arrowSource()); - `, - ], - invalid: [ - fromFixture( - stripIndent` - // ignored - import { Observable, of } from "rxjs"; - - function functionSource() { - return of(42); - } - - functionSource(); - ~~~~~~~~~~~~~~~~ [forbidden] - `, - ), - fromFixture( - stripIndent` - // ignored arrow - import { Observable, of } from "rxjs"; - - const arrowSource = () => of(42); - - arrowSource(); - ~~~~~~~~~~~~~ [forbidden] - `, - ), - ], -}); From bc32335316c4720929335538540cd467dace65a5 Mon Sep 17 00:00:00 2001 From: Jason Weinzierl Date: Tue, 26 Nov 2024 14:17:32 -0600 Subject: [PATCH 13/13] docs(no-floating-observables): add rename to migration guide --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e96fa26d..7eb0a38a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ There are some breaking changes: - If you need to continue using this old format, use the original `eslint-plugin-rxjs` or a different fork. - The plugin namespace specified in the `recommended` config was changed from `rxjs` to `rxjs-x`. - e.g. In your ESLint config, `rxjs/no-subject-value` should be renamed to `rxjs-x/no-subject-value`. +- The rule `rxjs/no-ignored-observable` is renamed to `rxjs-x/no-floating-observables`. A complete description of all changes are documented in the [CHANGELOG](CHANGELOG.md) file.