Skip to content

Commit 30e6d70

Browse files
no-empty-lookarounds-assertion: Better detection (#113)
1 parent 61bcd20 commit 30e6d70

File tree

3 files changed

+115
-23
lines changed

3 files changed

+115
-23
lines changed

docs/rules/no-empty-lookarounds-assertion.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ since: "v0.1.0"
1515

1616
This rule reports empty lookahead assertion or empty lookbehind assertion.
1717

18+
### What are _empty lookarounds_?
19+
20+
An empty lookaround is a lookaround for which at least one path in the lookaround expression contains only elements that do not consume characters and do not assert characters. This means that the lookaround expression will trivially accept any input string.
21+
22+
**Examples:**
23+
24+
- `(?=)`: One of simplest empty lookarounds.
25+
- `(?=a*)`: It is possible for `a*` to not consume characters, therefore the lookahead is _empty_.
26+
- `(?=a|b*)`: Only one path has to not consume characters. Since it is possible for `b*` to not consume characters, the lookahead is _empty_.
27+
- `(?=a|$)`: This is **not** an empty lookaround. `$` does not _consume_ characters but it does _assert_ characters. Similarly, all other standard assertions (`\b`, `\B`, `^`) are also not empty.
28+
29+
### Why are empty lookarounds a problem?
30+
31+
Because empty lookarounds accept the empty string, they are essentially non-functional. They will always trivially reject/accept.
32+
33+
E.g. `(?=b?)\w` will match `a` just fine. `(?=b?)` will always trivially accept no matter the input string. The same also happens for negated lookarounds but they will trivially reject. E.g. `(?!b?)\w` won't match any input strings.
34+
35+
The only way to fix empty lookarounds is to either remove them or to rewrite the lookaround expression to be non-empty.
36+
1837
<eslint-code-block>
1938

2039
```js
@@ -31,6 +50,8 @@ var foo = /x(?=)/;
3150
var foo = /x(?!)/;
3251
var foo = /(?<=)x/;
3352
var foo = /(?<!)x/;
53+
var foo = /(?=b?)\w/;
54+
var foo = /(?!b?)\w/;
3455
```
3556

3657
</eslint-code-block>

lib/rules/no-empty-lookarounds-assertion.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Expression } from "estree"
2+
import { isPotentiallyEmpty } from "regexp-ast-analysis"
23
import type { RegExpVisitor } from "regexpp/visitor"
34
import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils"
45

