1
- import type { CharSet } from "refa"
2
- import { Chars , toCharSet } from "regexp-ast-analysis"
1
+ import { CharSet , JS } from "refa"
2
+ import { Chars , toUnicodeSet } from "regexp-ast-analysis"
3
3
import type {
4
4
CharacterClass ,
5
5
CharacterClassElement ,
6
+ Node ,
7
+ StringAlternative ,
6
8
} from "@eslint-community/regexpp/ast"
7
9
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
8
10
import type { RegExpContext } from "../utils"
@@ -18,34 +20,29 @@ import type {
18
20
} from "../utils/ast-utils/pattern-source"
19
21
import type { Rule } from "eslint"
20
22
import { UsageOfPattern } from "../utils/get-usage-of-pattern"
23
+ import { cachedFn } from "../utils/util"
21
24
22
- // FIXME: TS Error
23
- // @ts -expect-error -- FIXME
24
- const ELEMENT_ORDER : Record < CharacterClassElement [ "type" ] , number > = {
25
+ type FlatClassElement = CharacterClassElement | StringAlternative
26
+
27
+ const ELEMENT_ORDER : Record < FlatClassElement [ "type" ] , number > = {
25
28
Character : 1 ,
26
29
CharacterClassRange : 2 ,
27
30
CharacterSet : 3 ,
31
+ CharacterClass : 4 ,
32
+ ExpressionCharacterClass : 5 ,
33
+ ClassStringDisjunction : 6 ,
34
+ StringAlternative : 7 ,
28
35
}
29
36
30
37
/**
31
38
* Finds all character class elements that do not contribute to the whole.
32
39
*/
33
40
function findUseless (
34
- elements : readonly CharacterClassElement [ ] ,
35
- getCharSet : ( e : CharacterClassElement ) => CharSet ,
36
- other : CharSet ,
37
- ) : Set < CharacterClassElement > {
38
- const cache = new Map < CharacterClassElement , CharSet > ( )
39
-
40
- /** A cached version of `getCharSet` */
41
- function get ( e : CharacterClassElement ) : CharSet {
42
- let cached = cache . get ( e )
43
- if ( cached === undefined ) {
44
- cached = getCharSet ( e )
45
- cache . set ( e , cached )
46
- }
47
- return cached
48
- }
41
+ elements : readonly FlatClassElement [ ] ,
42
+ getChars : ( e : FlatClassElement ) => JS . UnicodeSet ,
43
+ other : JS . UnicodeSet ,
44
+ ) : Set < FlatClassElement > {
45
+ const get = cachedFn ( getChars )
49
46
50
47
// When searching for useless elements, we want to first
51
48
// search for useless characters, then useless ranges, and
@@ -55,7 +52,7 @@ function findUseless(
55
52
. reverse ( )
56
53
. sort ( ( a , b ) => ELEMENT_ORDER [ a . type ] - ELEMENT_ORDER [ b . type ] )
57
54
58
- const useless = new Set < CharacterClassElement > ( )
55
+ const useless = new Set < FlatClassElement > ( )
59
56
60
57
for ( const e of sortedElements ) {
61
58
const cs = get ( e )
@@ -88,20 +85,51 @@ function without<T>(iter: Iterable<T>, set: ReadonlySet<T>): T[] {
88
85
}
89
86
90
87
/**
91
- * Removes all the given ranges from the given pattern.
92
- *
93
- * This assumes that all ranges are disjoint
88
+ * Removes all the given nodes from the given pattern.
94
89
*/
95
90
function removeAll (
96
91
fixer : Rule . RuleFixer ,
97
92
patternSource : PatternSource ,
98
- ranges : readonly PatternRange [ ] ,
93
+ nodes : readonly Node [ ] ,
99
94
) {
100
- const sorted = [ ...ranges ] . sort ( ( a , b ) => b . start - a . start )
101
- let pattern = patternSource . value
95
+ // we abuse CharSet to merge adjacent and overlapping ranges
96
+ const charSet = CharSet . empty ( Number . MAX_SAFE_INTEGER ) . union (
97
+ nodes . map ( ( n ) => {
98
+ let min = n . start
99
+ let max = n . end - 1
100
+
101
+ if ( n . type === "StringAlternative" ) {
102
+ const parent = n . parent
103
+ if (
104
+ parent . alternatives . length === 1 ||
105
+ parent . alternatives . every ( ( a ) => nodes . includes ( a ) )
106
+ ) {
107
+ // we have to remove the whole disjunction
108
+ min = parent . start
109
+ max = parent . end - 1
110
+ } else {
111
+ const isFirst = parent . alternatives . at ( 0 ) === n
112
+ if ( isFirst ) {
113
+ max ++
114
+ } else {
115
+ min --
116
+ }
117
+ }
118
+ }
119
+
120
+ return { min, max }
121
+ } ) ,
122
+ )
123
+ const sorted = charSet . ranges . map (
124
+ ( { min, max } ) : PatternRange => ( { start : min , end : max + 1 } ) ,
125
+ )
102
126
127
+ let pattern = patternSource . value
128
+ let removed = 0
103
129
for ( const { start, end } of sorted ) {
104
- pattern = pattern . slice ( 0 , start ) + pattern . slice ( end )
130
+ pattern =
131
+ pattern . slice ( 0 , start - removed ) + pattern . slice ( end - removed )
132
+ removed += end - start
105
133
}
106
134
107
135
const range = patternSource . getReplaceRange ( {
@@ -114,6 +142,23 @@ function removeAll(
114
142
return null
115
143
}
116
144
145
+ /**
146
+ * Adds the `i` flag to the given flags string.
147
+ */
148
+ function getIgnoreCaseFlagsString ( flags : string ) : string {
149
+ if ( flags . includes ( "i" ) ) {
150
+ return flags
151
+ }
152
+
153
+ // keep flags sorted
154
+ for ( let i = 0 ; i < flags . length ; i ++ ) {
155
+ if ( flags [ i ] > "i" ) {
156
+ return `${ flags . slice ( 0 , i ) } i${ flags . slice ( i ) } `
157
+ }
158
+ }
159
+ return `${ flags } i`
160
+ }
161
+
117
162
export default createRule ( "use-ignore-case" , {
118
163
meta : {
119
164
docs : {
@@ -162,37 +207,42 @@ export default createRule("use-ignore-case", {
162
207
return { }
163
208
}
164
209
165
- const uselessElements : CharacterClassElement [ ] = [ ]
210
+ const uselessElements : FlatClassElement [ ] = [ ]
166
211
const ccs : CharacterClass [ ] = [ ]
167
212
168
213
return {
169
214
onCharacterClassEnter ( ccNode ) {
170
- const invariantElement = ccNode . elements . filter (
215
+ const elements = ccNode . elements . flatMap (
216
+ ( e : CharacterClassElement ) : FlatClassElement [ ] => {
217
+ if ( e . type === "ClassStringDisjunction" ) {
218
+ return e . alternatives
219
+ }
220
+ return [ e ]
221
+ } ,
222
+ )
223
+ const invariantElement = elements . filter (
171
224
( e ) => ! isCaseVariant ( e , flags ) ,
172
225
)
173
- if ( invariantElement . length === ccNode . elements . length ) {
226
+ if ( invariantElement . length === elements . length ) {
174
227
// all elements are case invariant
175
228
return
176
229
}
177
230
178
- const invariant = Chars . empty ( flags ) . union (
179
- // FIXME: TS Error
180
- // @ts -expect-error -- FIXME
181
- ...invariantElement . map ( ( e ) => toCharSet ( e , flags ) ) ,
231
+ const empty = JS . UnicodeSet . empty ( Chars . maxChar ( flags ) )
232
+ const invariant = empty . union (
233
+ ...invariantElement . map ( ( e ) => toUnicodeSet ( e , flags ) ) ,
182
234
)
183
235
184
236
let variantElements = without (
185
- ccNode . elements ,
237
+ elements ,
186
238
new Set ( invariantElement ) ,
187
239
)
188
240
189
241
// find all elements that are useless even without
190
242
// the i flag
191
243
const alwaysUseless = findUseless (
192
244
variantElements ,
193
- // FIXME: TS Error
194
- // @ts -expect-error -- FIXME
195
- ( e ) => toCharSet ( e , flags ) ,
245
+ ( e ) => toUnicodeSet ( e , flags ) ,
196
246
invariant ,
197
247
)
198
248
@@ -203,9 +253,7 @@ export default createRule("use-ignore-case", {
203
253
const iFlags = getIgnoreCaseFlags ( flags )
204
254
const useless = findUseless (
205
255
variantElements ,
206
- // FIXME: TS Error
207
- // @ts -expect-error -- FIXME
208
- ( e ) => toCharSet ( e , iFlags ) ,
256
+ ( e ) => toUnicodeSet ( e , iFlags ) ,
209
257
invariant ,
210
258
)
211
259
@@ -236,7 +284,7 @@ export default createRule("use-ignore-case", {
236
284
}
237
285
238
286
const flagsFix = fixReplaceFlags (
239
- ` ${ flagsString } i` ,
287
+ getIgnoreCaseFlagsString ( flagsString ) ,
240
288
false ,
241
289
) ( fixer )
242
290
if ( ! flagsFix ) {
0 commit comments