Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp/no-useless-set-operand rule #625

Merged
merged 5 commits into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": patch
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
---

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
54 changes: 54 additions & 0 deletions docs/rules/no-useless-set-operand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
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 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)
242 changes: 242 additions & 0 deletions lib/rules/no-useless-set-operand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
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
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
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",
// TODO Switch to recommended in the major version.

Check warning on line 74 in lib/rules/no-useless-set-operand.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO Switch to recommended in the major...'

Check warning on line 74 in lib/rules/no-useless-set-operand.ts

View workflow job for this annotation

GitHub Actions / update

Unexpected 'todo' comment: 'TODO Switch to recommended in the major...'
// recommended: true,
recommended: false,
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
},
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 @@ -129,6 +130,7 @@ export const rules = [
noUselessNonGreedy,
noUselessQuantifier,
noUselessRange,
noUselessSetOperand,
noUselessTwoNumsQuantifier,
noZeroQuantifier,
optimalLookaroundQuantifier,
Expand Down
Loading
Loading