Skip to content

Commit f3602fd

Browse files
feat(no-import-operators): add fixer and suggestions
It's ugly and needs refactoring, but the tests pass.
1 parent 13ad7d4 commit f3602fd

File tree

5 files changed

+245
-44
lines changed

5 files changed

+245
-44
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ The package includes the following rules.
8888
| [no-ignored-subscription](docs/rules/no-ignored-subscription.md) | Disallow ignoring the subscription returned by `subscribe`. | | | | 💭 | |
8989
| [no-ignored-takewhile-value](docs/rules/no-ignored-takewhile-value.md) | Disallow ignoring the value within `takeWhile`. || | | | |
9090
| [no-implicit-any-catch](docs/rules/no-implicit-any-catch.md) | Disallow implicit `any` error parameters in `catchError` operators. || 🔧 | 💡 | 💭 | |
91-
| [no-import-operators](docs/rules/no-import-operators.md) | Disallow importing operators from `rxjs/operators`. | | | 💡 | | |
91+
| [no-import-operators](docs/rules/no-import-operators.md) | Disallow importing operators from `rxjs/operators`. | | 🔧 | 💡 | | |
9292
| [no-index](docs/rules/no-index.md) | Disallow importing index modules. || | | | |
9393
| [no-internal](docs/rules/no-internal.md) | Disallow importing internal modules. || 🔧 | 💡 | | |
9494
| [no-nested-subscribe](docs/rules/no-nested-subscribe.md) | Disallow calling `subscribe` within a `subscribe` callback. || | | 💭 | |

docs/rules/no-import-operators.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
# Disallow importing operators from `rxjs/operators` (`rxjs-x/no-import-operators`)
22

3-
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
3+
🔧💡 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).
44

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

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

12+
Note that because a few operators were renamed or not migrated to the `rxjs` export site,
13+
this rule may not provide an automatic fixer if renaming the import path is not guaranteed to be safe.
14+
See the documentation linked below.
15+
1216
## Rule details
1317

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

src/etc/is.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier {
6262
return node.type === AST_NODE_TYPES.Identifier;
6363
}
6464

65+
export function isImportSpecifier(node: TSESTree.Node): node is TSESTree.ImportSpecifier {
66+
return node.type === AST_NODE_TYPES.ImportSpecifier;
67+
}
68+
6569
export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal {
6670
return node.type === AST_NODE_TYPES.Literal;
6771
}

src/rules/no-import-operators.ts

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1-
import { TSESTree as es } from '@typescript-eslint/utils';
1+
import { TSESTree as es, TSESLint } from '@typescript-eslint/utils';
2+
import { isIdentifier, isImportSpecifier, isLiteral } from '../etc';
23
import { ruleCreator } from '../utils';
34

