-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
regexp/no-useless-set-operand
rule (#625)
* 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
1 parent
fb15338
commit f75cbba
Showing
8 changed files
with
412 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.