|
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" |
14 | 2 |
|
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" |
41 | 4 |
|
42 | 5 | export default createRule("order-in-character-class", { |
43 | 6 | meta: { |
| 7 | + ...sortCharacterClassElements.meta, |
44 | 8 | docs: { |
45 | | - description: "enforces elements order in character class", |
46 | | - category: "Stylistic Issues", |
| 9 | + ...sortCharacterClassElements.meta.docs, |
47 | 10 | recommended: false, |
| 11 | + replacedBy: ["sort-character-class-elements"], |
48 | 12 | }, |
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, |
67 | 15 | }, |
68 | 16 | 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) |
247 | 18 | }, |
248 | 19 | }) |
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