eslint-plugin-absolute 0.2.6 → 0.2.8

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.
@@ -1,1244 +0,0 @@
1
- import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
-
3
- /**
4
- * @fileoverview Enforce sorted keys in object literals (like ESLint's built-in sort-keys)
5
- * with an auto-fix for simple cases that preserves comments.
6
- *
7
- * Note: This rule reports errors just like the original sort-keys rule.
8
- * However, the auto-fix only applies if all properties are "fixable" – i.e.:
9
- * - They are of type Property (not SpreadElement, etc.)
10
- * - They are not computed (e.g. [foo])
11
- * - They are an Identifier or a Literal.
12
- *
13
- * Comments attached to the properties are preserved in the auto-fix.
14
- *
15
- * Use this rule with a grain of salt. I did not test every edge case and there's
16
- * a reason the original rule doesn't have auto-fix. Computed keys, spread elements,
17
- * comments, and formatting are not handled perfectly.
18
- */
19
-
20
- type SortKeysOptions = {
21
- order?: "asc" | "desc";
22
- caseSensitive?: boolean;
23
- natural?: boolean;
24
- minKeys?: number;
25
- variablesBeforeFunctions?: boolean;
26
- };
27
-
28
- type Options = [SortKeysOptions?];
29
- type MessageIds = "unsorted";
30
-
31
- type KeyInfo = {
32
- keyName: string | null;
33
- node: TSESTree.Property | TSESTree.SpreadElement;
34
- isFunction: boolean;
35
- };
36
-
37
- type TopLevelBinding =
38
- | {
39
- kind: "function";
40
- node:
41
- | TSESTree.ArrowFunctionExpression
42
- | TSESTree.FunctionDeclaration
43
- | TSESTree.FunctionExpression;
44
- }
45
- | {
46
- kind: "import";
47
- }
48
- | {
49
- kind: "value";
50
- node: TSESTree.Expression;
51
- };
52
-
53
- const SORT_BEFORE = -1;
54
- const PURE_CONSTRUCTORS = new Set(["Date"]);
55
- const PURE_GLOBAL_FUNCTIONS = new Set(["Boolean", "Number", "String"]);
56
- const PURE_MEMBER_METHODS = new Set([
57
- "getDay",
58
- "getHours",
59
- "getMilliseconds",
60
- "getMinutes",
61
- "getSeconds",
62
- "padStart"
63
- ]);
64
-
65
- const hasDuplicateNames = (names: Array<string | null>) => {
66
- const seen = new Set<string>();
67
- const nonNullNames = names.flatMap((name) => (name === null ? [] : [name]));
68
-
69
- for (const name of nonNullNames) {
70
- if (seen.has(name)) {
71
- return true;
72
- }
73
- seen.add(name);
74
- }
75
-
76
- return false;
77
- };
78
-
79
- export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
80
- create(context) {
81
- const { sourceCode } = context;
82
- const [option] = context.options;
83
- const topLevelBindings = new Map<string, TopLevelBinding>();
84
- const pureFunctionCache = new Map<TSESTree.Node, boolean>();
85
- const pureFunctionInProgress = new Set<TSESTree.Node>();
86
-
87
- const order: "asc" | "desc" =
88
- option && option.order ? option.order : "asc";
89
-
90
- const caseSensitive =
91
- option && typeof option.caseSensitive === "boolean"
92
- ? option.caseSensitive
93
- : false;
94
-
95
- const natural =
96
- option && typeof option.natural === "boolean"
97
- ? option.natural
98
- : false;
99
-
100
- const minKeys =
101
- option && typeof option.minKeys === "number" ? option.minKeys : 2;
102
-
103
- const variablesBeforeFunctions =
104
- option && typeof option.variablesBeforeFunctions === "boolean"
105
- ? option.variablesBeforeFunctions
106
- : false;
107
-
108
- /**
109
- * Compare two key strings based on the provided options.
110
- * This function mimics the behavior of the built-in rule.
111
- */
112
- const compareKeys = (keyLeft: string, keyRight: string) => {
113
- let left = keyLeft;
114
- let right = keyRight;
115
-
116
- if (!caseSensitive) {
117
- left = left.toLowerCase();
118
- right = right.toLowerCase();
119
- }
120
-
121
- if (natural) {
122
- return left.localeCompare(right, undefined, {
123
- numeric: true
124
- });
125
- }
126
-
127
- return left.localeCompare(right);
128
- };
129
-
130
- const addImportBindings = (statement: TSESTree.ImportDeclaration) => {
131
- if (statement.specifiers.length === 0) {
132
- return;
133
- }
134
-
135
- for (const specifier of statement.specifiers) {
136
- topLevelBindings.set(specifier.local.name, {
137
- kind: "import"
138
- });
139
- }
140
- };
141
-
142
- const addVariableBinding = (
143
- declaration: TSESTree.VariableDeclarator
144
- ) => {
145
- if (declaration.id.type !== "Identifier" || !declaration.init) {
146
- return;
147
- }
148
-
149
- if (
150
- declaration.init.type === "ArrowFunctionExpression" ||
151
- declaration.init.type === "FunctionExpression"
152
- ) {
153
- topLevelBindings.set(declaration.id.name, {
154
- kind: "function",
155
- node: declaration.init
156
- });
157
- return;
158
- }
159
-
160
- topLevelBindings.set(declaration.id.name, {
161
- kind: "value",
162
- node: declaration.init
163
- });
164
- };
165
-
166
- const addTopLevelBindings = (statement: TSESTree.ProgramStatement) => {
167
- if (statement.type === "ImportDeclaration") {
168
- addImportBindings(statement);
169
- return;
170
- }
171
-
172
- if (statement.type === "FunctionDeclaration" && statement.id) {
173
- topLevelBindings.set(statement.id.name, {
174
- kind: "function",
175
- node: statement
176
- });
177
- return;
178
- }
179
-
180
- if (
181
- statement.type !== "VariableDeclaration" ||
182
- statement.kind !== "const"
183
- ) {
184
- return;
185
- }
186
-
187
- for (const declaration of statement.declarations) {
188
- addVariableBinding(declaration);
189
- }
190
- };
191
-
192
- for (const statement of sourceCode.ast.body) {
193
- addTopLevelBindings(statement);
194
- }
195
-
196
- const addBoundIdentifiers = (
197
- node: TSESTree.Node | null,
198
- stableLocals: Set<string>
199
- ) => {
200
- if (!node) {
201
- return;
202
- }
203
-
204
- switch (node.type) {
205
- case "Identifier":
206
- stableLocals.add(node.name);
207
- return;
208
- case "AssignmentPattern":
209
- addBoundIdentifiers(node.left, stableLocals);
210
- return;
211
- case "RestElement":
212
- addBoundIdentifiers(node.argument, stableLocals);
213
- return;
214
- case "ArrayPattern":
215
- for (const element of node.elements.filter(Boolean)) {
216
- addBoundIdentifiers(element, stableLocals);
217
- }
218
- break;
219
- case "ObjectPattern":
220
- for (const property of node.properties) {
221
- const bindingNode =
222
- property.type === "RestElement"
223
- ? property.argument
224
- : property.value;
225
- addBoundIdentifiers(bindingNode, stableLocals);
226
- }
227
- break;
228
- default:
229
- break;
230
- }
231
- };
232
-
233
- const addFunctionParamBindings = (
234
- functionNode:
235
- | TSESTree.ArrowFunctionExpression
236
- | TSESTree.FunctionDeclaration
237
- | TSESTree.FunctionExpression,
238
- stableLocals: Set<string>
239
- ) => {
240
- for (const parameter of functionNode.params) {
241
- addBoundIdentifiers(parameter, stableLocals);
242
- }
243
- };
244
-
245
- const addAncestorConstBindings = (
246
- ancestor: TSESTree.BlockStatement | TSESTree.Program,
247
- node: TSESTree.Node,
248
- stableLocals: Set<string>
249
- ) => {
250
- const addDeclarationBindings = (statement: TSESTree.Statement) => {
251
- if (
252
- statement.type !== "VariableDeclaration" ||
253
- statement.kind !== "const"
254
- ) {
255
- return;
256
- }
257
-
258
- for (const declaration of statement.declarations) {
259
- addBoundIdentifiers(declaration.id, stableLocals);
260
- }
261
- };
262
-
263
- for (const statement of ancestor.body) {
264
- if (statement.range[0] >= node.range[0]) {
265
- return;
266
- }
267
-
268
- addDeclarationBindings(statement);
269
- }
270
- };
271
-
272
- const addAncestorBindingsForNode = (
273
- ancestor: TSESTree.Node,
274
- node: TSESTree.Node,
275
- stableLocals: Set<string>
276
- ) => {
277
- if (
278
- ancestor.type !== "Program" &&
279
- ancestor.type !== "BlockStatement"
280
- ) {
281
- return;
282
- }
283
-
284
- addAncestorConstBindings(ancestor, node, stableLocals);
285
- };
286
-
287
- const addFunctionBindingsForAncestor = (
288
- ancestor: TSESTree.Node,
289
- stableLocals: Set<string>
290
- ) => {
291
- if (
292
- ancestor.type !== "FunctionDeclaration" &&
293
- ancestor.type !== "FunctionExpression" &&
294
- ancestor.type !== "ArrowFunctionExpression"
295
- ) {
296
- return;
297
- }
298
-
299
- addFunctionParamBindings(ancestor, stableLocals);
300
- };
301
-
302
- const getStableLocalsForNode = (node: TSESTree.Node) => {
303
- const stableLocals = new Set<string>();
304
- const ancestors = sourceCode.getAncestors(node);
305
-
306
- for (const ancestor of ancestors) {
307
- addFunctionBindingsForAncestor(ancestor, stableLocals);
308
- }
309
-
310
- for (const ancestor of ancestors) {
311
- addAncestorBindingsForNode(ancestor, node, stableLocals);
312
- }
313
-
314
- return stableLocals;
315
- };
316
-
317
- const getStaticMemberName = (
318
- memberExpression: TSESTree.MemberExpression
319
- ) => {
320
- if (
321
- !memberExpression.computed &&
322
- memberExpression.property.type === "Identifier"
323
- ) {
324
- return memberExpression.property.name;
325
- }
326
-
327
- if (
328
- memberExpression.computed &&
329
- memberExpression.property.type === "Literal" &&
330
- typeof memberExpression.property.value === "string"
331
- ) {
332
- return memberExpression.property.value;
333
- }
334
-
335
- return null;
336
- };
337
-
338
- const isStableIdentifier = (
339
- name: string,
340
- stableLocals: ReadonlySet<string>
341
- ) => {
342
- if (stableLocals.has(name)) {
343
- return true;
344
- }
345
-
346
- const binding = topLevelBindings.get(name);
347
- if (!binding) {
348
- return false;
349
- }
350
-
351
- if (binding.kind === "import") {
352
- return true;
353
- }
354
-
355
- if (binding.kind === "value") {
356
- return isPureRuntimeExpression(binding.node, stableLocals);
357
- }
358
-
359
- return false;
360
- };
361
-
362
- const isPureConstStatement = (
363
- statement: TSESTree.VariableDeclaration,
364
- stableLocals: Set<string>,
365
- checkExpression: (expression: TSESTree.Expression) => boolean
366
- ) => {
367
- if (statement.kind !== "const") {
368
- return false;
369
- }
370
-
371
- for (const declaration of statement.declarations) {
372
- if (declaration.id.type !== "Identifier" || !declaration.init) {
373
- return false;
374
- }
375
-
376
- if (!checkExpression(declaration.init)) {
377
- return false;
378
- }
379
-
380
- stableLocals.add(declaration.id.name);
381
- }
382
-
383
- return true;
384
- };
385
-
386
- const isPureFunctionStatement = (
387
- statement: TSESTree.Statement,
388
- stableLocals: Set<string>,
389
- checkExpression: (expression: TSESTree.Expression) => boolean
390
- ) => {
391
- if (statement.type === "ReturnStatement") {
392
- return (
393
- !statement.argument || checkExpression(statement.argument)
394
- );
395
- }
396
-
397
- if (statement.type === "VariableDeclaration") {
398
- return isPureConstStatement(
399
- statement,
400
- stableLocals,
401
- checkExpression
402
- );
403
- }
404
-
405
- return false;
406
- };
407
-
408
- const isPureFunctionBody = (
409
- body: TSESTree.BlockStatement,
410
- stableLocals: Set<string>,
411
- checkExpression: (expression: TSESTree.Expression) => boolean
412
- ) => {
413
- for (const statement of body.body) {
414
- const statementIsPure = isPureFunctionStatement(
415
- statement,
416
- stableLocals,
417
- checkExpression
418
- );
419
- if (!statementIsPure) {
420
- return false;
421
- }
422
- }
423
-
424
- return true;
425
- };
426
-
427
- const isPureTopLevelFunction = (
428
- functionNode:
429
- | TSESTree.ArrowFunctionExpression
430
- | TSESTree.FunctionDeclaration
431
- | TSESTree.FunctionExpression
432
- ) => {
433
- const cached = pureFunctionCache.get(functionNode);
434
- if (cached !== undefined) {
435
- return cached;
436
- }
437
-
438
- if (pureFunctionInProgress.has(functionNode)) {
439
- return false;
440
- }
441
-
442
- pureFunctionInProgress.add(functionNode);
443
-
444
- const stableLocals = new Set<string>();
445
- addFunctionParamBindings(functionNode, stableLocals);
446
- const checkExpression = (expression: TSESTree.Expression) =>
447
- isPureRuntimeExpression(expression, stableLocals);
448
- const isPure =
449
- functionNode.body.type === "BlockStatement"
450
- ? isPureFunctionBody(
451
- functionNode.body,
452
- stableLocals,
453
- checkExpression
454
- )
455
- : checkExpression(functionNode.body);
456
-
457
- pureFunctionInProgress.delete(functionNode);
458
- pureFunctionCache.set(functionNode, isPure);
459
- return isPure;
460
- };
461
-
462
- const isPureIdentifierCall = (
463
- callExpression: TSESTree.CallExpression
464
- ) => {
465
- if (callExpression.callee.type !== "Identifier") {
466
- return false;
467
- }
468
-
469
- if (PURE_GLOBAL_FUNCTIONS.has(callExpression.callee.name)) {
470
- return true;
471
- }
472
-
473
- const binding = topLevelBindings.get(callExpression.callee.name);
474
- return binding?.kind === "function"
475
- ? isPureTopLevelFunction(binding.node)
476
- : false;
477
- };
478
-
479
- const isPureRuntimeExpression: (
480
- node: TSESTree.Node | null,
481
- stableLocals: ReadonlySet<string>
482
- ) => boolean = (node, stableLocals) => {
483
- if (!node || node.type === "PrivateIdentifier") {
484
- return false;
485
- }
486
-
487
- switch (node.type) {
488
- case "Identifier":
489
- return isStableIdentifier(node.name, stableLocals);
490
- case "Literal":
491
- case "FunctionExpression":
492
- case "ArrowFunctionExpression":
493
- case "ClassExpression":
494
- return true;
495
- case "ThisExpression":
496
- return stableLocals.has("this");
497
- case "TemplateLiteral":
498
- return node.expressions.every((expression) =>
499
- isPureRuntimeExpression(expression, stableLocals)
500
- );
501
- case "UnaryExpression":
502
- return isPureRuntimeExpression(node.argument, stableLocals);
503
- case "BinaryExpression":
504
- case "LogicalExpression":
505
- return (
506
- isPureRuntimeExpression(node.left, stableLocals) &&
507
- isPureRuntimeExpression(node.right, stableLocals)
508
- );
509
- case "ConditionalExpression":
510
- return (
511
- isPureRuntimeExpression(node.test, stableLocals) &&
512
- isPureRuntimeExpression(
513
- node.consequent,
514
- stableLocals
515
- ) &&
516
- isPureRuntimeExpression(node.alternate, stableLocals)
517
- );
518
- case "ArrayExpression":
519
- return node.elements.every((element) => {
520
- if (!element || element.type === "SpreadElement") {
521
- return false;
522
- }
523
-
524
- return isPureRuntimeExpression(element, stableLocals);
525
- });
526
- case "ObjectExpression":
527
- return node.properties.every((property) => {
528
- if (
529
- property.type !== "Property" ||
530
- property.computed ||
531
- property.kind !== "init"
532
- ) {
533
- return false;
534
- }
535
-
536
- if (
537
- property.key.type !== "Identifier" &&
538
- property.key.type !== "Literal"
539
- ) {
540
- return false;
541
- }
542
-
543
- if (property.method) {
544
- return true;
545
- }
546
-
547
- return isPureRuntimeExpression(
548
- property.value,
549
- stableLocals
550
- );
551
- });
552
- case "MemberExpression":
553
- return (
554
- isPureRuntimeExpression(node.object, stableLocals) &&
555
- (!node.computed ||
556
- isPureRuntimeExpression(
557
- node.property,
558
- stableLocals
559
- ))
560
- );
561
- case "NewExpression":
562
- return (
563
- node.callee.type === "Identifier" &&
564
- PURE_CONSTRUCTORS.has(node.callee.name) &&
565
- node.arguments.every((argument) => {
566
- if (argument.type === "SpreadElement") {
567
- return false;
568
- }
569
-
570
- return isPureRuntimeExpression(
571
- argument,
572
- stableLocals
573
- );
574
- })
575
- );
576
- case "CallExpression": {
577
- const argsArePure = node.arguments.every((argument) => {
578
- if (argument.type === "SpreadElement") {
579
- return false;
580
- }
581
-
582
- return isPureRuntimeExpression(argument, stableLocals);
583
- });
584
-
585
- if (!argsArePure) {
586
- return false;
587
- }
588
-
589
- if (node.callee.type === "Identifier") {
590
- return isPureIdentifierCall(node);
591
- }
592
-
593
- if (node.callee.type !== "MemberExpression") {
594
- return false;
595
- }
596
-
597
- const memberName = getStaticMemberName(node.callee);
598
- if (!memberName || !PURE_MEMBER_METHODS.has(memberName)) {
599
- return false;
600
- }
601
-
602
- return isPureRuntimeExpression(
603
- node.callee.object,
604
- stableLocals
605
- );
606
- }
607
- default:
608
- return false;
609
- }
610
- };
611
-
612
- const isSafeJSXAttributeValue = (
613
- value: TSESTree.JSXAttribute["value"],
614
- scopeNode: TSESTree.Node
615
- ) => {
616
- if (value === null) {
617
- return true;
618
- }
619
-
620
- if (value.type === "Literal") {
621
- return true;
622
- }
623
-
624
- if (value.type !== "JSXExpressionContainer") {
625
- return false;
626
- }
627
-
628
- if (value.expression.type === "JSXEmptyExpression") {
629
- return false;
630
- }
631
-
632
- return isPureRuntimeExpression(
633
- value.expression,
634
- getStableLocalsForNode(scopeNode)
635
- );
636
- };
637
-
638
- /**
639
- * Determines if a property is a function property.
640
- */
641
- const isFunctionProperty = (prop: TSESTree.Property) => {
642
- const { value } = prop;
643
- return (
644
- Boolean(value) &&
645
- (value.type === "FunctionExpression" ||
646
- value.type === "ArrowFunctionExpression" ||
647
- prop.method === true)
648
- );
649
- };
650
-
651
- /**
652
- * Safely extracts a key name from a Property that we already know
653
- * only uses Identifier or Literal keys in the fixer.
654
- */
655
- const getPropertyKeyName = (prop: TSESTree.Property) => {
656
- const { key } = prop;
657
- if (key.type === "Identifier") {
658
- return key.name;
659
- }
660
- if (key.type === "Literal") {
661
- const { value } = key;
662
- if (typeof value === "string") {
663
- return value;
664
- }
665
- return String(value);
666
- }
667
- return "";
668
- };
669
-
670
- /**
671
- * Get leading comments for a property, excluding any comments that
672
- * are on the same line as the previous property (those are trailing
673
- * comments of the previous property).
674
- */
675
- const getLeadingComments = (
676
- prop: TSESTree.Property,
677
- prevProp: TSESTree.Property | null
678
- ) => {
679
- const comments = sourceCode.getCommentsBefore(prop);
680
- if (!prevProp || comments.length === 0) {
681
- return comments;
682
- }
683
- // Filter out comments on the same line as the previous property
684
- return comments.filter(
685
- (comment) => comment.loc.start.line !== prevProp.loc.end.line
686
- );
687
- };
688
-
689
- /**
690
- * Get trailing comments for a property that are on the same line.
691
- * This includes both getCommentsAfter AND any getCommentsBefore of the
692
- * next property that are on the same line as this property.
693
- */
694
- const getTrailingComments = (
695
- prop: TSESTree.Property,
696
- nextProp: TSESTree.Property | null
697
- ) => {
698
- const after = sourceCode
699
- .getCommentsAfter(prop)
700
- .filter(
701
- (comment) => comment.loc.start.line === prop.loc.end.line
702
- );
703
- if (!nextProp) {
704
- return after;
705
- }
706
-
707
- const beforeNext = sourceCode.getCommentsBefore(nextProp);
708
- const trailingOfPrev = beforeNext.filter(
709
- (comment) => comment.loc.start.line === prop.loc.end.line
710
- );
711
- // Merge, avoiding duplicates
712
- const newComments = trailingOfPrev.filter(
713
- (comment) =>
714
- !after.some(
715
- (existing) => existing.range[0] === comment.range[0]
716
- )
717
- );
718
- after.push(...newComments);
719
- return after;
720
- };
721
-
722
- const getChunkStart = (
723
- idx: number,
724
- fixableProps: TSESTree.Property[],
725
- rangeStart: number,
726
- fullStart: number
727
- ) => {
728
- if (idx === 0) {
729
- return rangeStart;
730
- }
731
-
732
- const prevProp = fixableProps[idx - 1]!;
733
- const currentProp = fixableProps[idx]!;
734
- const prevTrailing = getTrailingComments(prevProp, currentProp);
735
- const prevEnd =
736
- prevTrailing.length > 0
737
- ? prevTrailing[prevTrailing.length - 1]!.range[1]
738
- : prevProp.range[1];
739
- // Find the comma after the previous property/comments
740
- const allTokens = sourceCode.getTokensBetween(
741
- prevProp,
742
- currentProp,
743
- {
744
- includeComments: false
745
- }
746
- );
747
- const tokenAfterPrev =
748
- allTokens.find((tok) => tok.range[0] >= prevEnd) ?? null;
749
- if (
750
- tokenAfterPrev &&
751
- tokenAfterPrev.value === "," &&
752
- tokenAfterPrev.range[1] <= fullStart
753
- ) {
754
- return tokenAfterPrev.range[1];
755
- }
756
- return prevEnd;
757
- };
758
-
759
- /**
760
- * Build the sorted text from the fixable properties while preserving
761
- * comments and formatting.
762
- */
763
- const buildSortedText = (
764
- fixableProps: TSESTree.Property[],
765
- rangeStart: number
766
- ) => {
767
- // For each property, capture its "chunk": the property text plus
768
- // its associated comments (leading comments on separate lines,
769
- // trailing comments on the same line).
770
- const chunks: {
771
- prop: TSESTree.Property;
772
- text: string;
773
- }[] = [];
774
-
775
- for (let idx = 0; idx < fixableProps.length; idx++) {
776
- const prop = fixableProps[idx]!;
777
- const prevProp = idx > 0 ? fixableProps[idx - 1]! : null;
778
- const nextProp =
779
- idx < fixableProps.length - 1
780
- ? fixableProps[idx + 1]!
781
- : null;
782
-
783
- const leading = getLeadingComments(prop, prevProp);
784
- const trailing = getTrailingComments(prop, nextProp);
785
-
786
- const fullStart =
787
- leading.length > 0 ? leading[0]!.range[0] : prop.range[0];
788
- const fullEnd =
789
- trailing.length > 0
790
- ? trailing[trailing.length - 1]!.range[1]
791
- : prop.range[1];
792
-
793
- const chunkStart = getChunkStart(
794
- idx,
795
- fixableProps,
796
- rangeStart,
797
- fullStart
798
- );
799
-
800
- const text = sourceCode.text.slice(chunkStart, fullEnd);
801
- chunks.push({ prop, text });
802
- }
803
-
804
- // Sort the chunks
805
- const sorted = chunks.slice().sort((left, right) => {
806
- if (variablesBeforeFunctions) {
807
- const leftIsFunc = isFunctionProperty(left.prop);
808
- const rightIsFunc = isFunctionProperty(right.prop);
809
- if (leftIsFunc !== rightIsFunc) {
810
- return leftIsFunc ? 1 : SORT_BEFORE;
811
- }
812
- }
813
-
814
- const leftKey = getPropertyKeyName(left.prop);
815
- const rightKey = getPropertyKeyName(right.prop);
816
-
817
- let res = compareKeys(leftKey, rightKey);
818
- if (order === "desc") {
819
- res = -res;
820
- }
821
- return res;
822
- });
823
-
824
- // Detect separator: check if the object is multiline by comparing
825
- // the first and last property lines. If multiline, use the
826
- // indentation of the first property.
827
- const firstPropLine = fixableProps[0]!.loc.start.line;
828
- const lastPropLine =
829
- fixableProps[fixableProps.length - 1]!.loc.start.line;
830
- const isMultiline = firstPropLine !== lastPropLine;
831
- let separator: string;
832
- if (isMultiline) {
833
- // Detect indentation from the first property's column
834
- const col = fixableProps[0]!.loc.start.column;
835
- const indent = sourceCode.text.slice(
836
- fixableProps[0]!.range[0] - col,
837
- fixableProps[0]!.range[0]
838
- );
839
- separator = `,\n${indent}`;
840
- } else {
841
- separator = ", ";
842
- }
843
-
844
- // Rebuild: first chunk keeps original leading whitespace,
845
- // subsequent chunks use the detected separator
846
- return sorted
847
- .map((chunk, idx) => {
848
- if (idx === 0) {
849
- const originalFirstChunk = chunks[0]!;
850
- const originalLeadingWs =
851
- originalFirstChunk.text.match(/^(\s*)/)?.[1] ?? "";
852
- const stripped = chunk.text.replace(/^\s*/, "");
853
- return originalLeadingWs + stripped;
854
- }
855
- const stripped = chunk.text.replace(/^\s*/, "");
856
- return separator + stripped;
857
- })
858
- .join("");
859
- };
860
-
861
- const getFixableProps = (node: TSESTree.ObjectExpression) =>
862
- node.properties.filter(
863
- (prop): prop is TSESTree.Property =>
864
- prop.type === "Property" &&
865
- !prop.computed &&
866
- (prop.key.type === "Identifier" ||
867
- prop.key.type === "Literal")
868
- );
869
-
870
- /**
871
- * Checks an ObjectExpression node for unsorted keys.
872
- * Reports an error on each out-of-order key.
873
- *
874
- * For auto-fix purposes, only simple properties are considered fixable.
875
- * (Computed keys, spread elements, or non-Identifier/Literal keys disable the fix.)
876
- */
877
- const checkObjectExpression = (node: TSESTree.ObjectExpression) => {
878
- if (node.properties.length < minKeys) {
879
- return;
880
- }
881
-
882
- let autoFixable = true;
883
-
884
- const keys: KeyInfo[] = node.properties.map((prop) => {
885
- let keyName: string | null = null;
886
- let isFunc = false;
887
-
888
- if (prop.type !== "Property") {
889
- autoFixable = false;
890
- return {
891
- isFunction: isFunc,
892
- keyName,
893
- node: prop
894
- };
895
- }
896
-
897
- if (prop.computed) {
898
- autoFixable = false;
899
- }
900
-
901
- if (prop.key.type === "Identifier") {
902
- keyName = prop.key.name;
903
- } else if (prop.key.type === "Literal") {
904
- const { value } = prop.key;
905
- keyName = typeof value === "string" ? value : String(value);
906
- } else {
907
- autoFixable = false;
908
- }
909
-
910
- if (isFunctionProperty(prop)) {
911
- isFunc = true;
912
- }
913
-
914
- return {
915
- isFunction: isFunc,
916
- keyName,
917
- node: prop
918
- };
919
- });
920
-
921
- if (hasDuplicateNames(keys.map((key) => key.keyName))) {
922
- autoFixable = false;
923
- }
924
-
925
- if (
926
- autoFixable &&
927
- keys.some(
928
- (key) =>
929
- key.node.type === "Property" &&
930
- !isPureRuntimeExpression(
931
- key.node.value,
932
- getStableLocalsForNode(key.node)
933
- )
934
- )
935
- ) {
936
- autoFixable = false;
937
- }
938
-
939
- let fixProvided = false;
940
-
941
- const createReportWithFix = (curr: KeyInfo, shouldFix: boolean) => {
942
- context.report({
943
- fix: shouldFix
944
- ? (fixer) => {
945
- const fixableProps = getFixableProps(node);
946
- if (fixableProps.length < minKeys) {
947
- return null;
948
- }
949
-
950
- const [firstProp] = fixableProps;
951
- const lastProp =
952
- fixableProps[fixableProps.length - 1];
953
-
954
- if (!firstProp || !lastProp) {
955
- return null;
956
- }
957
-
958
- const firstLeading = getLeadingComments(
959
- firstProp,
960
- null
961
- );
962
- const [firstLeadingComment] = firstLeading;
963
- const rangeStart = firstLeadingComment
964
- ? firstLeadingComment.range[0]
965
- : firstProp.range[0];
966
- const lastTrailing = getTrailingComments(
967
- lastProp,
968
- null
969
- );
970
- const rangeEnd =
971
- lastTrailing.length > 0
972
- ? lastTrailing[lastTrailing.length - 1]!
973
- .range[1]
974
- : lastProp.range[1];
975
- const sortedText = buildSortedText(
976
- fixableProps,
977
- rangeStart
978
- );
979
-
980
- return fixer.replaceTextRange(
981
- [rangeStart, rangeEnd],
982
- sortedText
983
- );
984
- }
985
- : null,
986
- messageId: "unsorted",
987
- node:
988
- curr.node.type === "Property"
989
- ? curr.node.key
990
- : curr.node
991
- });
992
- fixProvided = true;
993
- };
994
-
995
- keys.forEach((curr, idx) => {
996
- if (idx === 0) {
997
- return;
998
- }
999
- const prev = keys[idx - 1];
1000
-
1001
- if (
1002
- !prev ||
1003
- !curr ||
1004
- prev.keyName === null ||
1005
- curr.keyName === null
1006
- ) {
1007
- return;
1008
- }
1009
-
1010
- const shouldFix = !fixProvided && autoFixable;
1011
-
1012
- if (
1013
- variablesBeforeFunctions &&
1014
- prev.isFunction &&
1015
- !curr.isFunction
1016
- ) {
1017
- createReportWithFix(curr, shouldFix);
1018
- return;
1019
- }
1020
-
1021
- if (
1022
- variablesBeforeFunctions &&
1023
- prev.isFunction === curr.isFunction &&
1024
- compareKeys(prev.keyName, curr.keyName) > 0
1025
- ) {
1026
- createReportWithFix(curr, shouldFix);
1027
- return;
1028
- }
1029
-
1030
- if (
1031
- !variablesBeforeFunctions &&
1032
- compareKeys(prev.keyName, curr.keyName) > 0
1033
- ) {
1034
- createReportWithFix(curr, shouldFix);
1035
- }
1036
- });
1037
- };
1038
-
1039
- // Also check object literals inside JSX prop expressions
1040
- const checkJSXAttributeObject = (attr: TSESTree.JSXAttribute) => {
1041
- const { value } = attr;
1042
- if (
1043
- value &&
1044
- value.type === "JSXExpressionContainer" &&
1045
- value.expression &&
1046
- value.expression.type === "ObjectExpression"
1047
- ) {
1048
- checkObjectExpression(value.expression);
1049
- }
1050
- };
1051
-
1052
- const getAttrName = (
1053
- attr: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute
1054
- ) => {
1055
- if (
1056
- attr.type !== "JSXAttribute" ||
1057
- attr.name.type !== "JSXIdentifier"
1058
- ) {
1059
- return "";
1060
- }
1061
- return attr.name.name;
1062
- };
1063
-
1064
- const compareAttrNames = (nameLeft: string, nameRight: string) => {
1065
- let res = compareKeys(nameLeft, nameRight);
1066
- if (order === "desc") {
1067
- res = -res;
1068
- }
1069
- return res;
1070
- };
1071
-
1072
- const isOutOfOrder = (names: string[]) =>
1073
- names.some((currName, idx) => {
1074
- if (idx === 0 || !currName) {
1075
- return false;
1076
- }
1077
- const prevName = names[idx - 1];
1078
- return (
1079
- prevName !== undefined &&
1080
- compareAttrNames(prevName, currName) > 0
1081
- );
1082
- });
1083
-
1084
- // Also sort JSX attributes on elements
1085
- const checkJSXOpeningElement = (node: TSESTree.JSXOpeningElement) => {
1086
- const attrs = node.attributes;
1087
- if (attrs.length < minKeys) {
1088
- return;
1089
- }
1090
-
1091
- if (attrs.some((attr) => attr.type !== "JSXAttribute")) {
1092
- return;
1093
- }
1094
- if (
1095
- attrs.some(
1096
- (attr) =>
1097
- attr.type === "JSXAttribute" &&
1098
- attr.name.type !== "JSXIdentifier"
1099
- )
1100
- ) {
1101
- return;
1102
- }
1103
-
1104
- const names = attrs.map((attr) => getAttrName(attr));
1105
-
1106
- if (!isOutOfOrder(names)) {
1107
- return;
1108
- }
1109
-
1110
- if (hasDuplicateNames(names)) {
1111
- context.report({
1112
- messageId: "unsorted",
1113
- node:
1114
- attrs[0]!.type === "JSXAttribute"
1115
- ? attrs[0]!.name
1116
- : attrs[0]!
1117
- });
1118
- return;
1119
- }
1120
-
1121
- if (
1122
- attrs.some(
1123
- (attr) =>
1124
- attr.type === "JSXAttribute" &&
1125
- !isSafeJSXAttributeValue(attr.value, attr)
1126
- )
1127
- ) {
1128
- context.report({
1129
- messageId: "unsorted",
1130
- node:
1131
- attrs[0]!.type === "JSXAttribute"
1132
- ? attrs[0]!.name
1133
- : attrs[0]!
1134
- });
1135
- return;
1136
- }
1137
-
1138
- // Be conservative: only fix if there are no JSX comments/braces between attributes.
1139
- const braceConflict = attrs.find((currAttr, idx) => {
1140
- if (idx === 0) {
1141
- return false;
1142
- }
1143
- const prevAttr = attrs[idx - 1];
1144
- if (!prevAttr) {
1145
- return false;
1146
- }
1147
- const between = sourceCode.text.slice(
1148
- prevAttr.range[1],
1149
- currAttr.range[0]
1150
- );
1151
- return between.includes("{");
1152
- });
1153
-
1154
- if (braceConflict) {
1155
- context.report({
1156
- messageId: "unsorted",
1157
- node:
1158
- braceConflict.type === "JSXAttribute"
1159
- ? braceConflict.name
1160
- : braceConflict
1161
- });
1162
- return;
1163
- }
1164
-
1165
- const sortedAttrs = attrs
1166
- .slice()
1167
- .sort((left, right) =>
1168
- compareAttrNames(getAttrName(left), getAttrName(right))
1169
- );
1170
-
1171
- const [firstAttr] = attrs;
1172
- const lastAttr = attrs[attrs.length - 1];
1173
-
1174
- if (!firstAttr || !lastAttr) {
1175
- return;
1176
- }
1177
-
1178
- const replacement = sortedAttrs
1179
- .map((attr) => sourceCode.getText(attr))
1180
- .join(" ");
1181
-
1182
- context.report({
1183
- fix(fixer) {
1184
- return fixer.replaceTextRange(
1185
- [firstAttr.range[0], lastAttr.range[1]],
1186
- replacement
1187
- );
1188
- },
1189
- messageId: "unsorted",
1190
- node:
1191
- firstAttr.type === "JSXAttribute"
1192
- ? firstAttr.name
1193
- : firstAttr
1194
- });
1195
- };
1196
-
1197
- return {
1198
- JSXAttribute(node: TSESTree.JSXAttribute) {
1199
- checkJSXAttributeObject(node);
1200
- },
1201
- JSXOpeningElement: checkJSXOpeningElement,
1202
- ObjectExpression: checkObjectExpression
1203
- };
1204
- },
1205
- defaultOptions: [{}],
1206
- meta: {
1207
- docs: {
1208
- description:
1209
- "enforce sorted keys in object literals with auto-fix (limited to simple cases, preserving comments)"
1210
- },
1211
- fixable: "code",
1212
- messages: {
1213
- unsorted: "Object keys are not sorted."
1214
- },
1215
- // The schema supports the same options as the built-in sort-keys rule plus:
1216
- // variablesBeforeFunctions: boolean (when true, non-function properties come before function properties)
1217
- schema: [
1218
- {
1219
- additionalProperties: false,
1220
- properties: {
1221
- caseSensitive: {
1222
- type: "boolean"
1223
- },
1224
- minKeys: {
1225
- minimum: 2,
1226
- type: "integer"
1227
- },
1228
- natural: {
1229
- type: "boolean"
1230
- },
1231
- order: {
1232
- enum: ["asc", "desc"],
1233
- type: "string"
1234
- },
1235
- variablesBeforeFunctions: {
1236
- type: "boolean"
1237
- }
1238
- },
1239
- type: "object"
1240
- }
1241
- ],
1242
- type: "suggestion"
1243
- }
1244
- };