Skip to content

Commit 26fe38b

Browse files
feat(no-subscribe-in-pipe)!: new rule to forbid calling subscribe within pipe operators (#59)
BREAKING CHANGE: new rule added to `recommended` config. New rule to forbid calling `subscribe` within `pipe`. This was previously caught by `no-nested-subscribe` until it was changed due to cartant#67. This rule was proposed to the original repo and was re-proposed here by request: cartant#131 (comment) Signed-off-by: Dane Vanderbilt <Danevandy99@users.noreply.github.com> Co-authored-by: Jason Weinzierl <weinzierljason@gmail.com>
1 parent f453132 commit 26fe38b

File tree

7 files changed

+311
-0
lines changed

7 files changed

+311
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ The package includes the following rules.
108108
| [no-subject-unsubscribe](docs/rules/no-subject-unsubscribe.md) | Disallow calling the `unsubscribe` method of subjects. | ✅ 🔒 | | | 💭 | |
109109
| [no-subject-value](docs/rules/no-subject-value.md) | Disallow accessing the `value` property of a `BehaviorSubject` instance. | | | | 💭 | |
110110
| [no-subscribe-handlers](docs/rules/no-subscribe-handlers.md) | Disallow passing handlers to `subscribe`. | | | | 💭 | |
111+
| [no-subscribe-in-pipe](docs/rules/no-subscribe-in-pipe.md) | Disallow calling of `subscribe` within any RxJS operator inside a `pipe`. | ✅ 🔒 | | | 💭 | |
111112
| [no-tap](docs/rules/no-tap.md) | Disallow the `tap` operator. | | | | ||
112113
| [no-topromise](docs/rules/no-topromise.md) | Disallow use of the `toPromise` method. | ✅ 🔒 | | 💡 | 💭 | |
113114
| [no-unbound-methods](docs/rules/no-unbound-methods.md) | Disallow passing unbound methods. | ✅ 🔒 | | | 💭 | |

docs/rules/no-subscribe-in-pipe.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Disallow calling of `subscribe` within any RxJS operator inside a `pipe` (`rxjs-x/no-subscribe-in-pipe`)
2+
3+
💼 This rule is enabled in the following configs: ✅ `recommended`, 🔒 `strict`.
4+
5+
💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
This rule effects failures if `subscribe` is called within any operator inside a `pipe` operation.
10+
11+
## Rule details
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```ts
16+
import { of } from "rxjs";
17+
import { map } from "rxjs/operators";
18+
19+
of(42, 54).pipe(
20+
map(value => {
21+
of(value).subscribe(console.log); // This will trigger the rule
22+
return value * 2;
23+
})
24+
).subscribe(result => console.log(result));
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```ts
30+
import { of } from "rxjs";
31+
import { map, tap } from "rxjs/operators";
32+
33+
of(42, 54).pipe(
34+
tap(value => console.log(value)),
35+
map(value => value * 2)
36+
).subscribe(result => console.log(result));
37+
```

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const createRecommendedConfig = (
2020
'rxjs-x/no-redundant-notify': 'error',
2121
'rxjs-x/no-sharereplay': 'error',
2222
'rxjs-x/no-subject-unsubscribe': 'error',
23+
'rxjs-x/no-subscribe-in-pipe': 'error',
2324
'rxjs-x/no-topromise': 'error',
2425
'rxjs-x/no-unbound-methods': 'error',
2526
'rxjs-x/no-unsafe-subject-next': 'error',

src/configs/strict.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const createStrictConfig = (
2929
'rxjs-x/no-sharereplay': 'error',
3030
'rxjs-x/no-subclass': 'error',
3131
'rxjs-x/no-subject-unsubscribe': 'error',
32+
'rxjs-x/no-subscribe-in-pipe': 'error',
3233
'rxjs-x/no-topromise': 'error',
3334
'rxjs-x/no-unbound-methods': 'error',
3435
'rxjs-x/no-unsafe-subject-next': 'error',

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { noSubclassRule } from './rules/no-subclass';
3535
import { noSubjectUnsubscribeRule } from './rules/no-subject-unsubscribe';
3636
import { noSubjectValueRule } from './rules/no-subject-value';
3737
import { noSubscribeHandlersRule } from './rules/no-subscribe-handlers';
38+
import { noSubscribeInPipeRule } from './rules/no-subscribe-in-pipe';
3839
import { noTapRule } from './rules/no-tap';
3940
import { noTopromiseRule } from './rules/no-topromise';
4041
import { noUnboundMethodsRule } from './rules/no-unbound-methods';
@@ -83,6 +84,7 @@ const plugin = {
8384
'no-subject-unsubscribe': noSubjectUnsubscribeRule,
8485
'no-subject-value': noSubjectValueRule,
8586
'no-subscribe-handlers': noSubscribeHandlersRule,
87+
'no-subscribe-in-pipe': noSubscribeInPipeRule,
8688
'no-tap': noTapRule,
8789
'no-topromise': noTopromiseRule,
8890
'no-unbound-methods': noUnboundMethodsRule,

src/rules/no-subscribe-in-pipe.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { AST_NODE_TYPES, TSESTree as es } from '@typescript-eslint/utils';
2+
import { getTypeServices } from '../etc';
3+
import { ruleCreator } from '../utils';
4+
5+
export const noSubscribeInPipeRule = ruleCreator({
6+
defaultOptions: [],
7+
meta: {
8+
docs: {
9+
description:
10+
'Disallow calling of `subscribe` within any RxJS operator inside a `pipe`.',
11+
recommended: 'recommended',
12+
requiresTypeChecking: true,
13+
},
14+
fixable: undefined,
15+
hasSuggestions: false,
16+
messages: {
17+
forbidden: 'Subscribe calls within pipe operators are forbidden.',
18+
},
19+
schema: [],
20+
type: 'problem',
21+
},
22+
name: 'no-subscribe-in-pipe',
23+
create: (context) => {
24+
const { couldBeObservable, couldBeType } = getTypeServices(context);
25+
26+
function isWithinPipe(node: es.Node): boolean {
27+
let parent = node.parent;
28+
29+
while (parent) {
30+
if (
31+
parent.type === AST_NODE_TYPES.CallExpression
32+
&& parent.callee.type === AST_NODE_TYPES.MemberExpression
33+
&& parent.callee.property.type === AST_NODE_TYPES.Identifier
34+
&& parent.callee.property.name === 'pipe'
35+
) {
36+
return true;
37+
}
38+
parent = parent.parent;
39+
}
40+
return false;
41+
}
42+
43+
return {
44+
'CallExpression > MemberExpression[property.name=\'subscribe\']': (
45+
node: es.MemberExpression,
46+
) => {
47+
if (
48+
!couldBeObservable(node.object)
49+
&& !couldBeType(node.object, 'Subscribable')
50+
) {
51+
return;
52+
}
53+
54+
if (isWithinPipe(node)) {
55+
context.report({
56+
messageId: 'forbidden',
57+
node: node.property,
58+
});
59+
}
60+
},
61+
};
62+
},
63+
});
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { stripIndent } from 'common-tags';
2+
import { noSubscribeInPipeRule } from '../../src/rules/no-subscribe-in-pipe';
3+
import { fromFixture } from '../etc';
4+
import { ruleTester } from '../rule-tester';
5+
6+
ruleTester({ types: true }).run('no-subscribe-in-pipe', noSubscribeInPipeRule, {
7+
valid: [
8+
stripIndent`
9+
// subscribe outside of pipe
10+
import { of } from "rxjs";
11+
of(47).subscribe(value => {
12+
console.log(value);
13+
});
14+
`,
15+
stripIndent`
16+
// pipe without subscribe
17+
import { of } from "rxjs";
18+
import { map } from "rxjs/operators";
19+
of(47).pipe(
20+
map(x => x * 2)
21+
).subscribe(value => {
22+
console.log(value);
23+
});
24+
`,
25+
stripIndent`
26+
// nested pipes without subscribe
27+
import { of } from "rxjs";
28+
import { map, mergeMap } from "rxjs/operators";
29+
of(47).pipe(
30+
mergeMap(x => of(x).pipe(
31+
map(y => y * 2)
32+
))
33+
).subscribe(value => {
34+
console.log(value);
35+
});
36+
`,
37+
stripIndent`
38+
// subscribe in a function outside pipe
39+
import { of } from "rxjs";
40+
import { map } from "rxjs/operators";
41+
const logValue = (value) => of(value).subscribe(console.log);
42+
of(47).pipe(
43+
map(x => x * 2)
44+
).subscribe(logValue);
45+
`,
46+
stripIndent`
47+
// subscribe method on a non-Observable object inside pipe
48+
import { of } from "rxjs";
49+
import { map } from "rxjs/operators";
50+
const customObject = { subscribe: () => {} };
51+
of(47).pipe(
52+
map(x => {
53+
customObject.subscribe();
54+
return x * 2;
55+
})
56+
).subscribe();
57+
`,
58+
stripIndent`
59+
// subscribe as a variable name inside pipe
60+
import { of } from "rxjs";
61+
import { map } from "rxjs/operators";
62+
of(47).pipe(
63+
map(x => {
64+
const subscribe = 5;
65+
return x * subscribe;
66+
})
67+
).subscribe();
68+
`,
69+
stripIndent`
70+
// subscribe in a comment inside pipe
71+
import { of } from "rxjs";
72+
import { map } from "rxjs/operators";
73+
of(47).pipe(
74+
map(x => {
75+
// of(x).subscribe(console.log);
76+
return x * 2;
77+
})
78+
).subscribe();
79+
`,
80+
stripIndent`
81+
// subscribe as a string inside pipe
82+
import { of } from "rxjs";
83+
import { map } from "rxjs/operators";
84+
of(47).pipe(
85+
map(x => {
86+
console.log("subscribe");
87+
return x * 2;
88+
})
89+
).subscribe();
90+
`,
91+
stripIndent`
92+
// subscribe inside of an Observable constructor
93+
import { Observable, of } from "rxjs";
94+
95+
new Observable<number>(subscriber => {
96+
of(42).subscribe(subscriber);
97+
}).subscribe();
98+
`,
99+
],
100+
invalid: [
101+
fromFixture(
102+
stripIndent`
103+
// subscribe inside map operator
104+
import { of } from "rxjs";
105+
import { map } from "rxjs/operators";
106+
of(47).pipe(
107+
map(x => {
108+
of(x).subscribe(console.log);
109+
~~~~~~~~~ [forbidden]
110+
return x * 2;
111+
})
112+
).subscribe();
113+
`,
114+
),
115+
fromFixture(
116+
stripIndent`
117+
// subscribe inside mergeMap operator
118+
import { of } from "rxjs";
119+
import { mergeMap } from "rxjs/operators";
120+
of(47).pipe(
121+
mergeMap(x => of(x).pipe(
122+
map(y => {
123+
of(y).subscribe(console.log);
124+
~~~~~~~~~ [forbidden]
125+
return y * 2;
126+
})
127+
))
128+
).subscribe();
129+
`,
130+
),
131+
fromFixture(
132+
stripIndent`
133+
// subscribe inside tap operator
134+
import { of } from "rxjs";
135+
import { tap } from "rxjs/operators";
136+
of(47).pipe(
137+
tap(x => {
138+
of(x).subscribe(console.log);
139+
~~~~~~~~~ [forbidden]
140+
})
141+
).subscribe();
142+
`,
143+
),
144+
fromFixture(
145+
stripIndent`
146+
// subscribe inside switchMap operator
147+
import { of } from "rxjs";
148+
import { switchMap } from "rxjs/operators";
149+
of(47).pipe(
150+
switchMap(x => {
151+
of(x).subscribe(console.log);
152+
~~~~~~~~~ [forbidden]
153+
return of(x * 2);
154+
})
155+
).subscribe();
156+
`,
157+
),
158+
fromFixture(
159+
stripIndent`
160+
// subscribe inside nested pipes
161+
import { of } from "rxjs";
162+
import { map, mergeMap } from "rxjs/operators";
163+
of(47).pipe(
164+
mergeMap(x => of(x).pipe(
165+
map(y => {
166+
of(y).subscribe(console.log);
167+
~~~~~~~~~ [forbidden]
168+
return y * 2;
169+
})
170+
))
171+
).subscribe();
172+
`,
173+
),
174+
fromFixture(
175+
stripIndent`
176+
// subscribe inside a deeply nested function in pipe
177+
import { of } from "rxjs";
178+
import { map } from "rxjs/operators";
179+
of(47).pipe(
180+
map(x => {
181+
const nestedFunc = () => {
182+
const deeplyNested = () => {
183+
of(x).subscribe(console.log);
184+
~~~~~~~~~ [forbidden]
185+
};
186+
deeplyNested();
187+
};
188+
nestedFunc();
189+
return x * 2;
190+
})
191+
).subscribe();
192+
`,
193+
),
194+
fromFixture(
195+
stripIndent`
196+
// subscribe in a ternary operator in pipe
197+
import { of } from "rxjs";
198+
import { map } from "rxjs/operators";
199+
of(47).pipe(
200+
map(x => x > 50 ? x : of(x).subscribe(console.log))
201+
~~~~~~~~~ [forbidden]
202+
).subscribe();
203+
`,
204+
),
205+
],
206+
});

0 commit comments

Comments
 (0)