Skip to content

Commit

Permalink
Add regexp/no-useless-set-operand rule (#625)
Browse files Browse the repository at this point in the history
* Add `regexp/no-useless-set-operand` rule

* Added docs

* Create nervous-yaks-destroy.md

* Apply suggestions from code review

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

* npm run update

---------

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
  • Loading branch information
RunDevelopment and ota-meshi authored Oct 1, 2023
1 parent fb15338 commit f75cbba
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-yaks-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-regexp": major
---

Add `regexp/no-useless-set-operand` rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
| [no-useless-lazy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-lazy.html) | disallow unnecessarily non-greedy quantifiers || | 🔧 | |
| [no-useless-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html) | disallow quantifiers that can be removed || | 🔧 | 💡 |
| [no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary character ranges || | 🔧 | |
| [no-useless-set-operand](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-set-operand.html) | disallow unnecessary elements in expression character classes || | 🔧 | |
| [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 || | 🔧 | |
| [no-zero-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html) | disallow quantifiers with a maximum of zero || | | 💡 |
| [optimal-lookaround-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-lookaround-quantifier.html) | disallow the alternatives of lookarounds that end with a non-constant quantifier | || | 💡 |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ sidebarDepth: 0
| [no-useless-lazy](no-useless-lazy.md) | disallow unnecessarily non-greedy quantifiers || | 🔧 | |
| [no-useless-quantifier](no-useless-quantifier.md) | disallow quantifiers that can be removed || | 🔧 | 💡 |
| [no-useless-range](no-useless-range.md) | disallow unnecessary character ranges || | 🔧 | |
| [no-useless-set-operand](no-useless-set-operand.md) | disallow unnecessary elements in expression character classes || | 🔧 | |
| [no-useless-two-nums-quantifier](no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier || | 🔧 | |
| [no-zero-quantifier](no-zero-quantifier.md) | disallow quantifiers with a maximum of zero || | | 💡 |
| [optimal-lookaround-quantifier](optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | || | 💡 |
Expand Down
56 changes: 56 additions & 0 deletions docs/rules/no-useless-set-operand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/no-useless-set-operand"
description: "disallow unnecessary elements in expression character classes"
---
# regexp/no-useless-set-operand

💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

> disallow unnecessary elements in expression character classes
## :book: Rule Details

The `v` flag added set operations for character classes, e.g. `[\w&&\D]` and `[\w--\d]`, but there are no limitations on what operands can be used. This rule reports any unnecessary operands.

<eslint-code-block fix>

```js
/* eslint regexp/no-useless-set-operand: "error" */

/* ✓ GOOD */
foo = /[\w--\d]/v
foo = /[\w--[\d_]]/v

/* ✗ BAD */
foo = /[\w--[\d$]]/v
foo = /[\w&&\d]/v
foo = /[\w&&\s]/v
foo = /[\w&&[\d\s]]/v
foo = /[\w&&[^\d\s]]/v
foo = /[\w--\s]/v
foo = /[\d--\w]/v
foo = /[\w--[\d\s]]/v
foo = /[\w--[^\d\s]]/v

```

</eslint-code-block>

## :wrench: Options

Nothing.

## :rocket: Version

:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-set-operand.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-set-operand.ts)
1 change: 1 addition & 0 deletions lib/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const rules = {
"regexp/no-useless-non-capturing-group": "error",
"regexp/no-useless-quantifier": "error",
"regexp/no-useless-range": "error",
"regexp/no-useless-set-operand": "error",
"regexp/no-useless-two-nums-quantifier": "error",
"regexp/no-zero-quantifier": "error",
"regexp/optimal-lookaround-quantifier": "warn",
Expand Down
240 changes: 240 additions & 0 deletions lib/rules/no-useless-set-operand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import type {
CharacterClassElement,
ClassSetOperand,
ExpressionCharacterClass,
Node,
StringAlternative,
} from "@eslint-community/regexpp/ast"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
import { toUnicodeSet } from "regexp-ast-analysis"

type FlatElement = CharacterClassElement | StringAlternative

function getFlatElements(
node: ClassSetOperand | ExpressionCharacterClass["expression"],
): readonly FlatElement[] {
if (node.type === "ClassStringDisjunction") {
return node.alternatives
}
if (node.type === "CharacterClass") {
const nested: FlatElement[] = []
// eslint-disable-next-line func-style -- x
const addElement = (element: CharacterClassElement) => {
if (element.type === "ClassStringDisjunction") {
nested.push(...element.alternatives)
} else if (element.type === "CharacterClass") {
if (!element.negate) {
nested.push(...element.elements)
}
nested.push(element)
} else {
nested.push(element)
}
}
node.elements.forEach(addElement)
return nested
}

return []
}

function removeDescendant(root: Node, e: FlatElement): string {
let { start, end } = e

if (e.type === "StringAlternative") {
if (e.parent.alternatives.length === 1) {
// we have to remove the whole string disjunction
// eslint-disable-next-line no-param-reassign -- x
e = e.parent
start = e.start
end = e.end
} else {
// remove one adjacent | symbol
if (e.parent.alternatives.at(-1) === e) {
start--
} else {
end++
}
}
}

const before = root.raw.slice(0, start - root.start)
const after = root.raw.slice(end - root.start)
return before + after
}

export default createRule("no-useless-set-operand", {
meta: {
docs: {
description:
"disallow unnecessary elements in expression character classes",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
intersectionDisjoint:
"'{{left}}' and '{{right}}' are disjoint, so the result of the intersection is always going to be the empty set.",
intersectionSubset:
"'{{sub}}' is a subset of '{{super}}', so the result of the intersection is always going to be '{{sub}}'.",
intersectionRemove:
"'{{expr}}' can be removed without changing the result of the intersection.",
subtractionDisjoint:
"'{{left}}' and '{{right}}' are disjoint, so the subtraction doesn't do anything.",
subtractionSubset:
"'{{left}}' is a subset of '{{right}}', so the result of the subtraction is always going to be the empty set.",
subtractionRemove:
"'{{expr}}' can be removed without changing the result of the subtraction.",
},
fixable: "code",
type: "suggestion",
},
create(context) {
function createVisitor(
regexpContext: RegExpContext,
): RegExpVisitor.Handlers {
const { node, flags, getRegexpLocation, fixReplaceNode } =
regexpContext

if (!flags.unicodeSets) {
// set operations are only available with the `v` flag
return {}
}

function fixRemoveExpression(
expr: ExpressionCharacterClass["expression"],
) {
if (expr.parent.type === "ExpressionCharacterClass") {
const cc = expr.parent
return fixReplaceNode(cc, cc.negate ? "[^]" : "[]")
}
return fixReplaceNode(expr, "[]")
}

return {
onClassIntersectionEnter(iNode) {
const leftSet = toUnicodeSet(iNode.left, flags)
const rightSet = toUnicodeSet(iNode.right, flags)

if (leftSet.isDisjointWith(rightSet)) {
context.report({
node,
loc: getRegexpLocation(iNode),
messageId: "intersectionDisjoint",
data: {
left: iNode.left.raw,
right: iNode.right.raw,
},
fix: fixRemoveExpression(iNode),
})
return
}

if (leftSet.isSubsetOf(rightSet)) {
context.report({
node,
loc: getRegexpLocation(iNode),
messageId: "intersectionSubset",
data: {
sub: iNode.left.raw,
super: iNode.right.raw,
},
fix: fixReplaceNode(iNode, iNode.left.raw),
})
return
}
if (rightSet.isSubsetOf(leftSet)) {
context.report({
node,
loc: getRegexpLocation(iNode),
messageId: "intersectionSubset",
data: {
sub: iNode.right.raw,
super: iNode.left.raw,
},
fix: fixReplaceNode(iNode, iNode.right.raw),
})
return
}

const toRemoveRight = getFlatElements(iNode.right).filter(
(e) => leftSet.isDisjointWith(toUnicodeSet(e, flags)),
)
const toRemoveLeft = getFlatElements(iNode.left).filter(
(e) => rightSet.isDisjointWith(toUnicodeSet(e, flags)),
)
for (const e of [...toRemoveRight, ...toRemoveLeft]) {
context.report({
node,
loc: getRegexpLocation(e),
messageId: "subtractionRemove",
data: {
expr: e.raw,
},
fix: fixReplaceNode(
iNode,
removeDescendant(iNode, e),
),
})
}
},
onClassSubtractionEnter(sNode) {
const leftSet = toUnicodeSet(sNode.left, flags)
const rightSet = toUnicodeSet(sNode.right, flags)

if (leftSet.isDisjointWith(rightSet)) {
context.report({
node,
loc: getRegexpLocation(sNode),
messageId: "subtractionDisjoint",
data: {
left: sNode.left.raw,
right: sNode.right.raw,
},
fix: fixReplaceNode(sNode, sNode.left.raw),
})
return
}

if (leftSet.isSubsetOf(rightSet)) {
context.report({
node,
loc: getRegexpLocation(sNode),
messageId: "subtractionSubset",
data: {
left: sNode.left.raw,
right: sNode.right.raw,
},
fix: fixRemoveExpression(sNode),
})
return
}

const toRemove = getFlatElements(sNode.right).filter((e) =>
leftSet.isDisjointWith(toUnicodeSet(e, flags)),
)
for (const e of toRemove) {
context.report({
node,
loc: getRegexpLocation(e),
messageId: "subtractionRemove",
data: {
expr: e.raw,
},
fix: fixReplaceNode(
sNode,
removeDescendant(sNode, e),
),
})
}
},
}
}

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 @@ -47,6 +47,7 @@ import noUselessNonCapturingGroup from "../rules/no-useless-non-capturing-group"
import noUselessNonGreedy from "../rules/no-useless-non-greedy"
import noUselessQuantifier from "../rules/no-useless-quantifier"
import noUselessRange from "../rules/no-useless-range"
import noUselessSetOperand from "../rules/no-useless-set-operand"
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
import noZeroQuantifier from "../rules/no-zero-quantifier"
import optimalLookaroundQuantifier from "../rules/optimal-lookaround-quantifier"
Expand Down Expand Up @@ -130,6 +131,7 @@ export const rules = [
noUselessNonGreedy,
noUselessQuantifier,
noUselessRange,
noUselessSetOperand,
noUselessTwoNumsQuantifier,
noZeroQuantifier,
optimalLookaroundQuantifier,
Expand Down
Loading

0 comments on commit f75cbba

Please sign in to comment.