Skip to content

Commit 1fa6d75

Browse files
committed
Fixed ruleset
1 parent ca97441 commit 1fa6d75

File tree

9 files changed

+222
-5
lines changed

9 files changed

+222
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8787
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
8888
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
8989
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
90+
| [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | |
9091
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
9192
| [regexp/no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html) | disallow unnecessary `{n,m}` quantifier | :star: |
9293
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
1919
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
2020
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
2121
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
22+
| [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | |
2223
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
2324
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
2425
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-backreference"
5+
description: "disallow useless backreferences in regular expressions"
6+
---
7+
# regexp/no-useless-backreference
8+
9+
> disallow useless backreferences in regular expressions
10+
11+
## :book: Rule Details
12+
13+
This rule is a copy of the ESLint core [no-useless-backreference] rule.
14+
The [no-useless-backreference] rule was added in ESLint 7.x, but this plugin supports ESLint 6.x.
15+
Copied to this plugin to allow the same [no-useless-backreference] rules to be used in ESLint 6.x.
16+
17+
## :wrench: Options
18+
19+
See [no-useless-backreference] document.
20+
21+
## :books: Further reading
22+
23+
- [no-useless-backreference]
24+
25+
[no-useless-backreference]: https://eslint.org/docs/rules/no-useless-backreference
26+
27+
## Implementation
28+
29+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-backreference.ts)
30+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-backreference.js)

