Skip to content

Commit c3b1afa

Browse files
authored
Add regexp/sort-character-class-elements rule that same regexp/order-in-character-class rule (#237)
1 parent 3bdde2d commit c3b1afa

9 files changed

+572
-503
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
169169
| [regexp/prefer-t](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-t.html) | enforce using `\t` | :star::wrench: |
170170
| [regexp/prefer-unicode-codepoint-escapes](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-unicode-codepoint-escapes.html) | enforce use of unicode codepoint escapes | :wrench: |
171171
| [regexp/prefer-w](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html) | enforce using `\w` | :star::wrench: |
172+
| [regexp/sort-character-class-elements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-character-class-elements.html) | enforces elements order in character class | :wrench: |
172173
| [regexp/sort-flags](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-flags.html) | require regex flags to be sorted | :wrench: |
173174
| [regexp/unicode-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/unicode-escape.html) | enforce consistent usage of unicode escape or unicode codepoint escape | :wrench: |
174175

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,6 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8383
| [regexp/prefer-t](./prefer-t.md) | enforce using `\t` | :star::wrench: |
8484
| [regexp/prefer-unicode-codepoint-escapes](./prefer-unicode-codepoint-escapes.md) | enforce use of unicode codepoint escapes | :wrench: |
8585
| [regexp/prefer-w](./prefer-w.md) | enforce using `\w` | :star::wrench: |
86+
| [regexp/sort-character-class-elements](./sort-character-class-elements.md) | enforces elements order in character class | :wrench: |
8687
| [regexp/sort-flags](./sort-flags.md) | require regex flags to be sorted | :wrench: |
8788
| [regexp/unicode-escape](./unicode-escape.md) | enforce consistent usage of unicode escape or unicode codepoint escape | :wrench: |

docs/rules/order-in-character-class.md

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,10 @@ since: "v0.4.0"
1313

1414
## :book: Rule Details
1515

16-
This rule checks elements of character classes are sorted.
16+
This rule is the same as the [regexp/sort-character-class-elements] rule. Use [regexp/sort-character-class-elements] instead.
17+
Replaced by [regexp/sort-character-class-elements] in v1.0.0, this rule will be marked as **deprecated**.
1718

18-
<eslint-code-block fix>
19-
20-
```js
21-
/* eslint regexp/order-in-character-class: "error" */
22-
23-
/* ✓ GOOD */
24-
var foo = /[abcdef]/
25-
var foo = /[ab-f]/
26-
27-
/* ✗ BAD */
28-
var foo = /[bcdefa]/
29-
var foo = /[b-fa]/
30-
```
31-
32-
</eslint-code-block>
33-
34-
## :wrench: Options
35-
36-
```json5
37-
{
38-
"regexp/order-in-character-class": ["error", {
39-
"order": [
40-
"\\s", // \s or \S
41-
"\\w", // \w or \W
42-
"\\d", // \d or \D
43-
"\\p", // \p{...} or \P{...}
44-
"*", // Others (A character or range of characters or an element you did not specify.)
45-
]
46-
}]
47-
}
48-
```
49-
50-
- `"order"` ... An array of your preferred order. The default is `["\\s", "\\w", "\\d", "\\p", "*",]`.
19+
[regexp/sort-character-class-elements]: no-useless-lazy.md
5120

5221
## :rocket: Version
5322

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/sort-character-class-elements"
5+
description: "enforces elements order in character class"
6+
---
7+
# regexp/sort-character-class-elements
8+
9+
> enforces elements order in character class
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 checks elements of character classes are sorted.
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/sort-character-class-elements: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /[abcdef]/
25+
var foo = /[ab-f]/
26+
27+
/* ✗ BAD */
28+
var foo = /[bcdefa]/
29+
var foo = /[b-fa]/
30+
```
31+
32+
</eslint-code-block>
33+
34+
## :wrench: Options
35+
36+
```json5
37+
{
38+
"regexp/sort-character-class-elements": ["error", {
39+
"order": [
40+
"\\s", // \s or \S
41+
"\\w", // \w or \W
42+
"\\d", // \d or \D
43+
"\\p", // \p{...} or \P{...}
44+
"*", // Others (A character or range of characters or an element you did not specify.)
45+
]
46+
}]
47+
}
48+
```
49+
50+
- `"order"` ... An array of your preferred order. The default is `["\\s", "\\w", "\\d", "\\p", "*",]`.
51+
52+
## :mag: Implementation
53+
54+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/sort-character-class-elements.ts)
55+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/sort-character-class-elements.ts)

lib/rules/order-in-character-class.ts

Lines changed: 8 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -1,271 +1,19 @@
1-
import type { RegExpVisitor } from "regexpp/visitor"
2-
import type {
3-
CharacterClass,
4-
CharacterClassElement,
5-
UnicodePropertyCharacterSet,
6-
} from "regexpp/ast"
7-
import type { RegExpContext } from "../utils"
8-
import {
9-
CP_DIGIT_ZERO,
10-
CP_SPACE,
11-
createRule,
12-
defineRegexpVisitor,
13-
} from "../utils"
1+
import { createRule } from "../utils"
142

15-
type CharacterClassElementKind = "\\w" | "\\d" | "\\s" | "\\p" | "*"
16-
const DEFAULT_ORDER: CharacterClassElementKind[] = [
17-
"\\s",
18-
"\\w",
19-
"\\d",
20-
"\\p",
21-
"*",
22-
]
23-
24-
/**
25-
* Get kind of CharacterClassElement for given CharacterClassElement
26-
*/
27-
function getCharacterClassElementKind(
28-
node: CharacterClassElement,
29-
): CharacterClassElementKind {
30-
if (node.type === "CharacterSet") {
31-
return node.kind === "word"
32-
? "\\w"
33-
: node.kind === "digit"
34-
? "\\d"
35-
: node.kind === "space"
36-
? "\\s"
37-
: "\\p"
38-
}
39-
return "*"
40-
}
3+
import sortCharacterClassElements from "./sort-character-class-elements"
414

425
export default createRule("order-in-character-class", {
436
meta: {
7+
...sortCharacterClassElements.meta,
448
docs: {
45-
description: "enforces elements order in character class",
46-
category: "Stylistic Issues",
9+
...sortCharacterClassElements.meta.docs,
4710
recommended: false,
11+
replacedBy: ["sort-character-class-elements"],
4812
},
49-
fixable: "code",
50-
schema: [
51-
{
52-
type: "object",
53-
properties: {
54-
order: {
55-
type: "array",
56-
items: { enum: ["\\w", "\\d", "\\s", "\\p", "*"] },
57-
},
58-
},
59-
additionalProperties: false,
60-
},
61-
],
62-
messages: {
63-
sortElements:
64-
"Expected character class elements to be in ascending order. '{{next}}' should be before '{{prev}}'.",
65-
},
66-
type: "layout",
13+
// TODO Switch to deprecated in the major version.
14+
// deprecated: true,
6715
},
6816
create(context) {
69-
const orderOption: {
70-
"*": number
71-
"\\w"?: number
72-
"\\d"?: number
73-
"\\s"?: number
74-
"\\p"?: number
75-
} = { "*": Infinity }
76-
77-
;((context.options[0]?.order ??
78-
DEFAULT_ORDER) as CharacterClassElementKind[]).forEach((o, i) => {
79-
orderOption[o] = i + 1
80-
})
81-
82-
/**
83-
* Create visitor
84-
*/
85-
function createVisitor({
86-
node,
87-
fixerApplyEscape,
88-
getRegexpLocation,
89-
getRegexpRange,
90-
}: RegExpContext): RegExpVisitor.Handlers {
91-
return {
92-
onCharacterClassEnter(ccNode) {
93-
const prevList: CharacterClassElement[] = []
94-
for (const next of ccNode.elements) {
95-
if (prevList.length) {
96-
const prev = prevList[0]
97-
if (!isValidOrder(prev, next)) {
98-
let moveTarget = prev
99-
for (const p of prevList) {
100-
if (isValidOrder(p, next)) {
101-
break
102-
} else {
103-
moveTarget = p
104-
}
105-
}
106-
context.report({
107-
node,
108-
loc: getRegexpLocation(next),
109-
messageId: "sortElements",
110-
data: {
111-
next: next.raw,
112-
prev: moveTarget.raw,
113-
},
114-
*fix(fixer) {
115-
const nextRange = getRegexpRange(next)
116-
const targetRange = getRegexpRange(
117-
moveTarget,
118-
)
119-
if (!targetRange || !nextRange) {
120-
return
121-
}
122-
123-
yield fixer.insertTextBeforeRange(
124-
targetRange,
125-
fixerApplyEscape(
126-
escapeRaw(next, moveTarget),
127-
),
128-
)
129-
130-
yield fixer.removeRange(nextRange)
131-
},
132-
})
133-
}
134-
}
135-
prevList.unshift(next)
136-
}
137-
},
138-
}
139-
}
140-
141-
/* eslint-disable complexity -- X( */
142-
/**
143-
* Check that the two given CharacterClassElements are in a valid order.
144-
*/
145-
function isValidOrder(
146-
/* eslint-enable complexity -- X( */
147-
prev: CharacterClassElement,
148-
next: CharacterClassElement,
149-
) {
150-
const prevKind = getCharacterClassElementKind(prev)
151-
const nextKind = getCharacterClassElementKind(next)
152-
const prevOrder = orderOption[prevKind] ?? orderOption["*"]
153-
const nextOrder = orderOption[nextKind] ?? orderOption["*"]
154-
if (prevOrder < nextOrder) {
155-
return true
156-
} else if (prevOrder > nextOrder) {
157-
return false
158-
}
159-
if (prev.type === "CharacterSet" && prev.kind === "property") {
160-
if (next.type === "CharacterSet") {
161-
if (next.kind === "property") {
162-
return isValidOrderForUnicodePropertyCharacterSet(
163-
prev,
164-
next,
165-
)
166-
}
167-
// e.g. /[\p{ASCII}\d]/
168-
return false
169-
}
170-
// e.g. /[\p{ASCII}a]/
171-
return true
172-
} else if (
173-
next.type === "CharacterSet" &&
174-
next.kind === "property"
175-
) {
176-
if (prev.type === "CharacterSet") {
177-
// e.g. /[\d\p{ASCII}]/
178-
return true
179-
}
180-
// e.g. /[a\p{ASCII}]/
181-
return false
182-
}
183-
if (prev.type === "CharacterSet" && next.type === "CharacterSet") {
184-
if (prev.kind === "word" && next.kind === "digit") {
185-
return true
186-
}
187-
if (prev.kind === "digit" && next.kind === "word") {
188-
return false
189-
}
190-
}
191-
const prevCP = getTargetCodePoint(prev)
192-
const nextCP = getTargetCodePoint(next)
193-
if (prevCP <= nextCP) {
194-
return true
195-
}
196-
return false
197-
}
198-
199-
/**
200-
* Check that the two given UnicodePropertyCharacterSet are in a valid order.
201-
*/
202-
function isValidOrderForUnicodePropertyCharacterSet(
203-
prev: UnicodePropertyCharacterSet,
204-
next: UnicodePropertyCharacterSet,
205-
) {
206-
if (prev.key < next.key) {
207-
return true
208-
} else if (prev.key > next.key) {
209-
return false
210-
}
211-
if (prev.value) {
212-
if (next.value) {
213-
if (prev.value <= next.value) {
214-
return true
215-
}
216-
return false
217-
}
218-
return false
219-
}
220-
return true
221-
}
222-
223-
/**
224-
* Gets the target code point for a given element.
225-
*/
226-
function getTargetCodePoint(
227-
node: Exclude<CharacterClassElement, UnicodePropertyCharacterSet>,
228-
) {
229-
if (node.type === "CharacterSet") {
230-
if (node.kind === "digit" || node.kind === "word") {
231-
return CP_DIGIT_ZERO
232-
}
233-
if (node.kind === "space") {
234-
return CP_SPACE
235-
}
236-
return Infinity
237-
}
238-
if (node.type === "CharacterClassRange") {
239-
return node.min.value
240-
}
241-
return node.value
242-
}
243-
244-
return defineRegexpVisitor(context, {
245-
createVisitor,
246-
})
17+
return sortCharacterClassElements.create(context)
24718
},
24819
})
249-
250-
/**
251-
* get the escape text from the given CharacterClassElement.
252-
*/
253-
function escapeRaw(node: CharacterClassElement, target: CharacterClassElement) {
254-
let raw = node.raw
255-
if (raw.startsWith("-")) {
256-
const parent = target.parent as CharacterClass
257-
const prev = parent.elements[parent.elements.indexOf(target) - 1]
258-
if (
259-
prev &&
260-
(prev.type === "Character" || prev.type === "CharacterSet")
261-
) {
262-
raw = `\\${raw}`
263-
}
264-
}
265-
if (target.raw.startsWith("-")) {
266-
if (node.type === "Character" || node.type === "CharacterSet") {
267-
raw = `${raw}\\`
268-
}
269-
}
270-
return raw
271-
}

0 commit comments

Comments
 (0)