eslint-plugin-lube 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +260 -0
- package/LICENSE +21 -0
- package/README.md +120 -9
- package/docs/ascii-order.md +81 -0
- package/docs/pretty-imports.md +70 -59
- package/docs/pretty-jsdoc-casting.md +31 -23
- package/docs/pretty-sequence.md +129 -117
- package/docs/pretty-ternary.md +55 -0
- package/docs/svelte-naming-convention.md +59 -46
- package/package.json +60 -36
- package/src/index.js +299 -0
- package/src/rules/ascii-order.js +940 -0
- package/src/rules/pretty-imports.js +176 -0
- package/src/rules/pretty-jsdoc-casting.js +278 -0
- package/src/rules/pretty-sequence.js +489 -0
- package/src/rules/pretty-ternary.js +227 -0
- package/src/rules/svelte-naming-convention.js +903 -0
- package/src/utils/fix_recorder.js +50 -0
- package/src/utils/keep_comments.js +99 -0
- package/src/utils/keep_line_breaks.js +56 -0
- package/src/utils/skip_hole_commas.js +32 -0
- package/src/utils/skip_optional_patterns.js +27 -0
- package/src/utils/skip_unchanged_fixes.js +35 -0
- package/src/utils/source_text.js +442 -0
- package/src/utils/split_declarations.js +75 -0
- package/src/utils/text_width.js +9 -0
- package/types/index.d.ts +29 -0
- package/types/index.d.ts.map +1 -0
- package/index.js +0 -125
- package/private.d.ts +0 -168
- package/rules/pretty-imports.js +0 -147
- package/rules/pretty-jsdoc-casting.js +0 -236
- package/rules/pretty-sequence.js +0 -441
- package/rules/svelte-naming-convention.js +0 -361
- package/test/pretty-imports/ExportNamedDeclaration.js +0 -123
- package/test/pretty-imports/ImportDeclaration.js +0 -125
- package/test/pretty-imports.js +0 -20
- package/test/pretty-jsdoc-casting/case_by_expression.js +0 -249
- package/test/pretty-jsdoc-casting/various_situations.js +0 -95
- package/test/pretty-jsdoc-casting.js +0 -22
- package/test/pretty-sequence/case_by_node_type.js +0 -340
- package/test/pretty-sequence/case_by_option.js +0 -142
- package/test/pretty-sequence/various_situations.js +0 -193
- package/test/pretty-sequence.js +0 -23
- package/test/svelte-naming-convention/basic_snake_case.js +0 -54
- package/test/svelte-naming-convention/case_by_identifier.js +0 -878
- package/test/svelte-naming-convention/snake_case_with_symbols.js +0 -46
- package/test/svelte-naming-convention.js +0 -25
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
import { create_source_text } from "../utils/source_text.js"
|
|
2
|
+
const not_static = Symbol("not static")
|
|
3
|
+
const portable_flags_regex = /^[gimsuy]*$/
|
|
4
|
+
const regexp_group_name_regex = /\(\?<([^!=>]+)>/g
|
|
5
|
+
const regexp_modifier_regex = /\(\?[-A-Za-z]/
|
|
6
|
+
const constant_globals = new Set(
|
|
7
|
+
[ "Infinity", "NaN", "undefined" ]
|
|
8
|
+
)
|
|
9
|
+
const pass_through_parents = new Set(
|
|
10
|
+
[
|
|
11
|
+
"ArrayExpression",
|
|
12
|
+
"ConditionalExpression",
|
|
13
|
+
"JSXExpressionContainer",
|
|
14
|
+
"LogicalExpression",
|
|
15
|
+
"SequenceExpression"
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
const test_functions = new Set([ "describe", "it", "test" ])
|
|
19
|
+
const type_wrappers = new Set(
|
|
20
|
+
[
|
|
21
|
+
"TSAsExpression",
|
|
22
|
+
"TSNonNullExpression",
|
|
23
|
+
"TSSatisfiesExpression",
|
|
24
|
+
"TSTypeAssertion"
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
const safe_constructors = new Set(
|
|
28
|
+
[
|
|
29
|
+
"Array",
|
|
30
|
+
"ArrayBuffer",
|
|
31
|
+
"BigInt64Array",
|
|
32
|
+
"BigUint64Array",
|
|
33
|
+
"DataView",
|
|
34
|
+
"Date",
|
|
35
|
+
"Error",
|
|
36
|
+
"EvalError",
|
|
37
|
+
"Float32Array",
|
|
38
|
+
"Float64Array",
|
|
39
|
+
"Int16Array",
|
|
40
|
+
"Int32Array",
|
|
41
|
+
"Int8Array",
|
|
42
|
+
"Map",
|
|
43
|
+
"RangeError",
|
|
44
|
+
"ReferenceError",
|
|
45
|
+
"RegExp",
|
|
46
|
+
"Set",
|
|
47
|
+
"SyntaxError",
|
|
48
|
+
"TypeError",
|
|
49
|
+
"URIError",
|
|
50
|
+
"URL",
|
|
51
|
+
"Uint16Array",
|
|
52
|
+
"Uint32Array",
|
|
53
|
+
"Uint8Array",
|
|
54
|
+
"Uint8ClampedArray",
|
|
55
|
+
"WeakMap",
|
|
56
|
+
"WeakSet"
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
const closers = new Set([ ")", "]", "}" ])
|
|
60
|
+
const member_separators = new Set([ ",", ";" ])
|
|
61
|
+
const shebang_types = new Set([ "Hashbang", "Shebang" ])
|
|
62
|
+
/**
|
|
63
|
+
* @param {import("../../private.js").OrderEntry} a
|
|
64
|
+
* @param {import("../../private.js").OrderEntry} b
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
function compare(a, b) {
|
|
68
|
+
const rank = (a.rank ?? 1) - (b.rank ?? 1)
|
|
69
|
+
if (rank) return rank
|
|
70
|
+
return a.name < b.name
|
|
71
|
+
? -1
|
|
72
|
+
: a.name > b.name
|
|
73
|
+
? 1
|
|
74
|
+
: 0
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @param {import("../../private.js").Comment[]} comments
|
|
78
|
+
* @param {number} line
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
function count_same_line(comments, line) {
|
|
82
|
+
let count = 0
|
|
83
|
+
for (const comment of comments) {
|
|
84
|
+
if (comment.loc.start.line != line) break
|
|
85
|
+
line = comment.loc.end.line
|
|
86
|
+
count++
|
|
87
|
+
}
|
|
88
|
+
return count
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* @param {import("eslint").Rule.RuleContext} context
|
|
92
|
+
*/
|
|
93
|
+
function create_reorder(context) {
|
|
94
|
+
const source_code = context.sourceCode
|
|
95
|
+
const source = create_source_text(source_code)
|
|
96
|
+
const text = source.text
|
|
97
|
+
/**
|
|
98
|
+
* @param {import("../../private.js").OrderEntry} entry
|
|
99
|
+
* @param {import("../../private.js").OrderLayout} layout
|
|
100
|
+
* @param {((node: import("../../private.js").OrderNode) => number)=} get_level
|
|
101
|
+
* @returns {import("../../private.js").OrderItem | undefined}
|
|
102
|
+
*/
|
|
103
|
+
function get_item(entry, layout, get_level) {
|
|
104
|
+
const first_token = /** @type {import("eslint").AST.Token} */(source_code.getFirstToken(
|
|
105
|
+
/** @type {import("estree").Node} */(entry.first)/**/
|
|
106
|
+
))/**/
|
|
107
|
+
const before = source_code.getTokenBefore(first_token)
|
|
108
|
+
const leading = /** @type {import("../../private.js").Comment[]} */(source_code.getCommentsBefore(first_token))/**/.filter(
|
|
109
|
+
comment => !shebang_types.has(comment.type)
|
|
110
|
+
)
|
|
111
|
+
const head = before ? count_same_line(leading, before.loc.end.line) : 0
|
|
112
|
+
const start = leading[head]?.range[0] ?? first_token.range[0]
|
|
113
|
+
let last_token = /** @type {import("eslint").AST.Token} */(source_code.getLastToken(
|
|
114
|
+
/** @type {import("estree").Node} */(entry.last)/**/
|
|
115
|
+
))/**/
|
|
116
|
+
/** @type {import("eslint").AST.Token | null} */
|
|
117
|
+
let separator = null
|
|
118
|
+
if (layout == "member" && member_separators.has(last_token.value)) {
|
|
119
|
+
separator = last_token
|
|
120
|
+
last_token = /** @type {import("eslint").AST.Token} */(source_code.getTokenBefore(last_token))/**/
|
|
121
|
+
} else if (layout == "sequence") {
|
|
122
|
+
const next = source_code.getTokenAfter(last_token)
|
|
123
|
+
if (next?.value == ",") separator = next
|
|
124
|
+
}
|
|
125
|
+
const inner = /** @type {import("../../private.js").Comment[]} */(source_code.getCommentsAfter(last_token))/**/
|
|
126
|
+
let code_end = last_token.range[1]
|
|
127
|
+
let line = last_token.loc.end.line
|
|
128
|
+
/** @type {import("../../private.js").Comment[]} */
|
|
129
|
+
let tail
|
|
130
|
+
if (separator) {
|
|
131
|
+
if (inner.some(
|
|
132
|
+
comment => comment.type == "Line"
|
|
133
|
+
)) return undefined
|
|
134
|
+
code_end = inner[inner.length - 1]?.range[1] ?? code_end
|
|
135
|
+
line = separator.loc.end.line
|
|
136
|
+
tail = /** @type {import("../../private.js").Comment[]} */(source_code.getCommentsAfter(separator))/**/
|
|
137
|
+
} else {
|
|
138
|
+
let index = 0
|
|
139
|
+
for (let comment = inner[0]; comment?.type == "Block" && comment.range[0] == code_end; comment = inner[++index]) {
|
|
140
|
+
code_end = comment.range[1]
|
|
141
|
+
line = comment.loc.end.line
|
|
142
|
+
}
|
|
143
|
+
tail = inner.slice(index)
|
|
144
|
+
}
|
|
145
|
+
const sep_end = separator ? separator.range[1] : code_end
|
|
146
|
+
const tail_count = count_same_line(tail, line)
|
|
147
|
+
const last_comment = tail[tail_count - 1]
|
|
148
|
+
const after = last_comment ? last_comment.range[1] : sep_end
|
|
149
|
+
let next = after
|
|
150
|
+
while (text[next] == " " || text[next] == "\t") next++
|
|
151
|
+
let previous = start
|
|
152
|
+
while (text[previous - 1] == " " || text[previous - 1] == "\t") previous--
|
|
153
|
+
const declaration = entry.last.type == "ExportNamedDeclaration"
|
|
154
|
+
? entry.last.declaration
|
|
155
|
+
: entry.last
|
|
156
|
+
return {
|
|
157
|
+
after,
|
|
158
|
+
break_after: next == text.length || text[next] == "\n" || text[next] == "\r",
|
|
159
|
+
break_before: previous == 0 || text[previous - 1] == "\n" || text[previous - 1] == "\r",
|
|
160
|
+
closes: closers.has(text[next] ?? ""),
|
|
161
|
+
code_end,
|
|
162
|
+
entry,
|
|
163
|
+
head: head > 0,
|
|
164
|
+
header: !before && leading.length > 0,
|
|
165
|
+
leading: start < first_token.range[0],
|
|
166
|
+
level: get_level ? get_level(entry.first) : 0,
|
|
167
|
+
semicolon: last_token.value == ";",
|
|
168
|
+
sep_end,
|
|
169
|
+
start,
|
|
170
|
+
tail_line: last_comment?.type == "Line",
|
|
171
|
+
tail_start: tail[0] && last_comment ? tail[0].range[0] : after,
|
|
172
|
+
terminated: last_token.value == ";" || declaration?.type == "FunctionDeclaration"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* @param {import("../../private.js").OrderEntry[]} entries
|
|
177
|
+
* @param {import("../../private.js").OrderLayout} layout
|
|
178
|
+
* @param {import("../../private.js").OrderEntry} reported
|
|
179
|
+
* @param {((node: import("../../private.js").OrderNode) => number)=} get_level
|
|
180
|
+
* @returns {{ range: [number, number], text: string } | import("../../private.js").OrderItem | null}
|
|
181
|
+
*/
|
|
182
|
+
function get_replacement(entries, layout, reported, get_level) {
|
|
183
|
+
/** @type {import("../../private.js").OrderItem[]} */
|
|
184
|
+
const items = []
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const item = get_item(entry, layout, get_level)
|
|
187
|
+
if (!item) return null
|
|
188
|
+
items.push(item)
|
|
189
|
+
}
|
|
190
|
+
const sorted = items.slice().sort(
|
|
191
|
+
(a, b) => compare(a.entry, b.entry)
|
|
192
|
+
)
|
|
193
|
+
const blocker = find_blocker(items, sorted, reported)
|
|
194
|
+
if (blocker) return blocker
|
|
195
|
+
const last_index = items.length - 1
|
|
196
|
+
let result = ""
|
|
197
|
+
for (let i = 0; i <= last_index; i++) {
|
|
198
|
+
const slot = /** @type {import("../../private.js").OrderItem} */(items[i])/**/
|
|
199
|
+
const item = /** @type {import("../../private.js").OrderItem} */(sorted[i])/**/
|
|
200
|
+
const has_tail = item.tail_start < item.after
|
|
201
|
+
if (
|
|
202
|
+
item.leading && !slot.break_before
|
|
203
|
+
|| i == 0 && slot.head && !slot.break_before
|
|
204
|
+
|| slot.header && item != slot
|
|
205
|
+
|| slot.tail_start < slot.after && !slot.break_after && (i < last_index || !slot.closes)
|
|
206
|
+
|| has_tail && !slot.break_after && (i < last_index || item.tail_line)
|
|
207
|
+
|| layout == "statement" && i < last_index && !item.terminated && !slot.break_after
|
|
208
|
+
) return null
|
|
209
|
+
result += source.render(item.start, item.code_end) + source.render(slot.code_end, slot.sep_end)
|
|
210
|
+
if (has_tail) result += " " + source.render(item.tail_start, item.after)
|
|
211
|
+
if (i < last_index) {
|
|
212
|
+
result += source.render(
|
|
213
|
+
slot.after,
|
|
214
|
+
/** @type {import("../../private.js").OrderItem} */(items[i + 1])/**/.start
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const first = /** @type {import("../../private.js").OrderItem} */(items[0])/**/
|
|
219
|
+
const last = /** @type {import("../../private.js").OrderItem} */(items[last_index])/**/
|
|
220
|
+
if (last.semicolon && !(/** @type {import("../../private.js").OrderItem} */(sorted[last_index])/**/).semicolon) return null
|
|
221
|
+
source.edit(first.start, last.after, result)
|
|
222
|
+
return {
|
|
223
|
+
range: [ first.start, last.after ],
|
|
224
|
+
text: result
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* @param {import("../../private.js").OrderNode} node
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
function get_snippet(node) {
|
|
232
|
+
const value = node.type == "Property"
|
|
233
|
+
? node.computed && node.key.type != "Literal"
|
|
234
|
+
? node.key
|
|
235
|
+
: node.value
|
|
236
|
+
: node
|
|
237
|
+
const snippet = source_code.getText(
|
|
238
|
+
/** @type {import("estree").Node} */(value)/**/
|
|
239
|
+
).replace(/\s+/g, " ")
|
|
240
|
+
return snippet.length > 30 ? snippet.slice(0, 29) + "…" : snippet
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* @param {import("../../private.js").OrderEntry[]} entries
|
|
244
|
+
* @param {string} message_id
|
|
245
|
+
* @param {import("../../private.js").OrderLayout} layout
|
|
246
|
+
* @param {((node: import("../../private.js").OrderNode) => number)=} get_level
|
|
247
|
+
* @returns {void}
|
|
248
|
+
*/
|
|
249
|
+
function verify(
|
|
250
|
+
entries,
|
|
251
|
+
message_id,
|
|
252
|
+
layout,
|
|
253
|
+
get_level
|
|
254
|
+
) {
|
|
255
|
+
const index = entries.findIndex(
|
|
256
|
+
(entry, i) => i > 0 && compare(
|
|
257
|
+
/** @type {import("../../private.js").OrderEntry} */(entries[i - 1])/**/,
|
|
258
|
+
entry
|
|
259
|
+
) > 0
|
|
260
|
+
)
|
|
261
|
+
const entry = entries[index]
|
|
262
|
+
const previous = entries[index - 1]
|
|
263
|
+
if (!entry || !previous) return
|
|
264
|
+
const replacement = get_replacement(entries, layout, entry, get_level)
|
|
265
|
+
const fix = replacement && "range" in replacement ? replacement : null
|
|
266
|
+
const blocker = replacement && "entry" in replacement ? replacement : null
|
|
267
|
+
context.report(
|
|
268
|
+
{
|
|
269
|
+
data: {
|
|
270
|
+
name: entry.name,
|
|
271
|
+
previous: previous.name,
|
|
272
|
+
reason: fix
|
|
273
|
+
? ""
|
|
274
|
+
: blocker
|
|
275
|
+
? "; it is not fixed automatically because moving `" + get_snippet(blocker.entry.first) + "` could change the order of side effects, so reorder it by hand"
|
|
276
|
+
: "; it is not fixed automatically because a comment or statement next to it would end up in the wrong place, so reorder it by hand"
|
|
277
|
+
},
|
|
278
|
+
fix: fix && (
|
|
279
|
+
fixer => fixer.replaceTextRange(fix.range, fix.text)
|
|
280
|
+
),
|
|
281
|
+
messageId: message_id,
|
|
282
|
+
node: entry.report
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
return { verify }
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* @param {import("../../private.js").OrderItem[]} items
|
|
290
|
+
* @param {import("../../private.js").OrderItem[]} sorted
|
|
291
|
+
* @param {import("../../private.js").OrderEntry} reported
|
|
292
|
+
* @returns {import("../../private.js").OrderItem | undefined}
|
|
293
|
+
*/
|
|
294
|
+
function find_blocker(items, sorted, reported) {
|
|
295
|
+
const positions = new Map(
|
|
296
|
+
sorted.map((item, i) => [ item, i ])
|
|
297
|
+
)
|
|
298
|
+
const blockers = items.filter(
|
|
299
|
+
(a, i) => a.level > 1 && items.some(
|
|
300
|
+
(b, j) => b.level && i != j && i < j != /** @type {number} */(positions.get(a))/**/ < /** @type {number} */(positions.get(b))/**/
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
return blockers.find(
|
|
304
|
+
blocker => blocker.entry == reported
|
|
305
|
+
) ?? blockers[0]
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* @param {import("../../private.js").OrderNode} node
|
|
309
|
+
* @returns {string | undefined}
|
|
310
|
+
*/
|
|
311
|
+
function get_name(node) {
|
|
312
|
+
switch (node.type) {
|
|
313
|
+
case "Identifier":
|
|
314
|
+
return node.name
|
|
315
|
+
case "Literal":
|
|
316
|
+
return String(node.value)
|
|
317
|
+
case "TemplateLiteral":
|
|
318
|
+
return node.expressions.length
|
|
319
|
+
? undefined
|
|
320
|
+
: /** @type {import("estree").TemplateElement} */(node.quasis[0])/**/.value.cooked ?? undefined
|
|
321
|
+
}
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @param {unknown} value
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
function is_large(value) {
|
|
329
|
+
if (typeof value == "object" && value) {
|
|
330
|
+
return !Array.isArray(value) && Object.values(value).some(
|
|
331
|
+
property => typeof property == "object" && property || is_large(property)
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
return Number(value) > 65536
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* @param {unknown[]} args
|
|
338
|
+
* @returns {boolean}
|
|
339
|
+
*/
|
|
340
|
+
function is_portable_regexp(args) {
|
|
341
|
+
const [ pattern, flags ] = args
|
|
342
|
+
const source = pattern instanceof RegExp ? pattern.source : pattern
|
|
343
|
+
const flag_text = flags === undefined && pattern instanceof RegExp ? pattern.flags : flags
|
|
344
|
+
if (typeof source != "string" && source !== undefined || typeof flag_text != "string" && flag_text !== undefined) return false
|
|
345
|
+
if (flag_text && !portable_flags_regex.test(flag_text) || source && regexp_modifier_regex.test(source)) return false
|
|
346
|
+
/** @type {Set<string>} */
|
|
347
|
+
const names = new Set()
|
|
348
|
+
for (const match of source?.matchAll(regexp_group_name_regex) ?? []) {
|
|
349
|
+
const name = /** @type {string} */(match[1])/**/
|
|
350
|
+
if (names.has(name)) return false
|
|
351
|
+
names.add(name)
|
|
352
|
+
}
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* @param {import("../../private.js").OrderNode} parent
|
|
357
|
+
* @param {import("../../private.js").OrderNode} child
|
|
358
|
+
* @returns {boolean}
|
|
359
|
+
*/
|
|
360
|
+
function passes_through(parent, child) {
|
|
361
|
+
switch (parent.type) {
|
|
362
|
+
case "BinaryExpression":
|
|
363
|
+
return parent.operator == "===" || parent.operator == "!=="
|
|
364
|
+
case "Property":
|
|
365
|
+
return parent.value == child
|
|
366
|
+
case "UnaryExpression":
|
|
367
|
+
return parent.operator == "!" || parent.operator == "typeof" || parent.operator == "void"
|
|
368
|
+
}
|
|
369
|
+
return pass_through_parents.has(parent.type)
|
|
370
|
+
}
|
|
371
|
+
/** @type {import("eslint").Rule.RuleModule} */
|
|
372
|
+
export default {
|
|
373
|
+
create(context) {
|
|
374
|
+
/** @type {import("../../private.js").RuleOptions["ascii-order"]} */
|
|
375
|
+
const option = context.options[0]
|
|
376
|
+
const check_declarations = option?.checkDeclarations ?? true
|
|
377
|
+
const check_imports = option?.checkImports ?? true
|
|
378
|
+
const check_keys = option?.checkKeys ?? true
|
|
379
|
+
const check_names = option?.checkNames ?? true
|
|
380
|
+
const check_tests = option?.checkTests ?? true
|
|
381
|
+
const visitor_keys = context.sourceCode.visitorKeys
|
|
382
|
+
const { verify } = create_reorder(context)
|
|
383
|
+
/** @type {Set<string | undefined>} */
|
|
384
|
+
const default_exports = new Set()
|
|
385
|
+
/** @type {Parameters<typeof verify>[]} */
|
|
386
|
+
const pending = []
|
|
387
|
+
/**
|
|
388
|
+
* @param {import("../../private.js").OrderNode} node
|
|
389
|
+
* @returns {number}
|
|
390
|
+
*/
|
|
391
|
+
function classify(node) {
|
|
392
|
+
switch (node.type) {
|
|
393
|
+
case "ArrowFunctionExpression":
|
|
394
|
+
case "FunctionExpression":
|
|
395
|
+
return 0
|
|
396
|
+
case "AssignmentExpression":
|
|
397
|
+
case "AwaitExpression":
|
|
398
|
+
case "CallExpression":
|
|
399
|
+
case "ClassExpression":
|
|
400
|
+
case "ImportExpression":
|
|
401
|
+
case "TaggedTemplateExpression":
|
|
402
|
+
case "UpdateExpression":
|
|
403
|
+
case "YieldExpression":
|
|
404
|
+
return 2
|
|
405
|
+
case "Identifier":
|
|
406
|
+
return is_constant_reference(node) ? 0 : 1
|
|
407
|
+
case "ThisExpression":
|
|
408
|
+
return 1
|
|
409
|
+
case "NewExpression":
|
|
410
|
+
return is_safe_construction(node) ? 1 : 2
|
|
411
|
+
case "Property":
|
|
412
|
+
return node.computed
|
|
413
|
+
? Math.max(
|
|
414
|
+
classify(node.key),
|
|
415
|
+
classify(node.value)
|
|
416
|
+
)
|
|
417
|
+
: classify(node.value)
|
|
418
|
+
case "TSAsExpression":
|
|
419
|
+
case "TSNonNullExpression":
|
|
420
|
+
case "TSSatisfiesExpression":
|
|
421
|
+
case "TSTypeAssertion":
|
|
422
|
+
return classify(node.expression)
|
|
423
|
+
case "UnaryExpression":
|
|
424
|
+
if (node.operator == "delete") return 2
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
let level = 0
|
|
428
|
+
some_child(
|
|
429
|
+
node,
|
|
430
|
+
child => {
|
|
431
|
+
level = Math.max(level, classify(child))
|
|
432
|
+
return level == 2
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
return level
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* @param {import("../../private.js").OrderNode} node
|
|
439
|
+
* @returns {number}
|
|
440
|
+
*/
|
|
441
|
+
function classify_pattern(node) {
|
|
442
|
+
return node.type == "MemberExpression" || node.type == "AssignmentPattern" && classify(node.right) || some_child(
|
|
443
|
+
node,
|
|
444
|
+
child => classify_pattern(child) == 2
|
|
445
|
+
)
|
|
446
|
+
? 2
|
|
447
|
+
: 1
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* @param {import("estree").Program} program
|
|
451
|
+
* @returns {void}
|
|
452
|
+
*/
|
|
453
|
+
function collect_default_exports(program) {
|
|
454
|
+
for (const statement of program.body) {
|
|
455
|
+
if (statement.type == "ExportDefaultDeclaration" && statement.declaration.type == "Identifier") {
|
|
456
|
+
default_exports.add(statement.declaration.name)
|
|
457
|
+
} else if (statement.type == "ExportNamedDeclaration" && !statement.source) {
|
|
458
|
+
for (const specifier of statement.specifiers) {
|
|
459
|
+
if (get_name(specifier.exported) == "default") default_exports.add(get_name(specifier.local))
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* @param {import("../../private.js").OrderNode | null} node
|
|
466
|
+
* @param {string[]} names
|
|
467
|
+
* @returns {string[]}
|
|
468
|
+
*/
|
|
469
|
+
function collect_targets(node, names) {
|
|
470
|
+
switch (node?.type) {
|
|
471
|
+
case "ArrayPattern":
|
|
472
|
+
for (const element of node.elements) collect_targets(element, names)
|
|
473
|
+
break
|
|
474
|
+
case "AssignmentPattern":
|
|
475
|
+
collect_targets(node.left, names)
|
|
476
|
+
break
|
|
477
|
+
case "Identifier":
|
|
478
|
+
names.push(node.name)
|
|
479
|
+
break
|
|
480
|
+
case "ObjectPattern":
|
|
481
|
+
for (const property of node.properties) collect_targets(property, names)
|
|
482
|
+
break
|
|
483
|
+
case "Property":
|
|
484
|
+
collect_targets(node.value, names)
|
|
485
|
+
break
|
|
486
|
+
case "RestElement":
|
|
487
|
+
collect_targets(node.argument, names)
|
|
488
|
+
break
|
|
489
|
+
}
|
|
490
|
+
return names
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* @param {import("estree").NewExpression} node
|
|
494
|
+
* @returns {unknown}
|
|
495
|
+
*/
|
|
496
|
+
function construct(node) {
|
|
497
|
+
const callee = node.callee
|
|
498
|
+
if (callee.type != "Identifier" || !safe_constructors.has(callee.name) || !is_global(callee)) return not_static
|
|
499
|
+
const args = node.arguments.map(static_value)
|
|
500
|
+
if (
|
|
501
|
+
args.includes(not_static)
|
|
502
|
+
|| (callee.name.includes("Array") || callee.name == "DataView") && args.some(is_large)
|
|
503
|
+
|| callee.name == "RegExp" && !is_portable_regexp(args)
|
|
504
|
+
) return not_static
|
|
505
|
+
try {
|
|
506
|
+
return new (/** @type {new (...values: unknown[]) => unknown} */(/** @type {Record<string, unknown>} */(/** @type {unknown} */(globalThis))/**/[callee.name])/**/)(...args)
|
|
507
|
+
} catch {
|
|
508
|
+
return not_static
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* @param {Parameters<typeof verify>} check
|
|
513
|
+
* @returns {void}
|
|
514
|
+
*/
|
|
515
|
+
function defer(...check) {
|
|
516
|
+
pending.push(check)
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* @param {import("../../private.js").OrderNode} node
|
|
520
|
+
* @returns {number}
|
|
521
|
+
*/
|
|
522
|
+
function get_condition_rank(node) {
|
|
523
|
+
const name = get_key_name(node)
|
|
524
|
+
return name == "types"
|
|
525
|
+
? 0
|
|
526
|
+
: name == "default"
|
|
527
|
+
? 2
|
|
528
|
+
: 1
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* @param {import("../../private.js").OrderNode} statement
|
|
532
|
+
* @returns {import("estree").Identifier | null | undefined}
|
|
533
|
+
*/
|
|
534
|
+
function get_function_id(statement) {
|
|
535
|
+
const node = statement.type == "ExportNamedDeclaration" ? statement.declaration : statement
|
|
536
|
+
return node?.type == "FunctionDeclaration" || node?.type == "TSDeclareFunction"
|
|
537
|
+
? node.id
|
|
538
|
+
: undefined
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* @param {import("../../private.js").OrderNode} statement
|
|
542
|
+
* @returns {import("estree").Literal | undefined}
|
|
543
|
+
*/
|
|
544
|
+
function get_import_source(statement) {
|
|
545
|
+
return statement.type == "ImportDeclaration" && statement.specifiers.length
|
|
546
|
+
? statement.source
|
|
547
|
+
: undefined
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* @param {import("../../private.js").OrderNode} node
|
|
551
|
+
* @returns {string | undefined}
|
|
552
|
+
*/
|
|
553
|
+
function get_key_name(node) {
|
|
554
|
+
const type = node.type
|
|
555
|
+
if (type != "Property" && type != "TSMethodSignature" && type != "TSPropertySignature") return undefined
|
|
556
|
+
return node.computed && node.key.type != "Literal" ? undefined : get_name(node.key)
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* @param {import("../../private.js").OrderNode} node
|
|
560
|
+
* @returns {string | undefined}
|
|
561
|
+
*/
|
|
562
|
+
function get_specifier_name(node) {
|
|
563
|
+
if (node.type == "ImportSpecifier") return get_name(node.imported)
|
|
564
|
+
return node.type == "ExportSpecifier" ? get_name(node.local) : undefined
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* @param {import("../../private.js").OrderNode} statement
|
|
568
|
+
* @returns {import("estree").Literal | import("estree").TemplateLiteral | undefined}
|
|
569
|
+
*/
|
|
570
|
+
function get_test_title(statement) {
|
|
571
|
+
const call = statement.type == "ExpressionStatement" ? statement.expression : undefined
|
|
572
|
+
if (call?.type != "CallExpression") return undefined
|
|
573
|
+
let callee = call.callee
|
|
574
|
+
while (callee.type == "MemberExpression" && !callee.computed) callee = callee.object
|
|
575
|
+
const title = call.arguments[0]
|
|
576
|
+
if (callee.type != "Identifier" || !test_functions.has(callee.name)) return undefined
|
|
577
|
+
return title?.type == "Literal" && typeof title.value == "string" || title?.type == "TemplateLiteral"
|
|
578
|
+
? title
|
|
579
|
+
: undefined
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* @param {import("estree").Identifier} node
|
|
583
|
+
* @returns {boolean}
|
|
584
|
+
*/
|
|
585
|
+
function is_constant_binding(node) {
|
|
586
|
+
const scope = context.sourceCode.getScope(node)
|
|
587
|
+
const variable = scope.references.find(
|
|
588
|
+
reference => reference.identifier == node
|
|
589
|
+
)?.resolved
|
|
590
|
+
if (!variable?.defs.length) return constant_globals.has(node.name) && is_global(node)
|
|
591
|
+
if (variable.defs.length > 1) return false
|
|
592
|
+
const def = /** @type {import("eslint").Scope.Definition} */(variable.defs[0])/**/
|
|
593
|
+
switch (def.type) {
|
|
594
|
+
case "ClassName":
|
|
595
|
+
return variable.references.every(
|
|
596
|
+
reference => !reference.isWrite()
|
|
597
|
+
) && is_initialized_before(node, scope, variable, def.node)
|
|
598
|
+
case "FunctionName":
|
|
599
|
+
return variable.references.every(
|
|
600
|
+
reference => !reference.isWrite()
|
|
601
|
+
)
|
|
602
|
+
case "ImportBinding":
|
|
603
|
+
return def.node.type == "ImportNamespaceSpecifier"
|
|
604
|
+
case "Variable":
|
|
605
|
+
return def.parent.kind == "const" && is_initialized_before(node, scope, variable, def.node)
|
|
606
|
+
}
|
|
607
|
+
return false
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* @param {import("estree").Identifier} node
|
|
611
|
+
* @returns {boolean}
|
|
612
|
+
*/
|
|
613
|
+
function is_constant_reference(node) {
|
|
614
|
+
/** @type {import("../../private.js").OrderNode} */
|
|
615
|
+
let child = node
|
|
616
|
+
/** @type {import("../../private.js").OrderNode | null} */
|
|
617
|
+
let parent = /** @type {import("eslint").Rule.Node} */(node)/**/.parent
|
|
618
|
+
while (parent && type_wrappers.has(parent.type)) {
|
|
619
|
+
child = parent
|
|
620
|
+
parent = /** @type {import("eslint").Rule.Node} */(/** @type {unknown} */(parent))/**/.parent
|
|
621
|
+
}
|
|
622
|
+
return !!parent && passes_through(parent, child) && is_constant_binding(node)
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* @param {import("estree").Identifier} node
|
|
626
|
+
* @returns {boolean}
|
|
627
|
+
*/
|
|
628
|
+
function is_global(node) {
|
|
629
|
+
let scope = context.sourceCode.getScope(node)
|
|
630
|
+
while (scope.upper && !scope.set.has(node.name)) scope = scope.upper
|
|
631
|
+
return !scope.set.get(node.name)?.defs.length
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* @param {import("estree").Identifier} node
|
|
635
|
+
* @param {import("eslint").Scope.Scope} scope
|
|
636
|
+
* @param {import("eslint").Scope.Variable} variable
|
|
637
|
+
* @param {import("estree").Node} declaration
|
|
638
|
+
* @returns {boolean}
|
|
639
|
+
*/
|
|
640
|
+
function is_initialized_before(node, scope, variable, declaration) {
|
|
641
|
+
const parent = /** @type {import("eslint").Rule.Node} */(declaration)/**/.parent
|
|
642
|
+
if (
|
|
643
|
+
parent?.type == "SwitchCase"
|
|
644
|
+
|| parent?.parent?.type == "SwitchCase"
|
|
645
|
+
|| /** @type {[number, number]} */(node.range)/**/[0] < /** @type {[number, number]} */(declaration.range)/**/[1]
|
|
646
|
+
) return false
|
|
647
|
+
for (let current = /** @type {import("eslint").Scope.Scope | null} */(scope)/**/; current && current != variable.scope; current = current.upper) {
|
|
648
|
+
if (current.block.type == "FunctionDeclaration") return false
|
|
649
|
+
}
|
|
650
|
+
return true
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* @param {import("../../private.js").OrderNode} node
|
|
654
|
+
* @returns {boolean}
|
|
655
|
+
*/
|
|
656
|
+
function is_package_condition(node) {
|
|
657
|
+
if (!/(?:^|[/\\])package\.json$/.test(context.filename)) return false
|
|
658
|
+
let parent = /** @type {import("eslint").Rule.Node} */(node)/**/.parent
|
|
659
|
+
while (parent && parent.type != "Program") {
|
|
660
|
+
const name = get_key_name(parent)
|
|
661
|
+
if (name == "exports" || name == "imports") return true
|
|
662
|
+
parent = parent.parent
|
|
663
|
+
}
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* @param {import("estree").NewExpression} node
|
|
668
|
+
* @returns {boolean}
|
|
669
|
+
*/
|
|
670
|
+
function is_safe_construction(node) {
|
|
671
|
+
return construct(node) !== not_static
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* @param {import("../../private.js").OrderNode} node
|
|
675
|
+
* @param {(child: import("../../private.js").OrderNode) => boolean} predicate
|
|
676
|
+
* @returns {boolean}
|
|
677
|
+
*/
|
|
678
|
+
function some_child(node, predicate) {
|
|
679
|
+
for (const key of visitor_keys[node.type] ?? []) {
|
|
680
|
+
const value = /** @type {Record<string, import("../../private.js").OrderNode | import("../../private.js").OrderNode[] | null | undefined>} */(/** @type {unknown} */(node))/**/[key]
|
|
681
|
+
for (const child of Array.isArray(value) ? value : [ value ]) {
|
|
682
|
+
if (child?.type && predicate(child)) return true
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return false
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* @param {import("../../private.js").OrderNode} node
|
|
689
|
+
* @returns {unknown}
|
|
690
|
+
*/
|
|
691
|
+
function static_value(node) {
|
|
692
|
+
switch (node.type) {
|
|
693
|
+
case "ArrayExpression": {
|
|
694
|
+
const values = node.elements.map(
|
|
695
|
+
element => element ? static_value(element) : undefined
|
|
696
|
+
)
|
|
697
|
+
return values.includes(not_static) ? not_static : values
|
|
698
|
+
}
|
|
699
|
+
case "Identifier":
|
|
700
|
+
return constant_globals.has(node.name) && is_global(node)
|
|
701
|
+
? /** @type {Record<string, unknown>} */(/** @type {unknown} */(globalThis))/**/[node.name]
|
|
702
|
+
: not_static
|
|
703
|
+
case "Literal":
|
|
704
|
+
return "regex" in node && node.value === null ? not_static : node.value
|
|
705
|
+
case "NewExpression":
|
|
706
|
+
return construct(node)
|
|
707
|
+
case "ObjectExpression": {
|
|
708
|
+
/** @type {Record<string, unknown>} */
|
|
709
|
+
const object = {}
|
|
710
|
+
for (const property of node.properties) {
|
|
711
|
+
if (property.type != "Property" || property.kind != "init" || property.computed || property.shorthand) return not_static
|
|
712
|
+
const value = static_value(property.value)
|
|
713
|
+
if (value === not_static) return not_static
|
|
714
|
+
object[String(get_name(property.key))] = value
|
|
715
|
+
}
|
|
716
|
+
return object
|
|
717
|
+
}
|
|
718
|
+
case "TSAsExpression":
|
|
719
|
+
case "TSSatisfiesExpression":
|
|
720
|
+
return static_value(node.expression)
|
|
721
|
+
case "TemplateLiteral":
|
|
722
|
+
return node.expressions.length ? not_static : node.quasis[0]?.value.cooked
|
|
723
|
+
case "UnaryExpression":
|
|
724
|
+
return node.operator == "-" && node.argument.type == "Literal"
|
|
725
|
+
? -(/** @type {number} */(node.argument.value)/**/)
|
|
726
|
+
: not_static
|
|
727
|
+
}
|
|
728
|
+
return not_static
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* @param {import("../../private.js").OrderNode[]} nodes
|
|
732
|
+
* @param {(node: import("../../private.js").OrderNode) => string | undefined} get_node_name
|
|
733
|
+
* @param {"conditions" | "keys" | "names"} message_id
|
|
734
|
+
* @param {import("../../private.js").OrderLayout} layout
|
|
735
|
+
* @param {((node: import("../../private.js").OrderNode) => number)=} get_level
|
|
736
|
+
* @param {((node: import("../../private.js").OrderNode) => number)=} get_rank
|
|
737
|
+
* @returns {void}
|
|
738
|
+
*/
|
|
739
|
+
function verify_nodes(
|
|
740
|
+
nodes,
|
|
741
|
+
get_node_name,
|
|
742
|
+
message_id,
|
|
743
|
+
layout,
|
|
744
|
+
get_level,
|
|
745
|
+
get_rank
|
|
746
|
+
) {
|
|
747
|
+
/** @type {import("../../private.js").OrderEntry[]} */
|
|
748
|
+
let entries = []
|
|
749
|
+
for (const node of [ ...nodes, null ]) {
|
|
750
|
+
const name = node && get_node_name(node)
|
|
751
|
+
if (name != null) {
|
|
752
|
+
entries.push(
|
|
753
|
+
{
|
|
754
|
+
first: /** @type {import("../../private.js").OrderNode} */(node)/**/,
|
|
755
|
+
last: /** @type {import("../../private.js").OrderNode} */(node)/**/,
|
|
756
|
+
name,
|
|
757
|
+
rank: get_rank?.(
|
|
758
|
+
/** @type {import("../../private.js").OrderNode} */(node)/**/
|
|
759
|
+
),
|
|
760
|
+
report: (/** @type {import("../../private.js").OrderNode & { key?: import("../../private.js").OrderNode }} */(node)/**/).key ?? /** @type {import("../../private.js").OrderNode} */(node)/**/
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
} else {
|
|
764
|
+
if (entries.length > 1) defer(
|
|
765
|
+
entries,
|
|
766
|
+
message_id,
|
|
767
|
+
layout,
|
|
768
|
+
get_level
|
|
769
|
+
)
|
|
770
|
+
entries = []
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* @param {import("../../private.js").OrderNode & { type: "BlockStatement" | "Program" | "StaticBlock" | "TSModuleBlock" }} node
|
|
776
|
+
* @returns {void}
|
|
777
|
+
*/
|
|
778
|
+
function verify_statements(node) {
|
|
779
|
+
if (!check_declarations && !check_imports && !check_tests) return
|
|
780
|
+
/** @type {import("../../private.js").OrderEntry[]} */
|
|
781
|
+
let entries = []
|
|
782
|
+
/** @type {"declarations" | "imports" | "tests" | undefined} */
|
|
783
|
+
let kind
|
|
784
|
+
for (const statement of [ ...node.body, null ]) {
|
|
785
|
+
let id = statement && check_declarations ? get_function_id(statement) : undefined
|
|
786
|
+
if (node.type == "Program" && default_exports.has(id?.name)) id = undefined
|
|
787
|
+
const source = statement && check_imports ? get_import_source(statement) : undefined
|
|
788
|
+
const title = statement && check_tests && !id ? get_test_title(statement) : undefined
|
|
789
|
+
const report = id ?? source ?? title
|
|
790
|
+
const name = report && get_name(report)
|
|
791
|
+
const next_kind = name == null
|
|
792
|
+
? undefined
|
|
793
|
+
: id
|
|
794
|
+
? "declarations"
|
|
795
|
+
: source
|
|
796
|
+
? "imports"
|
|
797
|
+
: "tests"
|
|
798
|
+
const last = entries[entries.length - 1]
|
|
799
|
+
if (id && last && kind == next_kind && last.name == name) {
|
|
800
|
+
last.last = /** @type {import("../../private.js").OrderNode} */(statement)/**/
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
if (kind != next_kind) {
|
|
804
|
+
if (kind && entries.length > 1) defer(entries, kind, "statement")
|
|
805
|
+
entries = []
|
|
806
|
+
kind = next_kind
|
|
807
|
+
}
|
|
808
|
+
if (name != null) {
|
|
809
|
+
entries.push(
|
|
810
|
+
{
|
|
811
|
+
first: /** @type {import("../../private.js").OrderNode} */(statement)/**/,
|
|
812
|
+
last: /** @type {import("../../private.js").OrderNode} */(statement)/**/,
|
|
813
|
+
name,
|
|
814
|
+
report: /** @type {import("../../private.js").OrderNode} */(report)/**/
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/** @type {import("../../private.js").OrderRuleListener} */
|
|
821
|
+
const listener = {
|
|
822
|
+
BlockStatement: verify_statements,
|
|
823
|
+
ExportNamedDeclaration: node => {
|
|
824
|
+
if (check_names) verify_nodes(
|
|
825
|
+
node.specifiers,
|
|
826
|
+
get_specifier_name,
|
|
827
|
+
"names",
|
|
828
|
+
"sequence"
|
|
829
|
+
)
|
|
830
|
+
},
|
|
831
|
+
ImportDeclaration: node => {
|
|
832
|
+
if (check_names) verify_nodes(
|
|
833
|
+
node.specifiers,
|
|
834
|
+
get_specifier_name,
|
|
835
|
+
"names",
|
|
836
|
+
"sequence"
|
|
837
|
+
)
|
|
838
|
+
},
|
|
839
|
+
ObjectExpression: node => {
|
|
840
|
+
if (!check_keys) return
|
|
841
|
+
const condition = is_package_condition(node)
|
|
842
|
+
verify_nodes(
|
|
843
|
+
node.properties,
|
|
844
|
+
get_key_name,
|
|
845
|
+
condition ? "conditions" : "keys",
|
|
846
|
+
"sequence",
|
|
847
|
+
classify,
|
|
848
|
+
condition ? get_condition_rank : undefined
|
|
849
|
+
)
|
|
850
|
+
},
|
|
851
|
+
ObjectPattern: node => {
|
|
852
|
+
if (check_keys) {
|
|
853
|
+
/** @type {Map<import("../../private.js").OrderNode, string[]>} */
|
|
854
|
+
const targets = new Map()
|
|
855
|
+
/** @type {Set<string>} */
|
|
856
|
+
const seen = new Set()
|
|
857
|
+
/** @type {Set<string>} */
|
|
858
|
+
const repeated = new Set()
|
|
859
|
+
for (const property of node.properties) {
|
|
860
|
+
const names = collect_targets(property, [])
|
|
861
|
+
for (const name of new Set(names)) {
|
|
862
|
+
if (seen.has(name)) repeated.add(name)
|
|
863
|
+
seen.add(name)
|
|
864
|
+
}
|
|
865
|
+
targets.set(property, names)
|
|
866
|
+
}
|
|
867
|
+
verify_nodes(
|
|
868
|
+
node.properties,
|
|
869
|
+
get_key_name,
|
|
870
|
+
"keys",
|
|
871
|
+
"sequence",
|
|
872
|
+
property => targets.get(property)?.some(name => repeated.has(name))
|
|
873
|
+
? 2
|
|
874
|
+
: classify_pattern(property)
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
Program: node => {
|
|
879
|
+
collect_default_exports(node)
|
|
880
|
+
verify_statements(node)
|
|
881
|
+
},
|
|
882
|
+
"Program:exit": () => {
|
|
883
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
884
|
+
verify(
|
|
885
|
+
.../** @type {Parameters<typeof verify>} */(pending[i])/**/
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
StaticBlock: verify_statements,
|
|
890
|
+
TSInterfaceBody: node => {
|
|
891
|
+
if (check_keys) verify_nodes(
|
|
892
|
+
node.body,
|
|
893
|
+
get_key_name,
|
|
894
|
+
"keys",
|
|
895
|
+
"member"
|
|
896
|
+
)
|
|
897
|
+
},
|
|
898
|
+
TSModuleBlock: verify_statements,
|
|
899
|
+
TSTypeLiteral: node => {
|
|
900
|
+
if (check_keys) verify_nodes(
|
|
901
|
+
node.members,
|
|
902
|
+
get_key_name,
|
|
903
|
+
"keys",
|
|
904
|
+
"member"
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return /** @type {import("eslint").Rule.RuleListener} */(/** @type {unknown} */(listener))/**/
|
|
909
|
+
},
|
|
910
|
+
meta: {
|
|
911
|
+
docs: {
|
|
912
|
+
description: "Enforces ASCII order of import statements, import and export names, object keys, function declarations and test cases.",
|
|
913
|
+
recommended: true,
|
|
914
|
+
url: "https://github.com/artxe/lube-series/blob/master/packages/eslint-plugin-lube/docs/ascii-order.md"
|
|
915
|
+
},
|
|
916
|
+
fixable: "code",
|
|
917
|
+
messages: {
|
|
918
|
+
conditions: "Move condition \"{{name}}\" before \"{{previous}}\" to keep \"types\" first, \"default\" last and the others in ASCII order{{reason}}",
|
|
919
|
+
declarations: "Move function \"{{name}}\" before \"{{previous}}\" to keep functions in ASCII order{{reason}}",
|
|
920
|
+
imports: "Move the import of \"{{name}}\" before \"{{previous}}\" to keep imports in ASCII order{{reason}}",
|
|
921
|
+
keys: "Move key \"{{name}}\" before \"{{previous}}\" to keep keys in ASCII order{{reason}}",
|
|
922
|
+
names: "Move \"{{name}}\" before \"{{previous}}\" to keep names in ASCII order{{reason}}",
|
|
923
|
+
tests: "Move test \"{{name}}\" before \"{{previous}}\" to keep tests in ASCII order{{reason}}"
|
|
924
|
+
},
|
|
925
|
+
schema: [
|
|
926
|
+
{
|
|
927
|
+
additionalProperties: false,
|
|
928
|
+
properties: {
|
|
929
|
+
checkDeclarations: { default: true, type: "boolean" },
|
|
930
|
+
checkImports: { default: true, type: "boolean" },
|
|
931
|
+
checkKeys: { default: true, type: "boolean" },
|
|
932
|
+
checkNames: { default: true, type: "boolean" },
|
|
933
|
+
checkTests: { default: true, type: "boolean" }
|
|
934
|
+
},
|
|
935
|
+
type: "object"
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
type: "suggestion"
|
|
939
|
+
}
|
|
940
|
+
}
|