├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── rules │ ├── ban-observables.md │ ├── ban-operators.md │ ├── finnish.md │ ├── just.md │ ├── macro.md │ ├── no-async-subscribe.md │ ├── no-compat.md │ ├── no-connectable.md │ ├── no-create.md │ ├── no-cyclic-action.md │ ├── no-explicit-generics.md │ ├── no-exposed-subjects.md │ ├── no-finnish.md │ ├── no-ignored-error.md │ ├── no-ignored-notifier.md │ ├── no-ignored-observable.md │ ├── no-ignored-replay-buffer.md │ ├── no-ignored-subscribe.md │ ├── no-ignored-subscription.md │ ├── no-ignored-takewhile-value.md │ ├── no-implicit-any-catch.md │ ├── no-index.md │ ├── no-internal.md │ ├── no-nested-subscribe.md │ ├── no-redundant-notify.md │ ├── no-sharereplay.md │ ├── no-subclass.md │ ├── no-subject-unsubscribe.md │ ├── no-subject-value.md │ ├── no-subscribe-handlers.md │ ├── no-tap.md │ ├── no-topromise.md │ ├── no-unbound-methods.md │ ├── no-unsafe-catch.md │ ├── no-unsafe-first.md │ ├── no-unsafe-subject-next.md │ ├── no-unsafe-switchmap.md │ ├── no-unsafe-takeuntil.md │ ├── prefer-observer.md │ ├── suffix-subjects.md │ └── throw-error.md ├── package.json ├── source ├── configs │ └── recommended.ts ├── constants.ts ├── index.ts ├── rules │ ├── ban-observables.ts │ ├── ban-operators.ts │ ├── finnish.ts │ ├── just.ts │ ├── macro.ts │ ├── no-async-subscribe.ts │ ├── no-compat.ts │ ├── no-connectable.ts │ ├── no-create.ts │ ├── no-cyclic-action.ts │ ├── no-explicit-generics.ts │ ├── no-exposed-subjects.ts │ ├── no-finnish.ts │ ├── no-ignored-error.ts │ ├── no-ignored-notifier.ts │ ├── no-ignored-observable.ts │ ├── no-ignored-replay-buffer.ts │ ├── no-ignored-subscribe.ts │ ├── no-ignored-subscription.ts │ ├── no-ignored-takewhile-value.ts │ ├── no-implicit-any-catch.ts │ ├── no-index.ts │ ├── no-internal.ts │ ├── no-nested-subscribe.ts │ ├── no-redundant-notify.ts │ ├── no-sharereplay.ts │ ├── no-subclass.ts │ ├── no-subject-unsubscribe.ts │ ├── no-subject-value.ts │ ├── no-subscribe-handlers.ts │ ├── no-tap.ts │ ├── no-topromise.ts │ ├── no-unbound-methods.ts │ ├── no-unsafe-catch.ts │ ├── no-unsafe-first.ts │ ├── no-unsafe-subject-next.ts │ ├── no-unsafe-switchmap.ts │ ├── no-unsafe-takeuntil.ts │ ├── prefer-observer.ts │ ├── suffix-subjects.ts │ └── throw-error.ts └── utils.ts ├── tests ├── .eslintrc.json ├── rules │ ├── ban-observables.ts │ ├── ban-operators.ts │ ├── finnish.ts │ ├── just.ts │ ├── macro.ts │ ├── no-async-subscribe.ts │ ├── no-compat.ts │ ├── no-connectable.ts │ ├── no-create.ts │ ├── no-cyclic-action.ts │ ├── no-explicit-generics.ts │ ├── no-exposed-subjects.ts │ ├── no-finnish.ts │ ├── no-ignored-error.ts │ ├── no-ignored-notifier.ts │ ├── no-ignored-observable.ts │ ├── no-ignored-replay-buffer.ts │ ├── no-ignored-subscribe.ts │ ├── no-ignored-subscription.ts │ ├── no-ignored-takewhile-value.ts │ ├── no-implicit-any-catch.ts │ ├── no-index.ts │ ├── no-internal.ts │ ├── no-nested-subscribe.ts │ ├── no-redundant-notify.ts │ ├── no-sharereplay.ts │ ├── no-subclass.ts │ ├── no-subject-unsubscribe.ts │ ├── no-subject-value.ts │ ├── no-subscribe-handlers.ts │ ├── no-tap.ts │ ├── no-topromise.ts │ ├── no-unbound-methods.ts │ ├── no-unsafe-catch.ts │ ├── no-unsafe-first.ts │ ├── no-unsafe-subject-next.ts │ ├── no-unsafe-switchmap.ts │ ├── no-unsafe-takeuntil.ts │ ├── prefer-observer.ts │ ├── suffix-subjects.ts │ └── throw-error.ts ├── tsconfig.json ├── utils-spec.ts └── utils.ts ├── tsconfig-dist.json ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # https://circleci.com/docs/2.0/docker-image-tags.json 6 | - image: circleci/node:current 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | name: Restore Yarn Package Cache 11 | keys: 12 | - yarn-packages-{{ checksum "yarn.lock" }} 13 | - run: 14 | name: Install Packages 15 | command: yarn install 16 | - save_cache: 17 | name: Save Yarn Package Cache 18 | key: yarn-packages-{{ checksum "yarn.lock" }} 19 | paths: 20 | - ~/.cache/yarn 21 | - run: 22 | name: Check Formatting 23 | command: yarn prettier:ci 24 | - run: 25 | name: Test 26 | command: yarn test 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@cartant/eslint-config"], 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2019, 6 | "project": "tsconfig.json", 7 | "sourceType": "module" 8 | }, 9 | "root": true, 10 | "rules": { 11 | "sort-keys": ["off"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cartant] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | /build 3 | /dist 4 | /node_modules 5 | /temp 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "mocha", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-r", 14 | "ts-node/register", 15 | "--timeout", 16 | "999999", 17 | "--colors", 18 | "${workspaceFolder}/tests/rules/*.ts", 19 | ], 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "protocol": "inspector" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | There's nothing here, yet. Thanks for looking. I'll eventually get around to filling this out. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nicholas Jamieson and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/rules/ban-observables.md: -------------------------------------------------------------------------------- 1 | # Avoid banned observable creators (`ban-observables`) 2 | 3 | This rule can be configured so that developers can ban any observable creators they want to avoid in their project. 4 | 5 | ## Options 6 | 7 | This rule accepts a single option which is an object the keys of which are the names of observable factory functions and the values are either booleans or strings containing the explanation for the ban. 8 | 9 | The following configuration bans `partition` and `onErrorResumeNext`: 10 | 11 | ```json 12 | { 13 | "rxjs/ban-observables": [ 14 | "error", 15 | { 16 | "partition": true, 17 | "of": false, 18 | "onErrorResumeNext": "What is this? Visual Basic?" 19 | } 20 | ] 21 | } 22 | ``` -------------------------------------------------------------------------------- /docs/rules/ban-operators.md: -------------------------------------------------------------------------------- 1 | # Avoid banned operators (`ban-operators`) 2 | 3 | This rule can be configured so that developers can ban any operators they want to avoid in their project. 4 | 5 | ## Options 6 | 7 | This rule accepts a single option which is an object the keys of which are the names of operators and the values are either booleans or strings containing the explanation for the ban. 8 | 9 | The following configuration bans `partition` and `onErrorResumeNext`: 10 | 11 | ```json 12 | { 13 | "rxjs/ban-operators": [ 14 | "error", 15 | { 16 | "partition": true, 17 | "map": false, 18 | "onErrorResumeNext": "What is this? Visual Basic?" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/rules/finnish.md: -------------------------------------------------------------------------------- 1 | # Use Finnish notation (`finnish`) 2 | 3 | This rule enforces the use of Finnish notation - i.e. the `$` suffix. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | const answers = of(42, 54); 11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```ts 16 | const answer$ = of(42, 54); 17 | ``` 18 | 19 | ## Options 20 | 21 | This rule accepts a single option which is an object with properties that determine whether Finnish notation is enforced for `functions`, `methods`, `parameters`, `properties` and `variables`. It also contains: 22 | 23 | - `names` and `types` properties that determine whether of not Finnish notation is to be enforced for specific names or types. 24 | - a `strict` property that, if `true`, allows the `$` suffix to be used _only_ with identifiers that have an `Observable` type. 25 | 26 | The default (Angular-friendly) configuration looks like this: 27 | 28 | ```json 29 | { 30 | "rxjs/finnish": [ 31 | "error", 32 | { 33 | "functions": true, 34 | "methods": true, 35 | "names": { 36 | "^(canActivate|canActivateChild|canDeactivate|canLoad|intercept|resolve|validate)$": false 37 | }, 38 | "parameters": true, 39 | "properties": true, 40 | "strict": false, 41 | "types": { 42 | "^EventEmitter$": false 43 | }, 44 | "variables": true 45 | } 46 | ] 47 | } 48 | ``` 49 | 50 | The properties in the options object are themselves optional; they do not all have to be specified. 51 | 52 | ## Further reading 53 | 54 | - [Observables and Finnish Notation](https://medium.com/@benlesh/observables-and-finnish-notation-df8356ed1c9b) 55 | -------------------------------------------------------------------------------- /docs/rules/just.md: -------------------------------------------------------------------------------- 1 | # Use `just` instead of `of` (`just`) 2 | 3 | This rule enforces the use of `just` instead of `of`. Some other languages with Rx implementations use the former and this rule is for developers who have that preference. 4 | 5 | ## Options 6 | 7 | This rule has no options. 8 | 9 | ## Further reading 10 | 11 | - [Rename `of` to `just`](https://github.com/ReactiveX/rxjs/issues/3747) -------------------------------------------------------------------------------- /docs/rules/macro.md: -------------------------------------------------------------------------------- 1 | # Use the RxJS Tools macro (`macro`) 2 | 3 | This rule ensures that modules that import `rxjs` also import the Babel macro for [RxJS Tools](https://rxjs.tools). 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-async-subscribe.md: -------------------------------------------------------------------------------- 1 | # Avoid passing async functions to `subscribe` (`no-async-subscribe`) 2 | 3 | This rule effects failures if async functions are passed to `subscribe`. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs"; 11 | of(42).subscribe(async () => console.log(value)); 12 | ``` 13 | 14 | Examples of **correct** code for this rule: 15 | 16 | ```ts 17 | import { of } from "rxjs"; 18 | of(42).subscribe(() => console.log(value)); 19 | ``` 20 | 21 | ## Options 22 | 23 | This rule has no options. 24 | -------------------------------------------------------------------------------- /docs/rules/no-compat.md: -------------------------------------------------------------------------------- 1 | # Avoid the `rxjs-compat` package (`no-compat`) 2 | 3 | This rule prevents the use of `rxjs-compat`. 4 | 5 | ## Options 6 | 7 | This rule has no options. 8 | 9 | ## Further reading 10 | 11 | - [Backwards compatibility](https://github.com/ReactiveX/rxjs/blob/a6590e971969c736a15b77154dabbc22275aa0d5/docs_app/content/guide/v6/migration.md#backwards-compatibility) -------------------------------------------------------------------------------- /docs/rules/no-connectable.md: -------------------------------------------------------------------------------- 1 | # Avoid connectable observables (`no-connectable`) 2 | 3 | This rule prevents the use of connectable observables. 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-create.md: -------------------------------------------------------------------------------- 1 | # Avoid the static `create` function (`no-create`) 2 | 3 | This rule prevents the use of the static `create` function in `Observable`. Developers should use `new` and the constructor instead. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | const answers = Observable.create(subscriber => { 11 | subscriber.next(42); 12 | subscriber.next(54); 13 | subscriber.complete(); 14 | }); 15 | ``` 16 | 17 | Examples of **correct** code for this rule: 18 | 19 | ```ts 20 | const answers = new Observable(subscriber => { 21 | subscriber.next(42); 22 | subscriber.next(54); 23 | subscriber.complete(); 24 | }); 25 | ``` 26 | 27 | ## Options 28 | 29 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-cyclic-action.md: -------------------------------------------------------------------------------- 1 | # Avoid cyclic actions in effects and epics (`no-cyclic-action`) 2 | 3 | This rule effects failures for effects and epics that emit actions that would pass their `ofType` filter. Such actions are cyclic and, upon emission, immediately re-trigger the effect or epic. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | actions.pipe( 11 | ofType("SOMETHING"), 12 | map(() => ({ type: "SOMETHING" })) 13 | ); 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```ts 19 | actions.pipe( 20 | ofType("SOMETHING"), 21 | map(() => ({ type: "SOMETHING_ELSE" })) 22 | ); 23 | ``` 24 | 25 | This rule can be used with effects _and epics_, so it makes __no attempt__ to discern whether or not dispatching is disabled for an NgRx effect. That is, code like this will effect (🙈) a failure: 26 | 27 | ```ts 28 | someEffect = createEffect(() => 29 | this.actions$.pipe( 30 | ofType("SOMETHING"), 31 | tap(() => console.log("do something")), 32 | ), 33 | { dispatch: false } 34 | ); 35 | ``` 36 | 37 | Instead, you can use the the RxJS [`ignoreElements`](https://rxjs.dev/api/operators/ignoreElements) operator: 38 | 39 | ```ts 40 | someEffect = createEffect(() => 41 | this.actions$.pipe( 42 | ofType("SOMETHING"), 43 | tap(() => console.log("do something")), 44 | ignoreElements() 45 | ) 46 | ); 47 | ``` 48 | 49 | Or you can use an ESLint [inline comment](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments) to disable the rule for a specific effect. 50 | 51 | ## Options 52 | 53 | This rule accepts a single option which is an object with an `observable` property that is a regular expression used to match an effect or epic's actions observable. The default `observable` regular expression should match most effect and epic action sources. 54 | 55 | ```json 56 | { 57 | "rxjs/no-cyclic-action": [ 58 | "error", 59 | { "observable": "[Aa]ction(s|s\\$|\\$)$" } 60 | ] 61 | } 62 | ``` -------------------------------------------------------------------------------- /docs/rules/no-explicit-generics.md: -------------------------------------------------------------------------------- 1 | # Avoid unnecessary explicit type arguments (`no-explicit-generics`) 2 | 3 | This rule prevents the use of explicit type arguments when the type arguments can be inferred. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { BehaviorSubject } from "rxjs"; 11 | const subject = new BehaviorSubject(42); 12 | ``` 13 | 14 | Examples of **correct** code for this rule: 15 | 16 | ```ts 17 | import { BehaviorSubject } from "rxjs"; 18 | const subject = new BehaviorSubject(42); 19 | ``` 20 | 21 | ## Options 22 | 23 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-exposed-subjects.md: -------------------------------------------------------------------------------- 1 | # Avoid public and protected subjects (`no-exposed-subjects`) 2 | 3 | This rule prevents the public or protected subjects. Developers should instead expose observables via the subjects' `toObservable` method. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { Subject } from "rxjs"; 11 | class Answers { 12 | public answers: Subject; 13 | } 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```ts 19 | import { Subject } from "rxjs"; 20 | class Answers { 21 | private _answers: Subject; 22 | get answers() { 23 | return this._answers.toObservable(); 24 | } 25 | } 26 | ``` 27 | 28 | ## Options 29 | 30 | This rule accepts a single option which is an object with an `allowProtected` property that determines whether or not protected subjects are allowed. By default, they are not. 31 | 32 | ```json 33 | { 34 | "rxjs/no-exposed-subjects": [ 35 | "error", 36 | { "allowProtected": true } 37 | ] 38 | } 39 | ``` -------------------------------------------------------------------------------- /docs/rules/no-finnish.md: -------------------------------------------------------------------------------- 1 | # Avoid Finnish notation (`no-finnish`) 2 | 3 | This rule prevents the use of Finnish notation. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | const answer$ = of(42, 54); 11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```ts 16 | const answers = of(42, 54); 17 | ``` 18 | 19 | ## Options 20 | 21 | This rule has no options. 22 | 23 | ## Further reading 24 | 25 | - [Observables and Finnish Notation](https://medium.com/@benlesh/observables-and-finnish-notation-df8356ed1c9b) -------------------------------------------------------------------------------- /docs/rules/no-ignored-error.md: -------------------------------------------------------------------------------- 1 | # Enforce the passing of error handlers (`no-ignored-error`) 2 | 3 | This rule enforces the passing of an error handler to `subscribe` calls. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | source.subscribe((value) => console.log(value)); 11 | ``` 12 | 13 | ```ts 14 | source.subscribe({ 15 | next: (value) => console.log(value) 16 | }); 17 | ``` 18 | 19 | Examples of **correct** code for this rule: 20 | 21 | ```ts 22 | source.subscribe( 23 | (value) => console.log(value), 24 | (error) => console.error(error) 25 | ); 26 | ``` 27 | 28 | ```ts 29 | source.subscribe({ 30 | next: (value) => console.log(value), 31 | error: (error) => console.error(error) 32 | }); 33 | ``` 34 | 35 | ## Options 36 | 37 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-ignored-notifier.md: -------------------------------------------------------------------------------- 1 | # Ensure `repeatWhen` or `retryWhen` notifiers are used (`no-ignored-notifier`) 2 | 3 | This rule effects failures if the notifier passed to a `repeatWhen` or `retryWhen` callback is not used. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { range } from "rxjs"; 11 | import { repeatWhen, take } from "rxjs/operators"; 12 | 13 | const repeating = source.pipe( 14 | repeatWhen(notifications => range(0, 3)) 15 | ); 16 | ``` 17 | 18 | Examples of **correct** code for this rule: 19 | 20 | ```ts 21 | import { repeatWhen, take } from "rxjs/operators"; 22 | 23 | const repeating = source.pipe( 24 | repeatWhen(notifications => notifications.pipe(take(3))) 25 | ); 26 | ``` 27 | 28 | ## Options 29 | 30 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-ignored-observable.md: -------------------------------------------------------------------------------- 1 | # Use returned observables (`no-ignored-observable`) 2 | 3 | The effects failures if an observable returned by a function is neither assigned to a variable or property or passed to a function. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs"; 11 | of(42, 54); 12 | ``` 13 | 14 | Examples of **correct** code for this rule: 15 | 16 | ```ts 17 | import { of } from "rxjs"; 18 | const answers = of(42, 54); 19 | ``` 20 | 21 | ## Options 22 | 23 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-ignored-replay-buffer.md: -------------------------------------------------------------------------------- 1 | # Avoid unbounded replay buffers (`no-ignored-replay-buffer`) 2 | 3 | This rule effects failures if the buffer size of a replay buffer is not explicitly specified. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { ReplaySubject } from "rxjs"; 11 | const subject = new ReplaySubject(); 12 | ``` 13 | 14 | Examples of **correct** code for this rule: 15 | 16 | ```ts 17 | import { ReplaySubject } from "rxjs"; 18 | const subject = new ReplaySubject(1); 19 | ``` 20 | 21 | ```ts 22 | import { ReplaySubject } from "rxjs"; 23 | const subject = new ReplaySubject(Infinity); 24 | ``` 25 | 26 | ## Options 27 | 28 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-ignored-subscribe.md: -------------------------------------------------------------------------------- 1 | # Enforce the passing of handlers to `subscribe` (`no-ignored-subscribe`) 2 | 3 | This rule effects failures whenever `subscribe` is called without handlers. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs"; 11 | import { tap } from "rxjs/operators"; 12 | 13 | of(42, 54).pipe( 14 | tap((value) => console.log(value)) 15 | ).subscribe(); 16 | ``` 17 | 18 | Examples of **correct** code for this rule: 19 | 20 | ```ts 21 | import { of } from "rxjs"; 22 | 23 | of(42, 54).subscribe((value) => console.log(value)); 24 | ``` 25 | 26 | ## Options 27 | 28 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-ignored-subscription.md: -------------------------------------------------------------------------------- 1 | # Use returned subscriptions (`no-ignored-subscription`) 2 | 3 | The effects failures if an subscription returned by call to `subscribe` is neither assigned to a variable or property or passed to a function. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | interval(1e3).subscribe( 11 | (value) => console.log(value) 12 | ); 13 | ``` 14 | 15 | Examples of **correct** code for this rule: 16 | 17 | ```ts 18 | const subscription = interval(1e3).subscribe( 19 | (value) => console.log(value) 20 | ); 21 | ``` 22 | 23 | When subscribers are passed to `subscribe` they are chained, so the returned subscription can be ignored: 24 | 25 | ```ts 26 | const numbers = new Observable(subscriber => { 27 | interval(1e3).subscribe(subscriber); 28 | }); 29 | ``` 30 | 31 | ## Options 32 | 33 | This rule has no options. 34 | -------------------------------------------------------------------------------- /docs/rules/no-ignored-takewhile-value.md: -------------------------------------------------------------------------------- 1 | # Avoid unused `takeWhile` values (`no-ignored-takewhile-value`) 2 | 3 | This rule effects failures if the value received by a `takeWhile` callback is not used in an expression. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { takeWhile } from "rxjs/operators"; 11 | 12 | let flag = true; 13 | const whilst = source.pipe(takeWhile(() => flag)); 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```ts 19 | import { takeWhile } from "rxjs/operators"; 20 | 21 | const whilst = source.pipe(takeWhile(value => value)); 22 | ``` 23 | 24 | ## Options 25 | 26 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-implicit-any-catch.md: -------------------------------------------------------------------------------- 1 | # Use type-safe error handlers (`no-implicit-any-catch`) 2 | 3 | This rule requires an explicit type annotation for error parameters in error handlers. It's similar to the TypeScript [`no-implicit-any-catch`](https://github.com/typescript-eslint/typescript-eslint/blob/e01204931e460f5e6731abc443c88d666ca0b07a/packages/eslint-plugin/docs/rules/no-implicit-any-catch.md) rule, but is for observables - not `try`/`catch` statements. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { throwError } from "rxjs"; 11 | import { catchError } from "rxjs/operators"; 12 | 13 | throwError(() => new Error("Kaboom!")).pipe( 14 | catchError((error) => console.error(error)) 15 | ); 16 | ``` 17 | 18 | ```ts 19 | import { throwError } from "rxjs"; 20 | 21 | throwError(() => new Error("Kaboom!")).subscribe({ 22 | error: (error) => console.error(error) 23 | }); 24 | ``` 25 | 26 | ```ts 27 | import { throwError } from "rxjs"; 28 | import { tap } from "rxjs/operators"; 29 | 30 | throwError(() => new Error("Kaboom!")).pipe( 31 | tap(undefined, (error) => console.error(error)) 32 | ); 33 | ``` 34 | 35 | Examples of **correct** code for this rule: 36 | 37 | ```ts 38 | import { throwError } from "rxjs"; 39 | import { catchError } from "rxjs/operators"; 40 | 41 | throwError(() => new Error("Kaboom!")).pipe( 42 | catchError((error: unknown) => console.error(error)) 43 | ); 44 | ``` 45 | 46 | ```ts 47 | import { throwError } from "rxjs"; 48 | 49 | throwError(() => new Error("Kaboom!")).subscribe({ 50 | error: (error: unknown) => console.error(error) 51 | }); 52 | ``` 53 | 54 | ```ts 55 | import { throwError } from "rxjs"; 56 | import { tap } from "rxjs/operators"; 57 | 58 | throwError(() => new Error("Kaboom!")).pipe( 59 | tap(undefined, (error: unknown) => console.error(error)) 60 | ); 61 | ``` 62 | 63 | ## Options 64 | 65 | This rule accepts a single option which is an object with an `allowExplicitAny` property that determines whether or not the error variable can be explicitly typed as `any`. By default, the use of explicit `any` is forbidden. 66 | 67 | ```json 68 | { 69 | "rxjs/no-implicit-any-catch": [ 70 | "error", 71 | { "allowExplicitAny": true } 72 | ] 73 | } 74 | ``` 75 | 76 | ## Further reading 77 | 78 | - [Catching Unknowns](https://ncjamieson.com/catching-unknowns/) -------------------------------------------------------------------------------- /docs/rules/no-index.md: -------------------------------------------------------------------------------- 1 | # Avoid importing index modules (`no-index`) 2 | 3 | This rule effects failures if an index module is specified as the import location. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs/index"; 11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```ts 16 | import { of } from "rxjs"; 17 | ``` 18 | 19 | ## Options 20 | 21 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-internal.md: -------------------------------------------------------------------------------- 1 | # Avoid importing internal modules (`no-internal`) 2 | 3 | This rule effects failures if an internal module is specified as the import location. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs/internal/observable/of"; 11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```ts 16 | import { of } from "rxjs"; 17 | ``` 18 | 19 | ## Options 20 | 21 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-nested-subscribe.md: -------------------------------------------------------------------------------- 1 | # Avoid nested `subscribe` calls (`no-nested-subscribe`) 2 | 3 | This rule effects failures if `subscribe` is called within a `subscribe` handler. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of, timer } from "rxjs"; 11 | 12 | of(42, 54).subscribe((value) => { 13 | timer(1e3).subscribe(() => console.log(value)); 14 | }); 15 | ``` 16 | 17 | Examples of **correct** code for this rule: 18 | 19 | ```ts 20 | import { of, timer } from "rxjs"; 21 | import { mapTo, mergeMap } from "rxjs/operators"; 22 | 23 | of(42, 54).pipe( 24 | mergeMap((value) => timer(1e3).pipe(mapTo(value))) 25 | ).subscribe((value) => console.log(value)); 26 | ``` 27 | 28 | ## Options 29 | 30 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-redundant-notify.md: -------------------------------------------------------------------------------- 1 | # Avoid sending redundant notifications (`no-redundant-notify`) 2 | 3 | This rule effects failures if an attempt is made to send a notification to an observer after a `complete` or `error` notification has already been sent. 4 | 5 | Note that the rule _does not perform extensive analysis_. It uses a straightforward and limited approach to catch obviously redundant notifications. 6 | 7 | ## Rule details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```ts 12 | import { Subject } from "rxjs"; 13 | 14 | const subject = new Subject(); 15 | subject.next(42); 16 | subject.error(new Error("Kaboom!")); 17 | subject.complete(); 18 | ``` 19 | 20 | Examples of **correct** code for this rule: 21 | 22 | ```ts 23 | import { Subject } from "rxjs"; 24 | 25 | const subject = new Subject(); 26 | subject.next(42); 27 | subject.error(new Error("Kaboom!")); 28 | ``` 29 | 30 | ## Options 31 | 32 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-sharereplay.md: -------------------------------------------------------------------------------- 1 | # Avoid `shareReplay` (`no-sharereplay`) 2 | 3 | This rule effects failures if the `shareReplay` operator is used - or if it is used without specifying a `config` argument. 4 | 5 | The behaviour of `shareReplay` has changed several times - see the blog post linked below. 6 | 7 | ## Options 8 | 9 | This rule accepts a single option which is an object with an `allowConfig` property that that determines whether `shareReplay` is allow if a config argument is specified. By default, `allowConfig` is `true`. 10 | 11 | ```json 12 | { 13 | "rxjs/no-sharereplay": [ 14 | "error", 15 | { "allowConfig": true } 16 | ] 17 | } 18 | ``` 19 | 20 | ## Further reading 21 | 22 | - [What's changed with shareReplay](https://ncjamieson.com/whats-changed-with-sharereplay/) -------------------------------------------------------------------------------- /docs/rules/no-subclass.md: -------------------------------------------------------------------------------- 1 | # Avoid subclassing RxJS classes (`no-subclass`) 2 | 3 | This rule effects failures if an RxJS class is subclassed. Developers are encouraged to avoid subclassing RxJS classes, as some public and protected implementation details might change in the future. 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-subject-unsubscribe.md: -------------------------------------------------------------------------------- 1 | # Avoid calling `unsubscribe` on subjects (`no-subject-unsubscribe`) 2 | 3 | This rule effects failures if the `unsubscribe` method is called on subjects. The method behaves differently to the `unsubsribe` method on subscriptions and is often an error. 4 | 5 | ## Options 6 | 7 | This rule has no options. 8 | 9 | ## Further reading 10 | 11 | - [Closed Subjects](https://ncjamieson.com/closed-subjects/) -------------------------------------------------------------------------------- /docs/rules/no-subject-value.md: -------------------------------------------------------------------------------- 1 | # Avoid using a behavior subject's value (`no-subject-value`) 2 | 3 | This rule effects an error if the `value` property - or `getValue` method - of a `BehaviorSubject` is used. 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-subscribe-handlers.md: -------------------------------------------------------------------------------- 1 | # Forbid the passing of handlers to `subscribe` (`no-subscribe-handlers`) 2 | 3 | This rule effects failures whenever `subscribe` is called with handlers. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs"; 11 | import { tap } from "rxjs/operators"; 12 | 13 | of(42, 54).subscribe((value) => console.log(value)); 14 | ``` 15 | 16 | 17 | ```ts 18 | import { of } from "rxjs"; 19 | import { tap } from "rxjs/operators"; 20 | 21 | of(42, 54).subscribe({ 22 | next: (value) => console.log(value), 23 | }); 24 | ``` 25 | 26 | Examples of **correct** code for this rule: 27 | 28 | 29 | ```ts 30 | import { of } from "rxjs"; 31 | 32 | of(42, 54) 33 | .pipe(tap((value) => console.log(value))) 34 | .subscribe(); 35 | ``` 36 | 37 | ## Options 38 | 39 | This rule has no options. 40 | -------------------------------------------------------------------------------- /docs/rules/no-tap.md: -------------------------------------------------------------------------------- 1 | # Avoid `tap` (`no-tap`) 2 | 3 | This rule effects failures if the `tap` operator is used. 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-topromise.md: -------------------------------------------------------------------------------- 1 | # Avoid `toPromise` (`no-topromise`) 2 | 3 | This rule effects failures if the `toPromise` method is used. 4 | 5 | ## Options 6 | 7 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-unbound-methods.md: -------------------------------------------------------------------------------- 1 | # Avoid using unbound methods as callbacks (`no-unbound-methods`) 2 | 3 | This rule effects failures if unbound methods are passed as callbacks. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | 10 | ```ts 11 | return this.http 12 | .get("https://api.some.com/things/1") 13 | .pipe( 14 | map(this.extractSomeProperty), 15 | catchError(this.handleError) 16 | ); 17 | ``` 18 | 19 | Examples of **correct** code for this rule: 20 | 21 | 22 | ```ts 23 | return this.http 24 | .get("https://api.some.com/things/1") 25 | .pipe( 26 | map((s) => this.extractSomeProperty(s)), 27 | catchError((e) => this.handleError(e)) 28 | ); 29 | ``` 30 | 31 | 32 | ```ts 33 | return this.http 34 | .get("https://api.some.com/things/1") 35 | .pipe( 36 | map(this.extractSomeProperty.bind(this)), 37 | catchError(this.handleError.bind(this)) 38 | ); 39 | ``` 40 | 41 | ## Options 42 | 43 | This rule has no options. 44 | 45 | ## Further reading 46 | 47 | - [Avoiding unbound methods](https://ncjamieson.com/avoiding-unbound-methods/) 48 | -------------------------------------------------------------------------------- /docs/rules/no-unsafe-catch.md: -------------------------------------------------------------------------------- 1 | # Avoid completing effects and epics (`no-unsafe-catch`) 2 | 3 | This rule effects failures if `catchError` is used in an effect or epic in a manner that will complete the outermost observable. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | actions.pipe( 11 | ofType("SOMETHING"), 12 | switchMap((action) => something(action)), 13 | catchError(handleError) 14 | ); 15 | ``` 16 | 17 | Examples of **correct** code for this rule: 18 | 19 | ```ts 20 | actions.pipe( 21 | ofType("SOMETHING"), 22 | switchMap((action) => something(action).pipe( 23 | catchError(handleError) 24 | )) 25 | ); 26 | ``` 27 | 28 | ```ts 29 | actions.pipe( 30 | ofType("SOMETHING"), 31 | switchMap((action) => something(action)), 32 | catchError((error, caught) => { 33 | handleError(error); 34 | return caught; 35 | }) 36 | ); 37 | ``` 38 | 39 | ## Options 40 | 41 | This rule accepts a single option which is an object with an `observable` property that is a regular expression used to match an effect or epic's actions observable. The default `observable` regular expression should match most effect and epic action sources. 42 | 43 | ```json 44 | { 45 | "rxjs/no-unsafe-catch": [ 46 | "error", 47 | { "observable": "[Aa]ction(s|s\\$|\\$)$" } 48 | ] 49 | } 50 | ``` -------------------------------------------------------------------------------- /docs/rules/no-unsafe-first.md: -------------------------------------------------------------------------------- 1 | # Avoid completing effects and epics (`no-unsafe-first`) 2 | 3 | This rule effects failures if `first` is used in an effect or epic in a manner that will complete the outermost observable. 4 | 5 | ## Options 6 | 7 | This rule accepts a single option which is an object with an `observable` property that is a regular expression used to match an effect or epic's actions observable. The default `observable` regular expression should match most effect and epic action sources. 8 | 9 | ```json 10 | { 11 | "rxjs/no-unsafe-first": [ 12 | "error", 13 | { "observable": "[Aa]ction(s|s\\$|\\$)$" } 14 | ] 15 | } 16 | ``` -------------------------------------------------------------------------------- /docs/rules/no-unsafe-subject-next.md: -------------------------------------------------------------------------------- 1 | # Avoid passing `undefined` to `next` (`no-unsafe-subject-next`) 2 | 3 | This rule effects failures if `next` is called without an argument and the subject's value type is not `void`. 4 | 5 | In RxJS version 6, the `next` method's `value` parameter is optional, but a value should always be specified for subjects with non-`void` element types. 6 | 7 | ## Rule details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```ts 12 | const subject = new Subject(); 13 | subject.next(); 14 | ``` 15 | 16 | Examples of **correct** code for this rule: 17 | 18 | ```ts 19 | const subject = new Subject(); 20 | subject.next(); 21 | ``` 22 | 23 | ```ts 24 | const subject = new Subject(); 25 | subject.next(0); 26 | ``` 27 | 28 | ## Options 29 | 30 | This rule has no options. -------------------------------------------------------------------------------- /docs/rules/no-unsafe-switchmap.md: -------------------------------------------------------------------------------- 1 | # Avoid `switchMap` bugs in effects and epics (`no-unsafe-switchmap`) 2 | 3 | This rule effects failures if `switchMap` is used in effects or epics that perform actions other than reads. For a detailed explanation, see the blog post linked below. 4 | 5 | ## Options 6 | 7 | This rule accepts a single option which is an object with `allow`, `disallow` and `observable` properties. 8 | 9 | The `observable` property is a regular expression used to match an effect or epic's actions observable. The default `observable` regular expression should match most effect and epic action sources. 10 | 11 | The `allow` or `disallow` properties are mutually exclusive. Whether or not `switchMap` is allowed will depend upon the matching of action types with `allow` or `disallow`. The properties can be specified as regular expression strings or as arrays of words. 12 | 13 | ```json 14 | { 15 | "rxjs/no-unsafe-switchmap": [ 16 | "error", 17 | { 18 | "disallow": [ 19 | "add", 20 | "create", 21 | "delete", 22 | "post", 23 | "put", 24 | "remove", 25 | "set", 26 | "update" 27 | ], 28 | "observable": "[Aa]ction(s|s\\$|\\$)$" 29 | } 30 | ] 31 | } 32 | ``` 33 | 34 | The properties in the options object are themselves optional; they do not all have to be specified. 35 | 36 | ## Further reading 37 | 38 | - [Avoiding switchMap-related bugs](https://ncjamieson.com/avoiding-switchmap-related-bugs/) -------------------------------------------------------------------------------- /docs/rules/no-unsafe-takeuntil.md: -------------------------------------------------------------------------------- 1 | # Avoid `takeUntil` subscription leaks (`no-unsafe-takeuntil`) 2 | 3 | This rule effects failures whenever `takeUntil` is used in observable compositions that can leak subscriptions. 4 | 5 | Although it's recommended that `takeUntil` be placed last - to ensure unsubscription from any inner observables - there are a number of operators that might _need_ to be placed after it - like `toArray` or any other operator that depends upon a `complete` notification. The rule is aware of these operators (see the rule's options, below) and will not effect failures if they are placed after `takeUntil`. 6 | 7 | ## Rule details 8 | 9 | Examples of **incorrect** code for this rule: 10 | 11 | ```ts 12 | const combined = source 13 | .pipe(takeUntil(notifier), combineLatest(b)) 14 | .subscribe((value) => console.log(value)); 15 | ``` 16 | 17 | Examples of **correct** code for this rule: 18 | 19 | ```ts 20 | const combined = source 21 | .pipe(combineLatest(b), takeUntil(notifier)) 22 | .subscribe((value) => console.log(value)); 23 | ``` 24 | 25 | ## Options 26 | 27 | This rule accepts a single option which is an object with `alias` and `allow` properties. The `alias` property is an array of names of operators that should be treated similarly to `takeUntil` and the `allow` property is an array of names of operators that are safe to use after `takeUntil`. 28 | 29 | By default, the `allow` property contains all of the built-in operators that are safe to use after `takeUntil`. 30 | 31 | ```json 32 | { 33 | "rxjs/no-unsafe-takeuntil": [ 34 | "error", 35 | { 36 | "alias": ["untilDestroyed"] 37 | } 38 | ] 39 | } 40 | ``` 41 | 42 | The properties in the options object are themselves optional; they do not all have to be specified. 43 | 44 | ## Further reading 45 | 46 | - [Avoiding takeUntil leaks](https://ncjamieson.com/avoiding-takeuntil-leaks/) 47 | -------------------------------------------------------------------------------- /docs/rules/prefer-observer.md: -------------------------------------------------------------------------------- 1 | # Avoid separate handlers (`prefer-observer`) 2 | 3 | This rule effects failures if `subscribe` - or `tap` - is called with separate handlers instead of an observer. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | import { of } from "rxjs"; 11 | of(42, 54).subscribe((value) => console.log(value)); 12 | ``` 13 | 14 | Examples of **correct** code for this rule: 15 | 16 | ```ts 17 | import { of } from "rxjs"; 18 | of(42, 54).subscribe({ 19 | next: (value) => console.log(value) 20 | }); 21 | ``` 22 | 23 | ## Options 24 | 25 | This rule accepts a single option which is an object with an `allowNext` property that determines whether a single `next` callback is allowed. By default, `allowNext` is `true`. 26 | 27 | ```json 28 | { 29 | "rxjs/prefer-observer": [ 30 | "error", 31 | { "allowNext": false } 32 | ] 33 | } 34 | ``` -------------------------------------------------------------------------------- /docs/rules/suffix-subjects.md: -------------------------------------------------------------------------------- 1 | # Identify subjects (`suffix-subjects`) 2 | 3 | This rule effects failures if subject variables, properties and parameters don't conform to a naming scheme that identifies them as subjects. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | const answers = new Subject(); 11 | ``` 12 | 13 | Examples of **correct** code for this rule: 14 | 15 | ```ts 16 | const answersSubject = new Subject(); 17 | ``` 18 | 19 | ## Options 20 | 21 | This rule accepts a single option which is an object with properties that determine whether Finnish notation is enforced for `parameters`, `properties` and `variables`. It also contains a `types` property that determine whether of not the naming convention is to be enforced for specific types and a `suffix` property. 22 | 23 | The default (Angular-friendly) configuration looks like this: 24 | 25 | ```json 26 | { 27 | "rxjs/suffix-subjects": [ 28 | "error", 29 | { 30 | "parameters": true, 31 | "properties": true, 32 | "suffix": "Subject", 33 | "types": { 34 | "^EventEmitter$": false 35 | }, 36 | "variables": true, 37 | } 38 | ] 39 | } 40 | ``` 41 | 42 | The properties in the options object are themselves optional; they do not all have to be specified. -------------------------------------------------------------------------------- /docs/rules/throw-error.md: -------------------------------------------------------------------------------- 1 | # Avoid throwing non-Error values (`throw-error`) 2 | 3 | This rule forbids throwing values that are neither `Error` nor `DOMException` instances. 4 | 5 | ## Rule details 6 | 7 | Examples of **incorrect** code for this rule: 8 | 9 | ```ts 10 | throw "Kaboom!"; 11 | ``` 12 | 13 | ```ts 14 | import { throwError } from "rxjs"; 15 | throwError("Kaboom!"); 16 | ``` 17 | 18 | ```ts 19 | import { throwError } from "rxjs"; 20 | throwError(() => "Kaboom!"); 21 | ``` 22 | 23 | Examples of **correct** code for this rule: 24 | 25 | ```ts 26 | throw new Error("Kaboom!"); 27 | ``` 28 | 29 | ```ts 30 | throw new RangeError("Kaboom!"); 31 | ``` 32 | 33 | ```ts 34 | throw new DOMException("Kaboom!"); 35 | ``` 36 | 37 | ```ts 38 | import { throwError } from "rxjs"; 39 | throwError(new Error("Kaboom!")); 40 | ``` 41 | 42 | ```ts 43 | import { throwError } from "rxjs"; 44 | throwError(() => new Error("Kaboom!")); 45 | ``` 46 | 47 | ## Options 48 | 49 | This rule has no options. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Nicholas Jamieson ", 3 | "bugs": { 4 | "url": "https://github.com/cartant/eslint-plugin-rxjs/issues" 5 | }, 6 | "dependencies": { 7 | "@typescript-eslint/experimental-utils": "^5.0.0", 8 | "common-tags": "^1.8.0", 9 | "decamelize": "^5.0.0", 10 | "eslint-etc": "^5.1.0", 11 | "requireindex": "~1.2.0", 12 | "rxjs-report-usage": "^1.0.4", 13 | "tslib": "^2.0.0", 14 | "tsutils": "^3.0.0", 15 | "tsutils-etc": "^1.4.1" 16 | }, 17 | "description": "ESLint rules for RxJS", 18 | "devDependencies": { 19 | "@cartant/eslint-config": "^3.0.0", 20 | "@types/chai": "^4.2.0", 21 | "@types/common-tags": "^1.8.0", 22 | "@types/eslint": "^8.0.0", 23 | "@types/mocha": "^9.0.0", 24 | "@types/node": "^18.0.0", 25 | "@typescript-eslint/parser": "^5.0.0", 26 | "chai": "^4.2.0", 27 | "eslint": "^8.0.0", 28 | "husky": "^8.0.0", 29 | "lint-staged": "^13.0.0", 30 | "mocha": "^9.0.0", 31 | "prettier": "^2.0.0", 32 | "rimraf": "^3.0.0", 33 | "rxjs": "^7.0.0", 34 | "ts-node": "^10.0.0", 35 | "typescript": "~4.7.4" 36 | }, 37 | "files": [ 38 | "dist", 39 | "docs" 40 | ], 41 | "homepage": "https://github.com/cartant/eslint-plugin-rxjs", 42 | "keywords": [ 43 | "lint", 44 | "rules", 45 | "eslint", 46 | "rxjs" 47 | ], 48 | "license": "MIT", 49 | "lint-staged": { 50 | "*.{js,ts}": "prettier --write" 51 | }, 52 | "main": "./dist/index.js", 53 | "name": "eslint-plugin-rxjs", 54 | "optionalDependencies": {}, 55 | "peerDependencies": { 56 | "eslint": "^8.0.0", 57 | "typescript": ">=4.0.0" 58 | }, 59 | "private": false, 60 | "publishConfig": { 61 | "tag": "latest" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/cartant/eslint-plugin-rxjs.git" 66 | }, 67 | "scripts": { 68 | "dist": "yarn run lint && yarn run dist:build", 69 | "dist:build": "yarn run dist:clean && tsc -p tsconfig-dist.json", 70 | "dist:clean": "rimraf dist", 71 | "lint": "eslint source/**/*.ts tests/**/*.ts", 72 | "prepare": "husky install", 73 | "prepublishOnly": "yarn run test && yarn run dist", 74 | "prettier": "prettier --write \"./{src,tests}/**/*.{js,json,ts,tsx}\"", 75 | "prettier:ci": "prettier --check \"./{src,tests}/**/*.{js,json,ts,tsx}\"", 76 | "test": "yarn run test:rules && yarn run test:specs", 77 | "test:debug": "mocha -r ts-node/register -t 5000 tests/rules/no-redundant-notify.ts", 78 | "test:rules": "mocha -r ts-node/register -t 5000 tests/rules/*.ts", 79 | "test:specs": "mocha -r ts-node/register tests/*-spec.ts" 80 | }, 81 | "version": "5.0.3" 82 | } 83 | -------------------------------------------------------------------------------- /source/configs/recommended.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | export = { 7 | plugins: ["rxjs"], 8 | rules: { 9 | "rxjs/no-async-subscribe": "error", 10 | "rxjs/no-create": "error", 11 | "rxjs/no-ignored-notifier": "error", 12 | "rxjs/no-ignored-replay-buffer": "error", 13 | "rxjs/no-ignored-takewhile-value": "error", 14 | "rxjs/no-implicit-any-catch": "error", 15 | "rxjs/no-index": "error", 16 | "rxjs/no-internal": "error", 17 | "rxjs/no-nested-subscribe": "error", 18 | "rxjs/no-redundant-notify": "error", 19 | "rxjs/no-sharereplay": ["error", { allowConfig: true }], 20 | "rxjs/no-subject-unsubscribe": "error", 21 | "rxjs/no-unbound-methods": "error", 22 | "rxjs/no-unsafe-subject-next": "error", 23 | "rxjs/no-unsafe-takeuntil": "error", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /source/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | export const defaultObservable = String.raw`[Aa]ction(s|s\$|\$)$`; 7 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | module.exports.configs = require("requireindex")(`${__dirname}/configs`); 7 | module.exports.rules = require("requireindex")(`${__dirname}/rules`); 8 | -------------------------------------------------------------------------------- /source/rules/ban-observables.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const defaultOptions: readonly Record[] = []; 11 | 12 | const rule = ruleCreator({ 13 | defaultOptions, 14 | meta: { 15 | docs: { 16 | description: "Forbids the use of banned observables.", 17 | recommended: false, 18 | }, 19 | fixable: undefined, 20 | hasSuggestions: false, 21 | messages: { 22 | forbidden: "RxJS observable is banned: {{name}}{{explanation}}.", 23 | }, 24 | schema: [ 25 | { 26 | type: "object", 27 | description: stripIndent` 28 | An object containing keys that are names of observable factory functions 29 | and values that are either booleans or strings containing the explanation for the ban.`, 30 | }, 31 | ], 32 | type: "problem", 33 | }, 34 | name: "ban-observables", 35 | create: (context, unused: typeof defaultOptions) => { 36 | let bans: { explanation: string; regExp: RegExp }[] = []; 37 | 38 | const [config] = context.options; 39 | if (!config) { 40 | return {}; 41 | } 42 | 43 | Object.entries(config).forEach(([key, value]) => { 44 | if (value !== false) { 45 | bans.push({ 46 | explanation: typeof value === "string" ? value : "", 47 | regExp: new RegExp(`^${key}$`), 48 | }); 49 | } 50 | }); 51 | 52 | function getFailure(name: string) { 53 | for (let b = 0, length = bans.length; b < length; ++b) { 54 | const ban = bans[b]; 55 | if (ban.regExp.test(name)) { 56 | const explanation = ban.explanation ? `: ${ban.explanation}` : ""; 57 | return { 58 | messageId: "forbidden", 59 | data: { name, explanation }, 60 | } as const; 61 | } 62 | } 63 | return undefined; 64 | } 65 | 66 | return { 67 | "ImportDeclaration[source.value='rxjs'] > ImportSpecifier": ( 68 | node: es.ImportSpecifier 69 | ) => { 70 | const identifier = node.imported; 71 | const failure = getFailure(identifier.name); 72 | if (failure) { 73 | context.report({ 74 | ...failure, 75 | node: identifier, 76 | }); 77 | } 78 | }, 79 | }; 80 | }, 81 | }); 82 | 83 | export = rule; 84 | -------------------------------------------------------------------------------- /source/rules/ban-operators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const defaultOptions: readonly Record[] = []; 11 | 12 | const rule = ruleCreator({ 13 | defaultOptions, 14 | meta: { 15 | docs: { 16 | description: "Forbids the use of banned operators.", 17 | recommended: false, 18 | }, 19 | fixable: undefined, 20 | hasSuggestions: false, 21 | messages: { 22 | forbidden: "RxJS operator is banned: {{name}}{{explanation}}.", 23 | }, 24 | schema: [ 25 | { 26 | type: "object", 27 | description: stripIndent` 28 | An object containing keys that are names of operators 29 | and values that are either booleans or strings containing the explanation for the ban.`, 30 | }, 31 | ], 32 | type: "problem", 33 | }, 34 | name: "ban-operators", 35 | create: (context, unused: typeof defaultOptions) => { 36 | let bans: { explanation: string; regExp: RegExp }[] = []; 37 | 38 | const [config] = context.options; 39 | if (!config) { 40 | return {}; 41 | } 42 | 43 | Object.entries(config).forEach(([key, value]) => { 44 | if (value !== false) { 45 | bans.push({ 46 | explanation: typeof value === "string" ? value : "", 47 | regExp: new RegExp(`^${key}$`), 48 | }); 49 | } 50 | }); 51 | 52 | function getFailure(name: string) { 53 | for (let b = 0, length = bans.length; b < length; ++b) { 54 | const ban = bans[b]; 55 | if (ban.regExp.test(name)) { 56 | const explanation = ban.explanation ? `: ${ban.explanation}` : ""; 57 | return { 58 | messageId: "forbidden", 59 | data: { name, explanation }, 60 | } as const; 61 | } 62 | } 63 | return undefined; 64 | } 65 | 66 | return { 67 | [String.raw`ImportDeclaration[source.value=/^rxjs\u002foperators$/] > ImportSpecifier`]: 68 | (node: es.ImportSpecifier) => { 69 | const identifier = node.imported; 70 | const failure = getFailure(identifier.name); 71 | if (failure) { 72 | context.report({ 73 | ...failure, 74 | node: identifier, 75 | }); 76 | } 77 | }, 78 | }; 79 | }, 80 | }); 81 | 82 | export = rule; 83 | -------------------------------------------------------------------------------- /source/rules/just.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { ruleCreator } from "../utils"; 8 | 9 | const rule = ruleCreator({ 10 | defaultOptions: [], 11 | meta: { 12 | docs: { 13 | description: "Enforces the use of a `just` alias for `of`.", 14 | recommended: false, 15 | }, 16 | fixable: "code", 17 | hasSuggestions: false, 18 | messages: { 19 | forbidden: "Use just alias.", 20 | }, 21 | schema: [], 22 | type: "problem", 23 | }, 24 | name: "just", 25 | create: (context) => { 26 | return { 27 | "ImportDeclaration[source.value='rxjs'] > ImportSpecifier[imported.name='of']": 28 | (node: es.ImportSpecifier) => { 29 | // import declaration has been renamed 30 | if ( 31 | node.local.range[0] !== node.imported.range[0] && 32 | node.local.range[1] !== node.imported.range[1] 33 | ) { 34 | return; 35 | } 36 | 37 | context.report({ 38 | messageId: "forbidden", 39 | node, 40 | fix: (fixer) => fixer.replaceTextRange(node.range, "of as just"), 41 | }); 42 | 43 | const [ofImport] = context.getDeclaredVariables(node); 44 | ofImport.references.forEach((ref) => { 45 | context.report({ 46 | messageId: "forbidden", 47 | node: ref.identifier, 48 | fix: (fixer) => 49 | fixer.replaceTextRange(ref.identifier.range, "just"), 50 | }); 51 | }); 52 | }, 53 | }; 54 | }, 55 | }); 56 | 57 | export = rule; 58 | -------------------------------------------------------------------------------- /source/rules/macro.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { 7 | TSESLint as eslint, 8 | TSESTree as es, 9 | } from "@typescript-eslint/experimental-utils"; 10 | import { ruleCreator } from "../utils"; 11 | 12 | const rule = ruleCreator({ 13 | defaultOptions: [], 14 | meta: { 15 | docs: { 16 | description: "Enforces the use of the RxJS Tools Babel macro.", 17 | recommended: false, 18 | }, 19 | fixable: "code", 20 | hasSuggestions: false, 21 | messages: { 22 | macro: "Use the RxJS Tools Babel macro.", 23 | }, 24 | schema: [], 25 | type: "problem", 26 | }, 27 | name: "macro", 28 | create: (context) => { 29 | let hasFailure = false; 30 | let hasMacroImport = false; 31 | let program: es.Program | undefined = undefined; 32 | 33 | function fix(fixer: eslint.RuleFixer) { 34 | return fixer.insertTextBefore( 35 | /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ 36 | program!, 37 | `import "babel-plugin-rxjs-tools/macro";\n` 38 | ); 39 | } 40 | 41 | return { 42 | "CallExpression[callee.property.name=/^(pipe|subscribe)$/]": ( 43 | node: es.CallExpression 44 | ) => { 45 | if (hasFailure || hasMacroImport) { 46 | return; 47 | } 48 | hasFailure = true; 49 | context.report({ 50 | fix, 51 | messageId: "macro", 52 | node: node.callee, 53 | }); 54 | }, 55 | "ImportDeclaration[source.value='babel-plugin-rxjs-tools/macro']": ( 56 | node: es.ImportDeclaration 57 | ) => { 58 | hasMacroImport = true; 59 | }, 60 | [String.raw`ImportDeclaration[source.value=/^rxjs(\u002f|$)/]`]: ( 61 | node: es.ImportDeclaration 62 | ) => { 63 | if (hasFailure || hasMacroImport) { 64 | return; 65 | } 66 | hasFailure = true; 67 | context.report({ 68 | fix, 69 | messageId: "macro", 70 | node, 71 | }); 72 | }, 73 | Program: (node: es.Program) => { 74 | program = node; 75 | }, 76 | }; 77 | }, 78 | }); 79 | 80 | export = rule; 81 | -------------------------------------------------------------------------------- /source/rules/no-async-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids passing `async` functions to `subscribe`.", 15 | recommended: "error", 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Passing async functions to subscribe is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-async-subscribe", 26 | create: (context) => { 27 | const { couldBeObservable } = getTypeServices(context); 28 | 29 | function checkNode( 30 | node: es.FunctionExpression | es.ArrowFunctionExpression 31 | ) { 32 | const parentNode = getParent(node) as es.CallExpression; 33 | const callee = parentNode.callee as es.MemberExpression; 34 | 35 | if (couldBeObservable(callee.object)) { 36 | const { loc } = node; 37 | // only report the `async` keyword 38 | const asyncLoc = { 39 | ...loc, 40 | end: { 41 | ...loc.start, 42 | column: loc.start.column + 5, 43 | }, 44 | }; 45 | 46 | context.report({ 47 | messageId: "forbidden", 48 | loc: asyncLoc, 49 | }); 50 | } 51 | } 52 | return { 53 | "CallExpression[callee.property.name='subscribe'] > FunctionExpression[async=true]": 54 | checkNode, 55 | "CallExpression[callee.property.name='subscribe'] > ArrowFunctionExpression[async=true]": 56 | checkNode, 57 | }; 58 | }, 59 | }); 60 | 61 | export = rule; 62 | -------------------------------------------------------------------------------- /source/rules/no-compat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { ruleCreator } from "../utils"; 8 | 9 | const rule = ruleCreator({ 10 | defaultOptions: [], 11 | meta: { 12 | docs: { 13 | description: 14 | "Forbids importation from locations that depend upon `rxjs-compat`.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "'rxjs-compat'-dependent import locations are forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-compat", 26 | create: (context) => { 27 | return { 28 | [String.raw`ImportDeclaration Literal[value=/^rxjs\u002f/]:not(Literal[value=/^rxjs\u002f(ajax|fetch|operators|testing|webSocket)/])`]: 29 | (node: es.Literal) => { 30 | context.report({ 31 | messageId: "forbidden", 32 | node, 33 | }); 34 | }, 35 | }; 36 | }, 37 | }); 38 | 39 | export = rule; 40 | -------------------------------------------------------------------------------- /source/rules/no-connectable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids operators that return connectable observables.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Connectable observables are forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-connectable", 26 | create: (context) => { 27 | const { couldBeFunction } = getTypeServices(context); 28 | return { 29 | "CallExpression[callee.name='multicast']": (node: es.CallExpression) => { 30 | if (node.arguments.length === 1) { 31 | context.report({ 32 | messageId: "forbidden", 33 | node: node.callee, 34 | }); 35 | } 36 | }, 37 | "CallExpression[callee.name=/^(publish|publishBehavior|publishLast|publishReplay)$/]": 38 | (node: es.CallExpression) => { 39 | if (!node.arguments.some((arg) => couldBeFunction(arg))) { 40 | context.report({ 41 | messageId: "forbidden", 42 | node: node.callee, 43 | }); 44 | } 45 | }, 46 | }; 47 | }, 48 | }); 49 | 50 | export = rule; 51 | -------------------------------------------------------------------------------- /source/rules/no-create.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids the calling of `Observable.create`.", 15 | recommended: "error", 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Observable.create is forbidden; use new Observable.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-create", 26 | create: (context) => { 27 | const { couldBeObservable } = getTypeServices(context); 28 | 29 | return { 30 | "CallExpression > MemberExpression[object.name='Observable'] > Identifier[name='create']": 31 | (node: es.Identifier) => { 32 | const memberExpression = getParent(node) as es.MemberExpression; 33 | if (couldBeObservable(memberExpression.object)) { 34 | context.report({ 35 | messageId: "forbidden", 36 | node, 37 | }); 38 | } 39 | }, 40 | }; 41 | }, 42 | }); 43 | 44 | export = rule; 45 | -------------------------------------------------------------------------------- /source/rules/no-cyclic-action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { getTypeServices, isCallExpression, isIdentifier } from "eslint-etc"; 9 | import ts from "typescript"; 10 | import { defaultObservable } from "../constants"; 11 | import { ruleCreator } from "../utils"; 12 | 13 | function isTypeReference(type: ts.Type): type is ts.TypeReference { 14 | return Boolean((type as any).target); 15 | } 16 | 17 | const defaultOptions: readonly { 18 | observable?: string; 19 | }[] = []; 20 | 21 | const rule = ruleCreator({ 22 | defaultOptions: [], 23 | meta: { 24 | docs: { 25 | description: "Forbids effects and epics that re-emit filtered actions.", 26 | recommended: false, 27 | }, 28 | fixable: undefined, 29 | hasSuggestions: false, 30 | messages: { 31 | forbidden: 32 | "Effects and epics that re-emit filtered actions are forbidden.", 33 | }, 34 | schema: [ 35 | { 36 | properties: { 37 | observable: { type: "string" }, 38 | }, 39 | type: "object", 40 | description: stripIndent` 41 | An optional object with an optional \`observable\` property. 42 | The property can be specified as a regular expression string and is used to identify the action observables from which effects and epics are composed.`, 43 | }, 44 | ], 45 | type: "problem", 46 | }, 47 | name: "no-cyclic-action", 48 | create: (context, unused: typeof defaultOptions) => { 49 | const [config = {}] = context.options; 50 | const { observable = defaultObservable } = config; 51 | const observableRegExp = new RegExp(observable); 52 | 53 | const { getType, typeChecker } = getTypeServices(context); 54 | 55 | function checkNode(pipeCallExpression: es.CallExpression) { 56 | const operatorCallExpression = pipeCallExpression.arguments.find( 57 | (arg) => 58 | isCallExpression(arg) && 59 | isIdentifier(arg.callee) && 60 | arg.callee.name === "ofType" 61 | ); 62 | if (!operatorCallExpression) { 63 | return; 64 | } 65 | const operatorType = getType(operatorCallExpression); 66 | const [signature] = typeChecker.getSignaturesOfType( 67 | operatorType, 68 | ts.SignatureKind.Call 69 | ); 70 | if (!signature) { 71 | return; 72 | } 73 | const operatorReturnType = 74 | typeChecker.getReturnTypeOfSignature(signature); 75 | if (!isTypeReference(operatorReturnType)) { 76 | return; 77 | } 78 | const [operatorElementType] = 79 | typeChecker.getTypeArguments(operatorReturnType); 80 | if (!operatorElementType) { 81 | return; 82 | } 83 | 84 | const pipeType = getType(pipeCallExpression); 85 | if (!isTypeReference(pipeType)) { 86 | return; 87 | } 88 | const [pipeElementType] = typeChecker.getTypeArguments(pipeType); 89 | if (!pipeElementType) { 90 | return; 91 | } 92 | 93 | const operatorActionTypes = getActionTypes(operatorElementType); 94 | const pipeActionTypes = getActionTypes(pipeElementType); 95 | for (const actionType of operatorActionTypes) { 96 | if (pipeActionTypes.includes(actionType)) { 97 | context.report({ 98 | messageId: "forbidden", 99 | node: pipeCallExpression.callee, 100 | }); 101 | return; 102 | } 103 | } 104 | } 105 | 106 | function getActionTypes(type: ts.Type): string[] { 107 | if (type.isUnion()) { 108 | const memberActionTypes: string[] = []; 109 | for (const memberType of type.types) { 110 | memberActionTypes.push(...getActionTypes(memberType)); 111 | } 112 | return memberActionTypes; 113 | } 114 | const symbol = typeChecker.getPropertyOfType(type, "type"); 115 | if (!symbol || !symbol.valueDeclaration) { 116 | return []; 117 | } 118 | const actionType = typeChecker.getTypeOfSymbolAtLocation( 119 | symbol, 120 | symbol.valueDeclaration 121 | ); 122 | return [typeChecker.typeToString(actionType)]; 123 | } 124 | 125 | return { 126 | [`CallExpression[callee.property.name='pipe'][callee.object.name=${observableRegExp}]`]: 127 | checkNode, 128 | [`CallExpression[callee.property.name='pipe'][callee.object.property.name=${observableRegExp}]`]: 129 | checkNode, 130 | }; 131 | }, 132 | }); 133 | 134 | export = rule; 135 | -------------------------------------------------------------------------------- /source/rules/no-explicit-generics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, isArrayExpression, isObjectExpression } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids explicit generic type arguments.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Explicit generic type arguments are forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-explicit-generics", 26 | create: (context) => { 27 | function report(node: es.Node) { 28 | context.report({ 29 | messageId: "forbidden", 30 | node, 31 | }); 32 | } 33 | 34 | function checkBehaviorSubjects(node: es.Node) { 35 | const parent = getParent(node) as es.NewExpression; 36 | const { 37 | arguments: [value], 38 | } = parent; 39 | if (isArrayExpression(value) || isObjectExpression(value)) { 40 | return; 41 | } 42 | report(node); 43 | } 44 | 45 | function checkNotifications(node: es.Node) { 46 | const parent = getParent(node) as es.NewExpression; 47 | const { 48 | arguments: [, value], 49 | } = parent; 50 | if (isArrayExpression(value) || isObjectExpression(value)) { 51 | return; 52 | } 53 | report(node); 54 | } 55 | 56 | return { 57 | "CallExpression[callee.property.name='pipe'] > CallExpression[typeParameters.params.length > 0] > Identifier": 58 | report, 59 | "NewExpression[typeParameters.params.length > 0] > Identifier[name='BehaviorSubject']": 60 | checkBehaviorSubjects, 61 | "CallExpression[typeParameters.params.length > 0] > Identifier[name=/^(from|of)$/]": 62 | report, 63 | "NewExpression[typeParameters.params.length > 0][arguments.0.value='N'] > Identifier[name='Notification']": 64 | checkNotifications, 65 | }; 66 | }, 67 | }); 68 | 69 | export = rule; 70 | -------------------------------------------------------------------------------- /source/rules/no-exposed-subjects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices, isIdentifier } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const defaultAllowedTypesRegExp = /^EventEmitter$/; 11 | const defaultOptions: readonly { 12 | allowProtected?: boolean; 13 | }[] = []; 14 | 15 | const rule = ruleCreator({ 16 | defaultOptions, 17 | meta: { 18 | docs: { 19 | description: "Forbids exposed (i.e. non-private) subjects.", 20 | recommended: false, 21 | }, 22 | fixable: undefined, 23 | hasSuggestions: false, 24 | messages: { 25 | forbidden: "Subject '{{subject}}' must be private.", 26 | forbiddenAllowProtected: 27 | "Subject '{{subject}}' must be private or protected.", 28 | }, 29 | schema: [ 30 | { 31 | properties: { 32 | allowProtected: { type: "boolean" }, 33 | }, 34 | type: "object", 35 | }, 36 | ], 37 | type: "problem", 38 | }, 39 | name: "no-exposed-subjects", 40 | create: (context, unused: typeof defaultOptions) => { 41 | const [config = {}] = context.options; 42 | const { allowProtected = false } = config; 43 | const { couldBeSubject, couldBeType } = getTypeServices(context); 44 | 45 | const messageId = allowProtected ? "forbiddenAllowProtected" : "forbidden"; 46 | const accessibilityRexExp = allowProtected 47 | ? /^(private|protected)$/ 48 | : /^private$/; 49 | 50 | function isSubject(node: es.Node) { 51 | return ( 52 | couldBeSubject(node) && !couldBeType(node, defaultAllowedTypesRegExp) 53 | ); 54 | } 55 | 56 | return { 57 | [`PropertyDefinition[accessibility!=${accessibilityRexExp}]`]: ( 58 | node: es.PropertyDefinition 59 | ) => { 60 | if (isSubject(node)) { 61 | const { key } = node; 62 | if (isIdentifier(key)) { 63 | context.report({ 64 | messageId, 65 | node: key, 66 | data: { 67 | subject: key.name, 68 | }, 69 | }); 70 | } 71 | } 72 | }, 73 | [`MethodDefinition[kind='constructor'] > FunctionExpression > TSParameterProperty[accessibility!=${accessibilityRexExp}] > Identifier`]: 74 | (node: es.Identifier) => { 75 | if (isSubject(node)) { 76 | const { loc } = node; 77 | context.report({ 78 | messageId, 79 | loc: { 80 | ...loc, 81 | end: { 82 | ...loc.start, 83 | column: loc.start.column + node.name.length, 84 | }, 85 | }, 86 | data: { 87 | subject: node.name, 88 | }, 89 | }); 90 | } 91 | }, 92 | [`MethodDefinition[accessibility!=${accessibilityRexExp}][kind=/^(get|set)$/]`]: 93 | (node: es.MethodDefinition) => { 94 | if (isSubject(node)) { 95 | const key = node.key as es.Identifier; 96 | context.report({ 97 | messageId, 98 | node: key, 99 | data: { 100 | subject: key.name, 101 | }, 102 | }); 103 | } 104 | }, 105 | [`MethodDefinition[accessibility!=${accessibilityRexExp}][kind='method']`]: 106 | (node: es.MethodDefinition) => { 107 | const functionExpression = node.value as any; 108 | const returnType = functionExpression.returnType; 109 | if (!returnType) { 110 | return; 111 | } 112 | 113 | const typeAnnotation = returnType.typeAnnotation; 114 | if (!typeAnnotation) { 115 | return; 116 | } 117 | 118 | if (isSubject(typeAnnotation)) { 119 | const key = node.key as es.Identifier; 120 | context.report({ 121 | messageId, 122 | node: key, 123 | data: { 124 | subject: key.name, 125 | }, 126 | }); 127 | } 128 | }, 129 | }; 130 | }, 131 | }); 132 | 133 | export = rule; 134 | -------------------------------------------------------------------------------- /source/rules/no-finnish.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { 8 | getLoc, 9 | getParent, 10 | getParserServices, 11 | getTypeServices, 12 | } from "eslint-etc"; 13 | import { ruleCreator } from "../utils"; 14 | 15 | const rule = ruleCreator({ 16 | defaultOptions: [], 17 | meta: { 18 | docs: { 19 | description: "Forbids the use of Finnish notation.", 20 | recommended: false, 21 | }, 22 | fixable: undefined, 23 | hasSuggestions: false, 24 | messages: { 25 | forbidden: "Finnish notation is forbidden.", 26 | }, 27 | schema: [], 28 | type: "problem", 29 | }, 30 | name: "no-finnish", 31 | create: (context) => { 32 | const { esTreeNodeToTSNodeMap } = getParserServices(context); 33 | const { couldBeObservable, couldReturnObservable } = 34 | getTypeServices(context); 35 | 36 | function checkNode(nameNode: es.Node, typeNode?: es.Node) { 37 | if ( 38 | couldBeObservable(typeNode || nameNode) || 39 | couldReturnObservable(typeNode || nameNode) 40 | ) { 41 | const tsNode = esTreeNodeToTSNodeMap.get(nameNode); 42 | if (/[$]+$/.test(tsNode.getText())) { 43 | context.report({ 44 | loc: getLoc(tsNode), 45 | messageId: "forbidden", 46 | }); 47 | } 48 | } 49 | } 50 | 51 | return { 52 | "ArrayPattern > Identifier[name=/[$]+$/]": (node: es.Identifier) => 53 | checkNode(node), 54 | "ArrowFunctionExpression > Identifier[name=/[$]+$/]": ( 55 | node: es.Identifier 56 | ) => { 57 | const parent = getParent(node) as es.ArrowFunctionExpression; 58 | if (node !== parent.body) { 59 | checkNode(node); 60 | } 61 | }, 62 | "PropertyDefinition[key.name=/[$]+$/] > Identifier": ( 63 | node: es.Identifier 64 | ) => checkNode(node, getParent(node)), 65 | "FunctionDeclaration > Identifier[name=/[$]+$/]": ( 66 | node: es.Identifier 67 | ) => { 68 | const parent = getParent(node) as es.FunctionDeclaration; 69 | if (node === parent.id) { 70 | checkNode(node, parent); 71 | } else { 72 | checkNode(node); 73 | } 74 | }, 75 | "FunctionExpression > Identifier[name=/[$]+$/]": ( 76 | node: es.Identifier 77 | ) => { 78 | const parent = getParent(node) as es.FunctionExpression; 79 | if (node === parent.id) { 80 | checkNode(node, parent); 81 | } else { 82 | checkNode(node); 83 | } 84 | }, 85 | "MethodDefinition[key.name=/[$]+$/]": (node: es.MethodDefinition) => 86 | checkNode(node.key, node), 87 | "ObjectExpression > Property[computed=false][key.name=/[$]+$/]": ( 88 | node: es.Property 89 | ) => checkNode(node.key), 90 | "ObjectPattern > Property[value.name=/[$]+$/]": (node: es.Property) => 91 | checkNode(node.value), 92 | "TSCallSignatureDeclaration > Identifier[name=/[$]+$/]": ( 93 | node: es.Node 94 | ) => checkNode(node), 95 | "TSConstructSignatureDeclaration > Identifier[name=/[$]+$/]": ( 96 | node: es.Node 97 | ) => checkNode(node), 98 | "TSParameterProperty > Identifier[name=/[$]+$/]": (node: es.Identifier) => 99 | checkNode(node), 100 | "TSPropertySignature > Identifier[name=/[$]+$/]": (node: es.Identifier) => 101 | checkNode(node, getParent(node)), 102 | "TSMethodSignature > Identifier[name=/[$]+$/]": (node: es.Identifier) => { 103 | const parent = getParent(node) as any; 104 | if (node === parent.key) { 105 | checkNode(node, parent); 106 | } else { 107 | checkNode(node); 108 | } 109 | }, 110 | "VariableDeclarator[id.name=/[$]+$/]": (node: es.VariableDeclarator) => 111 | checkNode(node.id, node.init ?? node), 112 | }; 113 | }, 114 | }); 115 | 116 | export = rule; 117 | -------------------------------------------------------------------------------- /source/rules/no-ignored-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids the calling of `subscribe` without specifying an error handler.", 16 | recommended: false, 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: "Calling subscribe without an error handler is forbidden.", 22 | }, 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-ignored-error", 27 | create: (context) => { 28 | const { couldBeObservable, couldBeFunction } = getTypeServices(context); 29 | 30 | return { 31 | "CallExpression[arguments.length > 0] > MemberExpression > Identifier[name='subscribe']": 32 | (node: es.Identifier) => { 33 | const memberExpression = getParent(node) as es.MemberExpression; 34 | const callExpression = getParent( 35 | memberExpression 36 | ) as es.CallExpression; 37 | 38 | if ( 39 | callExpression.arguments.length < 2 && 40 | couldBeObservable(memberExpression.object) && 41 | couldBeFunction(callExpression.arguments[0]) 42 | ) { 43 | context.report({ 44 | messageId: "forbidden", 45 | node, 46 | }); 47 | } 48 | }, 49 | }; 50 | }, 51 | }); 52 | 53 | export = rule; 54 | -------------------------------------------------------------------------------- /source/rules/no-ignored-notifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { 8 | getTypeServices, 9 | isArrowFunctionExpression, 10 | isFunctionExpression, 11 | } from "eslint-etc"; 12 | import { ruleCreator } from "../utils"; 13 | 14 | const rule = ruleCreator({ 15 | defaultOptions: [], 16 | meta: { 17 | docs: { 18 | description: 19 | "Forbids observables not composed from the `repeatWhen` or `retryWhen` notifier.", 20 | recommended: "error", 21 | }, 22 | fixable: undefined, 23 | hasSuggestions: false, 24 | messages: { 25 | forbidden: "Ignoring the notifier is forbidden.", 26 | }, 27 | schema: [], 28 | type: "problem", 29 | }, 30 | name: "no-ignored-notifier", 31 | create: (context) => { 32 | const { couldBeMonoTypeOperatorFunction } = getTypeServices(context); 33 | 34 | type Entry = { 35 | node: es.Node; 36 | param: es.Identifier; 37 | sightings: number; 38 | }; 39 | const entries: Entry[] = []; 40 | 41 | function getEntry() { 42 | const { length, [length - 1]: entry } = entries; 43 | return entry; 44 | } 45 | 46 | return { 47 | "CallExpression[callee.name=/^(repeatWhen|retryWhen)$/]": ( 48 | node: es.CallExpression 49 | ) => { 50 | if (couldBeMonoTypeOperatorFunction(node)) { 51 | const [arg] = node.arguments; 52 | if (isArrowFunctionExpression(arg) || isFunctionExpression(arg)) { 53 | const [param] = arg.params as es.Identifier[]; 54 | if (param) { 55 | entries.push({ 56 | node, 57 | param, 58 | sightings: 0, 59 | }); 60 | } else { 61 | context.report({ 62 | messageId: "forbidden", 63 | node: node.callee, 64 | }); 65 | } 66 | } 67 | } 68 | }, 69 | "CallExpression[callee.name=/^(repeatWhen|retryWhen)$/]:exit": ( 70 | node: es.CallExpression 71 | ) => { 72 | const entry = getEntry(); 73 | if (!entry) { 74 | return; 75 | } 76 | if (entry.node === node) { 77 | if (entry.sightings < 2) { 78 | context.report({ 79 | messageId: "forbidden", 80 | node: node.callee, 81 | }); 82 | } 83 | entries.pop(); 84 | } 85 | }, 86 | Identifier: (node: es.Identifier) => { 87 | const entry = getEntry(); 88 | if (!entry) { 89 | return; 90 | } 91 | if (node.name === entry.param.name) { 92 | ++entry.sightings; 93 | } 94 | }, 95 | }; 96 | }, 97 | }); 98 | 99 | export = rule; 100 | -------------------------------------------------------------------------------- /source/rules/no-ignored-observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids the ignoring of observables returned by functions.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Ignoring a returned Observable is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-ignored-observable", 26 | create: (context) => { 27 | const { couldBeObservable } = getTypeServices(context); 28 | 29 | return { 30 | "ExpressionStatement > CallExpression": (node: es.CallExpression) => { 31 | if (couldBeObservable(node)) { 32 | context.report({ 33 | messageId: "forbidden", 34 | node, 35 | }); 36 | } 37 | }, 38 | }; 39 | }, 40 | }); 41 | 42 | export = rule; 43 | -------------------------------------------------------------------------------- /source/rules/no-ignored-replay-buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids using `ReplaySubject`, `publishReplay` or `shareReplay` without specifying the buffer size.", 16 | recommended: "error", 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: "Ignoring the buffer size is forbidden.", 22 | }, 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-ignored-replay-buffer", 27 | create: (context) => { 28 | function checkNode( 29 | node: es.Node, 30 | { arguments: args }: { arguments: es.Node[] } 31 | ) { 32 | if (!args || args.length === 0) { 33 | context.report({ 34 | messageId: "forbidden", 35 | node, 36 | }); 37 | } 38 | } 39 | 40 | return { 41 | "NewExpression > Identifier[name='ReplaySubject']": ( 42 | node: es.Identifier 43 | ) => { 44 | const newExpression = getParent(node) as es.NewExpression; 45 | checkNode(node, newExpression); 46 | }, 47 | "NewExpression > MemberExpression > Identifier[name='ReplaySubject']": ( 48 | node: es.Identifier 49 | ) => { 50 | const memberExpression = getParent(node) as es.MemberExpression; 51 | const newExpression = getParent(memberExpression) as es.NewExpression; 52 | checkNode(node, newExpression); 53 | }, 54 | "CallExpression > Identifier[name=/^(publishReplay|shareReplay)$/]": ( 55 | node: es.Identifier 56 | ) => { 57 | const callExpression = getParent(node) as es.CallExpression; 58 | checkNode(node, callExpression); 59 | }, 60 | }; 61 | }, 62 | }); 63 | 64 | export = rule; 65 | -------------------------------------------------------------------------------- /source/rules/no-ignored-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids the calling of `subscribe` without specifying arguments.", 16 | recommended: false, 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: "Calling subscribe without arguments is forbidden.", 22 | }, 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-ignored-subscribe", 27 | create: (context) => { 28 | const { couldBeObservable, couldBeType } = getTypeServices(context); 29 | 30 | return { 31 | "CallExpression[arguments.length = 0][callee.property.name='subscribe']": 32 | (node: es.CallExpression) => { 33 | const callee = node.callee as es.MemberExpression; 34 | if ( 35 | couldBeObservable(callee.object) || 36 | couldBeType(callee.object, "Subscribable") 37 | ) { 38 | context.report({ 39 | messageId: "forbidden", 40 | node: callee.property, 41 | }); 42 | } 43 | }, 44 | }; 45 | }, 46 | }); 47 | 48 | export = rule; 49 | -------------------------------------------------------------------------------- /source/rules/no-ignored-subscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids ignoring the subscription returned by `subscribe`.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Ignoring returned subscriptions is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-ignored-subscription", 26 | create: (context) => { 27 | const { couldBeObservable, couldBeType } = getTypeServices(context); 28 | 29 | return { 30 | "ExpressionStatement > CallExpression > MemberExpression[property.name='subscribe']": 31 | (node: es.MemberExpression) => { 32 | if (couldBeObservable(node.object)) { 33 | const callExpression = getParent(node) as es.CallExpression; 34 | if ( 35 | callExpression.arguments.length === 1 && 36 | couldBeType(callExpression.arguments[0], "Subscriber") 37 | ) { 38 | return; 39 | } 40 | context.report({ 41 | messageId: "forbidden", 42 | node: node.property, 43 | }); 44 | } 45 | }, 46 | }; 47 | }, 48 | }); 49 | 50 | export = rule; 51 | -------------------------------------------------------------------------------- /source/rules/no-ignored-takewhile-value.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { 8 | isArrayPattern, 9 | isIdentifier, 10 | isImport, 11 | isObjectPattern, 12 | } from "eslint-etc"; 13 | import { ruleCreator } from "../utils"; 14 | 15 | const rule = ruleCreator({ 16 | defaultOptions: [], 17 | meta: { 18 | docs: { 19 | description: "Forbids ignoring the value within `takeWhile`.", 20 | recommended: "error", 21 | }, 22 | fixable: undefined, 23 | hasSuggestions: false, 24 | messages: { 25 | forbidden: "Ignoring the value within takeWhile is forbidden.", 26 | }, 27 | schema: [], 28 | type: "problem", 29 | }, 30 | name: "no-ignored-takewhile-value", 31 | create: (context) => { 32 | function checkNode( 33 | expression: es.ArrowFunctionExpression | es.FunctionExpression 34 | ) { 35 | const scope = context.getScope(); 36 | if (!isImport(scope, "takeWhile", /^rxjs\/?/)) { 37 | return; 38 | } 39 | let ignored = true; 40 | const [param] = expression.params; 41 | if (param) { 42 | if (isIdentifier(param)) { 43 | const variable = scope.variables.find( 44 | ({ name }) => name === param.name 45 | ); 46 | if (variable && variable.references.length > 0) { 47 | ignored = false; 48 | } 49 | } else if (isArrayPattern(param)) { 50 | ignored = false; 51 | } else if (isObjectPattern(param)) { 52 | ignored = false; 53 | } 54 | } 55 | if (ignored) { 56 | context.report({ 57 | messageId: "forbidden", 58 | node: expression, 59 | }); 60 | } 61 | } 62 | 63 | return { 64 | "CallExpression[callee.name='takeWhile'] > ArrowFunctionExpression": ( 65 | node: es.ArrowFunctionExpression 66 | ) => checkNode(node), 67 | "CallExpression[callee.name='takeWhile'] > FunctionExpression": ( 68 | node: es.FunctionExpression 69 | ) => checkNode(node), 70 | }; 71 | }, 72 | }); 73 | 74 | export = rule; 75 | -------------------------------------------------------------------------------- /source/rules/no-index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { ruleCreator } from "../utils"; 8 | 9 | const rule = ruleCreator({ 10 | defaultOptions: [], 11 | meta: { 12 | docs: { 13 | description: "Forbids the importation from index modules.", 14 | recommended: "error", 15 | }, 16 | fixable: undefined, 17 | hasSuggestions: false, 18 | messages: { 19 | forbidden: "RxJS imports from index modules are forbidden.", 20 | }, 21 | schema: [], 22 | type: "problem", 23 | }, 24 | name: "no-index", 25 | create: (context) => { 26 | return { 27 | [String.raw`ImportDeclaration Literal[value=/^rxjs(?:\u002f\w+)?\u002findex/]`]: 28 | (node: es.Literal) => { 29 | context.report({ 30 | messageId: "forbidden", 31 | node, 32 | }); 33 | }, 34 | }; 35 | }, 36 | }); 37 | 38 | export = rule; 39 | -------------------------------------------------------------------------------- /source/rules/no-internal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { 7 | TSESTree as es, 8 | TSESLint as eslint, 9 | } from "@typescript-eslint/experimental-utils"; 10 | import { ruleCreator } from "../utils"; 11 | 12 | const rule = ruleCreator({ 13 | defaultOptions: [], 14 | meta: { 15 | docs: { 16 | description: "Forbids the importation of internals.", 17 | recommended: "error", 18 | }, 19 | fixable: "code", 20 | hasSuggestions: true, 21 | messages: { 22 | forbidden: "RxJS imports from internal are forbidden.", 23 | suggest: "Import from a non-internal location.", 24 | }, 25 | schema: [], 26 | type: "problem", 27 | }, 28 | name: "no-internal", 29 | create: (context) => { 30 | function getReplacement(location: string) { 31 | const match = location.match(/^\s*('|")/); 32 | if (!match) { 33 | return undefined; 34 | } 35 | const [, quote] = match; 36 | if (/^['"]rxjs\/internal\/ajax/.test(location)) { 37 | return `${quote}rxjs/ajax${quote}`; 38 | } 39 | if (/^['"]rxjs\/internal\/observable\/dom\/fetch/.test(location)) { 40 | return `${quote}rxjs/fetch${quote}`; 41 | } 42 | if (/^['"]rxjs\/internal\/observable\/dom\/webSocket/i.test(location)) { 43 | return `${quote}rxjs/webSocket${quote}`; 44 | } 45 | if (/^['"]rxjs\/internal\/observable/.test(location)) { 46 | return `${quote}rxjs${quote}`; 47 | } 48 | if (/^['"]rxjs\/internal\/operators/.test(location)) { 49 | return `${quote}rxjs/operators${quote}`; 50 | } 51 | if (/^['"]rxjs\/internal\/scheduled/.test(location)) { 52 | return `${quote}rxjs${quote}`; 53 | } 54 | if (/^['"]rxjs\/internal\/scheduler/.test(location)) { 55 | return `${quote}rxjs${quote}`; 56 | } 57 | if (/^['"]rxjs\/internal\/testing/.test(location)) { 58 | return `${quote}rxjs/testing${quote}`; 59 | } 60 | return undefined; 61 | } 62 | 63 | return { 64 | [String.raw`ImportDeclaration Literal[value=/^rxjs\u002finternal/]`]: ( 65 | node: es.Literal 66 | ) => { 67 | const replacement = getReplacement(node.raw); 68 | if (replacement) { 69 | function fix(fixer: eslint.RuleFixer) { 70 | return fixer.replaceText(node, replacement as string); 71 | } 72 | context.report({ 73 | fix, 74 | messageId: "forbidden", 75 | node, 76 | suggest: [{ fix, messageId: "suggest" }], 77 | }); 78 | } else { 79 | context.report({ 80 | messageId: "forbidden", 81 | node, 82 | }); 83 | } 84 | }, 85 | }; 86 | }, 87 | }); 88 | 89 | export = rule; 90 | -------------------------------------------------------------------------------- /source/rules/no-nested-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids the calling of `subscribe` within a `subscribe` callback.", 16 | recommended: "error", 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: "Nested subscribe calls are forbidden.", 22 | }, 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-nested-subscribe", 27 | create: (context) => { 28 | const { couldBeObservable, couldBeType } = getTypeServices(context); 29 | const argumentsMap = new WeakMap(); 30 | return { 31 | [`CallExpression > MemberExpression[property.name='subscribe']`]: ( 32 | node: es.MemberExpression 33 | ) => { 34 | if ( 35 | !couldBeObservable(node.object) && 36 | !couldBeType(node.object, "Subscribable") 37 | ) { 38 | return; 39 | } 40 | const callExpression = getParent(node) as es.CallExpression; 41 | let parent = getParent(callExpression); 42 | while (parent) { 43 | if (argumentsMap.has(parent)) { 44 | context.report({ 45 | messageId: "forbidden", 46 | node: node.property, 47 | }); 48 | return; 49 | } 50 | parent = getParent(parent); 51 | } 52 | for (const arg of callExpression.arguments) { 53 | argumentsMap.set(arg); 54 | } 55 | }, 56 | }; 57 | }, 58 | }); 59 | 60 | export = rule; 61 | -------------------------------------------------------------------------------- /source/rules/no-redundant-notify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { 7 | TSESLint as eslint, 8 | TSESTree as es, 9 | } from "@typescript-eslint/experimental-utils"; 10 | import { 11 | getParent, 12 | getTypeServices, 13 | isBlockStatement, 14 | isCallExpression, 15 | isIdentifier, 16 | isMemberExpression, 17 | isProgram, 18 | } from "eslint-etc"; 19 | import { ruleCreator } from "../utils"; 20 | 21 | const rule = ruleCreator({ 22 | defaultOptions: [], 23 | meta: { 24 | docs: { 25 | description: 26 | "Forbids redundant notifications from completed or errored observables.", 27 | recommended: "error", 28 | }, 29 | fixable: undefined, 30 | hasSuggestions: false, 31 | messages: { 32 | forbidden: "Redundant notifications are forbidden.", 33 | }, 34 | schema: [], 35 | type: "problem", 36 | }, 37 | name: "no-redundant-notify", 38 | create: (context) => { 39 | const sourceCode = context.getSourceCode(); 40 | const { couldBeType } = getTypeServices(context); 41 | return { 42 | "ExpressionStatement[expression.callee.property.name=/^(complete|error)$/] + ExpressionStatement[expression.callee.property.name=/^(next|complete|error)$/]": 43 | (node: es.ExpressionStatement) => { 44 | const parent = getParent(node); 45 | if (!parent) { 46 | return; 47 | } 48 | if (!isBlockStatement(parent) && !isProgram(parent)) { 49 | return; 50 | } 51 | const { body } = parent; 52 | const index = body.indexOf(node); 53 | const sibling = body[index - 1] as es.ExpressionStatement; 54 | if ( 55 | getExpressionText(sibling, sourceCode) !== 56 | getExpressionText(node, sourceCode) 57 | ) { 58 | return; 59 | } 60 | if ( 61 | !isExpressionObserver(sibling, couldBeType) || 62 | !isExpressionObserver(node, couldBeType) 63 | ) { 64 | return; 65 | } 66 | const { expression } = node; 67 | if (isCallExpression(expression)) { 68 | const { callee } = expression; 69 | if (isMemberExpression(callee)) { 70 | const { property } = callee; 71 | if (isIdentifier(property)) { 72 | context.report({ 73 | messageId: "forbidden", 74 | node: property, 75 | }); 76 | } 77 | } 78 | } 79 | }, 80 | }; 81 | }, 82 | }); 83 | 84 | function getExpressionText( 85 | expressionStatement: es.ExpressionStatement, 86 | sourceCode: eslint.SourceCode 87 | ): string | undefined { 88 | if (!isCallExpression(expressionStatement.expression)) { 89 | return undefined; 90 | } 91 | const callExpression = expressionStatement.expression; 92 | if (!isMemberExpression(callExpression.callee)) { 93 | return undefined; 94 | } 95 | const { object } = callExpression.callee; 96 | return sourceCode.getText(object); 97 | } 98 | 99 | function isExpressionObserver( 100 | expressionStatement: es.ExpressionStatement, 101 | couldBeType: ( 102 | node: es.Node, 103 | name: string | RegExp, 104 | qualified?: { name: RegExp } 105 | ) => boolean 106 | ): boolean { 107 | if (!isCallExpression(expressionStatement.expression)) { 108 | return false; 109 | } 110 | const callExpression = expressionStatement.expression; 111 | if (!isMemberExpression(callExpression.callee)) { 112 | return false; 113 | } 114 | const { object } = callExpression.callee; 115 | return couldBeType(object, /^(Subject|Subscriber)$/); 116 | } 117 | 118 | export = rule; 119 | -------------------------------------------------------------------------------- /source/rules/no-sharereplay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { ruleCreator } from "../utils"; 8 | 9 | const defaultOptions: readonly { 10 | allowConfig?: boolean; 11 | }[] = []; 12 | 13 | const rule = ruleCreator({ 14 | defaultOptions, 15 | meta: { 16 | docs: { 17 | description: "Forbids using the `shareReplay` operator.", 18 | recommended: "error", 19 | }, 20 | fixable: undefined, 21 | hasSuggestions: false, 22 | messages: { 23 | forbidden: "shareReplay is forbidden.", 24 | forbiddenWithoutConfig: 25 | "shareReplay is forbidden unless a config argument is passed.", 26 | }, 27 | schema: [ 28 | { 29 | properties: { 30 | allowConfig: { type: "boolean" }, 31 | }, 32 | type: "object", 33 | }, 34 | ], 35 | type: "problem", 36 | }, 37 | name: "no-sharereplay", 38 | create: (context, unused: typeof defaultOptions) => { 39 | const [config = {}] = context.options; 40 | const { allowConfig = true } = config; 41 | return { 42 | "CallExpression[callee.name='shareReplay']": ( 43 | node: es.CallExpression 44 | ) => { 45 | let report = true; 46 | if (allowConfig) { 47 | report = 48 | node.arguments.length !== 1 || 49 | node.arguments[0].type !== "ObjectExpression"; 50 | } 51 | if (report) { 52 | context.report({ 53 | messageId: allowConfig ? "forbiddenWithoutConfig" : "forbidden", 54 | node: node.callee, 55 | }); 56 | } 57 | }, 58 | }; 59 | }, 60 | }); 61 | 62 | export = rule; 63 | -------------------------------------------------------------------------------- /source/rules/no-subclass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids subclassing RxJS classes.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Subclassing RxJS classes is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-subclass", 26 | create: (context) => { 27 | const { couldBeType } = getTypeServices(context); 28 | 29 | const queryNames = [ 30 | "AsyncSubject", 31 | "BehaviorSubject", 32 | "Observable", 33 | "ReplaySubject", 34 | "Scheduler", 35 | "Subject", 36 | "Subscriber", 37 | ]; 38 | 39 | return { 40 | [`ClassDeclaration[superClass.name=/^(${queryNames.join( 41 | "|" 42 | )})$/] > Identifier.superClass`]: (node: es.Identifier) => { 43 | if ( 44 | queryNames.some((name) => 45 | couldBeType(node, name, { name: /[\/\\]rxjs[\/\\]/ }) 46 | ) 47 | ) { 48 | context.report({ 49 | messageId: "forbidden", 50 | node, 51 | }); 52 | } 53 | }, 54 | }; 55 | }, 56 | }); 57 | 58 | export = rule; 59 | -------------------------------------------------------------------------------- /source/rules/no-subject-unsubscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids calling the `unsubscribe` method of a subject instance.", 16 | recommended: "error", 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: "Calling unsubscribe on a subject is forbidden.", 22 | }, 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-subject-unsubscribe", 27 | create: (context) => { 28 | const { couldBeSubject, couldBeSubscription } = getTypeServices(context); 29 | 30 | return { 31 | "MemberExpression[property.name='unsubscribe']": ( 32 | node: es.MemberExpression 33 | ) => { 34 | if (couldBeSubject(node.object)) { 35 | context.report({ 36 | messageId: "forbidden", 37 | node: node.property, 38 | }); 39 | } 40 | }, 41 | "CallExpression[callee.property.name='add'][arguments.length > 0]": ( 42 | node: es.CallExpression 43 | ) => { 44 | const memberExpression = node.callee as es.MemberExpression; 45 | if (couldBeSubscription(memberExpression.object)) { 46 | const [arg] = node.arguments; 47 | if (couldBeSubject(arg)) { 48 | context.report({ 49 | messageId: "forbidden", 50 | node: arg, 51 | }); 52 | } 53 | } 54 | }, 55 | }; 56 | }, 57 | }); 58 | 59 | export = rule; 60 | -------------------------------------------------------------------------------- /source/rules/no-subject-value.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParent, getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: 15 | "Forbids accessing the `value` property of a `BehaviorSubject` instance.", 16 | recommended: "error", 17 | }, 18 | fixable: undefined, 19 | hasSuggestions: false, 20 | messages: { 21 | forbidden: 22 | "Accessing the value property of a BehaviorSubject is forbidden.", 23 | }, 24 | schema: [], 25 | type: "problem", 26 | }, 27 | name: "no-subject-value", 28 | create: (context) => { 29 | const { couldBeBehaviorSubject } = getTypeServices(context); 30 | 31 | return { 32 | "Identifier[name=/^(value|getValue)$/]": (node: es.Identifier) => { 33 | const parent = getParent(node); 34 | 35 | if (!parent || !("object" in parent)) { 36 | return; 37 | } 38 | 39 | if (couldBeBehaviorSubject(parent.object)) { 40 | context.report({ 41 | messageId: "forbidden", 42 | node, 43 | }); 44 | } 45 | }, 46 | }; 47 | }, 48 | }); 49 | 50 | export = rule; 51 | -------------------------------------------------------------------------------- /source/rules/no-subscribe-handlers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids the passing of handlers to `subscribe`.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "Passing handlers to subscribe is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-subscribe-handlers", 26 | create: (context) => { 27 | const { couldBeObservable, couldBeType } = getTypeServices(context); 28 | 29 | return { 30 | "CallExpression[arguments.length > 0][callee.property.name='subscribe']": 31 | (node: es.CallExpression) => { 32 | const callee = node.callee as es.MemberExpression; 33 | if ( 34 | couldBeObservable(callee.object) || 35 | couldBeType(callee.object, "Subscribable") 36 | ) { 37 | context.report({ 38 | messageId: "forbidden", 39 | node: callee.property, 40 | }); 41 | } 42 | }, 43 | }; 44 | }, 45 | }); 46 | 47 | export = rule; 48 | -------------------------------------------------------------------------------- /source/rules/no-tap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { ruleCreator } from "../utils"; 8 | 9 | const rule = ruleCreator({ 10 | defaultOptions: [], 11 | meta: { 12 | deprecated: true, 13 | docs: { 14 | description: "Forbids the use of the `tap` operator.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "The tap operator is forbidden.", 21 | }, 22 | replacedBy: ["ban-operators"], 23 | schema: [], 24 | type: "problem", 25 | }, 26 | name: "no-tap", 27 | create: (context) => { 28 | return { 29 | [String.raw`ImportDeclaration[source.value=/^rxjs(\u002foperators)?$/] > ImportSpecifier[imported.name='tap']`]: 30 | (node: es.ImportSpecifier) => { 31 | const { loc } = node; 32 | context.report({ 33 | messageId: "forbidden", 34 | loc: { 35 | ...loc, 36 | end: { 37 | ...loc.start, 38 | column: loc.start.column + 3, 39 | }, 40 | }, 41 | }); 42 | }, 43 | }; 44 | }, 45 | }); 46 | 47 | export = rule; 48 | -------------------------------------------------------------------------------- /source/rules/no-topromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getTypeServices } from "eslint-etc"; 8 | import { ruleCreator } from "../utils"; 9 | 10 | const rule = ruleCreator({ 11 | defaultOptions: [], 12 | meta: { 13 | docs: { 14 | description: "Forbids the use of the `toPromise` method.", 15 | recommended: false, 16 | }, 17 | fixable: undefined, 18 | hasSuggestions: false, 19 | messages: { 20 | forbidden: "The toPromise method is forbidden.", 21 | }, 22 | schema: [], 23 | type: "problem", 24 | }, 25 | name: "no-topromise", 26 | create: (context) => { 27 | const { couldBeObservable } = getTypeServices(context); 28 | return { 29 | [`MemberExpression[property.name="toPromise"]`]: ( 30 | node: es.MemberExpression 31 | ) => { 32 | if (couldBeObservable(node.object)) { 33 | context.report({ 34 | messageId: "forbidden", 35 | node: node.property, 36 | }); 37 | } 38 | }, 39 | }; 40 | }, 41 | }); 42 | 43 | export = rule; 44 | -------------------------------------------------------------------------------- /source/rules/no-unbound-methods.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { 8 | getParent, 9 | getTypeServices, 10 | isCallExpression, 11 | isMemberExpression, 12 | } from "eslint-etc"; 13 | import { ruleCreator } from "../utils"; 14 | 15 | const rule = ruleCreator({ 16 | defaultOptions: [], 17 | meta: { 18 | docs: { 19 | description: "Forbids the passing of unbound methods.", 20 | recommended: "error", 21 | }, 22 | fixable: undefined, 23 | hasSuggestions: false, 24 | messages: { 25 | forbidden: "Unbound methods are forbidden.", 26 | }, 27 | schema: [], 28 | type: "problem", 29 | }, 30 | name: "no-unbound-methods", 31 | create: (context) => { 32 | const { couldBeObservable, couldBeSubscription, getType } = 33 | getTypeServices(context); 34 | const nodeMap = new WeakMap(); 35 | 36 | function mapArguments(node: es.CallExpression | es.NewExpression) { 37 | node.arguments.filter(isMemberExpression).forEach((arg) => { 38 | const argType = getType(arg); 39 | if (argType.getCallSignatures().length > 0) { 40 | nodeMap.set(arg); 41 | } 42 | }); 43 | } 44 | 45 | function isObservableOrSubscription( 46 | node: es.CallExpression, 47 | action: (node: es.CallExpression) => void 48 | ) { 49 | if (!isMemberExpression(node.callee)) { 50 | return; 51 | } 52 | 53 | if ( 54 | couldBeObservable(node.callee.object) || 55 | couldBeSubscription(node.callee.object) 56 | ) { 57 | action(node); 58 | } 59 | } 60 | 61 | return { 62 | "CallExpression[callee.property.name='pipe']": ( 63 | node: es.CallExpression 64 | ) => { 65 | isObservableOrSubscription(node, ({ arguments: args }) => { 66 | args.filter(isCallExpression).forEach(mapArguments); 67 | }); 68 | }, 69 | "CallExpression[callee.property.name=/^(add|subscribe)$/]": ( 70 | node: es.CallExpression 71 | ) => { 72 | isObservableOrSubscription(node, mapArguments); 73 | }, 74 | "NewExpression[callee.name='Subscription']": mapArguments, 75 | ThisExpression: (node: es.ThisExpression) => { 76 | let parent = getParent(node); 77 | while (parent) { 78 | if (nodeMap.has(parent)) { 79 | context.report({ 80 | messageId: "forbidden", 81 | node: parent, 82 | }); 83 | return; 84 | } 85 | parent = getParent(parent); 86 | } 87 | }, 88 | }; 89 | }, 90 | }); 91 | 92 | export = rule; 93 | -------------------------------------------------------------------------------- /source/rules/no-unsafe-catch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { 9 | getTypeServices, 10 | isArrowFunctionExpression, 11 | isCallExpression, 12 | isFunctionDeclaration, 13 | isIdentifier, 14 | } from "eslint-etc"; 15 | import { defaultObservable } from "../constants"; 16 | import { ruleCreator } from "../utils"; 17 | 18 | const defaultOptions: readonly { 19 | observable?: string; 20 | }[] = []; 21 | 22 | const rule = ruleCreator({ 23 | defaultOptions, 24 | meta: { 25 | docs: { 26 | description: "Forbids unsafe `catchError` usage in effects and epics.", 27 | recommended: false, 28 | }, 29 | fixable: undefined, 30 | hasSuggestions: false, 31 | messages: { 32 | forbidden: "Unsafe catchError usage in effects and epics are forbidden.", 33 | }, 34 | schema: [ 35 | { 36 | properties: { 37 | observable: { type: "string" }, 38 | }, 39 | type: "object", 40 | description: stripIndent` 41 | An optional object with an optional \`observable\` property. 42 | The property can be specified as a regular expression string and is used to identify the action observables from which effects and epics are composed.`, 43 | }, 44 | ], 45 | type: "problem", 46 | }, 47 | name: "no-unsafe-catch", 48 | create: (context, unused: typeof defaultOptions) => { 49 | const invalidOperatorsRegExp = /^(catchError)$/; 50 | 51 | const [config = {}] = context.options; 52 | const { observable = defaultObservable } = config; 53 | const observableRegExp = new RegExp(observable); 54 | 55 | const { couldBeObservable } = getTypeServices(context); 56 | 57 | function isUnsafe([arg]: es.Node[]) { 58 | if ( 59 | arg && 60 | (isFunctionDeclaration(arg) || isArrowFunctionExpression(arg)) 61 | ) { 62 | // It's only unsafe if it receives a single function argument. If the 63 | // source argument is received, assume that it's used to effect a 64 | // resubscription to the source and that the effect won't complete. 65 | return arg.params.length < 2; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | function checkNode(node: es.CallExpression) { 72 | if (!node.arguments || !couldBeObservable(node)) { 73 | return; 74 | } 75 | 76 | node.arguments.forEach((arg) => { 77 | if (isCallExpression(arg) && isIdentifier(arg.callee)) { 78 | if ( 79 | invalidOperatorsRegExp.test(arg.callee.name) && 80 | isUnsafe(arg.arguments) 81 | ) { 82 | context.report({ 83 | messageId: "forbidden", 84 | node: arg.callee, 85 | }); 86 | } 87 | } 88 | }); 89 | } 90 | 91 | return { 92 | [`CallExpression[callee.property.name='pipe'][callee.object.name=${observableRegExp}]`]: 93 | checkNode, 94 | [`CallExpression[callee.property.name='pipe'][callee.object.property.name=${observableRegExp}]`]: 95 | checkNode, 96 | }; 97 | }, 98 | }); 99 | 100 | export = rule; 101 | -------------------------------------------------------------------------------- /source/rules/no-unsafe-first.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { getTypeServices, isCallExpression, isIdentifier } from "eslint-etc"; 9 | import { defaultObservable } from "../constants"; 10 | import { ruleCreator } from "../utils"; 11 | 12 | const defaultOptions: readonly { 13 | observable?: string; 14 | }[] = []; 15 | 16 | const rule = ruleCreator({ 17 | defaultOptions, 18 | meta: { 19 | docs: { 20 | description: "Forbids unsafe `first`/`take` usage in effects and epics.", 21 | recommended: false, 22 | }, 23 | fixable: undefined, 24 | hasSuggestions: false, 25 | messages: { 26 | forbidden: 27 | "Unsafe first and take usage in effects and epics are forbidden.", 28 | }, 29 | schema: [ 30 | { 31 | properties: { 32 | observable: { type: "string" }, 33 | }, 34 | type: "object", 35 | description: stripIndent` 36 | An optional object with an optional \`observable\` property. 37 | The property can be specified as a regular expression string and is used to identify the action observables from which effects and epics are composed.`, 38 | }, 39 | ], 40 | type: "problem", 41 | }, 42 | name: "no-unsafe-first", 43 | create: (context, unused: typeof defaultOptions) => { 44 | const invalidOperatorsRegExp = /^(take|first)$/; 45 | 46 | const [config = {}] = context.options; 47 | const { observable = defaultObservable } = config; 48 | const observableRegExp = new RegExp(observable); 49 | 50 | const { couldBeObservable } = getTypeServices(context); 51 | const nodes: es.CallExpression[] = []; 52 | 53 | function checkNode(node: es.CallExpression) { 54 | if (!node.arguments || !couldBeObservable(node)) { 55 | return; 56 | } 57 | 58 | node.arguments.forEach((arg) => { 59 | if (isCallExpression(arg) && isIdentifier(arg.callee)) { 60 | if (invalidOperatorsRegExp.test(arg.callee.name)) { 61 | context.report({ 62 | messageId: "forbidden", 63 | node: arg.callee, 64 | }); 65 | } 66 | } 67 | }); 68 | } 69 | 70 | return { 71 | [`CallExpression[callee.property.name='pipe'][callee.object.name=${observableRegExp}]`]: 72 | (node: es.CallExpression) => { 73 | if (nodes.push(node) === 1) { 74 | checkNode(node); 75 | } 76 | }, 77 | [`CallExpression[callee.property.name='pipe'][callee.object.name=${observableRegExp}]:exit`]: 78 | () => { 79 | nodes.pop(); 80 | }, 81 | [`CallExpression[callee.property.name='pipe'][callee.object.property.name=${observableRegExp}]`]: 82 | (node: es.CallExpression) => { 83 | if (nodes.push(node) === 1) { 84 | checkNode(node); 85 | } 86 | }, 87 | [`CallExpression[callee.property.name='pipe'][callee.object.property.name=${observableRegExp}]:exit`]: 88 | () => { 89 | nodes.pop(); 90 | }, 91 | }; 92 | }, 93 | }); 94 | 95 | export = rule; 96 | -------------------------------------------------------------------------------- /source/rules/no-unsafe-subject-next.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { 8 | getParserServices, 9 | getTypeServices, 10 | isMemberExpression, 11 | } from "eslint-etc"; 12 | import * as tsutils from "tsutils"; 13 | import { couldBeType, isReferenceType, isUnionType } from "tsutils-etc"; 14 | import * as ts from "typescript"; 15 | import { ruleCreator } from "../utils"; 16 | 17 | const rule = ruleCreator({ 18 | defaultOptions: [], 19 | meta: { 20 | docs: { 21 | description: "Forbids unsafe optional `next` calls.", 22 | recommended: "error", 23 | }, 24 | fixable: undefined, 25 | hasSuggestions: false, 26 | messages: { 27 | forbidden: "Unsafe optional next calls are forbidden.", 28 | }, 29 | schema: [], 30 | type: "problem", 31 | }, 32 | name: "no-unsafe-subject-next", 33 | create: (context) => { 34 | const { esTreeNodeToTSNodeMap } = getParserServices(context); 35 | const { typeChecker } = getTypeServices(context); 36 | return { 37 | [`CallExpression[callee.property.name='next']`]: ( 38 | node: es.CallExpression 39 | ) => { 40 | if (node.arguments.length === 0 && isMemberExpression(node.callee)) { 41 | const type = typeChecker.getTypeAtLocation( 42 | esTreeNodeToTSNodeMap.get(node.callee.object) 43 | ); 44 | if (isReferenceType(type) && couldBeType(type, "Subject")) { 45 | const [typeArg] = typeChecker.getTypeArguments(type); 46 | if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Any)) { 47 | return; 48 | } 49 | if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Unknown)) { 50 | return; 51 | } 52 | if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Void)) { 53 | return; 54 | } 55 | if ( 56 | isUnionType(typeArg) && 57 | typeArg.types.some((t) => 58 | tsutils.isTypeFlagSet(t, ts.TypeFlags.Void) 59 | ) 60 | ) { 61 | return; 62 | } 63 | context.report({ 64 | messageId: "forbidden", 65 | node: node.callee.property, 66 | }); 67 | } 68 | } 69 | }, 70 | }; 71 | }, 72 | }); 73 | 74 | export = rule; 75 | -------------------------------------------------------------------------------- /source/rules/no-unsafe-switchmap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import decamelize from "decamelize"; 9 | import { 10 | getTypeServices, 11 | isCallExpression, 12 | isIdentifier, 13 | isLiteral, 14 | isMemberExpression, 15 | } from "eslint-etc"; 16 | import { defaultObservable } from "../constants"; 17 | import { createRegExpForWords, ruleCreator } from "../utils"; 18 | 19 | const defaultOptions: readonly { 20 | allow?: string | string[]; 21 | disallow?: string | string[]; 22 | observable?: string; 23 | }[] = []; 24 | 25 | const rule = ruleCreator({ 26 | defaultOptions, 27 | meta: { 28 | docs: { 29 | description: "Forbids unsafe `switchMap` usage in effects and epics.", 30 | recommended: false, 31 | }, 32 | fixable: undefined, 33 | hasSuggestions: false, 34 | messages: { 35 | forbidden: "Unsafe switchMap usage in effects and epics is forbidden.", 36 | }, 37 | schema: [ 38 | { 39 | properties: { 40 | allow: { 41 | oneOf: [ 42 | { type: "string" }, 43 | { type: "array", items: { type: "string" } }, 44 | ], 45 | }, 46 | disallow: { 47 | oneOf: [ 48 | { type: "string" }, 49 | { type: "array", items: { type: "string" } }, 50 | ], 51 | }, 52 | observable: { 53 | oneOf: [ 54 | { type: "string" }, 55 | { type: "array", items: { type: "string" } }, 56 | ], 57 | }, 58 | }, 59 | type: "object", 60 | description: stripIndent` 61 | An optional object with optional \`allow\`, \`disallow\` and \`observable\` properties. 62 | The properties can be specified as regular expression strings or as arrays of words. 63 | The \`allow\` or \`disallow\` properties are mutually exclusive. Whether or not 64 | \`switchMap\` is allowed will depend upon the matching of action types with \`allow\` or \`disallow\`. 65 | The \`observable\` property is used to identify the action observables from which effects and epics are composed. 66 | `, 67 | }, 68 | ], 69 | type: "problem", 70 | }, 71 | name: "no-unsafe-switchmap", 72 | create: (context, unused: typeof defaultOptions) => { 73 | const defaultDisallow = [ 74 | "add", 75 | "create", 76 | "delete", 77 | "post", 78 | "put", 79 | "remove", 80 | "set", 81 | "update", 82 | ]; 83 | 84 | let allowRegExp: RegExp | undefined; 85 | let disallowRegExp: RegExp | undefined; 86 | let observableRegExp: RegExp; 87 | 88 | const [config = {}] = context.options; 89 | if (config.allow || config.disallow) { 90 | allowRegExp = createRegExpForWords(config.allow ?? []); 91 | disallowRegExp = createRegExpForWords(config.disallow ?? []); 92 | observableRegExp = new RegExp(config.observable ?? defaultObservable); 93 | } else { 94 | allowRegExp = undefined; 95 | disallowRegExp = createRegExpForWords(defaultDisallow); 96 | observableRegExp = new RegExp(defaultObservable); 97 | } 98 | 99 | const { couldBeObservable } = getTypeServices(context); 100 | 101 | function shouldDisallow(args: es.Node[]): boolean { 102 | const names = args 103 | .map((arg) => { 104 | if (isLiteral(arg) && typeof arg.value === "string") { 105 | return arg.value; 106 | } 107 | if (isIdentifier(arg)) { 108 | return arg.name; 109 | } 110 | if (isMemberExpression(arg) && isIdentifier(arg.property)) { 111 | return arg.property.name; 112 | } 113 | 114 | return ""; 115 | }) 116 | .map((name) => decamelize(name)); 117 | 118 | if (allowRegExp) { 119 | return !names.every((name) => allowRegExp?.test(name)); 120 | } 121 | if (disallowRegExp) { 122 | return names.some((name) => disallowRegExp?.test(name)); 123 | } 124 | 125 | return false; 126 | } 127 | 128 | function checkNode(node: es.CallExpression) { 129 | if (!node.arguments || !couldBeObservable(node)) { 130 | return; 131 | } 132 | 133 | const hasUnsafeOfType = node.arguments.some((arg) => { 134 | if ( 135 | isCallExpression(arg) && 136 | isIdentifier(arg.callee) && 137 | arg.callee.name === "ofType" 138 | ) { 139 | return shouldDisallow(arg.arguments); 140 | } 141 | return false; 142 | }); 143 | if (!hasUnsafeOfType) { 144 | return; 145 | } 146 | 147 | node.arguments.forEach((arg) => { 148 | if ( 149 | isCallExpression(arg) && 150 | isIdentifier(arg.callee) && 151 | arg.callee.name === "switchMap" 152 | ) { 153 | context.report({ 154 | messageId: "forbidden", 155 | node: arg.callee, 156 | }); 157 | } 158 | }); 159 | } 160 | 161 | return { 162 | [`CallExpression[callee.property.name='pipe'][callee.object.name=${observableRegExp}]`]: 163 | checkNode, 164 | [`CallExpression[callee.property.name='pipe'][callee.object.property.name=${observableRegExp}]`]: 165 | checkNode, 166 | }; 167 | }, 168 | }); 169 | 170 | export = rule; 171 | -------------------------------------------------------------------------------- /source/rules/no-unsafe-takeuntil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { stripIndent } from "common-tags"; 8 | import { 9 | getParent, 10 | getTypeServices, 11 | isCallExpression, 12 | isIdentifier, 13 | isMemberExpression, 14 | } from "eslint-etc"; 15 | import { ruleCreator } from "../utils"; 16 | 17 | const defaultOptions: readonly { 18 | alias?: string[]; 19 | allow?: string[]; 20 | }[] = []; 21 | 22 | const rule = ruleCreator({ 23 | defaultOptions, 24 | meta: { 25 | docs: { 26 | description: "Forbids the application of operators after `takeUntil`.", 27 | recommended: "error", 28 | }, 29 | fixable: undefined, 30 | hasSuggestions: false, 31 | messages: { 32 | forbidden: "Applying operators after takeUntil is forbidden.", 33 | }, 34 | schema: [ 35 | { 36 | properties: { 37 | alias: { type: "array", items: { type: "string" } }, 38 | allow: { type: "array", items: { type: "string" } }, 39 | }, 40 | type: "object", 41 | description: stripIndent` 42 | An optional object with optional \`alias\` and \`allow\` properties. 43 | The \`alias\` property is an array containing the names of operators that aliases for \`takeUntil\`. 44 | The \`allow\` property is an array containing the names of the operators that are allowed to follow \`takeUntil\`.`, 45 | }, 46 | ], 47 | type: "problem", 48 | }, 49 | name: "no-unsafe-takeuntil", 50 | create: (context, unused: typeof defaultOptions) => { 51 | let checkedOperatorsRegExp = /^takeUntil$/; 52 | const allowedOperators = [ 53 | "count", 54 | "defaultIfEmpty", 55 | "endWith", 56 | "every", 57 | "finalize", 58 | "finally", 59 | "isEmpty", 60 | "last", 61 | "max", 62 | "min", 63 | "publish", 64 | "publishBehavior", 65 | "publishLast", 66 | "publishReplay", 67 | "reduce", 68 | "share", 69 | "shareReplay", 70 | "skipLast", 71 | "takeLast", 72 | "throwIfEmpty", 73 | "toArray", 74 | ]; 75 | const [config = {}] = context.options; 76 | const { alias, allow = allowedOperators } = config; 77 | 78 | if (alias) { 79 | checkedOperatorsRegExp = new RegExp( 80 | `^(${alias.concat("takeUntil").join("|")})$` 81 | ); 82 | } 83 | 84 | const { couldBeObservable } = getTypeServices(context); 85 | 86 | function checkNode(node: es.CallExpression) { 87 | const pipeCallExpression = getParent(node) as es.CallExpression; 88 | if ( 89 | !pipeCallExpression.arguments || 90 | !couldBeObservable(pipeCallExpression) 91 | ) { 92 | return; 93 | } 94 | 95 | type State = "allowed" | "disallowed" | "taken"; 96 | 97 | pipeCallExpression.arguments.reduceRight((state, arg) => { 98 | if (state === "taken") { 99 | return state; 100 | } 101 | 102 | if (!isCallExpression(arg)) { 103 | return "disallowed"; 104 | } 105 | 106 | let operatorName: string; 107 | if (isIdentifier(arg.callee)) { 108 | operatorName = arg.callee.name; 109 | } else if ( 110 | isMemberExpression(arg.callee) && 111 | isIdentifier(arg.callee.property) 112 | ) { 113 | operatorName = arg.callee.property.name; 114 | } else { 115 | return "disallowed"; 116 | } 117 | 118 | if (checkedOperatorsRegExp.test(operatorName)) { 119 | if (state === "disallowed") { 120 | context.report({ 121 | messageId: "forbidden", 122 | node: arg.callee, 123 | }); 124 | } 125 | return "taken"; 126 | } 127 | 128 | if (!allow.includes(operatorName)) { 129 | return "disallowed"; 130 | } 131 | return state; 132 | }, "allowed" as State); 133 | } 134 | 135 | return { 136 | [`CallExpression[callee.property.name='pipe'] > CallExpression[callee.name=${checkedOperatorsRegExp}]`]: 137 | checkNode, 138 | [`CallExpression[callee.property.name='pipe'] > CallExpression[callee.property.name=${checkedOperatorsRegExp}]`]: 139 | checkNode, 140 | }; 141 | }, 142 | }); 143 | 144 | export = rule; 145 | -------------------------------------------------------------------------------- /source/rules/prefer-observer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { 7 | TSESTree as es, 8 | TSESLint as eslint, 9 | } from "@typescript-eslint/experimental-utils"; 10 | import { 11 | getTypeServices, 12 | isArrowFunctionExpression, 13 | isFunctionExpression, 14 | isMemberExpression, 15 | } from "eslint-etc"; 16 | import { ruleCreator } from "../utils"; 17 | 18 | const defaultOptions: readonly { 19 | allowNext?: boolean; 20 | }[] = []; 21 | 22 | const rule = ruleCreator({ 23 | defaultOptions, 24 | meta: { 25 | docs: { 26 | description: 27 | "Forbids the passing separate handlers to `subscribe` and `tap`.", 28 | recommended: false, 29 | }, 30 | fixable: "code", 31 | hasSuggestions: true, 32 | messages: { 33 | forbidden: 34 | "Passing separate handlers is forbidden; pass an observer instead.", 35 | }, 36 | schema: [ 37 | { 38 | properties: { 39 | allowNext: { type: "boolean" }, 40 | }, 41 | type: "object", 42 | }, 43 | ], 44 | type: "problem", 45 | }, 46 | name: "prefer-observer", 47 | create: (context, unused: typeof defaultOptions) => { 48 | const { couldBeFunction, couldBeObservable } = getTypeServices(context); 49 | const [config = {}] = context.options; 50 | const { allowNext = true } = config; 51 | 52 | function checkArgs(callExpression: es.CallExpression, reportNode: es.Node) { 53 | const { arguments: args, callee } = callExpression; 54 | if (isMemberExpression(callee) && !couldBeObservable(callee.object)) { 55 | return; 56 | } 57 | 58 | function* fix(fixer: eslint.RuleFixer) { 59 | const sourceCode = context.getSourceCode(); 60 | const [nextArg, errorArg, completeArg] = args; 61 | const nextArgText = nextArg ? sourceCode.getText(nextArg) : ""; 62 | const errorArgText = errorArg ? sourceCode.getText(errorArg) : ""; 63 | const completeArgText = completeArg 64 | ? sourceCode.getText(completeArg) 65 | : ""; 66 | let observer = "{"; 67 | if ( 68 | nextArgText && 69 | nextArgText !== "undefined" && 70 | nextArgText !== "null" 71 | ) { 72 | observer += ` next: ${nextArgText}${ 73 | isValidArgText(errorArgText) || isValidArgText(completeArgText) 74 | ? "," 75 | : "" 76 | }`; 77 | } 78 | if ( 79 | errorArgText && 80 | errorArgText !== "undefined" && 81 | errorArgText !== "null" 82 | ) { 83 | observer += ` error: ${errorArgText}${ 84 | isValidArgText(completeArgText) ? "," : "" 85 | }`; 86 | } 87 | if ( 88 | completeArgText && 89 | completeArgText !== "undefined" && 90 | completeArgText !== "null" 91 | ) { 92 | observer += ` complete: ${completeArgText}`; 93 | } 94 | observer += " }"; 95 | 96 | yield fixer.replaceText(callExpression.arguments[0], observer); 97 | 98 | const [, start] = callExpression.arguments[0].range; 99 | const [, end] = 100 | callExpression.arguments[callExpression.arguments.length - 1].range; 101 | yield fixer.removeRange([start, end]); 102 | } 103 | 104 | if (args.length > 1) { 105 | context.report({ 106 | messageId: "forbidden", 107 | node: reportNode, 108 | fix, 109 | suggest: [ 110 | { 111 | messageId: "forbidden", 112 | fix, 113 | }, 114 | ], 115 | }); 116 | } else if (args.length === 1 && !allowNext) { 117 | const [arg] = args; 118 | if ( 119 | isArrowFunctionExpression(arg) || 120 | isFunctionExpression(arg) || 121 | couldBeFunction(arg) 122 | ) { 123 | context.report({ 124 | messageId: "forbidden", 125 | node: reportNode, 126 | fix, 127 | suggest: [ 128 | { 129 | messageId: "forbidden", 130 | fix, 131 | }, 132 | ], 133 | }); 134 | } 135 | } 136 | } 137 | 138 | return { 139 | "CallExpression[callee.property.name='pipe'] > CallExpression[callee.name='tap']": 140 | (node: es.CallExpression) => checkArgs(node, node.callee), 141 | "CallExpression[callee.property.name='subscribe']": ( 142 | node: es.CallExpression 143 | ) => checkArgs(node, (node.callee as es.MemberExpression).property), 144 | }; 145 | }, 146 | }); 147 | 148 | export = rule; 149 | 150 | function isValidArgText(argText: string) { 151 | return argText && argText !== "undefined" && argText !== "null"; 152 | } 153 | -------------------------------------------------------------------------------- /source/rules/throw-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { TSESTree as es } from "@typescript-eslint/experimental-utils"; 7 | import { getParserServices, getTypeServices } from "eslint-etc"; 8 | import { couldBeFunction, couldBeType, isAny, isUnknown } from "tsutils-etc"; 9 | import * as ts from "typescript"; 10 | import { ruleCreator } from "../utils"; 11 | 12 | const rule = ruleCreator({ 13 | defaultOptions: [], 14 | meta: { 15 | docs: { 16 | description: 17 | "Enforces the passing of `Error` values to error notifications.", 18 | recommended: false, 19 | }, 20 | fixable: undefined, 21 | hasSuggestions: false, 22 | messages: { 23 | forbidden: "Passing non-Error values are forbidden.", 24 | }, 25 | schema: [], 26 | type: "problem", 27 | }, 28 | name: "throw-error", 29 | create: (context) => { 30 | const { esTreeNodeToTSNodeMap, program } = getParserServices(context); 31 | const { couldBeObservable, getType } = getTypeServices(context); 32 | 33 | function checkNode(node: es.Node) { 34 | let type = getType(node); 35 | if (couldBeFunction(type)) { 36 | const tsNode = esTreeNodeToTSNodeMap.get(node); 37 | const annotation = (tsNode as ts.ArrowFunction).type; 38 | const body = (tsNode as ts.ArrowFunction).body; 39 | type = program.getTypeChecker().getTypeAtLocation(annotation ?? body); 40 | } 41 | if ( 42 | !isAny(type) && 43 | !isUnknown(type) && 44 | !couldBeType(type, /^(Error|DOMException)$/) 45 | ) { 46 | context.report({ 47 | messageId: "forbidden", 48 | node, 49 | }); 50 | } 51 | } 52 | 53 | return { 54 | "ThrowStatement > *": checkNode, 55 | "CallExpression[callee.name='throwError']": (node: es.CallExpression) => { 56 | if (couldBeObservable(node)) { 57 | const [arg] = node.arguments; 58 | if (arg) { 59 | checkNode(arg); 60 | } 61 | } 62 | }, 63 | }; 64 | }, 65 | }); 66 | 67 | export = rule; 68 | -------------------------------------------------------------------------------- /source/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { ESLintUtils } from "@typescript-eslint/experimental-utils"; 7 | 8 | export function createRegExpForWords( 9 | config: string | string[] 10 | ): RegExp | undefined { 11 | if (!config || !config.length) { 12 | return undefined; 13 | } 14 | const flags = "i"; 15 | if (typeof config === "string") { 16 | return new RegExp(config, flags); 17 | } 18 | const words = config; 19 | const joined = words.map((word) => String.raw`(\b|_)${word}(\b|_)`).join("|"); 20 | return new RegExp(`(${joined})`, flags); 21 | } 22 | 23 | export function escapeRegExp(text: string): string { 24 | // https://stackoverflow.com/a/3561711/6680611 25 | return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); 26 | } 27 | 28 | export const ruleCreator = ESLintUtils.RuleCreator( 29 | (name) => 30 | `https://github.com/cartant/eslint-plugin-rxjs/tree/main/docs/rules/${name}.md` 31 | ); 32 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "project": "tsconfig.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/rules/ban-observables.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/ban-observables"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("ban-observables", rule, { 12 | valid: [ 13 | { 14 | code: `import { of, Observable } from "rxjs";`, 15 | }, 16 | ], 17 | invalid: [ 18 | fromFixture( 19 | stripIndent` 20 | import { of, Observable as o, Subject } from "rxjs"; 21 | ~~ [forbidden { "name": "of", "explanation": "" }] 22 | ~~~~~~~~~~ [forbidden { "name": "Observable", "explanation": ": because I say so" }] 23 | `, 24 | { 25 | options: [ 26 | { 27 | of: true, 28 | Observable: "because I say so", 29 | Subject: false, 30 | }, 31 | ], 32 | } 33 | ), 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /tests/rules/ban-operators.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/ban-operators"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("ban-operators", rule, { 12 | valid: [ 13 | { 14 | code: `import { concat, merge as m, mergeMap as mm } from "rxjs/operators";`, 15 | }, 16 | { 17 | code: `import { concat, merge as m, mergeMap as mm } from "rxjs";`, 18 | }, 19 | { 20 | // This won't effect errors, because only imports from "rxjs/operators" 21 | // are checked. To support banning operators from "rxjs", it'll need to 22 | // check types. 23 | code: `import { concat, merge as m, mergeMap as mm } from "rxjs";`, 24 | options: [ 25 | { 26 | concat: true, 27 | merge: "because I say so", 28 | mergeMap: false, 29 | }, 30 | ], 31 | }, 32 | ], 33 | invalid: [ 34 | fromFixture( 35 | stripIndent` 36 | import { concat, merge as m, mergeMap as mm } from "rxjs/operators"; 37 | ~~~~~~ [forbidden { "name": "concat", "explanation": "" }] 38 | ~~~~~ [forbidden { "name": "merge", "explanation": ": because I say so" }] 39 | `, 40 | { 41 | options: [ 42 | { 43 | concat: true, 44 | merge: "because I say so", 45 | mergeMap: false, 46 | }, 47 | ], 48 | } 49 | ), 50 | ], 51 | }); 52 | -------------------------------------------------------------------------------- /tests/rules/just.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/just"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("just", rule, { 12 | valid: [ 13 | stripIndent` 14 | // non-RxJS of 15 | function foo(): void { 16 | function of(): void {} 17 | of(); 18 | } 19 | 20 | function bar(of: Function): void { 21 | of(); 22 | } 23 | 24 | function baz(): void { 25 | const of = () => {}; 26 | of(); 27 | } 28 | `, 29 | stripIndent` 30 | // aliased as bar 31 | import { of as bar } from "rxjs"; 32 | 33 | const a = bar("a"); 34 | `, 35 | stripIndent` 36 | // aliased as of 37 | import { of as of } from "rxjs"; 38 | 39 | const a = of("a"); 40 | `, 41 | ], 42 | invalid: [ 43 | fromFixture( 44 | stripIndent` 45 | // imported of 46 | import { of } from "rxjs"; 47 | ~~ [forbidden] 48 | 49 | const a = of("a"); 50 | ~~ [forbidden] 51 | const b = of("b"); 52 | ~~ [forbidden] 53 | `, 54 | { 55 | output: stripIndent` 56 | // imported of 57 | import { of as just } from "rxjs"; 58 | 59 | const a = just("a"); 60 | const b = just("b"); 61 | `, 62 | } 63 | ), 64 | fromFixture( 65 | stripIndent` 66 | // imported of with non-RxJS of 67 | import { of } from "rxjs"; 68 | ~~ [forbidden] 69 | 70 | function foo(): void { 71 | function of(): void {} 72 | of(); 73 | } 74 | 75 | function bar(of: Function): void { 76 | of(); 77 | } 78 | 79 | function baz(): void { 80 | const of = () => {}; 81 | of(); 82 | } 83 | `, 84 | { 85 | output: stripIndent` 86 | // imported of with non-RxJS of 87 | import { of as just } from "rxjs"; 88 | 89 | function foo(): void { 90 | function of(): void {} 91 | of(); 92 | } 93 | 94 | function bar(of: Function): void { 95 | of(); 96 | } 97 | 98 | function baz(): void { 99 | const of = () => {}; 100 | of(); 101 | } 102 | `, 103 | } 104 | ), 105 | ], 106 | }); 107 | -------------------------------------------------------------------------------- /tests/rules/macro.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/macro"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("macro", rule, { 12 | valid: [ 13 | stripIndent` 14 | // no macro; no RxJS 15 | import { foo } from "bar"; 16 | `, 17 | stripIndent` 18 | // macro; RxJS imports 19 | import "babel-plugin-rxjs-tools/macro"; 20 | import { of } from "rxjs"; 21 | `, 22 | stripIndent` 23 | // macro; pipe 24 | import "babel-plugin-rxjs-tools/macro"; 25 | import { foo, goo } from "bar"; 26 | const hoo = foo.pipe(goo()); 27 | `, 28 | stripIndent` 29 | // macro; subscribe 30 | import "babel-plugin-rxjs-tools/macro"; 31 | import { foo } from "bar"; 32 | foo.subscribe(); 33 | `, 34 | ], 35 | invalid: [ 36 | fromFixture( 37 | stripIndent` 38 | // no macro; RxJS imports 39 | import { of } from "rxjs"; 40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ [macro] 41 | `, 42 | { 43 | output: stripIndent` 44 | // no macro; RxJS imports 45 | import "babel-plugin-rxjs-tools/macro"; 46 | import { of } from "rxjs"; 47 | `, 48 | } 49 | ), 50 | fromFixture( 51 | stripIndent` 52 | // no macro; pipe 53 | import { foo, goo } from "bar"; 54 | const hoo = foo.pipe(goo()); 55 | ~~~~~~~~ [macro] 56 | `, 57 | { 58 | output: stripIndent` 59 | // no macro; pipe 60 | import "babel-plugin-rxjs-tools/macro"; 61 | import { foo, goo } from "bar"; 62 | const hoo = foo.pipe(goo()); 63 | `, 64 | } 65 | ), 66 | fromFixture( 67 | stripIndent` 68 | // no macro; subscribe 69 | import { foo } from "bar"; 70 | foo.subscribe(); 71 | ~~~~~~~~~~~~~ [macro] 72 | `, 73 | { 74 | output: stripIndent` 75 | // no macro; subscribe 76 | import "babel-plugin-rxjs-tools/macro"; 77 | import { foo } from "bar"; 78 | foo.subscribe(); 79 | `, 80 | } 81 | ), 82 | ], 83 | }); 84 | -------------------------------------------------------------------------------- /tests/rules/no-async-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-async-subscribe"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-async-subscribe", rule, { 12 | valid: [ 13 | stripIndent` 14 | // sync arrow function 15 | import { of } from "rxjs"; 16 | 17 | of("a").subscribe(() => {}); 18 | `, 19 | stripIndent` 20 | // sync function 21 | import { of } from "rxjs"; 22 | 23 | of("a").subscribe(function() {}); 24 | `, 25 | stripIndent` 26 | // https://github.com/cartant/eslint-plugin-rxjs/issues/46 27 | import React, { FC } from "react"; 28 | const SomeComponent: FC<{}> = () => some component; 29 | const someElement = ; 30 | `, 31 | stripIndent` 32 | // https://github.com/cartant/eslint-plugin-rxjs/issues/61 33 | const whatever = { 34 | subscribe(next?: (value: unknown) => void) {} 35 | }; 36 | whatever.subscribe(async () => { await 42; }); 37 | `, 38 | ], 39 | invalid: [ 40 | fromFixture( 41 | stripIndent` 42 | // async arrow function 43 | import { of } from "rxjs"; 44 | 45 | of("a").subscribe(async () => { 46 | ~~~~~ [forbidden] 47 | return await "a"; 48 | }); 49 | ` 50 | ), 51 | fromFixture( 52 | stripIndent` 53 | // async function 54 | import { of } from "rxjs"; 55 | 56 | of("a").subscribe(async function() { 57 | ~~~~~ [forbidden] 58 | return await "a"; 59 | }); 60 | ` 61 | ), 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /tests/rules/no-compat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-compat"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-compat", rule, { 12 | valid: [ 13 | `import { Observable } from "rxjs";`, 14 | `import { ajax } from "rxjs/ajax";`, 15 | `import { fromFetch } from "rxjs/fetch";`, 16 | `import { concatMap } from "rxjs/operators";`, 17 | `import { TestScheduler } from "rxjs/testing";`, 18 | `import { webSocket } from "rxjs/webSocket";`, 19 | `import * as prefixedPackage from "rxjs-prefixed-package";`, 20 | ], 21 | invalid: [ 22 | fromFixture( 23 | stripIndent` 24 | import * as Rx from "rxjs/Rx"; 25 | ~~~~~~~~~ [forbidden] 26 | ` 27 | ), 28 | fromFixture( 29 | stripIndent` 30 | import { Observable } from "rxjs/Observable"; 31 | ~~~~~~~~~~~~~~~~~ [forbidden] 32 | ` 33 | ), 34 | fromFixture( 35 | stripIndent` 36 | import { Subject } from "rxjs/Subject"; 37 | ~~~~~~~~~~~~~~ [forbidden] 38 | ` 39 | ), 40 | fromFixture( 41 | stripIndent` 42 | import { merge } from "rxjs/observable/merge"; 43 | ~~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 44 | ` 45 | ), 46 | fromFixture( 47 | stripIndent` 48 | import { merge } from "rxjs/operator/merge"; 49 | ~~~~~~~~~~~~~~~~~~~~~ [forbidden] 50 | ` 51 | ), 52 | fromFixture( 53 | stripIndent` 54 | import { asap } from "rxjs/scheduler/asap"; 55 | ~~~~~~~~~~~~~~~~~~~~~ [forbidden] 56 | ` 57 | ), 58 | fromFixture( 59 | stripIndent` 60 | import "rxjs/add/observable/merge"; 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 62 | ` 63 | ), 64 | fromFixture( 65 | stripIndent` 66 | import "rxjs/add/operator/mergeMap"; 67 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 68 | ` 69 | ), 70 | ], 71 | }); 72 | -------------------------------------------------------------------------------- /tests/rules/no-connectable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-connectable"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-connectable", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // multicast with selector 16 | import { of, Subject } from "rxjs"; 17 | import { multicast } from "rxjs/operators"; 18 | const result = of(42).pipe( 19 | multicast(new Subject(), p => p) 20 | );`, 21 | }, 22 | { 23 | code: stripIndent` 24 | // multicast with factory and selector 25 | import { of, Subject } from "rxjs"; 26 | import { multicast } from "rxjs/operators"; 27 | const result = of(42).pipe( 28 | multicast(() => new Subject(), p => p) 29 | );`, 30 | }, 31 | { 32 | code: stripIndent` 33 | // multicast with selector variable 34 | import { of, Subject } from "rxjs"; 35 | import { multicast } from "rxjs/operators"; 36 | const selector = p => p; 37 | const result = of(42).pipe( 38 | multicast(() => new Subject(), selector) 39 | );`, 40 | }, 41 | { 42 | code: stripIndent` 43 | // publish with selector 44 | import { of, Subject } from "rxjs"; 45 | import { multicast } from "rxjs/operators"; 46 | const result = of(42).pipe( 47 | publish(p => p) 48 | );`, 49 | }, 50 | { 51 | code: stripIndent` 52 | // publishReplay with selector 53 | import { of, Subject } from "rxjs"; 54 | import { publishReplay } from "rxjs/operators"; 55 | const result = of(42).pipe( 56 | publishReplay(1, p => p) 57 | )`, 58 | }, 59 | ], 60 | invalid: [ 61 | fromFixture( 62 | stripIndent` 63 | // publish 64 | import { of, Subject } from "rxjs"; 65 | import { publish } from "rxjs/operators"; 66 | const result = of(42).pipe( 67 | publish() 68 | ~~~~~~~ [forbidden] 69 | ); 70 | ` 71 | ), 72 | fromFixture( 73 | stripIndent` 74 | // publishBehavior 75 | import { of, Subject } from "rxjs"; 76 | import { publishBehavior } from "rxjs/operators"; 77 | const result = of(42).pipe( 78 | publishBehavior(1) 79 | ~~~~~~~~~~~~~~~ [forbidden] 80 | ); 81 | ` 82 | ), 83 | fromFixture( 84 | stripIndent` 85 | // publishLast 86 | import { of, Subject } from "rxjs"; 87 | import { publishLast } from "rxjs/operators"; 88 | const result = of(42).pipe( 89 | publishLast() 90 | ~~~~~~~~~~~ [forbidden] 91 | ); 92 | ` 93 | ), 94 | fromFixture( 95 | stripIndent` 96 | // publishReplay 97 | import { of, Subject } from "rxjs"; 98 | import { publishReplay } from "rxjs/operators"; 99 | const result = of(42).pipe( 100 | publishReplay(1) 101 | ~~~~~~~~~~~~~ [forbidden] 102 | ); 103 | ` 104 | ), 105 | fromFixture( 106 | stripIndent` 107 | // multicast 108 | import { of, Subject } from "rxjs"; 109 | import { multicast } from "rxjs/operators"; 110 | const result = of(42).pipe( 111 | multicast(new Subject()) 112 | ~~~~~~~~~ [forbidden] 113 | ); 114 | ` 115 | ), 116 | fromFixture( 117 | stripIndent` 118 | // multicast with factory 119 | import { of, Subject } from "rxjs"; 120 | import { multicast } from "rxjs/operators"; 121 | const result = of(42).pipe( 122 | multicast(() => new Subject()) 123 | ~~~~~~~~~ [forbidden] 124 | ); 125 | ` 126 | ), 127 | ], 128 | }); 129 | -------------------------------------------------------------------------------- /tests/rules/no-create.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-create"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-create", rule, { 12 | valid: [], 13 | invalid: [ 14 | fromFixture( 15 | stripIndent` 16 | // create 17 | import { Observable, Observer } from "rxjs"; 18 | 19 | const ob = Observable.create((observer: Observer) => { 20 | ~~~~~~ [forbidden] 21 | observer.next("Hello, world."); 22 | observer.complete(); 23 | return () => {}; 24 | }); 25 | ` 26 | ), 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /tests/rules/no-cyclic-action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-cyclic-action"); 9 | import { ruleTester } from "../utils"; 10 | 11 | const setup = stripIndent` 12 | import { Observable, of } from "rxjs"; 13 | import { mapTo } from "rxjs/operators"; 14 | 15 | type Action = { type: T }; 16 | type ActionOfType = T extends string ? Action : never; 17 | 18 | function ofType(...types: T): (source: Observable>) => Observable> { 19 | return source => source as any; 20 | } 21 | 22 | type Actions = Observable>; 23 | const actions = of>(); 24 | 25 | const SOMETHING = "SOMETHING"; 26 | const SOMETHING_ELSE = "SOMETHING_ELSE"; 27 | `.replace(/\n/g, ""); 28 | 29 | ruleTester({ types: true }).run("no-cyclic-action", rule, { 30 | valid: [ 31 | { 32 | code: stripIndent` 33 | // effect SOMETHING to SOMETHING_ELSE 34 | ${setup} 35 | const a = actions.pipe(ofType("SOMETHING"), mapTo({ type: "SOMETHING_ELSE" as const })); 36 | const b = actions.pipe(ofType("SOMETHING"), mapTo({ type: SOMETHING_ELSE } as const)); 37 | const c = actions.pipe(ofType(SOMETHING), mapTo({ type: "SOMETHING_ELSE" as const })); 38 | const d = actions.pipe(ofType(SOMETHING), mapTo({ type: SOMETHING_ELSE } as const)); 39 | `, 40 | }, 41 | { 42 | code: stripIndent` 43 | // epic SOMETHING to SOMETHING_ELSE 44 | ${setup} 45 | const a = (action$: Actions) => action$.pipe(ofType("SOMETHING"), mapTo({ type: "SOMETHING_ELSE" as const })); 46 | const b = (action$: Actions) => action$.pipe(ofType("SOMETHING"), mapTo({ type: SOMETHING_ELSE } as const)); 47 | const c = (action$: Actions) => action$.pipe(ofType(SOMETHING), mapTo({ type: "SOMETHING_ELSE" as const })); 48 | const d = (action$: Actions) => action$.pipe(ofType(SOMETHING), mapTo({ type: SOMETHING_ELSE } as const)); 49 | `, 50 | }, 51 | { 52 | code: stripIndent` 53 | // https://github.com/cartant/eslint-plugin-rxjs/issues/54 54 | ${setup} 55 | const a = actions.pipe(ofType("SOMETHING"), map(() => {})); 56 | `, 57 | }, 58 | ], 59 | invalid: [ 60 | fromFixture( 61 | stripIndent` 62 | // effect SOMETHING to SOMETHING 63 | ${setup} 64 | const a = actions.pipe(ofType("SOMETHING"), mapTo({ type: "SOMETHING" as const })); 65 | ~~~~~~~~~~~~ [forbidden] 66 | const b = actions.pipe(ofType("SOMETHING"), mapTo({ type: SOMETHING } as const)); 67 | ~~~~~~~~~~~~ [forbidden] 68 | const c = actions.pipe(ofType(SOMETHING), mapTo({ type: "SOMETHING" as const })); 69 | ~~~~~~~~~~~~ [forbidden] 70 | const d = actions.pipe(ofType(SOMETHING), mapTo({ type: SOMETHING } as const)); 71 | ~~~~~~~~~~~~ [forbidden] 72 | ` 73 | ), 74 | fromFixture( 75 | stripIndent` 76 | // epic SOMETHING to SOMETHING 77 | ${setup} 78 | const a = (action$: Actions) => action$.pipe(ofType("SOMETHING"), mapTo({ type: "SOMETHING" as const })); 79 | ~~~~~~~~~~~~ [forbidden] 80 | const b = (action$: Actions) => action$.pipe(ofType("SOMETHING"), mapTo({ type: SOMETHING } as const)); 81 | ~~~~~~~~~~~~ [forbidden] 82 | const c = (action$: Actions) => action$.pipe(ofType(SOMETHING), mapTo({ type: "SOMETHING" as const })); 83 | ~~~~~~~~~~~~ [forbidden] 84 | const d = (action$: Actions) => action$.pipe(ofType(SOMETHING), mapTo({ type: SOMETHING } as const)); 85 | ~~~~~~~~~~~~ [forbidden] 86 | ` 87 | ), 88 | fromFixture( 89 | stripIndent` 90 | // effect SOMETHING | SOMETHING_ELSE to SOMETHING 91 | ${setup} 92 | const a = actions.pipe(ofType("SOMETHING", "SOMETHING_ELSE"), mapTo({ type: "SOMETHING" as const })); 93 | ~~~~~~~~~~~~ [forbidden] 94 | const b = actions.pipe(ofType("SOMETHING", "SOMETHING_ELSE"), mapTo({ type: SOMETHING } as const)); 95 | ~~~~~~~~~~~~ [forbidden] 96 | const c = actions.pipe(ofType(SOMETHING, SOMETHING_ELSE), mapTo({ type: "SOMETHING" as const })); 97 | ~~~~~~~~~~~~ [forbidden] 98 | const d = actions.pipe(ofType(SOMETHING, SOMETHING_ELSE), mapTo({ type: SOMETHING } as const)); 99 | ~~~~~~~~~~~~ [forbidden] 100 | ` 101 | ), 102 | fromFixture( 103 | stripIndent` 104 | // epic SOMETHING | SOMETHING_ELSE to SOMETHING 105 | ${setup} 106 | const a = (action$: Actions) => action$.pipe(ofType("SOMETHING", "SOMETHING_ELSE"), mapTo({ type: "SOMETHING" as const })); 107 | ~~~~~~~~~~~~ [forbidden] 108 | const b = (action$: Actions) => action$.pipe(ofType("SOMETHING", "SOMETHING_ELSE"), mapTo({ type: SOMETHING } as const)); 109 | ~~~~~~~~~~~~ [forbidden] 110 | const c = (action$: Actions) => action$.pipe(ofType(SOMETHING, SOMETHING_ELSE), mapTo({ type: "SOMETHING" as const })); 111 | ~~~~~~~~~~~~ [forbidden] 112 | const d = (action$: Actions) => action$.pipe(ofType(SOMETHING, SOMETHING_ELSE), mapTo({ type: SOMETHING } as const)); 113 | ~~~~~~~~~~~~ [forbidden] 114 | ` 115 | ), 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /tests/rules/no-explicit-generics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-explicit-generics"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-explicit-generics", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // without type arguments 16 | import { BehaviorSubject, from, of, Notification } from "rxjs"; 17 | import { scan } from "rxjs/operators"; 18 | const a = of(42, 54); 19 | const b1 = a.pipe( 20 | scan((acc: string, value: number) => acc + value, "") 21 | ); 22 | const b2 = a.pipe( 23 | scan((acc, value): string => acc + value, "") 24 | ); 25 | const c = new BehaviorSubject(42); 26 | const d = from([42, 54]); 27 | const e = of(42, 54); 28 | const f = new Notification("N", 42); 29 | const g = new Notification("E", undefined, "Kaboom!"); 30 | const h = new Notification("C");`, 31 | }, 32 | { 33 | code: stripIndent` 34 | // with array and object literals 35 | import { BehaviorSubject, Notification } from "rxjs"; 36 | const a = new BehaviorSubject([42]); 37 | const b = new BehaviorSubject([]); 38 | const c = new BehaviorSubject<{ answer: number }>({ answer: 42 }); 39 | const d = new BehaviorSubject<{ answer?: number }>({}); 40 | const e = new Notification("N", [42]); 41 | const f = new Notification("N", []); 42 | const g = new Notification<{ answer: number }>("N", { answer: 42 }); 43 | const h = new Notification<{ answer?: number }>("N", {}); 44 | `, 45 | }, 46 | ], 47 | invalid: [ 48 | fromFixture( 49 | stripIndent` 50 | // scan with type arguments 51 | const a = of(42, 54); 52 | const b = a.pipe( 53 | scan((acc, value) => acc + value, "") 54 | ~~~~ [forbidden] 55 | ); 56 | ` 57 | ), 58 | fromFixture( 59 | stripIndent` 60 | const b = new BehaviorSubject(42); 61 | ~~~~~~~~~~~~~~~ [forbidden] 62 | ` 63 | ), 64 | fromFixture( 65 | stripIndent` 66 | const f = from([42, 54]); 67 | ~~~~ [forbidden] 68 | ` 69 | ), 70 | fromFixture( 71 | stripIndent` 72 | const o = of(42, 54); 73 | ~~ [forbidden] 74 | ` 75 | ), 76 | fromFixture( 77 | stripIndent` 78 | const n = new Notification("N", 42); 79 | ~~~~~~~~~~~~ [forbidden] 80 | ` 81 | ), 82 | ], 83 | }); 84 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-error"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-ignored-error", rule, { 12 | valid: [ 13 | stripIndent` 14 | // noop 15 | import { of } from "rxjs"; 16 | const observable = of([1, 2]); 17 | observable.subscribe(() => {}, () => {}); 18 | `, 19 | stripIndent` 20 | // subject 21 | import { Subject } from "rxjs"; 22 | const subject = new Subject(); 23 | const observable = of([1, 2]); 24 | observable.subscribe(subject); 25 | `, 26 | stripIndent` 27 | // https://github.com/cartant/eslint-plugin-rxjs/issues/61 28 | const whatever = { 29 | subscribe( 30 | next?: (value: unknown) => void, 31 | error?: (error: unknown) => void 32 | ) {} 33 | }; 34 | whatever.subscribe(); 35 | `, 36 | ], 37 | invalid: [ 38 | fromFixture( 39 | stripIndent` 40 | // arrow next ignored error 41 | import { of } from "rxjs"; 42 | const observable = of([1, 2]); 43 | observable.subscribe(() => {}); 44 | ~~~~~~~~~ [forbidden] 45 | ` 46 | ), 47 | fromFixture( 48 | stripIndent` 49 | // variable next ignored error 50 | import { of } from "rxjs"; 51 | const observable = of([1, 2]); 52 | const next = () => {}; 53 | observable.subscribe(next); 54 | ~~~~~~~~~ [forbidden] 55 | ` 56 | ), 57 | fromFixture( 58 | stripIndent` 59 | // subject arrow next ignored error 60 | import { Subject } from "rxjs"; 61 | const subject = new Subject(); 62 | subject.subscribe(() => {}); 63 | ~~~~~~~~~ [forbidden] 64 | ` 65 | ), 66 | fromFixture( 67 | stripIndent` 68 | // subject variable next ignored error 69 | import { Subject } from "rxjs"; 70 | const next = () => {}; 71 | const subject = new Subject(); 72 | subject.subscribe(next); 73 | ~~~~~~~~~ [forbidden] 74 | ` 75 | ), 76 | fromFixture( 77 | stripIndent` 78 | // https://github.com/cartant/eslint-plugin-rxjs/issues/60 79 | import { Observable } from "rxjs" 80 | interface ISomeExtension { 81 | sayHi(): void 82 | } 83 | function subscribeObservable(obs: Observable) { 84 | return obs.subscribe((v: T) => {}) 85 | ~~~~~~~~~ [forbidden] 86 | } 87 | function subscribeObservableWithExtension(obs: Observable & ISomeExtension) { 88 | return obs.subscribe((v: T) => {}) 89 | ~~~~~~~~~ [forbidden] 90 | } 91 | ` 92 | ), 93 | ], 94 | }); 95 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-notifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-notifier"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-ignored-notifier", rule, { 12 | valid: [ 13 | stripIndent` 14 | // repeatWhen not ignored 15 | import { of } from "rxjs"; 16 | import { repeatWhen } from "rxjs/operators"; 17 | 18 | const source = of(42); 19 | 20 | const a = source.pipe( 21 | repeatWhen(notifications => notifications) 22 | ); 23 | 24 | const b = source.pipe( 25 | repeatWhen( 26 | function (notifications) { 27 | return notifications; 28 | } 29 | ) 30 | ); 31 | `, 32 | stripIndent` 33 | // retryWhen not ignored 34 | import { of } from "rxjs"; 35 | import { retryWhen } from "rxjs/operators"; 36 | 37 | const source = of(42); 38 | 39 | const g = source.pipe( 40 | retryWhen(errors => errors) 41 | ); 42 | 43 | const h = source.pipe( 44 | retryWhen( 45 | function (errors) { 46 | return errors; 47 | } 48 | ) 49 | ); 50 | `, 51 | ], 52 | invalid: [ 53 | fromFixture( 54 | stripIndent` 55 | // repeatWhen ignored parameter 56 | import { of, range } from "rxjs"; 57 | import { repeatWhen } from "rxjs/operators"; 58 | 59 | const source = of(42); 60 | 61 | const c = source.pipe( 62 | repeatWhen(notifications => range(0, 3)) 63 | ~~~~~~~~~~ [forbidden] 64 | ); 65 | ` 66 | ), 67 | fromFixture( 68 | stripIndent` 69 | // repeatWhen no parameter 70 | import { of, range } from "rxjs"; 71 | import { repeatWhen } from "rxjs/operators"; 72 | 73 | const source = of(42); 74 | 75 | const c = source.pipe( 76 | repeatWhen(() => range(0, 3)) 77 | ~~~~~~~~~~ [forbidden] 78 | ); 79 | ` 80 | ), 81 | fromFixture( 82 | stripIndent` 83 | // repeatWhen non-arrow ignored parameter 84 | import { of, range } from "rxjs"; 85 | import { repeatWhen } from "rxjs/operators"; 86 | 87 | const source = of(42); 88 | 89 | const c = source.pipe( 90 | repeatWhen( 91 | ~~~~~~~~~~ [forbidden] 92 | function (notifications) { 93 | return range(0, 3); 94 | } 95 | ) 96 | ); 97 | ` 98 | ), 99 | fromFixture( 100 | stripIndent` 101 | // repeatWhen non-arrow no parameter 102 | import { of, range } from "rxjs"; 103 | import { repeatWhen } from "rxjs/operators"; 104 | 105 | const source = of(42); 106 | 107 | const c = source.pipe( 108 | repeatWhen( 109 | ~~~~~~~~~~ [forbidden] 110 | function () { 111 | return range(0, 3); 112 | } 113 | ) 114 | ); 115 | ` 116 | ), 117 | fromFixture( 118 | stripIndent` 119 | // retryWhen ignored parameter 120 | import { of } from "rxjs"; 121 | import { retryWhen } from "rxjs/operators"; 122 | 123 | const source = of(42); 124 | 125 | const h = source.pipe( 126 | retryWhen(errors => range(0, 3)) 127 | ~~~~~~~~~ [forbidden] 128 | ); 129 | ` 130 | ), 131 | fromFixture( 132 | stripIndent` 133 | // retryWhen no parameter 134 | import { of } from "rxjs"; 135 | import { retryWhen } from "rxjs/operators"; 136 | 137 | const source = of(42); 138 | 139 | const h = source.pipe( 140 | retryWhen(() => range(0, 3)) 141 | ~~~~~~~~~ [forbidden] 142 | ); 143 | ` 144 | ), 145 | fromFixture( 146 | stripIndent` 147 | // retryWhen non-arrow ignored parameter 148 | import { of } from "rxjs"; 149 | import { retryWhen } from "rxjs/operators"; 150 | 151 | const source = of(42); 152 | 153 | const h = source.pipe( 154 | retryWhen( 155 | ~~~~~~~~~ [forbidden] 156 | function (errors) { 157 | return range(0, 3); 158 | } 159 | ) 160 | ); 161 | ` 162 | ), 163 | fromFixture( 164 | stripIndent` 165 | // retryWhen non-arrow no parameter 166 | import { of } from "rxjs"; 167 | import { retryWhen } from "rxjs/operators"; 168 | 169 | const source = of(42); 170 | 171 | const h = source.pipe( 172 | retryWhen( 173 | ~~~~~~~~~ [forbidden] 174 | function () { 175 | return range(0, 3); 176 | } 177 | ) 178 | ); 179 | ` 180 | ), 181 | ], 182 | }); 183 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-observable"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-ignored-observable", rule, { 12 | valid: [ 13 | stripIndent` 14 | // not ignored 15 | import { Observable, of } from "rxjs"; 16 | 17 | function functionSource() { 18 | return of(42); 19 | } 20 | 21 | function sink(source: Observable) { 22 | } 23 | 24 | const a = functionSource(); 25 | sink(functionSource()); 26 | `, 27 | stripIndent` 28 | // not ignored arrow 29 | import { Observable, of } from "rxjs"; 30 | 31 | const arrowSource = () => of(42); 32 | 33 | function sink(source: Observable) { 34 | } 35 | 36 | const a = arrowSource(); 37 | sink(arrowSource()); 38 | `, 39 | ], 40 | invalid: [ 41 | fromFixture( 42 | stripIndent` 43 | // ignored 44 | import { Observable, of } from "rxjs"; 45 | 46 | function functionSource() { 47 | return of(42); 48 | } 49 | 50 | functionSource(); 51 | ~~~~~~~~~~~~~~~~ [forbidden] 52 | ` 53 | ), 54 | fromFixture( 55 | stripIndent` 56 | // ignored arrow 57 | import { Observable, of } from "rxjs"; 58 | 59 | const arrowSource = () => of(42); 60 | 61 | arrowSource(); 62 | ~~~~~~~~~~~~~ [forbidden] 63 | ` 64 | ), 65 | ], 66 | }); 67 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-replay-buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-replay-buffer"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-ignored-replay-buffer", rule, { 12 | valid: [ 13 | stripIndent` 14 | // ReplaySubject not ignored 15 | import { ReplaySubject } from "rxjs"; 16 | 17 | const a = new ReplaySubject(1); 18 | const b = new Thing(new ReplaySubject(1)); 19 | `, 20 | stripIndent` 21 | // publishReplay not ignored 22 | import { of } from "rxjs"; 23 | import { publishReplay } from "rxjs/operators"; 24 | 25 | const a = of(42).pipe(publishReplay(1)); 26 | `, 27 | stripIndent` 28 | // shareReplay not ignored 29 | import { of } from "rxjs"; 30 | import { shareReplay } from "rxjs/operators"; 31 | 32 | const a = of(42).pipe(shareReplay(1)); 33 | `, 34 | stripIndent` 35 | // namespace ReplaySubject not ignored 36 | import * as Rx from "rxjs"; 37 | 38 | const a = new Rx.ReplaySubject(1); 39 | const b = new Thing(new Rx.ReplaySubject(1)); 40 | `, 41 | stripIndent` 42 | // namespace publishReplay not ignored 43 | import * as Rx from "rxjs"; 44 | import { publishReplay } from "rxjs/operators"; 45 | 46 | const a = Rx.of(42).pipe(publishReplay(1)); 47 | `, 48 | stripIndent` 49 | // namespace shareReplay not ignored 50 | import * as Rx from "rxjs"; 51 | import { shareReplay } from "rxjs/operators"; 52 | 53 | const a = Rx.of(42).pipe(shareReplay(1)); 54 | `, 55 | stripIndent` 56 | // namespace class not ignored 57 | import * as Rx from "rxjs"; 58 | 59 | class Mock { 60 | private valid: Rx.ReplaySubject; 61 | constructor(){ 62 | this.valid = new Rx.ReplaySubject(1); 63 | } 64 | } 65 | `, 66 | ], 67 | invalid: [ 68 | fromFixture( 69 | stripIndent` 70 | // ReplaySubject ignored 71 | import { ReplaySubject } from "rxjs"; 72 | 73 | const a = new ReplaySubject(); 74 | ~~~~~~~~~~~~~ [forbidden] 75 | const b = new Thing(new ReplaySubject()); 76 | ~~~~~~~~~~~~~ [forbidden] 77 | ` 78 | ), 79 | fromFixture( 80 | stripIndent` 81 | // publishReplay ignored 82 | import { of } from "rxjs"; 83 | import { publishReplay } from "rxjs/operators"; 84 | 85 | const a = of(42).pipe(publishReplay()); 86 | ~~~~~~~~~~~~~ [forbidden] 87 | ` 88 | ), 89 | fromFixture( 90 | stripIndent` 91 | // shareReplay ignored 92 | import { of } from "rxjs"; 93 | import { shareReplay } from "rxjs/operators"; 94 | 95 | const a = of(42).pipe(shareReplay()); 96 | ~~~~~~~~~~~ [forbidden] 97 | ` 98 | ), 99 | fromFixture( 100 | stripIndent` 101 | // namespace ReplaySubject ignored 102 | import * as Rx from "rxjs"; 103 | 104 | const a = new Rx.ReplaySubject(); 105 | ~~~~~~~~~~~~~ [forbidden] 106 | const b = new Thing(new Rx.ReplaySubject()); 107 | ~~~~~~~~~~~~~ [forbidden] 108 | ` 109 | ), 110 | fromFixture( 111 | stripIndent` 112 | // namespace publishReplay ignored 113 | import * as Rx from "rxjs"; 114 | import { publishReplay } from "rxjs/operators"; 115 | 116 | const a = Rx.of(42).pipe(publishReplay()); 117 | ~~~~~~~~~~~~~ [forbidden] 118 | ` 119 | ), 120 | fromFixture( 121 | stripIndent` 122 | // namespace shareReplay ignored 123 | import * as Rx from "rxjs"; 124 | import { shareReplay } from "rxjs/operators"; 125 | 126 | const a = Rx.of(42).pipe(shareReplay()); 127 | ~~~~~~~~~~~ [forbidden] 128 | ` 129 | ), 130 | fromFixture( 131 | stripIndent` 132 | // namespace class ignored 133 | import * as Rx from "rxjs"; 134 | 135 | class Mock { 136 | private invalid: Rx.ReplaySubject; 137 | constructor(){ 138 | this.invalid = new Rx.ReplaySubject(); 139 | ~~~~~~~~~~~~~ [forbidden] 140 | } 141 | } 142 | ` 143 | ), 144 | ], 145 | }); 146 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-subscribe"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-ignored-subscribe", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // not ignored 16 | import { of } from "rxjs"; 17 | 18 | const observable = of([1, 2]); 19 | observable.subscribe(value => console.log(value)); 20 | `, 21 | }, 22 | { 23 | code: stripIndent` 24 | // subject not ignored 25 | import { Subject } from "rxjs"; 26 | 27 | const subject = new Subject(); 28 | subject.subscribe(value => console.log(value)); 29 | `, 30 | }, 31 | { 32 | code: stripIndent` 33 | // not ignored non-arrow 34 | import { of } from "rxjs"; 35 | 36 | function log(value) { 37 | console.log(value) 38 | } 39 | 40 | const observable = of([1, 2]); 41 | observable.subscribe(log); 42 | `, 43 | }, 44 | { 45 | code: stripIndent` 46 | // https://github.com/cartant/eslint-plugin-rxjs/issues/61 47 | const whatever = { 48 | subscribe(callback?: (value: unknown) => void) {} 49 | }; 50 | whatever.subscribe(); 51 | `, 52 | }, 53 | { 54 | code: stripIndent` 55 | // https://github.com/cartant/eslint-plugin-rxjs/issues/69 56 | import { Subscribable } from "rxjs"; 57 | declare const subscribable: Subscribable; 58 | subscribable.subscribe((value) => console.log(value)); 59 | `, 60 | }, 61 | ], 62 | invalid: [ 63 | fromFixture( 64 | stripIndent` 65 | // ignored 66 | import { of } from "rxjs"; 67 | 68 | const observable = of([1, 2]); 69 | observable.subscribe(); 70 | ~~~~~~~~~ [forbidden] 71 | ` 72 | ), 73 | fromFixture( 74 | stripIndent` 75 | // subject ignored 76 | import { Subject } from "rxjs"; 77 | 78 | const subject = new Subject(); 79 | subject.subscribe(); 80 | ~~~~~~~~~ [forbidden] 81 | ` 82 | ), 83 | fromFixture( 84 | stripIndent` 85 | // https://github.com/cartant/eslint-plugin-rxjs/issues/69 86 | import { Subscribable } from "rxjs"; 87 | declare const subscribable: Subscribable; 88 | subscribable.subscribe(); 89 | ~~~~~~~~~ [forbidden] 90 | ` 91 | ), 92 | ], 93 | }); 94 | -------------------------------------------------------------------------------- /tests/rules/no-ignored-subscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-ignored-subscription"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-ignored-subscription", rule, { 12 | valid: [ 13 | stripIndent` 14 | // const and add 15 | import { of } from "rxjs"; 16 | const a = of(42).subscribe(); 17 | a.add(of(42).subscribe()); 18 | `, 19 | stripIndent` 20 | // let 21 | import { Subscription } from "rxjs"; 22 | let b: Subscription; 23 | b = of(42).subscribe(); 24 | `, 25 | stripIndent` 26 | // array element 27 | import { of } from "rxjs"; 28 | const c = [of(42).subscribe()]; 29 | `, 30 | stripIndent` 31 | // object property 32 | import { of } from "rxjs"; 33 | const d = { subscription: of(42).subscribe() }; 34 | `, 35 | stripIndent` 36 | // subscriber 37 | import { of, Subscriber } from "rxjs"; 38 | const subscriber = new Subscriber(); 39 | of(42).subscribe(subscriber); 40 | `, 41 | stripIndent` 42 | // https://github.com/cartant/eslint-plugin-rxjs/issues/61 43 | const whatever = { 44 | subscribe(callback?: (value: unknown) => void) {} 45 | }; 46 | whatever.subscribe(() => {}); 47 | `, 48 | ], 49 | invalid: [ 50 | fromFixture( 51 | stripIndent` 52 | // ignored 53 | import { of } from "rxjs"; 54 | of(42).subscribe(); 55 | ~~~~~~~~~ [forbidden] 56 | ` 57 | ), 58 | fromFixture( 59 | stripIndent` 60 | // ignored subject 61 | import { Subject } from "rxjs"; 62 | const s = new Subject() 63 | s.subscribe(); 64 | ~~~~~~~~~ [forbidden] 65 | ` 66 | ), 67 | ], 68 | }); 69 | -------------------------------------------------------------------------------- /tests/rules/no-index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-index"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-index", rule, { 12 | valid: [ 13 | stripIndent` 14 | // no index double quote 15 | import { Observable } from "rxjs"; 16 | import { map } from "rxjs/operators"; 17 | import { TestScheduler } from "rxjs/testing"; 18 | import { WebSocketSubject } from "rxjs/webSocket"; 19 | `, 20 | stripIndent` 21 | // no index single quote 22 | import { Observable } from 'rxjs'; 23 | import { map } from 'rxjs/operators'; 24 | import { TestScheduler } from 'rxjs/testing'; 25 | import { WebSocketSubject } from 'rxjs/webSocket'; 26 | `, 27 | ], 28 | invalid: [ 29 | fromFixture( 30 | stripIndent` 31 | // index double quote 32 | import { Observable } from "rxjs/index"; 33 | ~~~~~~~~~~~~ [forbidden] 34 | import { map } from "rxjs/operators/index"; 35 | ~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 36 | import { TestScheduler } from "rxjs/testing/index"; 37 | ~~~~~~~~~~~~~~~~~~~~ [forbidden] 38 | import { WebSocketSubject } from "rxjs/webSocket/index"; 39 | ~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 40 | ` 41 | ), 42 | fromFixture( 43 | stripIndent` 44 | // index single quote 45 | import { Observable } from 'rxjs/index'; 46 | ~~~~~~~~~~~~ [forbidden] 47 | import { map } from 'rxjs/operators/index'; 48 | ~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 49 | import { TestScheduler } from 'rxjs/testing/index'; 50 | ~~~~~~~~~~~~~~~~~~~~ [forbidden] 51 | import { WebSocketSubject } from 'rxjs/webSocket/index'; 52 | ~~~~~~~~~~~~~~~~~~~~~~ [forbidden] 53 | ` 54 | ), 55 | ], 56 | }); 57 | -------------------------------------------------------------------------------- /tests/rules/no-nested-subscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-nested-subscribe"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-nested-subscribe", rule, { 12 | valid: [ 13 | stripIndent` 14 | // not nested in next argument 15 | import { Observable } from "rxjs"; 16 | of(47).subscribe(value => { 17 | console.log(value); 18 | }) 19 | `, 20 | stripIndent` 21 | // not nested in observer properties 22 | import { Observable } from "rxjs"; 23 | of(47).subscribe({ 24 | next: value => console.log(value), 25 | error: value => console.log(value), 26 | complete: () => console.log(value) 27 | }) 28 | `, 29 | stripIndent` 30 | // not nested in observer methods 31 | import { Observable } from "rxjs"; 32 | of(47).subscribe({ 33 | next(value) { console.log(value); }, 34 | error(value) { console.log(value); }, 35 | complete() { console.log(value); } 36 | }) 37 | `, 38 | stripIndent` 39 | // prototype property 40 | import { Observable } from "rxjs"; 41 | const observableSubscribe = Observable.prototype.subscribe; 42 | expect(Observable.prototype.subscribe).to.equal(observableSubscribe); 43 | `, 44 | stripIndent` 45 | // https://github.com/cartant/eslint-plugin-rxjs/issues/38 46 | import {of} from "rxjs"; 47 | of(3).subscribe(result => { 48 | const test = result as boolean; 49 | if(test > 1) { 50 | console.log(test); 51 | } 52 | }); 53 | `, 54 | stripIndent` 55 | // https://github.com/cartant/eslint-plugin-rxjs/issues/61 56 | const whatever = { 57 | subscribe(callback?: (value: unknown) => void) {} 58 | }; 59 | whatever.subscribe(() => { 60 | whatever.subscribe(() => {}) 61 | }); 62 | `, 63 | stripIndent` 64 | // https://github.com/cartant/eslint-plugin-rxjs/issues/67 65 | import { Observable, of } from "rxjs"; 66 | new Observable(subscriber => { 67 | of(42).subscribe(subscriber); 68 | }).subscribe(); 69 | `, 70 | ], 71 | invalid: [ 72 | fromFixture( 73 | stripIndent` 74 | // nested in next argument 75 | import { of } from "rxjs"; 76 | of("foo").subscribe( 77 | value => of("bar").subscribe() 78 | ~~~~~~~~~ [forbidden] 79 | ); 80 | ` 81 | ), 82 | fromFixture( 83 | stripIndent` 84 | // nested in next property 85 | import { of } from "rxjs"; 86 | of("foo").subscribe({ 87 | next: value => of("bar").subscribe() 88 | ~~~~~~~~~ [forbidden] 89 | }); 90 | ` 91 | ), 92 | fromFixture( 93 | stripIndent` 94 | // nested in next method 95 | import { of } from "rxjs"; 96 | of("foo").subscribe({ 97 | next(value) { of("bar").subscribe(); } 98 | ~~~~~~~~~ [forbidden] 99 | }); 100 | ` 101 | ), 102 | fromFixture( 103 | stripIndent` 104 | // nested in error argument 105 | import { of } from "rxjs"; 106 | of("foo").subscribe( 107 | undefined, 108 | error => of("bar").subscribe() 109 | ~~~~~~~~~ [forbidden] 110 | ); 111 | ` 112 | ), 113 | fromFixture( 114 | stripIndent` 115 | // nested in error property 116 | import { of } from "rxjs"; 117 | of("foo").subscribe({ 118 | error: error => of("bar").subscribe() 119 | ~~~~~~~~~ [forbidden] 120 | }); 121 | ` 122 | ), 123 | fromFixture( 124 | stripIndent` 125 | // nested in error method 126 | import { of } from "rxjs"; 127 | of("foo").subscribe({ 128 | error(error) { of("bar").subscribe(); } 129 | ~~~~~~~~~ [forbidden] 130 | }); 131 | ` 132 | ), 133 | fromFixture( 134 | stripIndent` 135 | // nested in complete argument 136 | import { of } from "rxjs"; 137 | of("foo").subscribe( 138 | undefined, 139 | undefined, 140 | () => of("bar").subscribe() 141 | ~~~~~~~~~ [forbidden] 142 | ); 143 | ` 144 | ), 145 | fromFixture( 146 | stripIndent` 147 | // nested in complete property 148 | import { of } from "rxjs"; 149 | of("foo").subscribe({ 150 | complete: () => of("bar").subscribe() 151 | ~~~~~~~~~ [forbidden] 152 | }); 153 | ` 154 | ), 155 | fromFixture( 156 | stripIndent` 157 | // nested in complete method 158 | import { of } from "rxjs"; 159 | of("foo").subscribe({ 160 | complete() { of("bar").subscribe(); } 161 | ~~~~~~~~~ [forbidden] 162 | }); 163 | ` 164 | ), 165 | fromFixture( 166 | stripIndent` 167 | // https://github.com/cartant/eslint-plugin-rxjs/issues/69 168 | import { Subscribable } from "rxjs"; 169 | declare const subscribable: Subscribable; 170 | subscribable.subscribe( 171 | () => subscribable.subscribe() 172 | ~~~~~~~~~ [forbidden] 173 | ); 174 | ` 175 | ), 176 | ], 177 | }); 178 | -------------------------------------------------------------------------------- /tests/rules/no-sharereplay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-sharereplay"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-sharereplay", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // config allowed refCount 16 | const shared = of(42).pipe( 17 | shareReplay({ refCount: true }) 18 | );`, 19 | }, 20 | { 21 | code: stripIndent` 22 | // config allowed no refCount 23 | const shared = of(42).pipe( 24 | shareReplay({ refCount: false }) 25 | );`, 26 | }, 27 | ], 28 | invalid: [ 29 | fromFixture( 30 | stripIndent` 31 | // no arguments 32 | const shared = of(42).pipe( 33 | shareReplay() 34 | ~~~~~~~~~~~ [forbidden] 35 | ); 36 | `, 37 | { options: [{ allowConfig: false }] } 38 | ), 39 | fromFixture( 40 | stripIndent` 41 | // config allowed no arguments 42 | const shared = of(42).pipe( 43 | shareReplay() 44 | ~~~~~~~~~~~ [forbiddenWithoutConfig] 45 | ); 46 | `, 47 | { options: [{ allowConfig: true }] } 48 | ), 49 | fromFixture( 50 | stripIndent` 51 | // one argument 52 | const shared = of(42).pipe( 53 | shareReplay(1) 54 | ~~~~~~~~~~~ [forbiddenWithoutConfig] 55 | ); 56 | ` 57 | ), 58 | fromFixture( 59 | stripIndent` 60 | // two arguments 61 | const shared = of(42).pipe( 62 | shareReplay(1, 100) 63 | ~~~~~~~~~~~ [forbiddenWithoutConfig] 64 | ); 65 | ` 66 | ), 67 | fromFixture( 68 | stripIndent` 69 | // three arguments 70 | const shared = of(42).pipe( 71 | shareReplay(1, 100, asapScheduler) 72 | ~~~~~~~~~~~ [forbiddenWithoutConfig] 73 | ); 74 | ` 75 | ), 76 | fromFixture( 77 | stripIndent` 78 | // config argument refCount 79 | const shared = of(42).pipe( 80 | shareReplay({ refCount: true }) 81 | ~~~~~~~~~~~ [forbidden] 82 | ); 83 | `, 84 | { options: [{ allowConfig: false }] } 85 | ), 86 | fromFixture( 87 | stripIndent` 88 | // config argument no refCount 89 | const shared = of(42).pipe( 90 | shareReplay({ refCount: false }) 91 | ~~~~~~~~~~~ [forbidden] 92 | ); 93 | `, 94 | { options: [{ allowConfig: false }] } 95 | ), 96 | ], 97 | }); 98 | -------------------------------------------------------------------------------- /tests/rules/no-subclass.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-subclass"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-subclass", rule, { 12 | valid: [ 13 | stripIndent` 14 | // non-RxJS Observable 15 | class Observable { t: T; } 16 | class StringObservable extends Observable {} 17 | `, 18 | ], 19 | invalid: [ 20 | fromFixture( 21 | stripIndent` 22 | // Observable 23 | import { Observable } from "rxjs"; 24 | class GenericObservable extends Observable {} 25 | ~~~~~~~~~~ [forbidden] 26 | class StringObservable extends Observable {} 27 | ~~~~~~~~~~ [forbidden] 28 | ` 29 | ), 30 | fromFixture( 31 | stripIndent` 32 | // Subject 33 | import { Subject } from "rxjs"; 34 | class GenericSubject extends Subject {} 35 | ~~~~~~~ [forbidden] 36 | class StringSubject extends Subject {} 37 | ~~~~~~~ [forbidden] 38 | ` 39 | ), 40 | fromFixture( 41 | stripIndent` 42 | // Subscriber 43 | import { Subscriber } from "rxjs"; 44 | class GenericSubscriber extends Subscriber {} 45 | ~~~~~~~~~~ [forbidden] 46 | class StringSubscriber extends Subscriber {} 47 | ~~~~~~~~~~ [forbidden] 48 | ` 49 | ), 50 | fromFixture( 51 | stripIndent` 52 | // AsyncSubject 53 | import { AsyncSubject } from "rxjs"; 54 | class GenericAsyncSubject extends AsyncSubject {} 55 | ~~~~~~~~~~~~ [forbidden] 56 | class StringAsyncSubject extends AsyncSubject {} 57 | ~~~~~~~~~~~~ [forbidden] 58 | ` 59 | ), 60 | fromFixture( 61 | stripIndent` 62 | // BehaviorSubject 63 | import { BehaviorSubject } from "rxjs"; 64 | class GenericBehaviorSubject extends BehaviorSubject {} 65 | ~~~~~~~~~~~~~~~ [forbidden] 66 | class StringBehaviorSubject extends BehaviorSubject {} 67 | ~~~~~~~~~~~~~~~ [forbidden] 68 | ` 69 | ), 70 | fromFixture( 71 | stripIndent` 72 | // ReplaySubject 73 | import { ReplaySubject } from "rxjs"; 74 | class GenericReplaySubject extends ReplaySubject {} 75 | ~~~~~~~~~~~~~ [forbidden] 76 | class StringReplaySubject extends ReplaySubject {} 77 | ~~~~~~~~~~~~~ [forbidden] 78 | ` 79 | ), 80 | fromFixture( 81 | stripIndent` 82 | // Scheduler 83 | import { Scheduler } from "rxjs/internal/Scheduler"; 84 | class AnotherScheduler extends Scheduler {} 85 | ~~~~~~~~~ [forbidden] 86 | ` 87 | ), 88 | ], 89 | }); 90 | -------------------------------------------------------------------------------- /tests/rules/no-subject-unsubscribe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-subject-unsubscribe"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-subject-unsubscribe", rule, { 12 | valid: [ 13 | stripIndent` 14 | // unsubscribe Subject subscription 15 | import { Subject } from "rxjs"; 16 | const a = new Subject(); 17 | const asub = a.subscribe(); 18 | asub.unsubscribe(); 19 | `, 20 | stripIndent` 21 | // unsubscribe AsyncSubject subscription 22 | import { AsyncSubject } from "rxjs"; 23 | const a = new AsyncSubject(); 24 | const asub = a.subscribe(); 25 | asub.unsubscribe(); 26 | `, 27 | ], 28 | invalid: [ 29 | fromFixture( 30 | stripIndent` 31 | // unsubscribe Subject 32 | import { Subject } from "rxjs"; 33 | const b = new Subject(); 34 | b.unsubscribe(); 35 | ~~~~~~~~~~~ [forbidden] 36 | ` 37 | ), 38 | fromFixture( 39 | stripIndent` 40 | // unsubscribe AsyncSubject 41 | import { AsyncSubject } from "rxjs"; 42 | const b = new AsyncSubject(); 43 | b.unsubscribe(); 44 | ~~~~~~~~~~~ [forbidden] 45 | ` 46 | ), 47 | fromFixture( 48 | stripIndent` 49 | // compose Subject 50 | import { Subject, Subscription } from "rxjs"; 51 | const csub = new Subscription(); 52 | const c = new Subject(); 53 | csub.add(c); 54 | ~ [forbidden] 55 | ` 56 | ), 57 | fromFixture( 58 | stripIndent` 59 | // compose AsyncSubject 60 | import { AsyncSubject, Subscription } from "rxjs"; 61 | const csub = new Subscription(); 62 | const c = new AsyncSubject(); 63 | csub.add(c); 64 | ~ [forbidden] 65 | ` 66 | ), 67 | ], 68 | }); 69 | -------------------------------------------------------------------------------- /tests/rules/no-subject-value.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-subject-value"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-subject-value", rule, { 12 | valid: [ 13 | stripIndent` 14 | // no value 15 | import { BehaviorSubject } from "rxjs"; 16 | const subject = new BehaviorSubject(1); 17 | `, 18 | ], 19 | invalid: [ 20 | fromFixture( 21 | stripIndent` 22 | // value property 23 | import { BehaviorSubject } from "rxjs"; 24 | const subject = new BehaviorSubject(1); 25 | console.log(subject.value); 26 | ~~~~~ [forbidden] 27 | ` 28 | ), 29 | fromFixture( 30 | stripIndent` 31 | // getValue method 32 | import { BehaviorSubject } from "rxjs"; 33 | const subject = new BehaviorSubject(1); 34 | console.log(subject.getValue()); 35 | ~~~~~~~~ [forbidden] 36 | ` 37 | ), 38 | ], 39 | }); 40 | -------------------------------------------------------------------------------- /tests/rules/no-subscribe-handlers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-subscribe-handlers"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-subscribe-handlers", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // ignored 16 | import { of } from "rxjs"; 17 | 18 | const observable = of([1, 2]); 19 | observable.subscribe(); 20 | `, 21 | }, 22 | { 23 | code: stripIndent` 24 | // subject ignored 25 | import { Subject } from "rxjs"; 26 | 27 | const subject = new Subject(); 28 | subject.subscribe(); 29 | `, 30 | }, 31 | { 32 | code: stripIndent` 33 | import { Subscribable } from "rxjs"; 34 | declare const subscribable: Subscribable; 35 | subscribable.subscribe(); 36 | `, 37 | }, 38 | { 39 | code: stripIndent` 40 | const whatever = { 41 | subscribe(callback?: (value: unknown) => void) {} 42 | }; 43 | whatever.subscribe(); 44 | `, 45 | }, 46 | { 47 | code: stripIndent` 48 | const whatever = { 49 | subscribe(callback?: (value: unknown) => void) {} 50 | }; 51 | whatever.subscribe(console.log); 52 | `, 53 | }, 54 | ], 55 | invalid: [ 56 | fromFixture( 57 | stripIndent` 58 | // not ignored 59 | import { of } from "rxjs"; 60 | 61 | const observable = of([1, 2]); 62 | observable.subscribe(value => console.log(value)); 63 | ~~~~~~~~~ [forbidden] 64 | ` 65 | ), 66 | fromFixture( 67 | stripIndent` 68 | // subject not ignored 69 | import { Subject } from "rxjs"; 70 | 71 | const subject = new Subject(); 72 | subject.subscribe(value => console.log(value)); 73 | ~~~~~~~~~ [forbidden] 74 | ` 75 | ), 76 | fromFixture( 77 | stripIndent` 78 | // not ignored non-arrow 79 | import { of } from "rxjs"; 80 | 81 | function log(value) { 82 | console.log(value) 83 | } 84 | 85 | const observable = of([1, 2]); 86 | observable.subscribe(log); 87 | ~~~~~~~~~ [forbidden] 88 | ` 89 | ), 90 | fromFixture( 91 | stripIndent` 92 | import { Subscribable } from "rxjs"; 93 | declare const subscribable: Subscribable; 94 | subscribable.subscribe((value) => console.log(value)); 95 | ~~~~~~~~~ [forbidden] 96 | ` 97 | ), 98 | fromFixture( 99 | stripIndent` 100 | import { Subscribable } from "rxjs"; 101 | declare const subscribable: Subscribable; 102 | subscribable.subscribe({ 103 | ~~~~~~~~~ [forbidden] 104 | next: (value) => console.log(value) 105 | }); 106 | ` 107 | ), 108 | ], 109 | }); 110 | -------------------------------------------------------------------------------- /tests/rules/no-tap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-tap"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: false }).run("no-tap", rule, { 12 | valid: [ 13 | stripIndent` 14 | // no tap 15 | import { of } from "rxjs"; 16 | import { map } from "rxjs/operators"; 17 | const ob = of(1).pipe( 18 | map(x => x * 2) 19 | ); 20 | `, 21 | stripIndent` 22 | // no tap with shallow import 23 | import { map, of } from "rxjs"; 24 | const ob = of(1).pipe( 25 | map(x => x * 2) 26 | ); 27 | `, 28 | ], 29 | invalid: [ 30 | fromFixture( 31 | stripIndent` 32 | // tap 33 | import { of } from "rxjs"; 34 | import { map, tap } from "rxjs/operators"; 35 | ~~~ [forbidden] 36 | const ob = of(1).pipe( 37 | map(x => x * 2), 38 | tap(value => console.log(value)) 39 | ); 40 | ` 41 | ), 42 | fromFixture( 43 | stripIndent` 44 | // tap with shallow import 45 | import { map, of, tap } from "rxjs"; 46 | ~~~ [forbidden] 47 | const ob = of(1).pipe( 48 | map(x => x * 2), 49 | tap(value => console.log(value)) 50 | ); 51 | ` 52 | ), 53 | fromFixture( 54 | stripIndent` 55 | // tap alias 56 | import { of } from "rxjs"; 57 | import { map, tap as tapAlias } from "rxjs/operators"; 58 | ~~~ [forbidden] 59 | const ob = of(1).pipe( 60 | map(x => x * 2), 61 | tapAlias(value => console.log(value)) 62 | ); 63 | ` 64 | ), 65 | ], 66 | }); 67 | -------------------------------------------------------------------------------- /tests/rules/no-topromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-topromise"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-topromise", rule, { 12 | valid: [ 13 | stripIndent` 14 | // no toPromise 15 | import { of, Subject } from "rxjs"; 16 | const a = of("a"); 17 | a.subscribe(value => console.log(value)); 18 | `, 19 | stripIndent` 20 | // non-observable toPromise 21 | const a = { 22 | toPromise() { 23 | return Promise.resolve("a"); 24 | } 25 | }; 26 | a.toPromise().then(value => console.log(value)); 27 | `, 28 | ], 29 | invalid: [ 30 | fromFixture( 31 | stripIndent` 32 | // observable toPromise 33 | import { of } from "rxjs"; 34 | const a = of("a"); 35 | a.toPromise().then(value => console.log(value)); 36 | ~~~~~~~~~ [forbidden] 37 | ` 38 | ), 39 | fromFixture( 40 | stripIndent` 41 | // subject toPromise 42 | import { Subject } from "rxjs"; 43 | const a = new Subject(); 44 | a.toPromise().then(value => console.log(value)); 45 | ~~~~~~~~~ [forbidden] 46 | ` 47 | ), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /tests/rules/no-unsafe-subject-next.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { stripIndent } from "common-tags"; 7 | import { fromFixture } from "eslint-etc"; 8 | import rule = require("../../source/rules/no-unsafe-subject-next"); 9 | import { ruleTester } from "../utils"; 10 | 11 | ruleTester({ types: true }).run("no-unsafe-subject-next", rule, { 12 | valid: [ 13 | { 14 | code: stripIndent` 15 | // number next 16 | import { Subject } from "rxjs"; 17 | const s = new Subject(); 18 | s.next(42); 19 | `, 20 | }, 21 | { 22 | code: stripIndent` 23 | // replay number next 24 | import { ReplaySubject } from "rxjs"; 25 | const s = new ReplaySubject(); 26 | s.next(42); 27 | `, 28 | }, 29 | { 30 | code: stripIndent` 31 | // any next 32 | import { Subject } from "rxjs"; 33 | const s = new Subject(); 34 | s.next(42); 35 | s.next(); 36 | `, 37 | }, 38 | { 39 | code: stripIndent` 40 | // unknown next 41 | import { Subject } from "rxjs"; 42 | const s = new Subject(); 43 | s.next(42); 44 | s.next(); 45 | `, 46 | }, 47 | { 48 | code: stripIndent` 49 | // void next 50 | import { Subject } from "rxjs"; 51 | const s = new Subject(); 52 | s.next(); 53 | `, 54 | }, 55 | { 56 | code: stripIndent` 57 | // void union next 58 | import { Subject } from "rxjs"; 59 | const s = new Subject(); 60 | s.next(42); 61 | s.next(); 62 | `, 63 | }, 64 | { 65 | code: stripIndent` 66 | // https://github.com/cartant/eslint-plugin-rxjs/issues/76 67 | import { Subject } from "rxjs"; 68 | const s = new Subject(); 69 | s.next(); 70 | `, 71 | }, 72 | ], 73 | invalid: [ 74 | fromFixture( 75 | stripIndent` 76 | // optional number next 77 | import { Subject } from "rxjs"; 78 | const s = new Subject(); 79 | s.next(); 80 | ~~~~ [forbidden] 81 | ` 82 | ), 83 | fromFixture( 84 | stripIndent` 85 | // optional replay number next 86 | import { ReplaySubject } from "rxjs"; 87 | const s = new ReplaySubject(); 88 | s.next(); 89 | ~~~~ [forbidden] 90 | ` 91 | ), 92 | ], 93 | }); 94 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "skipLibCheck": true, 7 | "target": "esnext" 8 | }, 9 | "files": ["file.tsx"] 10 | } 11 | -------------------------------------------------------------------------------- /tests/utils-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { expect } from "chai"; 7 | import decamelize from "decamelize"; 8 | import { createRegExpForWords } from "../source/utils"; 9 | 10 | describe("utils", () => { 11 | describe("createRegExpForWords", () => { 12 | const regExp = createRegExpForWords(["add"]) as RegExp; 13 | 14 | it("should match action literals", () => { 15 | expect(`"ADD"`).to.match(regExp); 16 | expect(`"ADD_SOMETHING"`).to.match(regExp); 17 | expect(`"SOMETHING_ADD"`).to.match(regExp); 18 | 19 | expect(`'ADD'`).to.match(regExp); 20 | expect(`'ADD_SOMETHING'`).to.match(regExp); 21 | expect(`'SOMETHING_ADD'`).to.match(regExp); 22 | 23 | expect("`ADD`").to.match(regExp); 24 | expect("`ADD_SOMETHING`").to.match(regExp); 25 | expect("`SOMETHING_ADD`").to.match(regExp); 26 | }); 27 | 28 | it("should match action symbols", () => { 29 | expect("ADD").to.match(regExp); 30 | expect("ADD_SOMETHING").to.match(regExp); 31 | expect("SOMETHING_ADD").to.match(regExp); 32 | 33 | expect(decamelize("Add")).to.match(regExp); 34 | expect(decamelize("AddSomething")).to.match(regExp); 35 | expect(decamelize("SomethingAdd")).to.match(regExp); 36 | }); 37 | 38 | it("should not match words within larger words", () => { 39 | expect("READD").to.not.match(regExp); 40 | expect("Readd").to.not.match(regExp); 41 | 42 | expect("ADDER").to.not.match(regExp); 43 | expect("Adder").to.not.match(regExp); 44 | 45 | expect("LADDER").to.not.match(regExp); 46 | expect("Ladder").to.not.match(regExp); 47 | }); 48 | 49 | it("should create a RegExp from a string", () => { 50 | expect((createRegExpForWords(".") as RegExp).toString()).to.equal("/./i"); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Use of this source code is governed by an MIT-style license that 3 | * can be found in the LICENSE file at https://github.com/cartant/eslint-plugin-rxjs 4 | */ 5 | 6 | import { createRuleTester } from "eslint-etc"; 7 | import { resolve } from "path"; 8 | 9 | export const ruleTester = createRuleTester({ 10 | filename: resolve("./tests/file.tsx"), 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist" 4 | }, 5 | "extends": "./tsconfig.json", 6 | "include": [ 7 | "source/**/*.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": false, 5 | "esModuleInterop": true, 6 | "importHelpers": true, 7 | "lib": ["esnext"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitHelpers": true, 11 | "outDir": "build", 12 | "paths": {}, 13 | "removeComments": true, 14 | "skipLibCheck": true, 15 | "sourceMap": false, 16 | "strict": true, 17 | "target": "es2018" 18 | }, 19 | "exclude": [], 20 | "include": [ 21 | "source/**/*.ts", 22 | "tests/**/*.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------