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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,6 @@ The package includes the following rules.
| [no-unsafe-takeuntil](docs/rules/no-unsafe-takeuntil.md) | Disallow applying operators after `takeUntil`. | ✅ | | | 💭 | |
| [prefer-observer](docs/rules/prefer-observer.md) | Disallow passing separate handlers to `subscribe` and `tap`. | | 🔧 | 💡 | 💭 | |
| [suffix-subjects](docs/rules/suffix-subjects.md) | Enforce the use of a suffix in subject identifiers. | | | | 💭 | |
| [throw-error](docs/rules/throw-error.md) | Enforce passing only `Error` values to error notifications. | | | | 💭 | |
| [throw-error](docs/rules/throw-error.md) | Enforce passing only `Error` values to `throwError`. | | | | 💭 | |

<!-- end auto-generated rules list -->
39 changes: 14 additions & 25 deletions docs/rules/throw-error.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,38 @@
# Enforce passing only `Error` values to error notifications (`rxjs-x/throw-error`)
# Enforce passing only `Error` values to `throwError` (`rxjs-x/throw-error`)

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

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

This rule forbids throwing values that are neither `Error` nor `DOMException` instances.
This rule forbids passing values that are not `Error` objects to `throwError`.
It's similar to the typescript-eslint [`only-throw-error`](https://typescript-eslint.io/rules/only-throw-error/) rule,
but is for the `throwError` Observable creation function - not `throw` statements.

## Rule details

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

```ts
throw "Kaboom!";
```

```ts
import { throwError } from "rxjs";
throwError("Kaboom!");
```

```ts
import { throwError } from "rxjs";
throwError(() => "Kaboom!");
```

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

```ts
throw new Error("Kaboom!");
```
import { throwError } from "rxjs";

```ts
throw new RangeError("Kaboom!");
throwError(() => new Error("Kaboom!"));
```

```ts
throw new DOMException("Kaboom!");
```
## Options

```ts
import { throwError } from "rxjs";
throwError(new Error("Kaboom!"));
```
<!-- begin auto-generated rule options list -->

```ts
import { throwError } from "rxjs";
throwError(() => new Error("Kaboom!"));
```
| Name | Description | Type | Default |
| :--------------------- | :---------------------------------------------------------- | :------ | :------ |
| `allowThrowingAny` | Whether to always allow throwing values typed as `any`. | Boolean | `true` |
| `allowThrowingUnknown` | Whether to always allow throwing values typed as `unknown`. | Boolean | `true` |

<!-- end auto-generated rule options list -->
74 changes: 53 additions & 21 deletions src/rules/throw-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,86 @@ import ts from 'typescript';
import { couldBeFunction, couldBeType, getTypeServices } from '../etc';
import { ruleCreator } from '../utils';

const defaultOptions: readonly {
allowThrowingAny?: boolean;
allowThrowingUnknown?: boolean;
}[] = [];

export const throwErrorRule = ruleCreator({
defaultOptions: [],
defaultOptions,
meta: {
docs: {
description:
'Enforce passing only `Error` values to error notifications.',
'Enforce passing only `Error` values to `throwError`.',
requiresTypeChecking: true,
},
messages: {
forbidden: 'Passing non-Error values are forbidden.',
forbidden: 'Passing non-Error values is forbidden.',
},
schema: [],
schema: [
{
properties: {
allowThrowingAny: { type: 'boolean', default: true, description: 'Whether to always allow throwing values typed as `any`.' },
allowThrowingUnknown: { type: 'boolean', default: true, description: 'Whether to always allow throwing values typed as `unknown`.' },
},
type: 'object',
},
],
type: 'problem',
},
name: 'throw-error',
create: (context) => {
const { esTreeNodeToTSNodeMap, program, getTypeAtLocation } = ESLintUtils.getParserServices(context);
const { couldBeObservable } = getTypeServices(context);
const [config = {}] = context.options;
const { allowThrowingAny = true, allowThrowingUnknown = true } = config;

function checkNode(node: es.Node) {
function checkThrowArgument(node: es.Node) {
let type = getTypeAtLocation(node);
let reportNode = node;

if (couldBeFunction(type)) {
reportNode = (node as es.ArrowFunctionExpression).body ?? node;

const tsNode = esTreeNodeToTSNodeMap.get(node);
const annotation = (tsNode as ts.ArrowFunction).type;
const body = (tsNode as ts.ArrowFunction).body;
type = program.getTypeChecker().getTypeAtLocation(annotation ?? body);
}
if (
!tsutils.isIntrinsicAnyType(type)
&& !tsutils.isIntrinsicUnknownType(type)
&& !couldBeType(type, /^(Error|DOMException)$/)
) {
context.report({
messageId: 'forbidden',
node,
});

if (allowThrowingAny && tsutils.isIntrinsicAnyType(type)) {
return;
}

if (allowThrowingUnknown && tsutils.isIntrinsicUnknownType(type)) {
return;
}

if (couldBeType(type, /^Error$/)) {
return;
}

context.report({
messageId: 'forbidden',
node: reportNode,
});
}

function checkNode(node: es.CallExpression) {
if (couldBeObservable(node)) {
const [arg] = node.arguments;
if (arg) {
checkThrowArgument(arg);
}
}
}

return {
'ThrowStatement > *': checkNode,
'CallExpression[callee.name=\'throwError\']': (node: es.CallExpression) => {
if (couldBeObservable(node)) {
const [arg] = node.arguments;
if (arg) {
checkNode(arg);
}
}
checkNode(node);
},
'CallExpression[callee.property.name=\'throwError\']': (node: es.CallExpression) => {
checkNode(node);
},
};
},
Expand Down
Loading