Skip to content

Commit 4efda8d

Browse files
authored
Add regexp/no-useless-flag rule (#139)
1 parent fe6d393 commit 4efda8d

17 files changed

+1519
-123
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
109109
| [regexp/no-useless-dollar-replacements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-dollar-replacements.html) | disallow useless `$` replacements in replacement string | |
110110
| [regexp/no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | |
111111
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
112+
| [regexp/no-useless-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-flag.html) | disallow unnecessary regex flags | :wrench: |
112113
| [regexp/no-useless-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary Non-capturing group | :wrench: |
113114
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessarily non-greedy quantifiers | :wrench: |
114115
| [regexp/no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary range of characters by using a hyphen | :wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
3737
| [regexp/no-useless-dollar-replacements](./no-useless-dollar-replacements.md) | disallow useless `$` replacements in replacement string | |
3838
| [regexp/no-useless-escape](./no-useless-escape.md) | disallow unnecessary escape characters in RegExp | |
3939
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
40+
| [regexp/no-useless-flag](./no-useless-flag.md) | disallow unnecessary regex flags | :wrench: |
4041
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
4142
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
4243
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |

docs/rules/no-useless-flag.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-flag"
5+
description: "disallow unnecessary regex flags"
6+
---
7+
# regexp/no-useless-flag
8+
9+
> disallow unnecessary regex flags
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
This will point out present regex flags that do not change the pattern.
17+
18+
### `i` flag (ignoreCase)
19+
20+
The `i` flag is only necessary if the pattern contains any characters with case
21+
variations. If the pattern contains no such characters, the flag will be
22+
unnecessary. E.g. `/\.{3}/i`
23+
24+
<eslint-code-block fix>
25+
26+
```js
27+
/* eslint regexp/no-useless-flag: "error" */
28+
29+
/* ✓ GOOD */
30+
var foo = /a|b/i;
31+
32+
/* ✗ BAD */
33+
var foo = /\.{3}/i;
34+
var foo = /\w+/i;
35+
```
36+
37+
</eslint-code-block>
38+
39+
### `m` flag (multiline)
40+
41+
The `m` flag changes the behavior of the `^` and `$` assertions. If the pattern
42+
doesn't contain these anchors, the `m` flag will be unnecessary. E.g. `/foo|[^\r\n]*/m`
43+
44+
<eslint-code-block fix>
45+
46+
```js
47+
/* eslint regexp/no-useless-flag: "error" */
48+
49+
/* ✓ GOOD */
50+
var foo = /^foo$/m;
51+
52+
/* ✗ BAD */
53+
var foo = /foo|[^\r\n]*/m;
54+
var foo = /a|b/m;
55+
```
56+
57+
</eslint-code-block>
58+
59+
### `s` flag (dotAll)
60+
61+
The `s` flag makes the dot (`.`) match all characters instead of the usually
62+
non-line-terminator characters. If the pattern doesn't contain a dot
63+
character set, the `s` flag will be unnecessary. E.g. `/[.:]/s`
64+
65+
<eslint-code-block fix>
66+
67+
```js
68+
/* eslint regexp/no-useless-flag: "error" */
69+
70+
/* ✓ GOOD */
71+
var foo = /a.*?b/s;
72+
73+
/* ✗ BAD */
74+
var foo = /[.:]/s;
75+
var foo = /^foo$/s;
76+
```
77+
78+
</eslint-code-block>
79+
80+
### `g` flag (global)
81+
82+
The `g` flag is used when you need to test a regular expression against all possible string match. If not, it will be unnecessary.
83+
84+
<eslint-code-block fix>
85+
86+
```js
87+
/* eslint regexp/no-useless-flag: "error" */
88+
89+
/* ✓ GOOD */
90+
const regex1 = /foo/g;
91+
const str = 'table football, foosball';
92+
while ((array = regex1.exec(str)) !== null) {
93+
//
94+
}
95+
96+
const regex2 = /foo/g;
97+
regex2.test(string);
98+
regex2.test(string);
99+
100+
str.replace(/foo/g, 'bar');
101+
str.replaceAll(/foo/g, 'bar');
102+
103+
/* ✗ BAD */
104+
/foo/g.test(string);
105+
const regex3 = /foo/g;
106+
regex3.test(string); // You have used it only once.
107+
108+
/foo/g.exec(string);
109+
const regex4 = /foo/g;
110+
regex4.exec(string); // You have used it only once.
111+
112+
new RegExp('foo', 'g').test(string);
113+
114+
str.search(/foo/g);
115+
```
116+
117+
</eslint-code-block>
118+
119+
### other flags
120+
121+
No other flags will be checked.
122+
123+
## :wrench: Options
124+
125+
```json5
126+
{
127+
"regexp/no-useless-flag": ["error",
128+
{
129+
"ignore": [] // An array of "i", "m", "s" and "g".
130+
}
131+
]
132+
}
133+
```
134+
135+
- `ignore` ... An array of flags to ignore from the check.
136+
137+
### `"ignore": ["s", "g"]`
138+
139+
<eslint-code-block fix>
140+
141+
```js
142+
/* eslint regexp/no-useless-flag: ["error", { "ignore": ["s", "g"] }] */
143+
144+
/* ✓ GOOD */
145+
var foo = /\w/s;
146+
/foo/g.test(string);
147+
148+
/* ✗ BAD */
149+
var foo = /\w/i;
150+
var foo = /\w/m;
151+
```
152+
153+
</eslint-code-block>
154+
155+
## :heart: Compatibility
156+
157+
This rule is compatible with [clean-regex/no-unnecessary-flag] rule.
158+
159+
[clean-regex/no-unnecessary-flag]: https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/docs/rules/no-unnecessary-flag.md
160+
161+
## :mag: Implementation
162+
163+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-flag.ts)
164+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-flag.ts)

lib/rules/match-any.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getRegexpRange,
1010
fixerApplyEscape,
1111
parseFlags,
12+
isRegexpLiteral,
1213
} from "../utils"
1314
import type { ReadonlyFlags } from "regexp-ast-analysis"
1415
import { matchesAllCharacters } from "regexp-ast-analysis"
@@ -71,40 +72,59 @@ export default createRule("match-any", {
7172
* Fix source code
7273
* @param fixer
7374
*/
74-
function fix(
75+
function* fix(
7576
fixer: Rule.RuleFixer,
7677
node: Expression,
7778
regexpNode: RegExpNode,
7879
flags: ReadonlyFlags,
7980
) {
8081
if (!preference) {
81-
return null
82+
return
8283
}
83-
if (preference === OPTION_DOTALL && !flags.dotAll) {
84-
// since we can't just add flags, we cannot fix this
85-
return null
84+
if (preference === OPTION_DOTALL) {
85+
if (!flags.dotAll) {
86+
// since we can't just add flags, we cannot fix this
87+
return
88+
}
89+
if (!isRegexpLiteral(node)) {
90+
// Flag conflicts may be unavoidable and will not be autofix.
91+
return
92+
}
8693
}
8794
const range = getRegexpRange(sourceCode, node, regexpNode)
8895
if (range == null) {
89-
return null
96+
return
9097
}
9198

9299
if (
93100
regexpNode.type === "CharacterClass" &&
94101
preference.startsWith("[") &&
95102
preference.endsWith("]")
96103
) {
97-
return fixer.replaceTextRange(
104+
yield fixer.replaceTextRange(
98105
[range[0] + 1, range[1] - 1],
99106
fixerApplyEscape(preference.slice(1, -1), node),
100107
)
108+
return
101109
}
102110

103111
const replacement = preference === OPTION_DOTALL ? "." : preference
104-
return fixer.replaceTextRange(
112+
yield fixer.replaceTextRange(
105113
range,
106114
fixerApplyEscape(replacement, node),
107115
)
116+
117+
if (preference === OPTION_DOTALL) {
118+
// Autofix to dotAll depends on the flag.
119+
// Modify the entire regular expression literal to avoid conflicts due to flag changes.
120+
121+
// Mark regular expression flag changes to avoid conflicts due to flag changes.
122+
const afterRange: [number, number] = [range[1], node.range![1]]
123+
yield fixer.replaceTextRange(
124+
afterRange,
125+
sourceCode.text.slice(...afterRange),
126+
)
127+
}
108128
}
109129

110130
/**

0 commit comments

Comments
 (0)