Skip to content

Commit 438bdfc

Browse files
feat(no-misused-observables): new rule like no-misused-promises
1 parent bc32335 commit 438bdfc

File tree

5 files changed

+119
-0
lines changed

5 files changed

+119
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ The package includes the following rules.
100100
| [no-implicit-any-catch](docs/rules/no-implicit-any-catch.md) | Disallow implicit `any` error parameters in `catchError` operators. | ✅ 🔒 | 🔧 | 💡 | 💭 | |
101101
| [no-index](docs/rules/no-index.md) | Disallow importing index modules. | ✅ 🔒 | | | | |
102102
| [no-internal](docs/rules/no-internal.md) | Disallow importing internal modules. | ✅ 🔒 | 🔧 | 💡 | | |
103+
| [no-misused-observables](docs/rules/no-misused-observables.md) | Disallow Observables in places not designed to handle them. | | | | 💭 | |
103104
| [no-nested-subscribe](docs/rules/no-nested-subscribe.md) | Disallow calling `subscribe` within a `subscribe` callback. | ✅ 🔒 | | | 💭 | |
104105
| [no-redundant-notify](docs/rules/no-redundant-notify.md) | Disallow sending redundant notifications from completed or errored observables. | ✅ 🔒 | | | 💭 | |
105106
| [no-sharereplay](docs/rules/no-sharereplay.md) | Disallow unsafe `shareReplay` usage. | ✅ 🔒 | | | | |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Disallow Observables in places not designed to handle them (`rxjs-x/no-misused-observables`)
2+
3+
💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
## Options
8+
9+
<!-- begin auto-generated rule options list -->
10+
11+
| Name | Description | Type | Default |
12+
| :-------------- | :-------------------------------------- | :------ | :------ |
13+
| `checksSpreads` | Disallow `...` spreading an Observable. | Boolean | `true` |
14+
15+
<!-- end auto-generated rule options list -->

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { noIgnoredTakewhileValueRule } from './rules/no-ignored-takewhile-value'
2727
import { noImplicitAnyCatchRule } from './rules/no-implicit-any-catch';
2828
import { noIndexRule } from './rules/no-index';
2929
import { noInternalRule } from './rules/no-internal';
30+
import { noMisusedObservablesRule } from './rules/no-misused-observables';
3031
import { noNestedSubscribeRule } from './rules/no-nested-subscribe';
3132
import { noRedundantNotifyRule } from './rules/no-redundant-notify';
3233
import { noSharereplayRule } from './rules/no-sharereplay';
@@ -74,6 +75,7 @@ const plugin = {
7475
'no-implicit-any-catch': noImplicitAnyCatchRule,
7576
'no-index': noIndexRule,
7677
'no-internal': noInternalRule,
78+
'no-misused-observables': noMisusedObservablesRule,
7779
'no-nested-subscribe': noNestedSubscribeRule,
7880
'no-redundant-notify': noRedundantNotifyRule,
7981
'no-sharereplay': noSharereplayRule,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { TSESLint } from '@typescript-eslint/utils';
2+
import { getTypeServices } from '../etc';
3+
import { ruleCreator } from '../utils';
4+
5+
// The implementation of this rule is similar to typescript-eslint's no-misused-promises. MIT License.
6+
// https://github.com/typescript-eslint/typescript-eslint/blob/fcd6cf063a774f73ea00af23705117a197f826d4/packages/eslint-plugin/src/rules/no-misused-promises.ts
7+
8+
const defaultOptions: readonly {
9+
checksSpreads?: boolean;
10+
}[] = [];
11+
12+
export const noMisusedObservablesRule = ruleCreator({
13+
defaultOptions,
14+
meta: {
15+
docs: {
16+
description: 'Disallow Observables in places not designed to handle them.',
17+
requiresTypeChecking: true,
18+
},
19+
messages: {
20+
forbiddenSpread: 'Expected a non-Observable value to be spread into an object.',
21+
},
22+
schema: [
23+
{
24+
properties: {
25+
checksSpreads: { type: 'boolean', default: true, description: 'Disallow `...` spreading an Observable.' },
26+
},
27+
type: 'object',
28+
},
29+
],
30+
type: 'problem',
31+
},
32+
name: 'no-misused-observables',
33+
create: (context) => {
34+
const { couldBeObservable } = getTypeServices(context);
35+
const [config = {}] = context.options;
36+
const { checksSpreads = true } = config;
37+
38+
const spreadChecks: TSESLint.RuleListener = {
39+
SpreadElement: (node) => {
40+
if (couldBeObservable(node.argument)) {
41+
context.report({
42+
messageId: 'forbiddenSpread',
43+
node: node.argument,
44+
});
45+
}
46+
},
47+
};
48+
49+
return {
50+
...(checksSpreads ? spreadChecks : {}),
51+
};
52+
},
53+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { stripIndent } from 'common-tags';
2+
import { noMisusedObservablesRule } from '../../src/rules/no-misused-observables';
3+
import { fromFixture } from '../etc';
4+
import { ruleTester } from '../rule-tester';
5+
6+
ruleTester({ types: true }).run('no-misused-observables', noMisusedObservablesRule, {
7+
valid: [
8+
{
9+
code: stripIndent`
10+
// spread; explicitly allowed
11+
import { of } from "rxjs";
12+
13+
const source = of(42);
14+
const foo = { ...source };
15+
`,
16+
options: [{ checksSpreads: false }],
17+
},
18+
stripIndent`
19+
// unrelated
20+
const foo = { bar: 42 };
21+
const baz = { ...foo };
22+
`,
23+
],
24+
invalid: [
25+
fromFixture(
26+
stripIndent`
27+
// spread variable
28+
import { of } from "rxjs";
29+
30+
const source = of(42);
31+
const foo = { ...source };
32+
~~~~~~ [forbiddenSpread]
33+
`,
34+
),
35+
fromFixture(
36+
stripIndent`
37+
// spread call function
38+
import { of } from "rxjs";
39+
40+
function source() {
41+
return of(42);
42+
}
43+
const foo = { ...source() };
44+
~~~~~~~~ [forbiddenSpread]
45+
`,
46+
),
47+
],
48+
});

0 commit comments

Comments
 (0)