Skip to content

Commit eca0cba

Browse files
feat(no-floating-observables): add option to ignore void expression
1 parent fa3f2c5 commit eca0cba

File tree

4 files changed

+76
-20
lines changed

4 files changed

+76
-20
lines changed

docs/rules/no-floating-observables.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,13 @@ Examples of **correct** code for this rule:
3535
import { of } from "rxjs";
3636
const answers = of(42, 54);
3737
```
38+
39+
## Options
40+
41+
<!-- begin auto-generated rule options list -->
42+
43+
| Name | Description | Type | Default |
44+
| :----------- | :------------------------------------ | :------ | :------ |
45+
| `ignoreVoid` | Whether to ignore `void` expressions. | Boolean | `true` |
46+
47+
<!-- end auto-generated rule options list -->

src/etc/is.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export function isTSTypeReference(node: TSESTree.Node): node is TSESTree.TSTypeR
124124
return node.type === AST_NODE_TYPES.TSTypeReference;
125125
}
126126

127+
export function isUnaryExpression(node: TSESTree.Node): node is TSESTree.UnaryExpression {
128+
return node.type === AST_NODE_TYPES.UnaryExpression;
129+
}
130+
127131
export function isVariableDeclarator(
128132
node: TSESTree.Node,
129133
): node is TSESTree.VariableDeclarator {

src/rules/no-floating-observables.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,66 @@
11
import { TSESTree as es } from '@typescript-eslint/utils';
2-
import { getTypeServices } from '../etc';
2+
import { getTypeServices, isUnaryExpression } from '../etc';
33
import { ruleCreator } from '../utils';
44

5+
const defaultOptions: readonly {
6+
ignoreVoid?: boolean;
7+
}[] = [];
8+
9+
const messageBase
10+
= 'Observables must be subscribed to, returned, converted to a promise and awaited, '
11+
+ 'or be explicitly marked as ignored with the `void` operator.';
12+
13+
const messageBaseNoVoid
14+
= 'Observables must be subscribed to, returned, or converted to a promise and awaited.';
15+
516
export const noFloatingObservablesRule = ruleCreator({
6-
defaultOptions: [],
17+
defaultOptions,
718
meta: {
819
docs: {
920
description: 'Require Observables to be handled appropriately.',
1021
recommended: 'strict',
1122
requiresTypeChecking: true,
1223
},
1324
messages: {
14-
forbidden:
15-
'Observables must be subscribed to, returned, converted to a promise and awaited, '
16-
+ 'or be explicitly marked as ignored with the `void` operator.',
25+
forbidden: messageBase,
26+
forbiddenNoVoid: messageBaseNoVoid,
1727
},
18-
schema: [],
28+
schema: [
29+
{
30+
properties: {
31+
ignoreVoid: { type: 'boolean', default: true, description: 'Whether to ignore `void` expressions.' },
32+
},
33+
type: 'object',
34+
},
35+
],
1936
type: 'problem',
2037
},
2138
name: 'no-floating-observables',
2239
create: (context) => {
2340
const { couldBeObservable } = getTypeServices(context);
41+
const [config = {}] = context.options;
42+
const { ignoreVoid = true } = config;
2443

2544
return {
26-
'ExpressionStatement > CallExpression': (node: es.CallExpression) => {
27-
if (couldBeObservable(node)) {
45+
ExpressionStatement: (node: es.ExpressionStatement) => {
46+
const { expression } = node;
47+
if (couldBeObservable(expression)) {
2848
context.report({
29-
messageId: 'forbidden',
49+
messageId: ignoreVoid ? 'forbidden' : 'forbiddenNoVoid',
3050
node,
3151
});
52+
return;
53+
}
54+
55+
if (!ignoreVoid && isUnaryExpression(expression)) {
56+
const { operator, argument } = expression;
57+
if (operator === 'void' && couldBeObservable(argument)) {
58+
context.report({
59+
messageId: 'forbiddenNoVoid',
60+
node: argument,
61+
});
62+
return;
63+
}
3264
}
3365
},
3466
};

tests/rules/no-floating-observables.test.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables
1616
function sink(source: Observable<number>) {
1717
}
1818
19+
functionSource().subscribe();
1920
const a = functionSource();
2021
sink(functionSource());
22+
void functionSource();
2123
`,
2224
stripIndent`
2325
// not ignored arrow
@@ -28,17 +30,9 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables
2830
function sink(source: Observable<number>) {
2931
}
3032
33+
functionSource().subscribe();
3134
const a = arrowSource();
3235
sink(arrowSource());
33-
`,
34-
stripIndent`
35-
// void operator
36-
import { of } from "rxjs";
37-
38-
function functionSource() {
39-
return of(42);
40-
}
41-
4236
void functionSource();
4337
`,
4438
],
@@ -53,7 +47,7 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables
5347
}
5448
5549
functionSource();
56-
~~~~~~~~~~~~~~~~ [forbidden]
50+
~~~~~~~~~~~~~~~~~ [forbidden]
5751
`,
5852
),
5953
fromFixture(
@@ -64,8 +58,24 @@ ruleTester({ types: true }).run('no-floating-observables', noFloatingObservables
6458
const arrowSource = () => of(42);
6559
6660
arrowSource();
67-
~~~~~~~~~~~~~ [forbidden]
61+
~~~~~~~~~~~~~~ [forbidden]
62+
`,
63+
),
64+
fromFixture(
65+
stripIndent`
66+
// ignoreVoid false
67+
import { Observable, of } from "rxjs";
68+
69+
function functionSource() {
70+
return of(42);
71+
}
72+
73+
void functionSource();
74+
~~~~~~~~~~~~~~~~ [forbiddenNoVoid]
6875
`,
76+
{
77+
options: [{ ignoreVoid: false }],
78+
},
6979
),
7080
],
7181
});

0 commit comments

Comments
 (0)