5+
// See https://rxjs.dev/guide/importing#how-to-migrate
6+
7+
const RENAMED_OPERATORS: Record<string, string> = {
8+
combineLatest: 'combineLatestWith',
9+
concat: 'concatWith',
10+
merge: 'mergeWith',
11+
onErrorResumeNext: 'onErrorResumeNextWith',
12+
race: 'raceWith',
13+
zip: 'zipWith',
14+
};
15+
16+
const DEPRECATED_OPERATORS = [
17+
'partition',
18+
];
19+
420
export const noImportOperatorsRule = ruleCreator({
521
defaultOptions: [],
622
meta: {
723
docs: {
824
description: 'Disallow importing operators from `rxjs/operators`.',
925
},
26+
fixable: 'code',
1027
hasSuggestions: true,
1128
messages: {
1229
forbidden: 'RxJS imports from `rxjs/operators` are forbidden.',
@@ -17,46 +34,143 @@ export const noImportOperatorsRule = ruleCreator({
1734
},
1835
name: 'no-import-operators',
1936
create: (context) => {
20-
function getReplacement(rawLocation: string) {
21-
const match = /^\s*('|")/.exec(rawLocation);
37+
function getQuote(raw: string): string | undefined {
38+
const match = /^\s*('|")/.exec(raw);
2239
if (!match) {
2340
return undefined;
2441
}
2542
const [, quote] = match;
43+
return quote;
44+
}
45+
46+
function getSourceReplacement(rawLocation: string): string | undefined {
47+
const quote = getQuote(rawLocation);
48+
if (!quote) {
49+
return undefined;
50+
}
2651
if (/^['"]rxjs\/operators/.test(rawLocation)) {
2752
return `${quote}rxjs${quote}`;
2853
}
2954
return undefined;
3055
}
3156

32-
function reportNode(node: es.Literal) {
33-
const replacement = getReplacement(node.raw);
34-
if (replacement) {
35-
context.report({
36-
messageId: 'forbidden',
37-
node,
38-
suggest: [{ messageId: 'suggest', fix: (fixer) => fixer.replaceText(node, replacement) }],
39-
});
57+
function getName(node: es.Identifier | es.StringLiteral): string {
58+
return isIdentifier(node) ? node.name : node.value;
59+
}
60+
61+
function getSpecifierReplacement(name: string): string | undefined {
62+
return RENAMED_OPERATORS[name];
63+
}
64+
65+
function reportNode(source: es.Literal, importSpecifiers?: es.ImportSpecifier[], exportSpecifiers?: es.ExportSpecifier[]): void {
66+
const replacement = getSourceReplacement(source.raw);
67+
if (
68+
replacement
69+
&& !importSpecifiers?.some(s => DEPRECATED_OPERATORS.includes(getName(s.imported)))
70+
&& !exportSpecifiers?.some(s => DEPRECATED_OPERATORS.includes(getName(s.exported)))
71+
) {
72+
if (importSpecifiers) {
73+
function* fix(fixer: TSESLint.RuleFixer) {
74+
// Rename the module name.
75+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
76+
yield fixer.replaceText(source, replacement!);
77+
78+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79+
for (const specifier of importSpecifiers!) {
80+
const operatorName = getName(specifier.imported);
81+
const specifierReplacement = getSpecifierReplacement(operatorName);
82+
if (specifierReplacement) {
83+
if (specifier.local.name === operatorName) {
84+
// concat -> concatWith as concat
85+
yield fixer.insertTextBefore(specifier.imported, specifierReplacement + ' as ');
86+
} else if (isIdentifier(specifier.imported)) {
87+
// concat as c -> concatWith as c
88+
yield fixer.replaceText(specifier.imported, specifierReplacement);
89+
} else {
90+
// 'concat' as c -> 'concatWith' as c
91+
const quote = getQuote(specifier.imported.raw);
92+
if (!quote) {
93+
continue;
94+
}
95+
yield fixer.replaceText(specifier.imported, quote + specifierReplacement + quote);
96+
}
97+
}
98+
}
99+
}
100+
context.report({
101+
fix,
102+
messageId: 'forbidden',
103+
node: source,
104+
suggest: [{ messageId: 'suggest', fix }],
105+
});
106+
} else if (exportSpecifiers) {
107+
function* fix(fixer: TSESLint.RuleFixer) {
108+
// Rename the module name.
109+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
110+
yield fixer.replaceText(source, replacement!);
111+
112+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113+
for (const specifier of exportSpecifiers!) {
114+
const operatorName = getName(specifier.local);
115+
const specifierReplacement = getSpecifierReplacement(operatorName);
116+
if (specifierReplacement) {
117+
const exportedName = getName(specifier.exported);
118+
if (exportedName === operatorName) {
119+
// concat -> concatWith as concat
120+
yield fixer.insertTextBefore(specifier.exported, specifierReplacement + ' as ');
121+
} else if (isIdentifier(specifier.local)) {
122+
// concat as c -> concatWith as c
123+
yield fixer.replaceText(specifier.local, specifierReplacement);
124+
} else {
125+
// 'concat' as c -> 'concatWith' as c
126+
const quote = getQuote(specifier.local.raw);
127+
if (!quote) {
128+
continue;
129+
}
130+
yield fixer.replaceText(specifier.local, quote + specifierReplacement + quote);
131+
}
132+
}
133+
}
134+
}
135+
context.report({
136+
fix,
137+
messageId: 'forbidden',
138+
node: source,
139+
suggest: [{ messageId: 'suggest', fix }],
140+
});
141+
} else {
142+
context.report({
143+
messageId: 'forbidden',
144+
node: source,
145+
suggest: [{ messageId: 'suggest', fix: (fixer) => fixer.replaceText(source, replacement) }],
146+
});
147+
}
40148
} else {
41149
context.report({
42150
messageId: 'forbidden',
43-
node,
151+
node: source,
44152
});
45153
}
46154
}
47155

48156
return {
49-
'ImportDeclaration Literal[value="rxjs/operators"]': (node: es.Literal) => {
50-
reportNode(node);
157+
'ImportDeclaration[source.value="rxjs/operators"]': (node: es.ImportDeclaration) => {
158+
// Exclude side effect imports, default imports, and namespace imports.
159+
const specifiers = node.specifiers.length && node.specifiers.every(s => isImportSpecifier(s))
160+
? node.specifiers
161+
: undefined;
162+
reportNode(node.source, specifiers);
51163
},
52-
'ImportExpression Literal[value="rxjs/operators"]': (node: es.Literal) => {
53-
reportNode(node);
164+
'ImportExpression[source.value="rxjs/operators"]': (node: es.ImportExpression) => {
165+
if (isLiteral(node.source)) {
166+
reportNode(node.source);
167+
}
54168
},
55-
'ExportNamedDeclaration Literal[value="rxjs/operators"]': (node: es.Literal) => {
56-
reportNode(node);
169+
'ExportNamedDeclaration[source.value="rxjs/operators"]': (node: es.ExportNamedDeclarationWithSource) => {
170+
reportNode(node.source, undefined, node.specifiers);
57171
},
58-
'ExportAllDeclaration Literal[value="rxjs/operators"]': (node: es.Literal) => {
59-
reportNode(node);
172+
'ExportAllDeclaration[source.value="rxjs/operators"]': (node: es.ExportAllDeclaration) => {
173+
reportNode(node.source);
60174
},
61175
};
62176
},

0 commit comments

Comments
 (0)