Skip to content

Commit 0033939

Browse files
authored
Improve reporting of new RegExp(). (#57)
1 parent 49b024f commit 0033939

24 files changed

+686
-38
lines changed

lib/rules/letter-case.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Character, CharacterClassRange } from "regexpp/ast"
44
import {
55
createRule,
66
defineRegexpVisitor,
7+
fixerApplyEscape,
78
getRegexpLocation,
89
getRegexpRange,
910
isLetter,
@@ -112,7 +113,10 @@ export default createRule("letter-case", {
112113
return null
113114
}
114115
const newText = convertText(CONVERTER[letterCase])
115-
return fixer.replaceTextRange(range, newText)
116+
return fixer.replaceTextRange(
117+
range,
118+
fixerApplyEscape(newText, node),
119+
)
116120
},
117121
})
118122
}

lib/rules/no-useless-character-class.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,13 @@ export default createRule("no-useless-character-class", {
134134
/^[$(-+./?[{|]$/u.test(text) ||
135135
(flags.includes("u") && text === "}")
136136
) {
137-
text = fixerApplyEscape("\\", node) + text
137+
text = `\\${text}`
138138
}
139139
}
140-
return fixer.replaceTextRange(range, text)
140+
return fixer.replaceTextRange(
141+
range,
142+
fixerApplyEscape(text, node),
143+
)
141144
},
142145
})
143146
},

lib/rules/prefer-character-class.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default createRule("prefer-character-class", {
103103
if (text.startsWith("-")) {
104104
newText += fixerApplyEscape("\\", node)
105105
}
106-
newText += text
106+
newText += fixerApplyEscape(text, node)
107107
}
108108
return fixer.replaceTextRange(
109109
replaceRange!,

lib/rules/prefer-quantifier.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Character, CharacterSet, Quantifier } from "regexpp/ast"
44
import {
55
createRule,
66
defineRegexpVisitor,
7+
fixerApplyEscape,
78
getRegexpRange,
89
isDigit,
910
isLetter,
@@ -249,7 +250,8 @@ export default createRule("prefer-quantifier", {
249250
}
250251
return fixer.replaceTextRange(
251252
range,
252-
buffer.target.raw + buffer.getQuantifier(),
253+
fixerApplyEscape(buffer.target.raw, node) +
254+
buffer.getQuantifier(),
253255
)
254256
},
255257
})

lib/rules/prefer-t.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getRegexpLocation,
77
getRegexpRange,
88
CP_TAB,
9+
fixerApplyEscape,
910
} from "../utils"
1011

1112
export default createRule("prefer-t", {
@@ -55,7 +56,10 @@ export default createRule("prefer-t", {
5556
if (range == null) {
5657
return null
5758
}
58-
return fixer.replaceTextRange(range, "\\t")
59+
return fixer.replaceTextRange(
60+
range,
61+
fixerApplyEscape("\\t", node),
62+
)
5963
},
6064
})
6165
}

lib/rules/prefer-unicode-codepoint-escapes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { RegExpVisitor } from "regexpp/visitor"
33
import {
44
createRule,
55
defineRegexpVisitor,
6+
fixerApplyEscape,
67
getRegexpLocation,
78
getRegexpRange,
89
} from "../utils"
@@ -62,7 +63,7 @@ export default createRule("prefer-unicode-codepoint-escapes", {
6263
}
6364
return fixer.replaceTextRange(
6465
range,
65-
`\\u{${text}}`,
66+
fixerApplyEscape(`\\u{${text}}`, node),
6667
)
6768
},
6869
})

lib/utils/index.ts

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
findVariable,
1717
} from "eslint-utils"
1818
import type { Rule, AST, SourceCode } from "eslint"
19+
import { parseStringTokens } from "./string-literal-parser"
1920
export * from "./unicode"
2021

