diff --git a/docs/rules/no-nested-pipe.md b/docs/rules/no-nested-pipe.md new file mode 100644 index 00000000..a9b0ddff --- /dev/null +++ b/docs/rules/no-nested-pipe.md @@ -0,0 +1,60 @@ +# Avoid nested `pipe` calls (`no-nested-pipe`) + +This rule effects failures if `pipe` is called within a `pipe` handler. + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +import { switchMap, map, of } from 'rxjs'; + +of('searchText1', 'searchText2') + .pipe( + switchMap(searchText => { + return callSearchAPI(searchText).pipe( + map(response => { + console.log(response); + return 'final ' + response; + // considering more lines here + }) + ); + }) + ) + .subscribe(value => console.log(value)); + +function callSearchAPI(searchText) { + return of('new' + searchText); +} + + +``` + +Examples of **correct** code for this rule: + +```ts +import { switchMap, map, of } from 'rxjs'; + +of('searchText1', 'searchText2') + .pipe( + switchMap(searchText => { + return callSearchAPI(searchText); + }) + ) + .subscribe(value => console.log(value)); + +function callSearchAPI(searchText) { + return of('new' + searchText).pipe( + map(response => { + console.log(response); + return 'final ' + response; + // considering more lines here + }) + ); +} + +``` + +## Options + +This rule has no options. diff --git a/source/rules/no-nested-pipe.ts b/source/rules/no-nested-pipe.ts new file mode 100644 index 00000000..99ccc33b --- /dev/null +++ b/source/rules/no-nested-pipe.ts @@ -0,0 +1,59 @@ +/** + * @license Use of this source code is governed by an MIT-style license that + * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs + */ + +import { TSESTree as es } from "@typescript-eslint/experimental-utils"; +import { getParent, getTypeServices } from "eslint-etc"; +import { ruleCreator } from "../utils"; + +const rule = ruleCreator({ + defaultOptions: [], + meta: { + docs: { + description: "Forbids the calling of `pipe` within a `pipe` callback.", + recommended: "error", + }, + fixable: undefined, + hasSuggestions: false, + messages: { + forbidden: "Nested pipe calls are forbidden.", + }, + schema: [], + type: "problem", + }, + name: "no-nested-pipe", + create: (context) => { + const { couldBeObservable, couldBeType } = getTypeServices(context); + const argumentsMap = new WeakMap(); + return { + [`CallExpression > MemberExpression[property.name='pipe']`]: ( + node: es.MemberExpression + ) => { + if ( + !couldBeObservable(node.object) && + !couldBeType(node.object, "Pipeable") + ) { + return; + } + const callExpression = getParent(node) as es.CallExpression; + let parent = getParent(callExpression); + while (parent) { + if (argumentsMap.has(parent)) { + context.report({ + messageId: "forbidden", + node: node.property, + }); + return; + } + parent = getParent(parent); + } + for (const arg of callExpression.arguments) { + argumentsMap.set(arg); + } + }, + }; + }, +}); + +export = rule; diff --git a/tests/rules/no-nested-pipe.ts b/tests/rules/no-nested-pipe.ts new file mode 100644 index 00000000..21984d37 --- /dev/null +++ b/tests/rules/no-nested-pipe.ts @@ -0,0 +1,94 @@ +/** + * @license Use of this source code is governed by an MIT-style license that + * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs + */ + +import { stripIndent } from "common-tags"; +import { fromFixture } from "eslint-etc"; +import rule = require("../../source/rules/no-nested-pipe"); +import { ruleTester } from "../utils"; + +ruleTester({ types: true }).run("no-nested-pipe", rule, { + valid: [ + stripIndent` + // not nested in pipe + import { Observable,of,switchMap } from "rxjs"; + of(47).pipe(switchMap(value => { + console.log('new' ,value); + })).subscribe(value => { + console.log(value); + }) + `, + stripIndent` + // not nested in pipe + import { Observable,switchMap } from "rxjs"; + of(47).pipe(switchMap(value => { + return someFunction(value) + })).subscribe(value => { + console.log(value); + }); + function someFunction(someParam: Observable): Observable { + return of(43).pipe( + switchMap(value => {value + someParam}) + ) + } + `, + stripIndent` + // not nested in pipe using function move to separate function + import { Observable,switchMap } from "rxjs"; + of(47).pipe(switchMap(value => { + return someFunction1(value) + }), + switchMap(value => { + return someFunction2(value) + }) + ).subscribe(value => { + console.log(value); + }); + function someFunction1(someParam: Observable): Observable { + return of(43).pipe( + switchMap(value => {value + someParam}) + ) + }; + function someFunction2(someParam: Observable): Observable { + return of(43).pipe( + switchMap(value => {value + someParam}) + ) + } +`, + ], + invalid: [ + fromFixture( + stripIndent` + // nested in pipe + import { of,switchMap,tap } from "rxjs"; + of("foo").pipe( + switchMap(value => { + return of("bar").pipe(tap(value => { console.log(value)}) + ~~~~ [forbidden] + )}) + ).subscribe(value => { + console.log(value); + }); + ` + ), + fromFixture( + stripIndent` + // nested in pipe with parallel pipes + import { of,switchMap,tap } from "rxjs"; + of("foo").pipe( + switchMap(value => { + return of("bar").pipe(tap(value => { console.log(value)}) + ~~~~ [forbidden] + )}), + switchMap(value => { + return of("bar").pipe(tap(value => { console.log(value)}) + ~~~~ [forbidden] + )}) + ).subscribe(value => { + console.log(value); + }); + ` + ), + ], +});