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