Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e2c3915
feat(no-misused-observables): new rule like `no-misused-promises`
JasonWeinzierl Nov 26, 2024
96641d1
chore(no-misused-observables): boilerplate for void return
JasonWeinzierl Nov 26, 2024
2f08572
feat(no-misused-observables): check void func args
JasonWeinzierl Nov 27, 2024
07b195d
feat(no-misused-observables): check ctors too
JasonWeinzierl Nov 27, 2024
1df7571
test(no-async-subscribe): only enable jsx when needed
JasonWeinzierl Nov 27, 2024
a57f488
feat(no-misused-observables): check jsx attributes
JasonWeinzierl Nov 27, 2024
ba42cfe
test(no-misused-observables): regions for scenarios
JasonWeinzierl Nov 27, 2024
1f23005
feat(no-misused-observables): check class declarations
JasonWeinzierl Nov 27, 2024
9882bd9
feat(no-misused-observables): class expressions
JasonWeinzierl Nov 28, 2024
02b8925
feat(no-misused-observables): interface declarations
JasonWeinzierl Dec 2, 2024
0a8af2c
feat(no-misused-observables): properties
JasonWeinzierl Dec 2, 2024
ad628d2
feat(no-misused-observables): void return return values
JasonWeinzierl Dec 2, 2024
f442dbd
feat(no-misused-observables): assignments
JasonWeinzierl Dec 2, 2024
8ffb56a
feat(no-misused-observables): variable declarations
JasonWeinzierl Dec 2, 2024
fa5e846
feat(no-misused-observables): sub-options for void return
JasonWeinzierl Dec 2, 2024
bb8fcbd
feat(strict)!: add no-misused-observables to strict
JasonWeinzierl Dec 3, 2024
01a327a
docs(no-floating-observables): link to misused rule
JasonWeinzierl Dec 3, 2024
329d45b
docs(no-misused-observables): examples & description
JasonWeinzierl Dec 3, 2024
65ce7ab
docs(no-misused-observables): add spread example
JasonWeinzierl Dec 3, 2024
a8063fc
test(no-misused-observables): test edge cases
JasonWeinzierl Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ The package includes the following rules.
| [no-implicit-any-catch](docs/rules/no-implicit-any-catch.md) | Disallow implicit `any` error parameters in `catchError` operators. | ✅ 🔒 | 🔧 | 💡 | 💭 | |
| [no-index](docs/rules/no-index.md) | Disallow importing index modules. | ✅ 🔒 | | | | |
| [no-internal](docs/rules/no-internal.md) | Disallow importing internal modules. | ✅ 🔒 | 🔧 | 💡 | | |
| [no-misused-observables](docs/rules/no-misused-observables.md) | Disallow Observables in places not designed to handle them. | 🔒 | | | 💭 | |
| [no-nested-subscribe](docs/rules/no-nested-subscribe.md) | Disallow calling `subscribe` within a `subscribe` callback. | ✅ 🔒 | | | 💭 | |
| [no-redundant-notify](docs/rules/no-redundant-notify.md) | Disallow sending redundant notifications from completed or errored observables. | ✅ 🔒 | | | 💭 | |
| [no-sharereplay](docs/rules/no-sharereplay.md) | Disallow unsafe `shareReplay` usage. | ✅ 🔒 | | | | |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/no-floating-observables.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This rule will report observable-valued statements that are not treated in one o

> [!TIP]
> `no-floating-observables` only detects apparently unhandled observable _statements_.
> See [`no-misused-observables`](./no-misused-observables.md) for detecting code that provides observables to _logical_ locations

## Rule details

Expand Down
85 changes: 85 additions & 0 deletions docs/rules/no-misused-observables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Disallow Observables in places not designed to handle them (`rxjs-x/no-misused-observables`)

💼 This rule is enabled in the 🔒 `strict` config.

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

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

This rule forbids providing observables to logical locations where the TypeScript compiler allows them but they are not handled properly.
These situations can often arise due to a misunderstanding of the way observables are handled.

> [!TIP]
> `no-misused-observables` only detects code that provides observables to incorrect _logical_ locations.
> See [`no-floating-observables`](./no-floating-observables.md) for detecting unhandled observable _statements_.

