Skip to content

Commit 30c3a1b

Browse files
Add no-non-standard-flag rule (#167)
* Add `no-non-standard-flags` rule * Renamed rule * Fixed example
1 parent f3d2a75 commit 30c3a1b

File tree

11 files changed

+364
-112
lines changed

11 files changed

+364
-112
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9898
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
9999
| [regexp/no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | |
100100
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | |
101+
| [regexp/no-non-standard-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-non-standard-flag.html) | disallow non-standard flags | |
101102
| [regexp/no-obscure-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-obscure-range.html) | disallow obscure character ranges | |
102103
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
103104
| [regexp/no-optional-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-optional-assertion.html) | disallow optional assertions | |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2626
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
2727
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | |
2828
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | |
29+
| [regexp/no-non-standard-flag](./no-non-standard-flag.md) | disallow non-standard flags | |
2930
| [regexp/no-obscure-range](./no-obscure-range.md) | disallow obscure character ranges | |
3031
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
3132
| [regexp/no-optional-assertion](./no-optional-assertion.md) | disallow optional assertions | |

docs/rules/no-non-standard-flag.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-non-standard-flag"
5+
description: "disallow non-standard flags"
6+
---
7+
# regexp/no-non-standard-flag
8+
9+
> disallow non-standard 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+
13+
## :book: Rule Details
14+
15+
This rule reports non-standard flags.
16+
17+
Some JavaScript runtime implementations allow special flags not defined in the ECMAScript standard. These flags are experimental and should not be used in production code.
18+
19+
<eslint-code-block>
20+
21+
```js
22+
/* eslint regexp/no-non-standard-flag: "error" */
23+
24+
/* ✓ GOOD */
25+
var foo = /a*b*c/guy;
26+
27+
/* ✗ BAD */
28+
var foo = RegExp("(?:a|a)*b", "l");
29+
```
30+
31+
</eslint-code-block>
32+
33+
## :wrench: Options
34+
35+
Nothing.
36+
37+
## :mag: Implementation
38+
39+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-non-standard-flag.ts)
40+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-non-standard-flag.ts)

lib/rules/no-non-standard-flag.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { RegExpVisitor } from "regexpp/visitor"
2+
import type { RegExpContext } from "../utils"
3+
import { createRule, defineRegexpVisitor } from "../utils"
4+
5+
const STANDARD_FLAGS = "gimsuy"
6+
7+
export default createRule("no-non-standard-flag", {
8+
meta: {
9+
docs: {
10+
description: "disallow non-standard flags",
11+
// TODO Switch to recommended in the major version.
12+
// recommended: true,
13+
recommended: false,
14+
},
15+
schema: [],
16+
messages: {
17+
unexpected: "Unexpected non-standard flag '{{flag}}'.",
18+
},
19+
type: "suggestion", // "problem",
20+
},
21+
create(context) {
22+
/**
23+
* Create visitor
24+
*/
25+
function createVisitor({
26+
node,
27+
getFlagsLocation,
28+
flagsString,
29+
}: RegExpContext): RegExpVisitor.Handlers {
30+
if (flagsString) {
31+
const nonStandard = [...flagsString].filter(
32+
(f) => !STANDARD_FLAGS.includes(f),
33+
)
34+
35+
if (nonStandard.length > 0) {
36+
context.report({
37+
node,
38+
loc: getFlagsLocation(),
39+
messageId: "unexpected",
40+
data: { flag: nonStandard[0] },
41+
})
42+
}
43+
}
44+
return {}
45+
}
46+
47+
return defineRegexpVisitor(context, {
48+
createVisitor,
49+
})
50+
},
51+
})

lib/rules/no-useless-flag.ts

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { KnownMethodCall } from "../utils/ast-utils"
1313
import { findVariable, isKnownMethodCall, getParent } from "../utils/ast-utils"
1414
import { createTypeTracker } from "../utils/type-tracker"
1515
import type { RuleListener } from "../types"
16-
import type { Rule, SourceCode } from "eslint"
16+
import type { Rule } from "eslint"
1717

