Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The package includes the following rules.
| [no-subject-unsubscribe](docs/rules/no-subject-unsubscribe.md) | Disallow calling the `unsubscribe` method of subjects. | ✅ 🔒 | | | 💭 | |
| [no-subject-value](docs/rules/no-subject-value.md) | Disallow accessing the `value` property of a `BehaviorSubject` instance. | | | | 💭 | |
| [no-subscribe-handlers](docs/rules/no-subscribe-handlers.md) | Disallow passing handlers to `subscribe`. | | | | 💭 | |
| [no-subscribe-in-pipe](docs/rules/no-subscribe-in-pipe.md) | Disallow calling of `subscribe` within any RxJS operator inside a `pipe`. | ✅ 🔒 | | | 💭 | |
| [no-tap](docs/rules/no-tap.md) | Disallow the `tap` operator. | | | | | ❌ |
| [no-topromise](docs/rules/no-topromise.md) | Disallow use of the `toPromise` method. | ✅ 🔒 | | 💡 | 💭 | |
| [no-unbound-methods](docs/rules/no-unbound-methods.md) | Disallow passing unbound methods. | ✅ 🔒 | | | 💭 | |
Expand Down
37 changes: 37 additions & 0 deletions docs/rules/no-subscribe-in-pipe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Disallow calling of `subscribe` within any RxJS operator inside a `pipe` (`rxjs-x/no-subscribe-in-pipe`)

💼 This rule is enabled in the following configs: ✅ `recommended`, 🔒 `strict`.

💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).

<!-- end auto-generated rule header -->

This rule effects failures if `subscribe` is called within any operator inside a `pipe` operation.

## Rule details

Examples of **incorrect** code for this rule:

```ts
import { of } from "rxjs";
import { map } from "rxjs/operators";

of(42, 54).pipe(
map(value => {
of(value).subscribe(console.log); // This will trigger the rule
return value * 2;
})
).subscribe(result => console.log(result));
```

Examples of **correct** code for this rule:

