Skip to content

Commit e973e28

Browse files
Add support for v flag to regexp/use-ignore-case (#617)
* Add support for `v` flag to `regexp/use-ignore-case` * Create cyan-rats-attend.md
1 parent 47dc791 commit e973e28

File tree

5 files changed

+227
-91
lines changed

5 files changed

+227
-91
lines changed

.changeset/cyan-rats-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
Add support for `v` flag to `regexp/use-ignore-case`

lib/rules/use-ignore-case.ts

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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"
33
import type {
44
CharacterClass,
55
CharacterClassElement,
6+
Node,
7+
StringAlternative,
68
} from "@eslint-community/regexpp/ast"
79
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
810
import type { RegExpContext } from "../utils"
@@ -18,34 +20,29 @@ import type {
1820
} from "../utils/ast-utils/pattern-source"
1921
import type { Rule } from "eslint"
2022
import { UsageOfPattern } from "../utils/get-usage-of-pattern"
23+
import { cachedFn } from "../utils/util"
2124

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> = {
2528
Character: 1,
2629
CharacterClassRange: 2,
2730
CharacterSet: 3,
31+
CharacterClass: 4,
32+
ExpressionCharacterClass: 5,
33+
ClassStringDisjunction: 6,
34+
StringAlternative: 7,
2835
}
2936

3037
/**
3138
* Finds all character class elements that do not contribute to the whole.
3239
*/
3340
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)
4946

5047
// When searching for useless elements, we want to first
5148
// search for useless characters, then useless ranges, and
@@ -55,7 +52,7 @@ function findUseless(
5552
.reverse()
5653
.sort((a, b) => ELEMENT_ORDER[a.type] - ELEMENT_ORDER[b.type])
5754

58-
const useless = new Set<CharacterClassElement>()
55+
const useless = new Set<FlatClassElement>()
5956

6057
for (const e of sortedElements) {
6158
const cs = get(e)
@@ -88,20 +85,51 @@ function without<T>(iter: Iterable<T>, set: ReadonlySet<T>): T[] {
8885
}
8986

9087
/**
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.
9489
*/
9590
function removeAll(
9691
fixer: Rule.RuleFixer,
9792
patternSource: PatternSource,
98-
ranges: readonly PatternRange[],
93+
nodes: readonly Node[],
9994
) {
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+
)
102126

127+
let pattern = patternSource.value
128+
let removed = 0
103129
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
105133
}
106134

107135
const range = patternSource.getReplaceRange({
@@ -114,6 +142,23 @@ function removeAll(
114142
return null
115143
}
116144

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+
117162
export default createRule("use-ignore-case", {
118163
meta: {
119164
docs: {
@@ -162,37 +207,42 @@ export default createRule("use-ignore-case", {
162207
return {}
163208
}
164209

165-
const uselessElements: CharacterClassElement[] = []
210+
const uselessElements: FlatClassElement[] = []
166211
const ccs: CharacterClass[] = []
167212

168213
return {
169214
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(
171224
(e) => !isCaseVariant(e, flags),
172225
)
173-
if (invariantElement.length === ccNode.elements.length) {
226+
if (invariantElement.length === elements.length) {
174227
// all elements are case invariant
175228
return
176229
}
177230

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)),
182234
)
183235

184236
let variantElements = without(
185-
ccNode.elements,
237+
elements,
186238
new Set(invariantElement),
187239
)
188240

189241
// find all elements that are useless even without
190242
// the i flag
191243
const alwaysUseless = findUseless(
192244
variantElements,
193-
// FIXME: TS Error
194-
// @ts-expect-error -- FIXME
195-
(e) => toCharSet(e, flags),
245+
(e) => toUnicodeSet(e, flags),
196246
invariant,
197247
)
198248

@@ -203,9 +253,7 @@ export default createRule("use-ignore-case", {
203253
const iFlags = getIgnoreCaseFlags(flags)
204254
const useless = findUseless(
205255
variantElements,
206-
// FIXME: TS Error
207-
// @ts-expect-error -- FIXME
208-
(e) => toCharSet(e, iFlags),
256+
(e) => toUnicodeSet(e, iFlags),
209257
invariant,
210258
)
211259

@@ -236,7 +284,7 @@ export default createRule("use-ignore-case", {
236284
}
237285

238286
const flagsFix = fixReplaceFlags(
239-
`${flagsString}i`,
287+
getIgnoreCaseFlagsString(flagsString),
240288
false,
241289
)(fixer)
242290
if (!flagsFix) {

0 commit comments

Comments
 (0)