2122
type RegexpRule = {
@@ -184,13 +185,21 @@ function buildRegexpVisitor(
184185
}
185186
})
186187
},
188+
// eslint-disable-next-line complexity -- X(
187189
Program() {
188190
const scope = context.getScope()
189191
const tracker = new ReferenceTracker(scope)
190192

191193
// Iterate calls of RegExp.
192194
// E.g., `new RegExp()`, `RegExp()`, `new window.RegExp()`,
193195
// `const {RegExp: a} = window; new a()`, etc...
196+
const regexpDataList: {
197+
newOrCall: ESTree.NewExpression | ESTree.CallExpression
198+
patternNode: ESTree.Expression
199+
pattern: string | null
200+
flagsNode: ESTree.Expression | ESTree.SpreadElement | undefined
201+
flags: string | null
202+
}[] = []
194203
for (const { node } of tracker.iterateGlobalReferences({
195204
RegExp: { [CALL]: true, [CONSTRUCT]: true },
196205
})) {
@@ -202,8 +211,19 @@ function buildRegexpVisitor(
202211
continue
203212
}
204213
const pattern = getStringIfConstant(patternNode, scope)
205-
const flags = getStringIfConstant(flagsNode, scope)
214+
const flags = flagsNode
215+
? getStringIfConstant(flagsNode, scope)
216+
: null
206217

218+
regexpDataList.push({
219+
newOrCall,
220+
patternNode,
221+
pattern,
222+
flagsNode,
223+
flags,
224+
})
225+
}
226+
for (const { patternNode, pattern, flags } of regexpDataList) {
207227
if (typeof pattern === "string") {
208228
let verifyPatternNode = patternNode
209229
if (patternNode.type === "Identifier") {
@@ -219,7 +239,30 @@ function buildRegexpVisitor(
219239
def.node.init &&
220240
def.node.init.type === "Literal"
221241
) {
222-
verifyPatternNode = def.node.init
242+
let useInit = false
243+
if (variable.references.length > 2) {
244+
if (
245+
variable.references.every((ref) => {
246+
if (ref.isWriteOnly()) {
247+
return true
248+
}
249+
return regexpDataList.some(
250+
(r) =>
251+
r.patternNode ===
252+
ref.identifier &&
253+
r.flags === flags,
254+
)
255+
})
256+
) {
257+
useInit = true
258+
}
259+
} else {
260+
useInit = true
261+
}
262+
263+
if (useInit) {
264+
verifyPatternNode = def.node.init
265+
}
223266
}
224267
}
225268
}
@@ -261,12 +304,53 @@ export function getRegexpRange(
261304
sourceCode: SourceCode,
262305
node: ESTree.Expression,
263306
regexpNode: RegExpNode,
307+
offsets?: [number, number],
264308
): AST.Range | null {
265-
if (!availableRegexpLocation(sourceCode, node)) {
266-
return null
309+
const startOffset = regexpNode.start + (offsets?.[0] ?? 0)
310+
const endOffset = regexpNode.end + (offsets?.[1] ?? 0)
311+
if (isRegexpLiteral(node)) {
312+
const nodeStart = node.range![0] + 1
313+
return [nodeStart + startOffset, nodeStart + endOffset]
267314
}
268-
const nodeStart = node.range![0] + 1
269-
return [nodeStart + regexpNode.start, nodeStart + regexpNode.end]
315+
if (isStringLiteral(node)) {
316+
let start: number | null = null
317+
let end: number | null = null
318+
try {
319+
const sourceText = sourceCode.text.slice(
320+
node.range![0] + 1,
321+
node.range![1] - 1,
322+
)
323+
let startIndex = 0
324+
for (const t of parseStringTokens(sourceText)) {
325+
const endIndex = startIndex + t.value.length
326+
327+
if (
328+
start == null &&
329+
startIndex <= startOffset &&
330+
startOffset < endIndex
331+
) {
332+
start = t.range[0]
333+
}
334+
if (
335+
start != null &&
336+
end == null &&
337+
startIndex < endOffset &&
338+
endOffset <= endIndex
339+
) {
340+
end = t.range[1]
341+
break
342+
}
343+
startIndex = endIndex
344+
}
345+
if (start != null && end != null) {
346+
const nodeStart = node.range![0] + 1
347+
return [nodeStart + start, nodeStart + end]
348+
}
349+
} catch {
350+
// ignore
351+
}
352+
}
353+
return null
270354
}
271355

272356
/**
@@ -300,28 +384,31 @@ export function getRegexpLocation(
300384
}
301385

302386
/**
303-
* Check if the location of the regular expression node is available.
304-
* @param sourceCode The ESLint source code instance.
305-
* @param node The node to check.
306-
* @returns `true` if the location of the regular expression node is available.
387+
* Check if the given expression node is regexp literal.
307388
*/
308-
export function availableRegexpLocation(
309-
sourceCode: SourceCode,
389+
function isRegexpLiteral(
310390
node: ESTree.Expression,
311-
): boolean {
391+
): node is ESTree.RegExpLiteral {
312392
if (node.type !== "Literal") {
313393
return false
314394
}
315395
if (!(node as ESTree.RegExpLiteral).regex) {
316-
if (typeof node.value !== "string") {
317-
return false
318-
}
319-
if (
320-
sourceCode.text.slice(node.range![0] + 1, node.range![1] - 1) !==
321-
node.value
322-
) {
323-
return false
324-
}
396+
return false
397+
}
398+
return true
399+
}
400+
401+
/**
402+
* Check if the given expression node is string literal.
403+
*/
404+
function isStringLiteral(
405+
node: ESTree.Expression,
406+
): node is ESTree.Literal & { value: string } {
407+
if (node.type !== "Literal") {
408+
return false
409+
}
410+
if (typeof node.value !== "string") {
411+
return false
325412
}
326413
return true
327414
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./parser"
2+
export * from "./tokens"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Tokenizer } from "./tokenizer"
2+
import type { Token } from "./tokens"
3+
4+
export type StringLiteral = {
5+
tokens: Token[]
6+
value: string
7+
range: [number, number]
8+
}
9+
export type EcmaVersion =
10+
| 3
11+
| 5
12+
| 6
13+
| 2015
14+
| 7
15+
| 2016
16+
| 8
17+
| 2017
18+
| 9
19+
| 2018
20+
| 10
21+
| 2019
22+
| 11
23+
| 2020
24+
| 12
25+
| 2021
26+
27+
/** Parse for string literal */
28+
export function parseStringLiteral(
29+
source: string,
30+
option?: {
31+
start?: number
32+
end?: number
33+
ecmaVersion?: EcmaVersion
34+
},
35+
): StringLiteral {
36+
const startIndex = option?.start ?? 0
37+
const cp = source.codePointAt(startIndex)
38+
const ecmaVersion = option?.ecmaVersion ?? Infinity
39+
const tokenizer = new Tokenizer(source, {
40+
start: startIndex + 1,
41+
end: option?.end,
42+
ecmaVersion:
43+
ecmaVersion >= 6 && ecmaVersion < 2015
44+
? ecmaVersion + 2009
45+
: ecmaVersion,
46+
})
47+
const tokens = [...tokenizer.parseTokens(cp)]
48+
return {
49+
tokens,
50+
get value() {
51+
return tokens.map((t) => t.value).join("")
52+
},
53+
range: [startIndex, tokenizer.pos],
54+
}
55+
}
56+
57+
/** Parse for string tokens */
58+
export function* parseStringTokens(
59+
source: string,
60+
option?: {
61+
start?: number
62+
end?: number
63+
ecmaVersion?: EcmaVersion
64+
},
65+
): Generator<Token> {
66+
const startIndex = option?.start ?? 0
67+
const ecmaVersion = option?.ecmaVersion ?? Infinity
68+
const tokenizer = new Tokenizer(source, {
69+
start: startIndex,
70+
end: option?.end,
71+
ecmaVersion:
72+
ecmaVersion >= 6 && ecmaVersion < 2015
73+
? ecmaVersion + 2009
74+
: ecmaVersion,
75+
})
76+
yield* tokenizer.parseTokens()
77+
}

0 commit comments

Comments
 (0)