1818
type CodePathStack = {
1919
codePathId: string
@@ -191,26 +191,11 @@ function getFlagLocation(
191191
* Returns a fixer that removes the given flag.
192192
*/
193193
function fixRemoveFlag(
194-
fixer: Rule.RuleFixer,
195-
node: RegExpExpression,
194+
{ flagsString, fixReplaceFlags }: RegExpContext,
196195
flag: "i" | "m" | "s" | "g",
197-
sourceCode: SourceCode,
198196
) {
199-
if (node.type === "Literal") {
200-
const flagIndex =
201-
node.range![1] -
202-
node.regex.flags.length +
203-
node.regex.flags.indexOf(flag)
204-
const beforeRange: [number, number] = [node.range![0], flagIndex]
205-
return [
206-
// Replace the range of regular expression literals to avoid conflicts.
207-
fixer.replaceTextRange(
208-
beforeRange,
209-
sourceCode.text.slice(...beforeRange),
210-
),
211-
// Remove flag
212-
fixer.removeRange([flagIndex, flagIndex + 1]),
213-
]
197+
if (flagsString) {
198+
return fixReplaceFlags(flagsString.replace(flag, ""))
214199
}
215200
return null
216201
}
@@ -220,8 +205,10 @@ function fixRemoveFlag(
220205
*/
221206
function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
222207
return defineRegexpVisitor(context, {
223-
createVisitor({ flags, regexpNode, toCharSet }: RegExpContext) {
224-
if (!flags.ignoreCase) {
208+
createVisitor(regExpContext: RegExpContext) {
209+
const { flags, regexpNode, toCharSet, ownsFlags } = regExpContext
210+
211+
if (!flags.ignoreCase || !ownsFlags) {
225212
return {}
226213
}
227214

@@ -273,14 +260,7 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
273260
node: regexpNode,
274261
loc: getFlagLocation(context, regexpNode, "i"),
275262
messageId: "uselessIgnoreCaseFlag",
276-
fix(fixer) {
277-
return fixRemoveFlag(
278-
fixer,
279-
regexpNode,
280-
"i",
281-
context.getSourceCode(),
282-
)
283-
},
263+
fix: fixRemoveFlag(regExpContext, "i"),
284264
})
285265
}
286266
},
@@ -294,8 +274,10 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
294274
*/
295275
function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
296276
return defineRegexpVisitor(context, {
297-
createVisitor({ flags, regexpNode }: RegExpContext) {
298-
if (!flags.multiline) {
277+
createVisitor(regExpContext: RegExpContext) {
278+
const { flags, regexpNode, ownsFlags } = regExpContext
279+
280+
if (!flags.multiline || !ownsFlags) {
299281
return {}
300282
}
301283
let unnecessary = true
@@ -311,14 +293,7 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
311293
node: regexpNode,
312294
loc: getFlagLocation(context, regexpNode, "m"),
313295
messageId: "uselessMultilineFlag",
314-
fix(fixer) {
315-
return fixRemoveFlag(
316-
fixer,
317-
regexpNode,
318-
"m",
319-
context.getSourceCode(),
320-
)
321-
},
296+
fix: fixRemoveFlag(regExpContext, "m"),
322297
})
323298
}
324299
},
@@ -332,8 +307,10 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
332307
*/
333308
function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
334309
return defineRegexpVisitor(context, {
335-
createVisitor({ flags, regexpNode }: RegExpContext) {
336-
if (!flags.dotAll) {
310+
createVisitor(regExpContext: RegExpContext) {
311+
const { flags, regexpNode, ownsFlags } = regExpContext
312+
313+
if (!flags.dotAll || !ownsFlags) {
337314
return {}
338315
}
339316
let unnecessary = true
@@ -349,14 +326,7 @@ function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
349326
node: regexpNode,
350327
loc: getFlagLocation(context, regexpNode, "s"),
351328
messageId: "uselessDotAllFlag",
352-
fix(fixer) {
353-
return fixRemoveFlag(
354-
fixer,
355-
regexpNode,
356-
"s",
357-
context.getSourceCode(),
358-
)
359-
},
329+
fix: fixRemoveFlag(regExpContext, "s"),
360330
})
361331
}
362332
},

lib/rules/sort-flags.ts

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Literal } from "estree"
21
import type { RegExpVisitor } from "regexpp/visitor"
32
import type { RegExpContext } from "../utils"
43
import { createRule, defineRegexpVisitor } from "../utils"
@@ -20,30 +19,6 @@ export default createRule("sort-flags", {
2019
type: "suggestion", // "problem",
2120
},
2221
create(context) {
23-
/**
24-
* Report
25-
*/
26-
function report(
27-
node: Literal,
28-
flags: string,
29-
sortedFlags: string,
30-
flagsRange: [number, number],
31-
) {
32-
const sourceCode = context.getSourceCode()
33-
context.report({
34-
node,
35-
loc: {
36-
start: sourceCode.getLocFromIndex(flagsRange[0]),
37-
end: sourceCode.getLocFromIndex(flagsRange[1]),
38-
},
39-
messageId: "sortFlags",
40-
data: { flags, sortedFlags },
41-
fix(fixer) {
42-
return fixer.replaceTextRange(flagsRange, sortedFlags)
43-
},
44-
})
45-
}
46-
4722
/**
4823
* Sort regexp flags
4924
*/
@@ -58,30 +33,21 @@ export default createRule("sort-flags", {
5833
*/
5934
function createVisitor({
6035
regexpNode,
36+
flagsString,
37+
ownsFlags,
38+
getFlagsLocation,
39+
fixReplaceFlags,
6140
}: RegExpContext): RegExpVisitor.Handlers {
62-
if (regexpNode.type === "Literal") {
63-
const flags = regexpNode.regex.flags
64-
const sortedFlags = sortFlags(flags)
65-
if (flags !== sortedFlags) {
66-
report(regexpNode, flags, sortedFlags, [
67-
regexpNode.range![1] - regexpNode.regex.flags.length,
68-
regexpNode.range![1],
69-
])
70-
}
71-
} else {
72-
const flagsArg = regexpNode.arguments[1]
73-
if (
74-
flagsArg.type === "Literal" &&
75-
typeof flagsArg.value === "string"
76-
) {
77-
const flags = flagsArg.value
78-
const sortedFlags = sortFlags(flags)
79-
if (flags !== sortedFlags) {
80-
report(flagsArg, flags, sortedFlags, [
81-
flagsArg.range![0] + 1,
82-
flagsArg.range![1] - 1,
83-
])
84-
}
41+
if (flagsString && ownsFlags) {
42+
const sortedFlags = sortFlags(flagsString)
43+
if (flagsString !== sortedFlags) {
44+
context.report({
45+
node: regexpNode,
46+
loc: getFlagsLocation(),
47+
messageId: "sortFlags",
48+
data: { flags: flagsString, sortedFlags },
49+
fix: fixReplaceFlags(sortedFlags),
50+
})
8551
}
8652
}
8753

0 commit comments

Comments
 (0)