Skip to content

Commit 2c051cd

Browse files
authored
Add regexp/no-useless-lazy rule that same regexp/no-useless-non-greedy rule (#174)
1 parent 58324c1 commit 2c051cd

File tree

11 files changed

+279
-209
lines changed

11 files changed

+279
-209
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
115115
| [regexp/no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | |
116116
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
117117
| [regexp/no-useless-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-flag.html) | disallow unnecessary regex flags | :wrench: |
118+
| [regexp/no-useless-lazy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-lazy.html) | disallow unnecessarily non-greedy quantifiers | :wrench: |
118119
| [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: |
119120
| [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: |
120121
| [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: |

docs/.vuepress/components/rules/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ const allRules = []
3737

3838
for (const k of Object.keys(plugin.rules)) {
3939
const rule = plugin.rules[k]
40+
if (rule.meta.deprecated) {
41+
continue
42+
}
4043
rule.meta.docs.category = rule.meta.docs.recommended
4144
? "recommended"
4245
: "uncategorized"

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
4343
| [regexp/no-useless-escape](./no-useless-escape.md) | disallow unnecessary escape characters in RegExp | |
4444
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
4545
| [regexp/no-useless-flag](./no-useless-flag.md) | disallow unnecessary regex flags | :wrench: |
46+
| [regexp/no-useless-lazy](./no-useless-lazy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
4647
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
4748
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
4849
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |

docs/rules/no-useless-lazy.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-lazy"
5+
description: "disallow unnecessarily non-greedy quantifiers"
6+
---
7+
# regexp/no-useless-lazy
8+
9+
> disallow unnecessarily non-greedy quantifiers
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
This rule reports lazy quantifiers that don't need to by lazy.
17+
18+
There are two reasons why a lazy quantifier doesn't have to lazy:
19+
20+
1. It's a constant quantifier (e.g. `a{3}?`).
21+
22+
2. The quantifier is effectively possessive (e.g. `a+?b`).
23+
24+
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_.
25+
26+
In the example above, the character `a` and the character `b` do not overlap. Therefore the quantifier `a+` is possessive.
27+
28+
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.
29+
30+
<eslint-code-block fix>
31+
32+
```js
33+
/* eslint regexp/no-useless-lazy: "error" */
34+
35+
/* ✓ GOOD */
36+
var foo = /a*?/;
37+
var foo = /a+?/;
38+
var foo = /a{4,}?/;
39+
var foo = /a{2,4}?/;
40+
var foo = /a[\s\S]*?bar/;
41+
42+
/* ✗ BAD */
43+
var foo = /a{1}?/;
44+
var foo = /a{4}?/;
45+
var foo = /a{2,2}?/;
46+
var foo = /ab+?c/;
47+
```
48+
49+
</eslint-code-block>
50+
51+
## :wrench: Options
52+
53+
Nothing.
54+
55+
## :mag: Implementation
56+
57+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-lazy.ts)
58+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-lazy.ts)

docs/rules/no-useless-non-greedy.md

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,10 @@ since: "v0.3.0"
1313

1414
## :book: Rule Details
1515

16-
This rule reports lazy quantifiers that don't need to by lazy.
16+
This rule is the same as the [regexp/no-useless-lazy] rule. Use [regexp/no-useless-lazy] instead.
17+
Replaced by [regexp/no-useless-lazy] in v1.0.0, this rule will be marked as **deprecated**.
1718

18-
There are two reasons why a lazy quantifier doesn't have to lazy:
19-
20-
1. It's a constant quantifier (e.g. `a{3}?`).
21-
22-
2. The quantifier is effectively possessive (e.g. `a+?b`).
23-
24-
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_.
25-
26-
In the example above, the character `a` and the character `b` do not overlap. Therefore the quantifier `a+` is possessive.
27-
28-
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.
29-
30-
<eslint-code-block fix>
31-
32-
```js
33-
/* eslint regexp/no-useless-non-greedy: "error" */
34-
35-
/* ✓ GOOD */
36-
var foo = /a*?/;
37-
var foo = /a+?/;
38-
var foo = /a{4,}?/;
39-
var foo = /a{2,4}?/;
40-
var foo = /a[\s\S]*?bar/;
41-
42-
/* ✗ BAD */
43-
var foo = /a{1}?/;
44-
var foo = /a{4}?/;
45-
var foo = /a{2,2}?/;
46-
var foo = /ab+?c/;
47-
```
48-
49-
</eslint-code-block>
50-
51-
## :wrench: Options
52-
53-
Nothing.
19+
[regexp/no-useless-lazy]: no-useless-lazy.md
5420

5521
## :rocket: Version
5622

lib/rules/no-useless-lazy.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { Rule } from "eslint"
2+
import type { SourceLocation } from "estree"
3+
import {
4+
getMatchingDirection,
5+
getFirstConsumedChar,
6+
getFirstCharAfter,
7+
} from "regexp-ast-analysis"
8+
import type { Quantifier } from "regexpp/ast"
9+
import type { RegExpVisitor } from "regexpp/visitor"
10+
import type { RegExpContext } from "../utils"
11+
import { createRule, defineRegexpVisitor } from "../utils"
12+
13+
/**
14+
* Returns a fix that makes the given quantifier greedy.
15+
*/
16+
function makeGreedy({ getRegexpRange }: RegExpContext, qNode: Quantifier) {
17+
return (fixer: Rule.RuleFixer): Rule.Fix | null => {
18+
const range = getRegexpRange(qNode)
19+
if (range == null) {
20+
return null
21+
}
22+
return fixer.removeRange([range[1] - 1, range[1]])
23+
}
24+
}
25+
26+
/**
27+
* Returns the source location of the lazy modifier of the given quantifier.
28+
*/
29+
function getLazyLoc(
30+
{ getRegexpLocation }: RegExpContext,
31+
qNode: Quantifier,
32+
): SourceLocation {
33+
const offset = qNode.raw.length - 1
34+
return getRegexpLocation(qNode, [offset, offset + 1])
35+
}
36+
37+
export default createRule("no-useless-lazy", {
38+
meta: {
39+
docs: {
40+
description: "disallow unnecessarily non-greedy quantifiers",
41+
// TODO In the major version
42+
// recommended: true,
43+
recommended: false,
44+
},
45+
fixable: "code",
46+
schema: [],
47+
messages: {
48+
constant: "Unexpected non-greedy constant quantifier.",
49+
possessive:
50+
"Unexpected non-greedy constant quantifier. The quantifier is effectively possessive, so it doesn't matter whether it is greedy or not.",
51+
},
52+
type: "suggestion", // "problem",
53+
},
54+
create(context) {
55+
/**
56+
* Create visitor
57+
*/
58+
function createVisitor(
59+
regexpContext: RegExpContext,
60+
): RegExpVisitor.Handlers {
61+
const { node, flags } = regexpContext
62+
return {
63+
onQuantifierEnter(qNode) {
64+
if (qNode.greedy) {
65+
return
66+
}
67+
68+
if (qNode.min === qNode.max) {
69+
// a constant lazy quantifier (e.g. /a{2}?/)
70+
71+
context.report({
72+
node,
73+
loc: getLazyLoc(regexpContext, qNode),
74+
messageId: "constant",
75+
fix: makeGreedy(regexpContext, qNode),
76+
})
77+
return
78+
}
79+
80+
// This is more tricky.
81+
// The basic idea here is that if the first character of the
82+
// quantified element and the first character of whatever
83+
// comes after the quantifier are always different, then the
84+
// lazy modifier doesn't matter.
85+
// E.g. /a+?b+/ == /a+b+/
86+
87+
const matchingDir = getMatchingDirection(qNode)
88+
const firstChar = getFirstConsumedChar(
89+
qNode,
90+
matchingDir,
91+
flags,
92+
)
93+
if (!firstChar.empty) {
94+
const after = getFirstCharAfter(
95+
qNode,
96+
matchingDir,
97+
flags,
98+
)
99+
if (
100+
!after.edge &&
101+
firstChar.char.isDisjointWith(after.char)
102+
) {
103+
context.report({
104+
node,
105+
loc: getLazyLoc(regexpContext, qNode),
106+
messageId: "possessive",
107+
fix: makeGreedy(regexpContext, qNode),
108+
})
109+
}
110+
}
111+
},
112+
}
113+
}
114+
115+
return defineRegexpVisitor(context, {
116+
createVisitor,
117+
})
118+
},
119+
})

lib/rules/no-useless-non-greedy.ts

Lines changed: 8 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,19 @@
1-
import type { Rule } from "eslint"
2-
import type { SourceLocation } from "estree"
3-
import {
4-
getMatchingDirection,
5-
getFirstConsumedChar,
6-
getFirstCharAfter,
7-
} from "regexp-ast-analysis"
8-
import type { Quantifier } from "regexpp/ast"
9-
import type { RegExpVisitor } from "regexpp/visitor"
10-
import type { RegExpContext } from "../utils"
11-
import { createRule, defineRegexpVisitor } from "../utils"
1+
import { createRule } from "../utils"
122

13-
/**
14-
* Returns a fix that makes the given quantifier greedy.
15-
*/
16-
function makeGreedy({ getRegexpRange }: RegExpContext, qNode: Quantifier) {
17-
return (fixer: Rule.RuleFixer): Rule.Fix | null => {
18-
const range = getRegexpRange(qNode)
19-
if (range == null) {
20-
return null
21-
}
22-
return fixer.removeRange([range[1] - 1, range[1]])
23-
}
24-
}
25-
26-
/**
27-
* Returns the source location of the lazy modifier of the given quantifier.
28-
*/
29-
function getLazyLoc(
30-
{ getRegexpLocation }: RegExpContext,
31-
qNode: Quantifier,
32-
): SourceLocation {
33-
const offset = qNode.raw.length - 1
34-
return getRegexpLocation(qNode, [offset, offset + 1])
35-
}
3+
import nonUselessLazy from "./no-useless-lazy"
364

375
export default createRule("no-useless-non-greedy", {
386
meta: {
7+
...nonUselessLazy.meta,
398
docs: {
40-
description: "disallow unnecessarily non-greedy quantifiers",
41-
// TODO In the major version
42-
// recommended: true,
9+
...nonUselessLazy.meta.docs,
4310
recommended: false,
11+
replacedBy: ["no-useless-lazy"],
4412
},
45-
fixable: "code",
46-
schema: [],
47-
messages: {
48-
constant: "Unexpected non-greedy constant quantifier.",
49-
possessive:
50-
"Unexpected non-greedy constant quantifier. The quantifier is effectively possessive, so it doesn't matter whether it is greedy or not.",
51-
},
52-
type: "suggestion", // "problem",
13+
// TODO Switch to deprecated in the major version.
14+
// deprecated: true,
5315
},
5416
create(context) {
55-
/**
56-
* Create visitor
57-
*/
58-
function createVisitor(
59-
regexpContext: RegExpContext,
60-
): RegExpVisitor.Handlers {
61-
const { node, flags } = regexpContext
62-
return {
63-
onQuantifierEnter(qNode) {
64-
if (qNode.greedy) {
65-
return
66-
}
67-
68-
if (qNode.min === qNode.max) {
69-
// a constant lazy quantifier (e.g. /a{2}?/)
70-
71-
context.report({
72-
node,
73-
loc: getLazyLoc(regexpContext, qNode),
74-
messageId: "constant",
75-
fix: makeGreedy(regexpContext, qNode),
76-
})
77-
return
78-
}
79-
80-
// This is more tricky.
81-
// The basic idea here is that if the first character of the
82-
// quantified element and the first character of whatever
83-
// comes after the quantifier are always different, then the
84-
// lazy modifier doesn't matter.
85-
// E.g. /a+?b+/ == /a+b+/
86-
87-
const matchingDir = getMatchingDirection(qNode)
88-
const firstChar = getFirstConsumedChar(
89-
qNode,
90-
matchingDir,
91-
flags,
92-
)
93-
if (!firstChar.empty) {
94-
const after = getFirstCharAfter(
95-
qNode,
96-
matchingDir,
97-
flags,
98-
)
99-
if (
100-
!after.edge &&
101-
firstChar.char.isDisjointWith(after.char)
102-
) {
103-
context.report({
104-
node,
105-
loc: getLazyLoc(regexpContext, qNode),
106-
messageId: "possessive",
107-
fix: makeGreedy(regexpContext, qNode),
108-
})
109-
}
110-
}
111-
},
112-
}
113-
}
114-
115-
return defineRegexpVisitor(context, {
116-
createVisitor,
117-
})
17+
return nonUselessLazy.create(context)
11818
},
11919
})

lib/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface RuleMetaData {
1515
url: string
1616
ruleId: string
1717
ruleName: string
18-
replacedBy?: []
18+
replacedBy?: string[]
1919
default?: "error" | "warn"
2020
}
2121
messages: { [messageId: string]: string }
@@ -34,7 +34,7 @@ export interface PartialRuleMetaData {
3434
docs: {
3535
description: string
3636
recommended: boolean
37-
replacedBy?: []
37+
replacedBy?: string[]
3838
default?: "error" | "warn"
3939
}
4040
messages: { [messageId: string]: string }

0 commit comments

Comments
 (0)