@@ -2,8 +2,15 @@ import type { TSESTree } from '@typescript-eslint/utils';
2
2
import { AST_NODE_TYPES } from '@typescript-eslint/utils' ;
3
3
import * as tsutils from 'ts-api-utils' ;
4
4
import type { Type } from 'typescript' ;
5
+ import * as ts from 'typescript' ;
5
6
6
- import { createRule , getParserServices } from '../util' ;
7
+ import {
8
+ createRule ,
9
+ getParserServices ,
10
+ isFunctionOrFunctionType ,
11
+ nullThrows ,
12
+ NullThrowsReasons ,
13
+ } from '../util' ;
7
14
8
15
export type Options = [
9
16
{
@@ -12,7 +19,7 @@ export type Options = [
12
19
} ,
13
20
] ;
14
21
15
- export type MessageIds = 'duplicate' ;
22
+ export type MessageIds = 'duplicate' | 'unnecessary' ;
16
23
17
24
const astIgnoreKeys = new Set ( [ 'range' , 'loc' , 'parent' ] ) ;
18
25
@@ -79,6 +86,8 @@ export default createRule<Options, MessageIds>({
79
86
fixable : 'code' ,
80
87
messages : {
81
88
duplicate : '{{type}} type constituent is duplicated with {{previous}}.' ,
89
+ unnecessary :
90
+ 'Explicit undefined is unnecessary on an optional parameter.' ,
82
91
} ,
83
92
schema : [
84
93
{
@@ -105,9 +114,14 @@ export default createRule<Options, MessageIds>({
105
114
] ,
106
115
create ( context , [ { ignoreIntersections, ignoreUnions } ] ) {
107
116
const parserServices = getParserServices ( context ) ;
117
+ const { sourceCode } = context ;
108
118
109
119
function checkDuplicate (
110
120
node : TSESTree . TSIntersectionType | TSESTree . TSUnionType ,
121
+ forEachNodeType ?: (
122
+ constituentNodeType : Type ,
123
+ report : ( messageId : MessageIds ) => void ,
124
+ ) => void ,
111
125
) : void {
112
126
const cachedTypeMap = new Map < Type , TSESTree . TypeNode > ( ) ;
113
127
node . types . reduce < TSESTree . TypeNode [ ] > (
@@ -118,94 +132,120 @@ export default createRule<Options, MessageIds>({
118
132
return uniqueConstituents ;
119
133
}
120
134
121
- const duplicatedPreviousConstituentInAst = uniqueConstituents . find (
122
- ele => isSameAstNode ( ele , constituentNode ) ,
123
- ) ;
124
- if ( duplicatedPreviousConstituentInAst ) {
125
- reportDuplicate (
126
- {
127
- duplicated : constituentNode ,
128
- duplicatePrevious : duplicatedPreviousConstituentInAst ,
129
- } ,
130
- node ,
135
+ const report = (
136
+ messageId : MessageIds ,
137
+ data ?: Record < string , unknown > ,
138
+ ) : void => {
139
+ const getUnionOrIntersectionToken = (
140
+ where : 'Before' | 'After' ,
141
+ at : number ,
142
+ ) : TSESTree . Token | undefined =>
143
+ sourceCode [ `getTokens${ where } ` ] ( constituentNode , {
144
+ filter : token => [ '|' , '&' ] . includes ( token . value ) ,
145
+ } ) . at ( at ) ;
146
+
147
+ const beforeUnionOrIntersectionToken = getUnionOrIntersectionToken (
148
+ 'Before' ,
149
+ - 1 ,
131
150
) ;
132
- return uniqueConstituents ;
133
- }
134
- const duplicatedPreviousConstituentInType =
135
- cachedTypeMap . get ( constituentNodeType ) ;
136
- if ( duplicatedPreviousConstituentInType ) {
137
- reportDuplicate (
138
- {
139
- duplicated : constituentNode ,
140
- duplicatePrevious : duplicatedPreviousConstituentInType ,
151
+ let afterUnionOrIntersectionToken : TSESTree . Token | undefined ;
152
+ let bracketBeforeTokens ;
153
+ let bracketAfterTokens ;
154
+ if ( beforeUnionOrIntersectionToken ) {
155
+ bracketBeforeTokens = sourceCode . getTokensBetween (
156
+ beforeUnionOrIntersectionToken ,
157
+ constituentNode ,
158
+ ) ;
159
+ bracketAfterTokens = sourceCode . getTokensAfter ( constituentNode , {
160
+ count : bracketBeforeTokens . length ,
161
+ } ) ;
162
+ } else {
163
+ afterUnionOrIntersectionToken = nullThrows (
164
+ getUnionOrIntersectionToken ( 'After' , 0 ) ,
165
+ NullThrowsReasons . MissingToken (
166
+ 'union or intersection token' ,
167
+ 'duplicate type constituent' ,
168
+ ) ,
169
+ ) ;
170
+ bracketAfterTokens = sourceCode . getTokensBetween (
171
+ constituentNode ,
172
+ afterUnionOrIntersectionToken ,
173
+ ) ;
174
+ bracketBeforeTokens = sourceCode . getTokensBefore (
175
+ constituentNode ,
176
+ {
177
+ count : bracketAfterTokens . length ,
178
+ } ,
179
+ ) ;
180
+ }
181
+ context . report ( {
182
+ data,
183
+ messageId,
184
+ node : constituentNode ,
185
+ loc : {
186
+ start : constituentNode . loc . start ,
187
+ end : ( bracketAfterTokens . at ( - 1 ) ?? constituentNode ) . loc . end ,
141
188
} ,
142
- node ,
143
- ) ;
189
+ fix : fixer =>
190
+ [
191
+ beforeUnionOrIntersectionToken ,
192
+ ...bracketBeforeTokens ,
193
+ constituentNode ,
194
+ ...bracketAfterTokens ,
195
+ afterUnionOrIntersectionToken ,
196
+ ] . flatMap ( token => ( token ? fixer . remove ( token ) : [ ] ) ) ,
197
+ } ) ;
198
+ } ;
199
+ const duplicatePrevious =
200
+ uniqueConstituents . find ( ele =>
201
+ isSameAstNode ( ele , constituentNode ) ,
202
+ ) ?? cachedTypeMap . get ( constituentNodeType ) ;
203
+ if ( duplicatePrevious ) {
204
+ report ( 'duplicate' , {
205
+ type :
206
+ node . type === AST_NODE_TYPES . TSIntersectionType
207
+ ? 'Intersection'
208
+ : 'Union' ,
209
+ previous : sourceCode . getText ( duplicatePrevious ) ,
210
+ } ) ;
144
211
return uniqueConstituents ;
145
212
}
213
+ forEachNodeType ?.( constituentNodeType , report ) ;
146
214
cachedTypeMap . set ( constituentNodeType , constituentNode ) ;
147
215
return [ ...uniqueConstituents , constituentNode ] ;
148
216
} ,
149
217
[ ] ,
150
218
) ;
151
219
}
152
- function reportDuplicate (
153
- duplicateConstituent : {
154
- duplicated : TSESTree . TypeNode ;
155
- duplicatePrevious : TSESTree . TypeNode ;
156
- } ,
157
- parentNode : TSESTree . TSIntersectionType | TSESTree . TSUnionType ,
158
- ) : void {
159
- const beforeTokens = context . sourceCode . getTokensBefore (
160
- duplicateConstituent . duplicated ,
161
- { filter : token => token . value === '|' || token . value === '&' } ,
162
- ) ;
163
- const beforeUnionOrIntersectionToken =
164
- beforeTokens [ beforeTokens . length - 1 ] ;
165
- const bracketBeforeTokens = context . sourceCode . getTokensBetween (
166
- beforeUnionOrIntersectionToken ,
167
- duplicateConstituent . duplicated ,
168
- ) ;
169
- const bracketAfterTokens = context . sourceCode . getTokensAfter (
170
- duplicateConstituent . duplicated ,
171
- { count : bracketBeforeTokens . length } ,
172
- ) ;
173
- const reportLocation : TSESTree . SourceLocation = {
174
- start : duplicateConstituent . duplicated . loc . start ,
175
- end :
176
- bracketAfterTokens . length > 0
177
- ? bracketAfterTokens [ bracketAfterTokens . length - 1 ] . loc . end
178
- : duplicateConstituent . duplicated . loc . end ,
179
- } ;
180
- context . report ( {
181
- data : {
182
- type :
183
- parentNode . type === AST_NODE_TYPES . TSIntersectionType
184
- ? 'Intersection'
185
- : 'Union' ,
186
- previous : context . sourceCode . getText (
187
- duplicateConstituent . duplicatePrevious ,
188
- ) ,
189
- } ,
190
- messageId : 'duplicate' ,
191
- node : duplicateConstituent . duplicated ,
192
- loc : reportLocation ,
193
- fix : fixer => {
194
- return [
195
- beforeUnionOrIntersectionToken ,
196
- ...bracketBeforeTokens ,
197
- duplicateConstituent . duplicated ,
198
- ...bracketAfterTokens ,
199
- ] . map ( token => fixer . remove ( token ) ) ;
200
- } ,
201
- } ) ;
202
- }
220
+
203
221
return {
204
222
...( ! ignoreIntersections && {
205
223
TSIntersectionType : checkDuplicate ,
206
224
} ) ,
207
225
...( ! ignoreUnions && {
208
- TSUnionType : checkDuplicate ,
226
+ TSUnionType : ( node ) : void =>
227
+ checkDuplicate ( node , ( constituentNodeType , report ) => {
228
+ const maybeTypeAnnotation = node . parent ;
229
+ if ( maybeTypeAnnotation . type === AST_NODE_TYPES . TSTypeAnnotation ) {
230
+ const maybeIdentifier = maybeTypeAnnotation . parent ;
231
+ if (
232
+ maybeIdentifier . type === AST_NODE_TYPES . Identifier &&
233
+ maybeIdentifier . optional
234
+ ) {
235
+ const maybeFunction = maybeIdentifier . parent ;
236
+ if (
237
+ isFunctionOrFunctionType ( maybeFunction ) &&
238
+ maybeFunction . params . includes ( maybeIdentifier ) &&
239
+ tsutils . isTypeFlagSet (
240
+ constituentNodeType ,
241
+ ts . TypeFlags . Undefined ,
242
+ )
243
+ ) {
244
+ report ( 'unnecessary' ) ;
245
+ }
246
+ }
247
+ }
248
+ } ) ,
209
249
} ) ,
210
250
} ;
211
251
} ,
0 commit comments