lib/configs/recommended.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
export default {
1+
import eslint from "eslint"
2+
3+
export = {
24
plugins: ["regexp"],
35
rules: {
46
// ESLint core rules
57
"no-control-regex": "error",
68
"no-invalid-regexp": "error",
79
"no-misleading-character-class": "error",
810
"no-regex-spaces": "error",
9-
"no-useless-backreference": "error",
1011
"prefer-regex-literals": "error",
12+
// If ESLint is 7 or higher, use core rule. If it is 6 or less, use the copied rule.
13+
[parseInt(eslint.Linter.version[0], 10) >= 7
14+
? "no-useless-backreference"
15+
: "regexp/no-useless-backreference"]: "error",
1116

1217
// eslint-plugin-regexp rules
1318
"regexp/match-any": "error",

lib/rules/no-useless-backreference.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Node as RegExpNode, LookaroundAssertion } from "regexpp/ast"
4+
import { createRule, defineRegexpVisitor } from "../utils"
5+
6+
/* istanbul ignore file */
7+
/**
8+
* Finds the path from the given `regexpp` AST node to the root node.
9+
* @param {regexpp.Node} node Node.
10+
* @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
11+
*/
12+
function getPathToRoot(node: RegExpNode) {
13+
const path = []
14+
let current = node
15+
16+
while (current) {
17+
path.push(current)
18+
if (!current.parent) {
19+
break
20+
}
21+
current = current.parent
22+
}
23+
24+
return path
25+
}
26+
27+
/**
28+
* Determines whether the given `regexpp` AST node is a lookaround node.
29+
* @param {regexpp.Node} node Node.
30+
* @returns {boolean} `true` if it is a lookaround node.
31+
*/
32+
function isLookaround(node: RegExpNode): node is LookaroundAssertion {
33+
return (
34+
node.type === "Assertion" &&
35+
(node.kind === "lookahead" || node.kind === "lookbehind")
36+
)
37+
}
38+
39+
/**
40+
* Determines whether the given `regexpp` AST node is a negative lookaround node.
41+
* @param {regexpp.Node} node Node.
42+
* @returns {boolean} `true` if it is a negative lookaround node.
43+
*/
44+
function isNegativeLookaround(node: RegExpNode) {
45+
return isLookaround(node) && node.negate
46+
}
47+
48+
/**
49+
* Get last element
50+
*/
51+
function last<T>(arr: T[]): T {
52+
return arr[arr.length - 1]
53+
}
54+
55+
export default createRule("no-useless-backreference", {
56+
meta: {
57+
docs: {
58+
description:
59+
"disallow useless backreferences in regular expressions",
60+
recommended: false,
61+
},
62+
schema: [],
63+
messages: {
64+
nested:
65+
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
66+
forward:
67+
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
68+
backward:
69+
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
70+
disjunctive:
71+
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
72+
intoNegativeLookaround:
73+
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround.",
74+
},
75+
type: "suggestion", // "problem",
76+
},
77+
create(context) {
78+
/**
79+
* Create visitor
80+
* @param node
81+
*/
82+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
83+
return {
84+
onBackreferenceEnter(bref) {
85+
const group = bref.resolved,
86+
brefPath = getPathToRoot(bref),
87+
groupPath = getPathToRoot(group)
88+
let messageId = null
89+
90+
if (brefPath.includes(group)) {
91+
// group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
92+
messageId = "nested"
93+
} else {
94+
// Start from the root to find the lowest common ancestor.
95+
let i = brefPath.length - 1,
96+
j = groupPath.length - 1
97+
98+
do {
99+
i--
100+
j--
101+
} while (brefPath[i] === groupPath[j])
102+
103+
const indexOfLowestCommonAncestor = j + 1,
104+
groupCut = groupPath.slice(
105+
0,
106+
indexOfLowestCommonAncestor,
107+
),
108+
commonPath = groupPath.slice(
109+
indexOfLowestCommonAncestor,
110+
),
111+
lowestCommonLookaround = commonPath.find(
112+
isLookaround,
113+
),
114+
isMatchingBackward =
115+
lowestCommonLookaround &&
116+
lowestCommonLookaround.kind === "lookbehind"
117+
118+
if (!isMatchingBackward && bref.end <= group.start) {
119+
// bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
120+
messageId = "forward"
121+
} else if (
122+
isMatchingBackward &&
123+
group.end <= bref.start
124+
) {
125+
// the opposite of the previous when the regex is matching backward in a lookbehind context.
126+
messageId = "backward"
127+
} else if (last(groupCut).type === "Alternative") {
128+
// group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
129+
messageId = "disjunctive"
130+
} else if (groupCut.some(isNegativeLookaround)) {
131+
// group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
132+
messageId = "intoNegativeLookaround"
133+
}
134+
}
135+
136+
if (messageId) {
137+
context.report({
138+
node,
139+
messageId,
140+
data: {
141+
bref: bref.raw,
142+
group: group.raw,
143+
},
144+
})
145+
}
146+
},
147+
}
148+
}
149+
150+
return defineRegexpVisitor(context, {
151+
createVisitor,
152+
})
153+
},
154+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion
77
import noEscapeBackspace from "../rules/no-escape-backspace"
88
import noInvisibleCharacter from "../rules/no-invisible-character"
99
import noOctal from "../rules/no-octal"
10+
import noUselessBackreference from "../rules/no-useless-backreference"
1011
import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
1112
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
1213
import preferD from "../rules/prefer-d"
@@ -25,6 +26,7 @@ export const rules = [
2526
noEscapeBackspace,
2627
noInvisibleCharacter,
2728
noOctal,
29+
noUselessBackreference,
2830
noUselessExactlyQuantifier,
2931
noUselessTwoNumsQuantifier,
3032
preferD,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-regexp",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "ESLint plugin for finding RegExp mistakes and RegExp style guide violations.",
55
"main": "dist/index.js",
66
"files": [
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-useless-backreference"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("no-useless-backreference", rule as any, {
12+
valid: ["/.(?=(b))\\1/"],
13+
invalid: [
14+
{
15+
code: "/(b)(\\2a)/",
16+
errors: [{ messageId: "nested" }],
17+
},
18+
],
19+
})

tools/update-rulesets.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,23 @@ const coreRules = [
1010
"no-invalid-regexp",
1111
"no-misleading-character-class",
1212
"no-regex-spaces",
13-
"no-useless-backreference",
1413
"prefer-regex-literals",
1514
// "prefer-named-capture-group", // modern
1615
// "require-unicode-regexp", // modern
1716
]
1817

1918
let content = `
20-
export default {
19+
import eslint from "eslint"
20+
21+
export = {
2122
plugins: ["regexp"],
2223
rules: {
2324
// ESLint core rules
2425
${coreRules.map((ruleName) => `"${ruleName}": "error"`).join(",\n")},
26+
// If ESLint is 7 or higher, use core rule. If it is 6 or less, use the copied rule.
27+
[parseInt(eslint.Linter.version[0], 10) >= 7
28+
? "no-useless-backreference"
29+
: "regexp/no-useless-backreference"]: "error",
2530
2631
// eslint-plugin-regexp rules
2732
${rules

0 commit comments

Comments
 (0)