This rule is like [no-misused-promises](https://typescript-eslint.io/rules/no-misused-promises) but for Observables.

> [!NOTE]
> Unlike `@typescript-eslint/no-misused-promises`, this rule does not check conditionals like `if` statements.
> Use `@typescript-eslint/no-unnecessary-condition` for linting those situations.

## Rule details

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

```ts
import { of } from "rxjs";

[1, 2, 3].forEach(i => of(i));

interface MySyncInterface {
foo(): void;
}
class MyRxClass implements MySyncInterface {
foo(): Observable<number> {
return of(42);
}
}

const a = of(42);
const b = { ...b };
```

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

```ts
import { of } from "rxjs";

[1, 2, 3].map(i => of(i));

interface MyRxInterface {
foo(): Observable<number>;
}
class MyRxClass implements MyRxInterface {
foo(): Observable<number> {
return of(42);
}
}
```

## Options

<!-- WARNING: not auto-generated! -->

| Name | Description | Type | Default |
| :----------------- | :-------------------------------------------------------------------------- | :------ | :------ |
| `checksSpreads` | Disallow `...` spreading an Observable. | Boolean | `true` |
| `checksVoidReturn` | Disallow returning an Observable from a function typed as returning `void`. | Object | `true` |

### `checksVoidReturn`

You can disable selective parts of the `checksVoidReturn` option. The following sub-options are supported:

| Name | Description | Type | Default |
| :----------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | :------ | :------ |
| `arguments` | Disallow passing an Observable-returning function as an argument where the parameter type expects a function that returns `void`. | Boolean | `true` |
| `attributes` | Disallow passing an Observable-returning function as a JSX attribute expected to be a function that returns `void`. | Boolean | `true` |
| `inheritedMethods` | Disallow providing an Observable-returning function where a function that returns `void` is expected by an extended or implemented type. | Boolean | `true` |
| `properties` | Disallow providing an Observable-returning function where a function that returns `void` is expected by a property. | Boolean | `true` |
| `returns` | Disallow returning an Observable-returning function where a function that returns `void` is expected. | Boolean | `true` |
| `variables` | Disallow assigning or declaring an Observable-returning function where a function that returns `void` is expected. | Boolean | `true` |

## Further reading

- [TypeScript void function assignability](https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void)
1 change: 1 addition & 0 deletions src/configs/strict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const createStrictConfig = (
}],
'rxjs-x/no-index': 'error',
'rxjs-x/no-internal': 'error',
'rxjs-x/no-misused-observables': 'error',
'rxjs-x/no-nested-subscribe': 'error',
'rxjs-x/no-redundant-notify': 'error',
'rxjs-x/no-sharereplay': 'error',
Expand Down
6 changes: 5 additions & 1 deletion src/etc/get-type-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ export function getTypeServices<
|| ts.isMethodDeclaration(tsNode)
|| ts.isFunctionExpression(tsNode)
) {
tsTypeNode = tsNode.type ?? tsNode.body;
tsTypeNode = tsNode.type ?? tsNode.body; // TODO(#57): this doesn't work for Block bodies.
} else if (
ts.isCallSignatureDeclaration(tsNode)
|| ts.isMethodSignature(tsNode)
) {
tsTypeNode = tsNode.type;
} else if (
ts.isPropertySignature(tsNode)
) {
// TODO(#66): this doesn't work for functions assigned to class properties, variables, params.
}
return Boolean(
tsTypeNode
Expand Down
12 changes: 12 additions & 0 deletions src/etc/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export function isImportSpecifier(node: TSESTree.Node): node is TSESTree.ImportS
return node.type === AST_NODE_TYPES.ImportSpecifier;
}

export function isJSXExpressionContainer(node: TSESTree.Node): node is TSESTree.JSXExpressionContainer {
return node.type === AST_NODE_TYPES.JSXExpressionContainer;
}

export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal {
return node.type === AST_NODE_TYPES.Literal;
}
Expand All @@ -86,6 +90,10 @@ export function isMemberExpression(node: TSESTree.Node): node is TSESTree.Member
return node.type === AST_NODE_TYPES.MemberExpression;
}

export function isMethodDefinition(node: TSESTree.Node): node is TSESTree.MethodDefinition {
return node.type === AST_NODE_TYPES.MethodDefinition;
}

export function isNewExpression(node: TSESTree.Node): node is TSESTree.NewExpression {
return node.type === AST_NODE_TYPES.NewExpression;
}
Expand All @@ -106,6 +114,10 @@ export function isProperty(node: TSESTree.Node): node is TSESTree.Property {
return node.type === AST_NODE_TYPES.Property;
}

export function isPropertyDefinition(node: TSESTree.Node): node is TSESTree.PropertyDefinition {
return node.type === AST_NODE_TYPES.PropertyDefinition;
}

export function isPrivateIdentifier(
node: TSESTree.Node,
): node is TSESTree.PrivateIdentifier {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { noIgnoredTakewhileValueRule } from './rules/no-ignored-takewhile-value'
import { noImplicitAnyCatchRule } from './rules/no-implicit-any-catch';
import { noIndexRule } from './rules/no-index';
import { noInternalRule } from './rules/no-internal';
import { noMisusedObservablesRule } from './rules/no-misused-observables';
import { noNestedSubscribeRule } from './rules/no-nested-subscribe';
import { noRedundantNotifyRule } from './rules/no-redundant-notify';
import { noSharereplayRule } from './rules/no-sharereplay';
Expand Down Expand Up @@ -74,6 +75,7 @@ const plugin = {
'no-implicit-any-catch': noImplicitAnyCatchRule,
'no-index': noIndexRule,
'no-internal': noInternalRule,
'no-misused-observables': noMisusedObservablesRule,
'no-nested-subscribe': noNestedSubscribeRule,
'no-redundant-notify': noRedundantNotifyRule,
'no-sharereplay': noSharereplayRule,
Expand Down
Loading
Loading