-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathno-empty-alternative.ts
154 lines (143 loc) · 5.68 KB
/
no-empty-alternative.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import type {
Alternative,
CapturingGroup,
ClassStringDisjunction,
Group,
Pattern,
} from "@eslint-community/regexpp/ast"
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
/**
* Returns the source before and after the alternatives of the given capturing group.
*/
function getCapturingGroupOuterSource(node: CapturingGroup): [string, string] {
const first = node.alternatives[0]
const last = node.alternatives[node.alternatives.length - 1]
const innerStart = first.start - node.start
const innerEnd = last.end - node.start
return [node.raw.slice(0, innerStart), node.raw.slice(innerEnd)]
}
function getFixedNode(
regexpNode: CapturingGroup | Group | Pattern,
alt: Alternative,
): string | null {
let quant
if (regexpNode.alternatives.at(0) === alt) {
quant = "??"
} else if (regexpNode.alternatives.at(-1) === alt) {
quant = "?"
} else {
return null
}
const innerAlternatives = regexpNode.alternatives
.filter((a) => a !== alt)
.map((a) => a.raw)
.join("|")
let replacement = `(?:${innerAlternatives})${quant}`
if (regexpNode.type === "CapturingGroup") {
const [before, after] = getCapturingGroupOuterSource(regexpNode)
replacement = `${before}${replacement}${after}`
} else if (regexpNode.parent?.type === "Quantifier") {
// if the parent is a quantifier, then we need to wrap the replacement
// in a non-capturing group
replacement = `(?:${replacement})`
}
return replacement
}
export default createRule("no-empty-alternative", {
meta: {
docs: {
description: "disallow alternatives without elements",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [],
hasSuggestions: true,
messages: {
empty: "This empty alternative might be a mistake. If not, use a quantifier instead.",
suggest: "Use a quantifier instead.",
},
type: "problem",
},
create(context) {
function createVisitor({
node,
getRegexpLocation,
fixReplaceNode,
}: RegExpContext): RegExpVisitor.Handlers {
function verifyAlternatives<
N extends
| CapturingGroup
| Group
| Pattern
| ClassStringDisjunction,
>(
regexpNode: N,
suggestFixer: (alt: N["alternatives"][number]) => string | null,
) {
if (regexpNode.alternatives.length >= 2) {
// We want to have at least two alternatives because the zero alternatives isn't possible because of
// the parser and one alternative is already handled by other rules.
for (let i = 0; i < regexpNode.alternatives.length; i++) {
const alt = regexpNode.alternatives[i]
const isLast = i === regexpNode.alternatives.length - 1
if (alt.elements.length === 0) {
// Since empty alternative have a width of 0, it's hard to underline their location.
// So we will report the location of the `|` that causes the empty alternative.
const index = alt.start
const loc = isLast
? getRegexpLocation({
start: index - 1,
end: index,
})
: getRegexpLocation({
start: index,
end: index + 1,
})
const fixed = suggestFixer(alt)
context.report({
node,
loc,
messageId: "empty",
suggest: fixed
? [
{
messageId: "suggest",
fix: fixReplaceNode(
regexpNode,
fixed,
),
},
]
: undefined,
})
// don't report the same node multiple times
return
}
}
}
}
return {
onGroupEnter: (gNode) =>
verifyAlternatives(gNode, (alt) =>
getFixedNode(gNode, alt),
),
onCapturingGroupEnter: (cgNode) =>
verifyAlternatives(cgNode, (alt) =>
getFixedNode(cgNode, alt),
),
onPatternEnter: (pNode) =>
verifyAlternatives(pNode, (alt) =>
getFixedNode(pNode, alt),
),
onClassStringDisjunctionEnter: (csdNode) =>
verifyAlternatives(csdNode, () => null),
}
}
return defineRegexpVisitor(context, {
createVisitor,
})
},
})