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' ;
2
3
import { ruleCreator } from '../utils' ;
3
4
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
+
4
20
export const noImportOperatorsRule = ruleCreator ( {
5
21
defaultOptions : [ ] ,
6
22
meta : {
7
23
docs : {
8
24
description : 'Disallow importing operators from `rxjs/operators`.' ,
9
25
} ,
26
+ fixable : 'code' ,
10
27
hasSuggestions : true ,
11
28
messages : {
12
29
forbidden : 'RxJS imports from `rxjs/operators` are forbidden.' ,
@@ -17,46 +34,143 @@ export const noImportOperatorsRule = ruleCreator({
17
34
} ,
18
35
name : 'no-import-operators' ,
19
36
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 ) ;
22
39
if ( ! match ) {
23
40
return undefined ;
24
41
}
25
42
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
+ }
26
51
if ( / ^ [ ' " ] r x j s \/ o p e r a t o r s / . test ( rawLocation ) ) {
27
52
return `${ quote } rxjs${ quote } ` ;
28
53
}
29
54
return undefined ;
30
55
}
31
56
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
+ }
40
148
} else {
41
149
context . report ( {
42
150
messageId : 'forbidden' ,
43
- node,
151
+ node : source ,
44
152
} ) ;
45
153
}
46
154
}
47
155
48
156
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 ) ;
51
163
} ,
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
+ }
54
168
} ,
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 ) ;
57
171
} ,
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 ) ;
60
174
} ,
61
175
} ;
62
176
} ,
0 commit comments