Skip to content

Commit

Permalink
Fixed ruleset
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Jul 7, 2020
1 parent ca97441 commit 1fa6d75
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
| [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | |
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
| [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: |
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
| [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | |
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
Expand Down
30 changes: 30 additions & 0 deletions docs/rules/no-useless-backreference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/no-useless-backreference"
description: "disallow useless backreferences in regular expressions"
---
# regexp/no-useless-backreference

> disallow useless backreferences in regular expressions
## :book: Rule Details

This rule is a copy of the ESLint core [no-useless-backreference] rule.
The [no-useless-backreference] rule was added in ESLint 7.x, but this plugin supports ESLint 6.x.
Copied to this plugin to allow the same [no-useless-backreference] rules to be used in ESLint 6.x.

## :wrench: Options

See [no-useless-backreference] document.

## :books: Further reading

- [no-useless-backreference]

[no-useless-backreference]: https://eslint.org/docs/rules/no-useless-backreference

## Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-backreference.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-backreference.js)
9 changes: 7 additions & 2 deletions lib/configs/recommended.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
export default {
import eslint from "eslint"

export = {
plugins: ["regexp"],
rules: {
// ESLint core rules
"no-control-regex": "error",
"no-invalid-regexp": "error",
"no-misleading-character-class": "error",
"no-regex-spaces": "error",
"no-useless-backreference": "error",
"prefer-regex-literals": "error",
// If ESLint is 7 or higher, use core rule. If it is 6 or less, use the copied rule.
[parseInt(eslint.Linter.version[0], 10) >= 7
? "no-useless-backreference"
: "regexp/no-useless-backreference"]: "error",

// eslint-plugin-regexp rules
"regexp/match-any": "error",
Expand Down
154 changes: 154 additions & 0 deletions lib/rules/no-useless-backreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { Expression } from "estree"
import type { RegExpVisitor } from "regexpp/visitor"
import type { Node as RegExpNode, LookaroundAssertion } from "regexpp/ast"
import { createRule, defineRegexpVisitor } from "../utils"

/* istanbul ignore file */
/**
* Finds the path from the given `regexpp` AST node to the root node.
* @param {regexpp.Node} node Node.
* @returns {regexpp.Node[]} Array that starts with the given node and ends with the root node.
*/
function getPathToRoot(node: RegExpNode) {
const path = []
let current = node

while (current) {
path.push(current)
if (!current.parent) {
break
}
current = current.parent
}

return path
}

/**
* Determines whether the given `regexpp` AST node is a lookaround node.
* @param {regexpp.Node} node Node.
* @returns {boolean} `true` if it is a lookaround node.
*/
function isLookaround(node: RegExpNode): node is LookaroundAssertion {
return (
node.type === "Assertion" &&
(node.kind === "lookahead" || node.kind === "lookbehind")
)
}

/**
* Determines whether the given `regexpp` AST node is a negative lookaround node.
* @param {regexpp.Node} node Node.
* @returns {boolean} `true` if it is a negative lookaround node.
*/
function isNegativeLookaround(node: RegExpNode) {
return isLookaround(node) && node.negate
}

/**
* Get last element
*/
function last<T>(arr: T[]): T {
return arr[arr.length - 1]
}

export default createRule("no-useless-backreference", {
meta: {
docs: {
description:
"disallow useless backreferences in regular expressions",
recommended: false,
},
schema: [],
messages: {
nested:
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
forward:
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
backward:
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
disjunctive:
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
intoNegativeLookaround:
"Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround.",
},
type: "suggestion", // "problem",
},
create(context) {
/**
* Create visitor
* @param node
*/
function createVisitor(node: Expression): RegExpVisitor.Handlers {
return {
onBackreferenceEnter(bref) {
const group = bref.resolved,
brefPath = getPathToRoot(bref),
groupPath = getPathToRoot(group)
let messageId = null

if (brefPath.includes(group)) {
// group is bref's ancestor => bref is nested ('nested reference') => group hasn't matched yet when bref starts to match.
messageId = "nested"
} else {
// Start from the root to find the lowest common ancestor.
let i = brefPath.length - 1,
j = groupPath.length - 1

do {
i--
j--
} while (brefPath[i] === groupPath[j])

const indexOfLowestCommonAncestor = j + 1,
groupCut = groupPath.slice(
0,
indexOfLowestCommonAncestor,
),
commonPath = groupPath.slice(
indexOfLowestCommonAncestor,
),
lowestCommonLookaround = commonPath.find(
isLookaround,
),
isMatchingBackward =
lowestCommonLookaround &&
lowestCommonLookaround.kind === "lookbehind"

if (!isMatchingBackward && bref.end <= group.start) {
// bref is left, group is right ('forward reference') => group hasn't matched yet when bref starts to match.
messageId = "forward"
} else if (
isMatchingBackward &&
group.end <= bref.start
) {
// the opposite of the previous when the regex is matching backward in a lookbehind context.
messageId = "backward"
} else if (last(groupCut).type === "Alternative") {
// group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
messageId = "disjunctive"
} else if (groupCut.some(isNegativeLookaround)) {
// group is in a negative lookaround which isn't bref's ancestor => group has already failed when bref starts to match.
messageId = "intoNegativeLookaround"
}
}

if (messageId) {
context.report({
node,
messageId,
data: {
bref: bref.raw,
group: group.raw,
},
})
}
},
}
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion
import noEscapeBackspace from "../rules/no-escape-backspace"
import noInvisibleCharacter from "../rules/no-invisible-character"
import noOctal from "../rules/no-octal"
import noUselessBackreference from "../rules/no-useless-backreference"
import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
import preferD from "../rules/prefer-d"
Expand All @@ -25,6 +26,7 @@ export const rules = [
noEscapeBackspace,
noInvisibleCharacter,
noOctal,
noUselessBackreference,
noUselessExactlyQuantifier,
noUselessTwoNumsQuantifier,
preferD,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-regexp",
"version": "0.1.0",
"version": "0.1.1",
"description": "ESLint plugin for finding RegExp mistakes and RegExp style guide violations.",
"main": "dist/index.js",
"files": [
Expand Down
19 changes: 19 additions & 0 deletions tests/lib/rules/no-useless-backreference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { RuleTester } from "eslint"
import rule from "../../../lib/rules/no-useless-backreference"

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
})

tester.run("no-useless-backreference", rule as any, {
valid: ["/.(?=(b))\\1/"],
invalid: [
{
code: "/(b)(\\2a)/",
errors: [{ messageId: "nested" }],
},
],
})
9 changes: 7 additions & 2 deletions tools/update-rulesets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ const coreRules = [
"no-invalid-regexp",
"no-misleading-character-class",
"no-regex-spaces",
"no-useless-backreference",
"prefer-regex-literals",
// "prefer-named-capture-group", // modern
// "require-unicode-regexp", // modern
]

let content = `
export default {
import eslint from "eslint"
export = {
plugins: ["regexp"],
rules: {
// ESLint core rules
${coreRules.map((ruleName) => `"${ruleName}": "error"`).join(",\n")},
// If ESLint is 7 or higher, use core rule. If it is 6 or less, use the copied rule.
[parseInt(eslint.Linter.version[0], 10) >= 7
? "no-useless-backreference"
: "regexp/no-useless-backreference"]: "error",
// eslint-plugin-regexp rules
${rules
Expand Down

0 comments on commit 1fa6d75

Please sign in to comment.