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.
Files changed (49) hide show
  1. package/CHANGELOG.md +260 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -9
  4. package/docs/ascii-order.md +81 -0
  5. package/docs/pretty-imports.md +70 -59
  6. package/docs/pretty-jsdoc-casting.md +31 -23
  7. package/docs/pretty-sequence.md +129 -117
  8. package/docs/pretty-ternary.md +55 -0
  9. package/docs/svelte-naming-convention.md +59 -46
  10. package/package.json +60 -36
  11. package/src/index.js +299 -0
  12. package/src/rules/ascii-order.js +940 -0
  13. package/src/rules/pretty-imports.js +176 -0
  14. package/src/rules/pretty-jsdoc-casting.js +278 -0
  15. package/src/rules/pretty-sequence.js +489 -0
  16. package/src/rules/pretty-ternary.js +227 -0
  17. package/src/rules/svelte-naming-convention.js +903 -0
  18. package/src/utils/fix_recorder.js +50 -0
  19. package/src/utils/keep_comments.js +99 -0
  20. package/src/utils/keep_line_breaks.js +56 -0
  21. package/src/utils/skip_hole_commas.js +32 -0
  22. package/src/utils/skip_optional_patterns.js +27 -0
  23. package/src/utils/skip_unchanged_fixes.js +35 -0
  24. package/src/utils/source_text.js +442 -0
  25. package/src/utils/split_declarations.js +75 -0
  26. package/src/utils/text_width.js +9 -0
  27. package/types/index.d.ts +29 -0
  28. package/types/index.d.ts.map +1 -0
  29. package/index.js +0 -125
  30. package/private.d.ts +0 -168
  31. package/rules/pretty-imports.js +0 -147
  32. package/rules/pretty-jsdoc-casting.js +0 -236
  33. package/rules/pretty-sequence.js +0 -441
  34. package/rules/svelte-naming-convention.js +0 -361
  35. package/test/pretty-imports/ExportNamedDeclaration.js +0 -123
  36. package/test/pretty-imports/ImportDeclaration.js +0 -125
  37. package/test/pretty-imports.js +0 -20
  38. package/test/pretty-jsdoc-casting/case_by_expression.js +0 -249
  39. package/test/pretty-jsdoc-casting/various_situations.js +0 -95
  40. package/test/pretty-jsdoc-casting.js +0 -22
  41. package/test/pretty-sequence/case_by_node_type.js +0 -340
  42. package/test/pretty-sequence/case_by_option.js +0 -142
  43. package/test/pretty-sequence/various_situations.js +0 -193
  44. package/test/pretty-sequence.js +0 -23
  45. package/test/svelte-naming-convention/basic_snake_case.js +0 -54
  46. package/test/svelte-naming-convention/case_by_identifier.js +0 -878
  47. package/test/svelte-naming-convention/snake_case_with_symbols.js +0 -46
  48. package/test/svelte-naming-convention.js +0 -25
  49. 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
+ }