Skip to content

Commit eb46f4e

Browse files
authored
Add disallowNeverMatch to regexp/no-dupe-disjunctions rule (#74)
* Add `disallowNeverMatch` to `regexp/no-dupe-disjunctions` rule * Update * update * update * Add testcase and fix * fix
1 parent 64c70e3 commit eb46f4e

File tree

10 files changed

+1548
-124
lines changed

10 files changed

+1548
-124
lines changed

docs/rules/no-dupe-disjunctions.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,43 @@ var foo = /[ab]|[ba]/
3535

3636
## :wrench: Options
3737

38-
Nothing.
38+
```json5
39+
{
40+
"regexp/no-dupe-disjunctions": [
41+
"error",
42+
{
43+
"disallowNeverMatch": false
44+
}
45+
]
46+
}
47+
```
48+
49+
- `disallowNeverMatch` ... If `true`, it reports a pattern that does not match as a result of a partial duplication of the previous pattern.
50+
51+
### `"disallowNeverMatch": true`
52+
53+
<eslint-code-block>
54+
55+
```js
56+
/* eslint regexp/no-dupe-disjunctions: ["error", { "disallowNeverMatch": true }] */
57+
58+
/* ✓ GOOD */
59+
var foo = /a|b/
60+
var foo = /(a|b)/
61+
var foo = /(?:a|b)/
62+
63+
/* ✗ BAD */
64+
65+
// Duplication
66+
var foo = /a|a/
67+
68+
// A string that matches the pattern on the right also matches the pattern on the left, so it doesn't make sense to process the pattern on the right.
69+
var foo = /a|abc/
70+
var foo = /.|abc/
71+
var foo = /.|a|b|c/
72+
```
73+
74+
</eslint-code-block>
3975

4076
## :rocket: Version
4177

lib/rules/no-dupe-disjunctions.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
Pattern,
88
} from "regexpp/ast"
99
import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils"
10-
import { isEqualNodes } from "../utils/regexp-ast"
10+
import { isCoveredNode, isEqualNodes } from "../utils/regexp-ast"
1111

1212
export default createRule("no-dupe-disjunctions", {
1313
meta: {
@@ -17,20 +17,37 @@ export default createRule("no-dupe-disjunctions", {
1717
// recommended: true,
1818
recommended: false,
1919
},
20-
schema: [],
20+
schema: [
21+
{
22+
type: "object",
23+
properties: {
24+
disallowNeverMatch: { type: "boolean" },
25+
},
26+
additionalProperties: false,
27+
},
28+
],
2129
messages: {
2230
duplicated: "The disjunctions are duplicated.",
31+
neverExecute:
32+
"This disjunction can never match. Its condition is covered by previous conditions in the disjunctions.",
2333
},
2434
type: "suggestion", // "problem",
2535
},
2636
create(context) {
37+
const disallowNeverMatch = Boolean(
38+
context.options[0]?.disallowNeverMatch,
39+
)
2740
const sourceCode = context.getSourceCode()
2841

2942
/**
3043
* Create visitor
3144
* @param node
3245
*/
33-
function createVisitor(node: Expression): RegExpVisitor.Handlers {
46+
function createVisitor(
47+
node: Expression,
48+
_p: string,
49+
flags: string,
50+
): RegExpVisitor.Handlers {
3451
/** Verify group node */
3552
function verify(
3653
regexpNode:
@@ -39,25 +56,36 @@ export default createRule("no-dupe-disjunctions", {
3956
| Pattern
4057
| LookaroundAssertion,
4158
) {
42-
const otherAlts = []
59+
const leftAlts = []
4360
for (const alt of regexpNode.alternatives) {
44-
const dupeAlt = otherAlts.find((o) =>
45-
isEqualNodes(alt, o, (a, _b) => {
46-
if (a.type === "CapturingGroup") {
47-
return false
48-
}
49-
return null
50-
}),
51-
)
61+
const dupeAlt = disallowNeverMatch
62+
? leftAlts.find((leftAlt) =>
63+
isCoveredNode(leftAlt, alt, {
64+
flags: { left: flags, right: flags },
65+
}),
66+
)
67+
: leftAlts.find((leftAlt) =>
68+
isEqualNodes(leftAlt, alt, (a, _b) => {
69+
if (a.type === "CapturingGroup") {
70+
return false
71+
}
72+
return null
73+
}),
74+
)
5275
if (dupeAlt) {
5376
context.report({
5477
node,
5578
loc: getRegexpLocation(sourceCode, node, alt),
56-
messageId: "duplicated",
79+
messageId:
80+
disallowNeverMatch &&
81+
!isEqualNodes(dupeAlt, alt)
82+
? "neverExecute"
83+
: "duplicated",
5784
})
58-
} else {
59-
otherAlts.push(alt)
85+
continue
6086
}
87+
88+
leftAlts.push(alt)
6189
}
6290
}
6391

lib/utils/regexp-ast/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import type { Node } from "regexpp/ast"
2+
export type ShortCircuit = (aNode: Node, bNode: Node) => boolean | null

lib/utils/regexp-ast/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { RegExpLiteral, Pattern } from "regexpp/ast"
2+
import type { Rule } from "eslint"
3+
import type { Expression } from "estree"
4+
import { parseRegExpLiteral, RegExpParser, visitRegExpAST } from "regexpp"
5+
import { getStaticValue } from "eslint-utils"
6+
export { ShortCircuit } from "./common"
7+
export * from "./is-covered"
8+
export * from "./is-equals"
9+
10+
const parser = new RegExpParser()
11+
/**
12+
* Get Reg Exp node from given expression node
13+
*/
14+
export function getRegExpNodeFromExpression(
15+
node: Expression,
16+
context: Rule.RuleContext,
17+
): RegExpLiteral | Pattern | null {
18+
if (node.type === "Literal") {
19+
if ("regex" in node && node.regex) {
20+
try {
21+
return parser.parsePattern(
22+
node.regex.pattern,
23+
0,
24+
node.regex.pattern.length,
25+
node.regex.flags.includes("u"),
26+
)
27+
} catch {
28+
return null
29+
}
30+
}
31+
return null
32+
}
33+
const evaluated = getStaticValue(node, context.getScope())
34+
if (!evaluated || !(evaluated.value instanceof RegExp)) {
35+
return null
36+
}
37+
try {
38+
return parseRegExpLiteral(evaluated.value)
39+
} catch {
40+
return null
41+
}
42+
}
43+
44+
/**
45+
* Extract capturing group data
46+
*/
47+
export function extractCaptures(
48+
patternNode: RegExpLiteral | Pattern,
49+
): { names: Set<string>; count: number } {
50+
let count = 0
51+
const names = new Set<string>()
52+
visitRegExpAST(patternNode, {
53+
onCapturingGroupEnter(cgNode) {
54+
count++
55+
if (cgNode.name != null) {
56+
names.add(cgNode.name)
57+
}
58+
},
59+
})
60+
61+
return { count, names }
62+
}

0 commit comments

Comments
 (0)