@@ -11,8 +12,8 @@ export default createRule("no-empty-lookarounds-assertion", {
1112
},
1213
schema: [],
1314
messages: {
14-
unexpectedLookahead: "Unexpected empty lookahead.",
15-
unexpectedLookbehind: "Unexpected empty lookbehind.",
15+
unexpected:
16+
"Unexpected empty {{kind}}. It will trivially {{result}} all inputs.",
1617
},
1718
type: "suggestion",
1819
},
@@ -32,18 +33,16 @@ export default createRule("no-empty-lookarounds-assertion", {
3233
) {
3334
return
3435
}
35-
if (
36-
aNode.alternatives.every(
37-
(alt) => alt.elements.length === 0,
38-
)
39-
) {
36+
37+
if (isPotentiallyEmpty(aNode.alternatives)) {
4038
context.report({
4139
node,
4240
loc: getRegexpLocation(sourceCode, node, aNode),
43-
messageId:
44-
aNode.kind === "lookahead"
45-
? "unexpectedLookahead"
46-
: "unexpectedLookbehind",
41+
messageId: "unexpected",
42+
data: {
43+
kind: aNode.kind,
44+
result: aNode.negate ? "reject" : "accept",
45+
},
4746
})
4847
}
4948
},

tests/lib/rules/no-empty-lookarounds-assertion.ts

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
1414
"/x(?!y)/",
1515
"/(?<=y)x/",
1616
"/(?<!y)x/",
17-
"/x(?=y|)/",
18-
"/x(?!y|)/",
19-
"/(?<=y|)x/",
20-
"/(?<!y|)x/",
2117
"/(^)x/",
2218
"/x($)/",
19+
"/(?=(?=.).*)/",
20+
"/(?=$|a)/",
21+
"/(?=\\ba*\\b)/",
22+
'/b?r(#*)"(?:[^"]|"(?!\\1))*"\\1/',
2323
],
2424
invalid: [
2525
{
2626
code: "/x(?=)/",
2727
errors: [
2828
{
29-
message: "Unexpected empty lookahead.",
29+
message:
30+
"Unexpected empty lookahead. It will trivially accept all inputs.",
3031
column: 3,
3132
endColumn: 7,
3233
},
@@ -36,7 +37,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
3637
code: "/x(?!)/",
3738
errors: [
3839
{
39-
message: "Unexpected empty lookahead.",
40+
message:
41+
"Unexpected empty lookahead. It will trivially reject all inputs.",
4042
column: 3,
4143
endColumn: 7,
4244
},
@@ -46,7 +48,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
4648
code: "/(?<=)x/",
4749
errors: [
4850
{
49-
message: "Unexpected empty lookbehind.",
51+
message:
52+
"Unexpected empty lookbehind. It will trivially accept all inputs.",
5053
column: 2,
5154
endColumn: 7,
5255
},
@@ -56,7 +59,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
5659
code: "/(?<!)x/",
5760
errors: [
5861
{
59-
message: "Unexpected empty lookbehind.",
62+
message:
63+
"Unexpected empty lookbehind. It will trivially reject all inputs.",
6064
column: 2,
6165
endColumn: 7,
6266
},
@@ -66,7 +70,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
6670
code: "/x(?=|)/",
6771
errors: [
6872
{
69-
message: "Unexpected empty lookahead.",
73+
message:
74+
"Unexpected empty lookahead. It will trivially accept all inputs.",
7075
column: 3,
7176
endColumn: 8,
7277
},
@@ -76,7 +81,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
7681
code: "/x(?!|)/",
7782
errors: [
7883
{
79-
message: "Unexpected empty lookahead.",
84+
message:
85+
"Unexpected empty lookahead. It will trivially reject all inputs.",
8086
column: 3,
8187
endColumn: 8,
8288
},
@@ -86,7 +92,8 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
8692
code: "/(?<=|)x/",
8793
errors: [
8894
{
89-
message: "Unexpected empty lookbehind.",
95+
message:
96+
"Unexpected empty lookbehind. It will trivially accept all inputs.",
9097
column: 2,
9198
endColumn: 8,
9299
},
@@ -96,11 +103,76 @@ tester.run("no-empty-lookarounds-assertion", rule as any, {
96103
code: "/(?<!|)x/",
97104
errors: [
98105
{
99-
message: "Unexpected empty lookbehind.",
106+
message:
107+
"Unexpected empty lookbehind. It will trivially reject all inputs.",
100108
column: 2,
101109
endColumn: 8,
102110
},
103111
],
104112
},
113+
114+
{
115+
code: "/x(?=y|)/",
116+
errors: [
117+
{
118+
message:
119+
"Unexpected empty lookahead. It will trivially accept all inputs.",
120+
column: 3,
121+
endColumn: 9,
122+
},
123+
],
124+
},
125+
{
126+
code: "/x(?!y|)/",
127+
errors: [
128+
{
129+
message:
130+
"Unexpected empty lookahead. It will trivially reject all inputs.",
131+
column: 3,
132+
endColumn: 9,
133+
},
134+
],
135+
},
136+
{
137+
code: "/(?<=y|)x/",
138+
errors: [
139+
{
140+
message:
141+
"Unexpected empty lookbehind. It will trivially accept all inputs.",
142+
column: 2,
143+
endColumn: 9,
144+
},
145+
],
146+
},
147+
{
148+
code: "/(?<!y|)x/",
149+
errors: [
150+
{
151+
message:
152+
"Unexpected empty lookbehind. It will trivially reject all inputs.",
153+
column: 2,
154+
endColumn: 9,
155+
},
156+
],
157+
},
158+
159+
{
160+
code: "/(?=a*)/",
161+
errors: [
162+
{
163+
message:
164+
"Unexpected empty lookahead. It will trivially accept all inputs.",
165+
},
166+
],
167+
},
168+
{
169+
code: "/(?=a|b*)/",
170+
errors: [
171+
{
172+
message:
173+
"Unexpected empty lookahead. It will trivially accept all inputs.",
174+
},
175+
],
176+
},
105177
],
106178
})

0 commit comments

Comments
 (0)