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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ The package includes the following rules.
| [no-unsafe-switchmap](docs/rules/no-unsafe-switchmap.md) | Disallow unsafe `switchMap` usage in effects and epics. | | | | 💭 | |
| [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`. | | 🔧 | 💡 | 💭 | |
| [prefer-root-operators](docs/rules/prefer-root-operators.md) | Disallow importing operators from `rxjs/operators`. | | 🔧 | 💡 | | |
| [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 `throwError`. | | | | 💭 | |

Expand Down
32 changes: 32 additions & 0 deletions docs/rules/prefer-root-operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Disallow importing operators from `rxjs/operators` (`rxjs-x/prefer-root-operators`)

🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

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

This rule prevents importing from the `rxjs/operators` export site.
Most operators were moved to the `rxjs` export site in RxJS v7.2.0
(excepting a few old and deprecated operators).
The `rxjs/operators` export site has since been deprecated and will be removed in a future major version.

Note that because a few operators were renamed or not migrated to the `rxjs` export site,
this rule may not provide an automatic fixer if renaming the import path is not guaranteed to be safe.
See the documentation linked below.

## Rule details

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

```ts
import { map } from 'rxjs/operators';
```

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

```ts
import { map } from 'rxjs';
```

## Further reading

- [Importing instructions](https://rxjs.dev/guide/importing)
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { noUnsafeSubjectNext } from './rules/no-unsafe-subject-next';
import { noUnsafeSwitchmapRule } from './rules/no-unsafe-switchmap';
import { noUnsafeTakeuntilRule } from './rules/no-unsafe-takeuntil';
import { preferObserverRule } from './rules/prefer-observer';
import { preferRootOperatorsRule } from './rules/prefer-root-operators';
import { suffixSubjectsRule } from './rules/suffix-subjects';
import { throwErrorRule } from './rules/throw-error';

Expand Down Expand Up @@ -88,6 +89,7 @@ const plugin = {
'no-unsafe-switchmap': noUnsafeSwitchmapRule,
'no-unsafe-takeuntil': noUnsafeTakeuntilRule,
'prefer-observer': preferObserverRule,
'prefer-root-operators': preferRootOperatorsRule,
'suffix-subjects': suffixSubjectsRule,
'throw-error': throwErrorRule,
},
Expand Down
167 changes: 167 additions & 0 deletions src/rules/prefer-root-operators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { TSESTree as es, TSESLint } from '@typescript-eslint/utils';
import { isIdentifier, isImportSpecifier, isLiteral } from '../etc';
import { ruleCreator } from '../utils';

// See https://rxjs.dev/guide/importing#how-to-migrate

const RENAMED_OPERATORS: Record<string, string> = {
combineLatest: 'combineLatestWith',
concat: 'concatWith',
merge: 'mergeWith',
onErrorResumeNext: 'onErrorResumeNextWith',
race: 'raceWith',
zip: 'zipWith',
};

const DEPRECATED_OPERATORS = [
'partition',
];

export const preferRootOperatorsRule = ruleCreator({
defaultOptions: [],
meta: {
docs: {
description: 'Disallow importing operators from `rxjs/operators`.',
},
fixable: 'code',
hasSuggestions: true,
messages: {
forbidden: 'RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead.',
forbiddenWithoutFix: 'RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead. Note some operators may have been renamed or deprecated.',
suggest: 'Replace with import from `rxjs`.',
},
schema: [],
type: 'suggestion',
},
name: 'prefer-root-operators',
create: (context) => {
function getQuote(raw: string): string | undefined {
const match = /^\s*('|")/.exec(raw);
if (!match) {
return undefined;
}
const [, quote] = match;
return quote;
}

function getSourceReplacement(rawLocation: string): string | undefined {
const quote = getQuote(rawLocation);
if (!quote) {
return undefined;
}
if (/^['"]rxjs\/operators/.test(rawLocation)) {
return `${quote}rxjs${quote}`;
}
return undefined;
}

function hasDeprecatedOperators(specifiers?: es.ImportSpecifier[] | es.ExportSpecifier[]): boolean {
return !!specifiers?.some(s => DEPRECATED_OPERATORS.includes(getName(getOperatorNode(s))));
}

function getName(node: es.Identifier | es.StringLiteral): string {
return isIdentifier(node) ? node.name : node.value;
}

function getOperatorNode(node: es.ImportSpecifier | es.ExportSpecifier): es.Identifier | es.StringLiteral {
return isImportSpecifier(node) ? node.imported : node.local;
}

function getAliasNode(node: es.ImportSpecifier | es.ExportSpecifier): es.Identifier | es.StringLiteral {
return isImportSpecifier(node) ? node.local : node.exported;
}

function getOperatorReplacement(name: string): string | undefined {
return RENAMED_OPERATORS[name];
}

function isNodesEqual(a: es.Node, b: es.Node): boolean {
return a.range[0] === b.range[0] && a.range[1] === b.range[1];
}

function createFix(source: es.Node, replacement: string, specifiers: es.ImportSpecifier[] | es.ExportSpecifier[]) {
return function* fix(fixer: TSESLint.RuleFixer) {
// Rename the module name.
yield fixer.replaceText(source, replacement);

// Rename the imported operators if necessary.
for (const specifier of specifiers) {
const operatorNode = getOperatorNode(specifier);
const operatorName = getName(operatorNode);

const operatorReplacement = getOperatorReplacement(operatorName);
if (!operatorReplacement) {
// The operator has the same name.
continue;
}

const aliasNode = getAliasNode(specifier);
if (isNodesEqual(aliasNode, operatorNode)) {
// concat -> concatWith as concat
yield fixer.insertTextBefore(operatorNode, operatorReplacement + ' as ');
} else if (isIdentifier(operatorNode)) {
// concat as c -> concatWith as c
yield fixer.replaceText(operatorNode, operatorReplacement);
} else {
// 'concat' as c -> 'concatWith' as c
const quote = getQuote(operatorNode.raw);
if (!quote) {
continue;
}
yield fixer.replaceText(operatorNode, quote + operatorReplacement + quote);
}
}
};
}

function reportNode(source: es.Literal, specifiers?: es.ImportSpecifier[] | es.ExportSpecifier[]): void {
const replacement = getSourceReplacement(source.raw);
if (!replacement || hasDeprecatedOperators(specifiers)) {
context.report({
messageId: 'forbiddenWithoutFix',
node: source,
});
return;
}

if (!specifiers) {
context.report({
messageId: 'forbiddenWithoutFix',
node: source,
suggest: [{ messageId: 'suggest', fix: (fixer) => fixer.replaceText(source, replacement) }],
});
return;
}

const fix = createFix(source, replacement, specifiers);
context.report({
fix,
messageId: 'forbidden',
node: source,
suggest: [{ messageId: 'suggest', fix }],
});
}

return {
'ImportDeclaration[source.value="rxjs/operators"]': (node: es.ImportDeclaration) => {
// Exclude side effect imports, default imports, and namespace imports.
const specifiers = node.specifiers.length && node.specifiers.every(importClause => isImportSpecifier(importClause))
? node.specifiers
: undefined;

reportNode(node.source, specifiers);
},
'ImportExpression[source.value="rxjs/operators"]': (node: es.ImportExpression) => {
if (isLiteral(node.source)) {
reportNode(node.source);
}
},
'ExportNamedDeclaration[source.value="rxjs/operators"]': (node: es.ExportNamedDeclarationWithSource) => {
reportNode(node.source, node.specifiers);
},
'ExportAllDeclaration[source.value="rxjs/operators"]': (node: es.ExportAllDeclaration) => {
reportNode(node.source);
},
};
},
});
Loading