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

Updated no-useless-non-greedy rule #143

Merged
merged 1 commit into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | |
| [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-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary Non-capturing group | :wrench: |
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessarily non-greedy quantifiers | :wrench: |
| [regexp/no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary range of characters by using a hyphen | :wrench: |
| [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::wrench: |
| [regexp/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
2 changes: 1 addition & 1 deletion docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-useless-escape](./no-useless-escape.md) | disallow unnecessary escape characters in RegExp | |
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star::wrench: |
| [regexp/optimal-lookaround-quantifier](./optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | |
Expand Down
20 changes: 17 additions & 3 deletions docs/rules/no-useless-non-greedy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/no-useless-non-greedy"
description: "disallow unnecessary quantifier non-greedy (`?`)"
description: "disallow unnecessarily non-greedy quantifiers"
since: "v0.3.0"
---
# regexp/no-useless-non-greedy

> disallow unnecessary quantifier non-greedy (`?`)
> disallow unnecessarily non-greedy quantifiers

- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.

## :book: Rule Details

This rule reports unnecessary quantifier non-greedy (`?`).
This rule reports lazy quantifiers that don't need to by lazy.

There are two reasons why a lazy quantifier doesn't have to lazy:

1. It's a constant quantifier (e.g. `a{3}?`).

2. The quantifier is effectively possessive (e.g. `a+?b`).

Whether a quantifier (let's call it _q_) is effectively possessive depends on the expression after it (let's call it _e_). _q_ is effectively possessive if _q_ cannot accept the character accepted by _e_ and _e_ cannot accept the characters accepted by _q_.

In the example above, the character `a` and the character `b` do not overlap. Therefore the quantifier `a+` is possessive.

Since an effectively possessive quantifier cannot give up characters to the expression after it, it doesn't matter whether the quantifier greedy or lazy. However, greedy quantifiers should be preferred because they require less characters to write and are easier to visually parse.

<eslint-code-block fix>

Expand All @@ -25,11 +37,13 @@ var foo = /a*?/;
var foo = /a+?/;
var foo = /a{4,}?/;
var foo = /a{2,4}?/;
var foo = /a[\s\S]*?bar/;

/* ✗ BAD */
var foo = /a{1}?/;
var foo = /a{4}?/;
var foo = /a{2,2}?/;
var foo = /ab+?c/;
```

</eslint-code-block>
Expand Down
117 changes: 92 additions & 25 deletions lib/rules/no-useless-non-greedy.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,63 @@
import type { Expression } from "estree"
import type { Rule, SourceCode } from "eslint"
import type { Expression, SourceLocation } from "estree"
import {
getMatchingDirection,
getFirstConsumedChar,
getFirstCharAfter,
} from "regexp-ast-analysis"
import type { Quantifier } from "regexpp/ast"
import type { RegExpVisitor } from "regexpp/visitor"
import {
createRule,
defineRegexpVisitor,
getRegexpLocation,
getRegexpRange,
parseFlags,
} from "../utils"

/**
* Returns a fix that makes the given quantifier greedy.
*/
function makeGreedy(
sourceCode: SourceCode,
node: Expression,
qNode: Quantifier,
) {
return (fixer: Rule.RuleFixer): Rule.Fix | null => {
const range = getRegexpRange(sourceCode, node, qNode)
if (range == null) {
return null
}
return fixer.removeRange([range[1] - 1, range[1]])
}
}

/**
* Returns the source location of the lazy modifier of the given quantifier.
*/
function getLazyLoc(
sourceCode: SourceCode,
node: Expression,
qNode: Quantifier,
): SourceLocation {
const offset = qNode.raw.length - 1
return getRegexpLocation(sourceCode, node, qNode, [offset, offset + 1])
}

export default createRule("no-useless-non-greedy", {
meta: {
docs: {
description: "disallow unnecessary quantifier non-greedy (`?`)",
description: "disallow unnecessarily non-greedy quantifiers",
// TODO In the major version
// recommended: true,
recommended: false,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected quantifier non-greedy.",
constant: "Unexpected non-greedy constant quantifier.",
possessive:
"Unexpected non-greedy constant quantifier. The quantifier is effectively possessive, so it doesn't matter whether it is greedy or not.",
},
type: "suggestion", // "problem",
},
Expand All @@ -29,33 +68,61 @@ export default createRule("no-useless-non-greedy", {
* Create visitor
* @param node
*/
function createVisitor(node: Expression): RegExpVisitor.Handlers {
function createVisitor(
node: Expression,
_pattern: string,
flagsStr: string,
): RegExpVisitor.Handlers {
const flags = parseFlags(flagsStr)

return {
onQuantifierEnter(qNode) {
if (qNode.greedy === false && qNode.min === qNode.max) {
const offset = qNode.raw.length - 1
if (qNode.greedy) {
return
}

if (qNode.min === qNode.max) {
// a constant lazy quantifier (e.g. /a{2}?/)

context.report({
node,
loc: getRegexpLocation(sourceCode, node, qNode, [
offset,
offset + 1,
]),
messageId: "unexpected",
fix(fixer) {
const range = getRegexpRange(
sourceCode,
node,
qNode,
)
if (range == null) {
return null
}
return fixer.removeRange([
range[1] - 1,
range[1],
])
},
loc: getLazyLoc(sourceCode, node, qNode),
messageId: "constant",
fix: makeGreedy(sourceCode, node, qNode),
})
return
}

// This is more tricky.
// The basic idea here is that if the first character of the
// quantified element and the first character of whatever
// comes after the quantifier are always different, then the
// lazy modifier doesn't matter.
// E.g. /a+?b+/ == /a+b+/

const matchingDir = getMatchingDirection(qNode)
const firstChar = getFirstConsumedChar(
qNode,
matchingDir,
flags,
)
if (!firstChar.empty) {
const after = getFirstCharAfter(
qNode,
matchingDir,
flags,
)
if (
!after.edge &&
firstChar.char.isDisjointWith(after.char)
) {
context.report({
node,
loc: getLazyLoc(sourceCode, node, qNode),
messageId: "possessive",
fix: makeGreedy(sourceCode, node, qNode),
})
}
}
},
}
Expand Down
33 changes: 27 additions & 6 deletions tests/lib/rules/no-useless-non-greedy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ const tester = new RuleTester({
})

tester.run("no-useless-non-greedy", rule as any, {
valid: [`/a*?/`, `/a+?/`, `/a{4,}?/`, `/a{2,4}?/`, `/a{2,2}/`, `/a{3}/`],
valid: [
`/a*?/`,
`/a+?/`,
`/a{4,}?/`,
`/a{2,4}?/`,
`/a{2,2}/`,
`/a{3}/`,
`/a+?b*/`,
`/[\\s\\S]+?bar/`,
`/a??a?/`,
],
invalid: [
{
code: `/a{1}?/`,
output: `/a{1}/`,
errors: [
{
message: "Unexpected quantifier non-greedy.",
messageId: "constant",
line: 1,
column: 6,
endLine: 1,
Expand All @@ -29,7 +39,7 @@ tester.run("no-useless-non-greedy", rule as any, {
output: `/a{4}/`,
errors: [
{
message: "Unexpected quantifier non-greedy.",
messageId: "constant",
line: 1,
column: 6,
endLine: 1,
Expand All @@ -40,20 +50,31 @@ tester.run("no-useless-non-greedy", rule as any, {
{
code: `/a{2,2}?/`,
output: `/a{2,2}/`,
errors: ["Unexpected quantifier non-greedy."],
errors: [{ messageId: "constant" }],
},
{
code: String.raw`const s = "\\d{1}?"
new RegExp(s)`,
output: String.raw`const s = "\\d{1}"
new RegExp(s)`,
errors: ["Unexpected quantifier non-greedy."],
errors: [{ messageId: "constant" }],
},
{
code: String.raw`const s = "\\d"+"{1}?"
new RegExp(s)`,
output: null,
errors: ["Unexpected quantifier non-greedy."],
errors: [{ messageId: "constant" }],
},

{
code: `/a+?b+/`,
output: `/a+b+/`,
errors: [{ messageId: "possessive" }],
},
{
code: `/(?:a|cd)+?(?:b+|zzz)/`,
output: `/(?:a|cd)+(?:b+|zzz)/`,
errors: [{ messageId: "possessive" }],
},
],
})