```ts
import { of } from "rxjs";
import { map, tap } from "rxjs/operators";

of(42, 54).pipe(
tap(value => console.log(value)),
map(value => value * 2)
).subscribe(result => console.log(result));
```
1 change: 1 addition & 0 deletions src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const createRecommendedConfig = (
'rxjs-x/no-redundant-notify': 'error',
'rxjs-x/no-sharereplay': 'error',
'rxjs-x/no-subject-unsubscribe': 'error',
'rxjs-x/no-subscribe-in-pipe': 'error',
'rxjs-x/no-topromise': 'error',
'rxjs-x/no-unbound-methods': 'error',
'rxjs-x/no-unsafe-subject-next': 'error',
Expand Down
1 change: 1 addition & 0 deletions src/configs/strict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const createStrictConfig = (
'rxjs-x/no-sharereplay': 'error',
'rxjs-x/no-subclass': 'error',
'rxjs-x/no-subject-unsubscribe': 'error',
'rxjs-x/no-subscribe-in-pipe': 'error',
'rxjs-x/no-topromise': 'error',
'rxjs-x/no-unbound-methods': 'error',
'rxjs-x/no-unsafe-subject-next': 'error',
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { noSubclassRule } from './rules/no-subclass';
import { noSubjectUnsubscribeRule } from './rules/no-subject-unsubscribe';
import { noSubjectValueRule } from './rules/no-subject-value';
import { noSubscribeHandlersRule } from './rules/no-subscribe-handlers';
import { noSubscribeInPipeRule } from './rules/no-subscribe-in-pipe';
import { noTapRule } from './rules/no-tap';
import { noTopromiseRule } from './rules/no-topromise';
import { noUnboundMethodsRule } from './rules/no-unbound-methods';
Expand Down Expand Up @@ -83,6 +84,7 @@ const plugin = {
'no-subject-unsubscribe': noSubjectUnsubscribeRule,
'no-subject-value': noSubjectValueRule,
'no-subscribe-handlers': noSubscribeHandlersRule,
'no-subscribe-in-pipe': noSubscribeInPipeRule,
'no-tap': noTapRule,
'no-topromise': noTopromiseRule,
'no-unbound-methods': noUnboundMethodsRule,
Expand Down
63 changes: 63 additions & 0 deletions src/rules/no-subscribe-in-pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { AST_NODE_TYPES, TSESTree as es } from '@typescript-eslint/utils';
import { getTypeServices } from '../etc';
import { ruleCreator } from '../utils';

export const noSubscribeInPipeRule = ruleCreator({
defaultOptions: [],
meta: {
docs: {
description:
'Disallow calling of `subscribe` within any RxJS operator inside a `pipe`.',
recommended: 'recommended',
requiresTypeChecking: true,
},
fixable: undefined,
hasSuggestions: false,
messages: {
forbidden: 'Subscribe calls within pipe operators are forbidden.',
},
schema: [],
type: 'problem',
},
name: 'no-subscribe-in-pipe',
create: (context) => {
const { couldBeObservable, couldBeType } = getTypeServices(context);

function isWithinPipe(node: es.Node): boolean {
let parent = node.parent;

while (parent) {
if (
parent.type === AST_NODE_TYPES.CallExpression
&& parent.callee.type === AST_NODE_TYPES.MemberExpression
&& parent.callee.property.type === AST_NODE_TYPES.Identifier
&& parent.callee.property.name === 'pipe'
) {
return true;
}
parent = node.parent;
}
return false;
}

return {
'CallExpression > MemberExpression[property.name=\'subscribe\']': (
node: es.MemberExpression,
) => {
if (
!couldBeObservable(node.object)
&& !couldBeType(node.object, 'Subscribable')
) {
return;
}

if (isWithinPipe(node)) {
context.report({
messageId: 'forbidden',
node: node.property,
});
}
},
};
},
});
206 changes: 206 additions & 0 deletions tests/rules/no-subscribe-in-pipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { stripIndent } from 'common-tags';
import { noSubscribeInPipeRule } from '../../src/rules/no-subscribe-in-pipe';
import { fromFixture } from '../etc';
import { ruleTester } from '../rule-tester';

ruleTester({ types: true }).run('no-subscribe-in-pipe', noSubscribeInPipeRule, {
valid: [
stripIndent`
// subscribe outside of pipe
import { of } from "rxjs";
of(47).subscribe(value => {
console.log(value);
});
`,
stripIndent`
// pipe without subscribe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => x * 2)
).subscribe(value => {
console.log(value);
});
`,
stripIndent`
// nested pipes without subscribe
import { of } from "rxjs";
import { map, mergeMap } from "rxjs/operators";
of(47).pipe(
mergeMap(x => of(x).pipe(
map(y => y * 2)
))
).subscribe(value => {
console.log(value);
});
`,
stripIndent`
// subscribe in a function outside pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
const logValue = (value) => of(value).subscribe(console.log);
of(47).pipe(
map(x => x * 2)
).subscribe(logValue);
`,
stripIndent`
// subscribe method on a non-Observable object inside pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
const customObject = { subscribe: () => {} };
of(47).pipe(
map(x => {
customObject.subscribe();
return x * 2;
})
).subscribe();
`,
stripIndent`
// subscribe as a variable name inside pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => {
const subscribe = 5;
return x * subscribe;
})
).subscribe();
`,
stripIndent`
// subscribe in a comment inside pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => {
// of(x).subscribe(console.log);
return x * 2;
})
).subscribe();
`,
stripIndent`
// subscribe as a string inside pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => {
console.log("subscribe");
return x * 2;
})
).subscribe();
`,
stripIndent`
// subscribe inside of an Observable constructor
import { Observable, of } from "rxjs";

new Observable<number>(subscriber => {
of(42).subscribe(subscriber);
}).subscribe();
`,
],
invalid: [
fromFixture(
stripIndent`
// subscribe inside map operator
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => {
of(x).subscribe(console.log);
~~~~~~~~~ [forbidden]
return x * 2;
})
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe inside mergeMap operator
import { of } from "rxjs";
import { mergeMap } from "rxjs/operators";
of(47).pipe(
mergeMap(x => of(x).pipe(
map(y => {
of(y).subscribe(console.log);
~~~~~~~~~ [forbidden]
return y * 2;
})
))
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe inside tap operator
import { of } from "rxjs";
import { tap } from "rxjs/operators";
of(47).pipe(
tap(x => {
of(x).subscribe(console.log);
~~~~~~~~~ [forbidden]
})
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe inside switchMap operator
import { of } from "rxjs";
import { switchMap } from "rxjs/operators";
of(47).pipe(
switchMap(x => {
of(x).subscribe(console.log);
~~~~~~~~~ [forbidden]
return of(x * 2);
})
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe inside nested pipes
import { of } from "rxjs";
import { map, mergeMap } from "rxjs/operators";
of(47).pipe(
mergeMap(x => of(x).pipe(
map(y => {
of(y).subscribe(console.log);
~~~~~~~~~ [forbidden]
return y * 2;
})
))
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe inside a deeply nested function in pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => {
const nestedFunc = () => {
const deeplyNested = () => {
of(x).subscribe(console.log);
~~~~~~~~~ [forbidden]
};
deeplyNested();
};
nestedFunc();
return x * 2;
})
).subscribe();
`,
),
fromFixture(
stripIndent`
// subscribe in a ternary operator in pipe
import { of } from "rxjs";
import { map } from "rxjs/operators";
of(47).pipe(
map(x => x > 50 ? x : of(x).subscribe(console.log))
~~~~~~~~~ [forbidden]
).subscribe();
`,
),
],
});
Loading