eslint-plugin-absolute 0.2.0 → 0.2.1

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,19 +1,19 @@
1
1
  import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
2
 
3
3
  /**
4
- * @fileoverview Enforce sorted keys in object literals (like ESLints built-in sort-keys)
4
+ * @fileoverview Enforce sorted keys in object literals (like ESLint's built-in sort-keys)
5
5
  * with an auto-fix for simple cases that preserves comments.
6
6
  *
7
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.:
8
+ * However, the auto-fix only applies if all properties are "fixable" – i.e.:
9
9
  * - They are of type Property (not SpreadElement, etc.)
10
10
  * - They are not computed (e.g. [foo])
11
11
  * - They are an Identifier or a Literal.
12
12
  *
13
13
  * Comments attached to the properties are preserved in the auto-fix.
14
14
  *
15
- * Use this rule with a grain of salt. I did not test every edge case and theres
16
- * a reason the original rule doesnt have auto-fix. Computed keys, spread elements,
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
17
  * comments, and formatting are not handled perfectly.
18
18
  */
19
19
 
@@ -34,51 +34,12 @@ type KeyInfo = {
34
34
  isFunction: boolean;
35
35
  };
36
36
 
37
- export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
38
- meta: {
39
- type: "suggestion",
40
- docs: {
41
- description:
42
- "enforce sorted keys in object literals with auto-fix (limited to simple cases, preserving comments)"
43
- },
44
- fixable: "code",
45
- // The schema supports the same options as the built-in sort-keys rule plus:
46
- // variablesBeforeFunctions: boolean (when true, non-function properties come before function properties)
47
- schema: [
48
- {
49
- type: "object",
50
- properties: {
51
- order: {
52
- type: "string",
53
- enum: ["asc", "desc"]
54
- },
55
- caseSensitive: {
56
- type: "boolean"
57
- },
58
- natural: {
59
- type: "boolean"
60
- },
61
- minKeys: {
62
- type: "integer",
63
- minimum: 2
64
- },
65
- variablesBeforeFunctions: {
66
- type: "boolean"
67
- }
68
- },
69
- additionalProperties: false
70
- }
71
- ],
72
- messages: {
73
- unsorted: "Object keys are not sorted."
74
- }
75
- },
76
-
77
- defaultOptions: [{}],
37
+ const SORT_BEFORE = -1;
78
38
 
39
+ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
79
40
  create(context) {
80
- const sourceCode = context.sourceCode;
81
- const option = context.options[0];
41
+ const { sourceCode } = context;
42
+ const [option] = context.options;
82
43
 
83
44
  const order: "asc" | "desc" =
84
45
  option && option.order ? option.order : "asc";
@@ -105,110 +66,153 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
105
66
  * Compare two key strings based on the provided options.
106
67
  * This function mimics the behavior of the built-in rule.
107
68
  */
108
- function compareKeys(a: string, b: string) {
109
- let keyA = a;
110
- let keyB = b;
69
+ const compareKeys = (keyLeft: string, keyRight: string) => {
70
+ let left = keyLeft;
71
+ let right = keyRight;
111
72
 
112
73
  if (!caseSensitive) {
113
- keyA = keyA.toLowerCase();
114
- keyB = keyB.toLowerCase();
74
+ left = left.toLowerCase();
75
+ right = right.toLowerCase();
115
76
  }
116
77
 
117
78
  if (natural) {
118
- return keyA.localeCompare(keyB, undefined, {
79
+ return left.localeCompare(right, undefined, {
119
80
  numeric: true
120
81
  });
121
82
  }
122
83
 
123
- return keyA.localeCompare(keyB);
124
- }
84
+ return left.localeCompare(right);
85
+ };
125
86
 
126
87
  /**
127
88
  * Determines if a property is a function property.
128
89
  */
129
- function isFunctionProperty(prop: TSESTree.Property) {
130
- const value = prop.value;
90
+ const isFunctionProperty = (prop: TSESTree.Property) => {
91
+ const { value } = prop;
131
92
  return (
132
- !!value &&
93
+ Boolean(value) &&
133
94
  (value.type === "FunctionExpression" ||
134
95
  value.type === "ArrowFunctionExpression" ||
135
96
  prop.method === true)
136
97
  );
137
- }
98
+ };
138
99
 
139
100
  /**
140
101
  * Safely extracts a key name from a Property that we already know
141
102
  * only uses Identifier or Literal keys in the fixer.
142
103
  */
143
- function getPropertyKeyName(prop: TSESTree.Property) {
144
- const key = prop.key;
104
+ const getPropertyKeyName = (prop: TSESTree.Property) => {
105
+ const { key } = prop;
145
106
  if (key.type === "Identifier") {
146
107
  return key.name;
147
108
  }
148
109
  if (key.type === "Literal") {
149
- const value = key.value;
110
+ const { value } = key;
150
111
  if (typeof value === "string") {
151
112
  return value;
152
113
  }
153
114
  return String(value);
154
115
  }
155
116
  return "";
156
- }
117
+ };
157
118
 
158
119
  /**
159
120
  * Get leading comments for a property, excluding any comments that
160
121
  * are on the same line as the previous property (those are trailing
161
122
  * comments of the previous property).
162
123
  */
163
- function getLeadingComments(
124
+ const getLeadingComments = (
164
125
  prop: TSESTree.Property,
165
126
  prevProp: TSESTree.Property | null
166
- ): TSESTree.Comment[] {
127
+ ) => {
167
128
  const comments = sourceCode.getCommentsBefore(prop);
168
129
  if (!prevProp || comments.length === 0) {
169
130
  return comments;
170
131
  }
171
132
  // Filter out comments on the same line as the previous property
172
133
  return comments.filter(
173
- (c) => c.loc.start.line !== prevProp.loc.end.line
134
+ (comment) => comment.loc.start.line !== prevProp.loc.end.line
174
135
  );
175
- }
136
+ };
176
137
 
177
138
  /**
178
139
  * Get trailing comments for a property that are on the same line.
179
140
  * This includes both getCommentsAfter AND any getCommentsBefore of the
180
141
  * next property that are on the same line as this property.
181
142
  */
182
- function getTrailingComments(
143
+ const getTrailingComments = (
183
144
  prop: TSESTree.Property,
184
145
  nextProp: TSESTree.Property | null
185
- ): TSESTree.Comment[] {
146
+ ) => {
186
147
  const after = sourceCode
187
148
  .getCommentsAfter(prop)
188
- .filter((c) => c.loc.start.line === prop.loc.end.line);
189
- if (nextProp) {
190
- const beforeNext = sourceCode.getCommentsBefore(nextProp);
191
- const trailingOfPrev = beforeNext.filter(
192
- (c) => c.loc.start.line === prop.loc.end.line
149
+ .filter(
150
+ (comment) => comment.loc.start.line === prop.loc.end.line
193
151
  );
194
- // Merge, avoiding duplicates
195
- for (const c of trailingOfPrev) {
196
- if (!after.some((a) => a.range[0] === c.range[0])) {
197
- after.push(c);
198
- }
199
- }
152
+ if (!nextProp) {
153
+ return after;
200
154
  }
155
+
156
+ const beforeNext = sourceCode.getCommentsBefore(nextProp);
157
+ const trailingOfPrev = beforeNext.filter(
158
+ (comment) => comment.loc.start.line === prop.loc.end.line
159
+ );
160
+ // Merge, avoiding duplicates
161
+ const newComments = trailingOfPrev.filter(
162
+ (comment) =>
163
+ !after.some(
164
+ (existing) => existing.range[0] === comment.range[0]
165
+ )
166
+ );
167
+ after.push(...newComments);
201
168
  return after;
202
- }
169
+ };
170
+
171
+ const getChunkStart = (
172
+ idx: number,
173
+ fixableProps: TSESTree.Property[],
174
+ rangeStart: number,
175
+ fullStart: number
176
+ ) => {
177
+ if (idx === 0) {
178
+ return rangeStart;
179
+ }
180
+
181
+ const prevProp = fixableProps[idx - 1]!;
182
+ const currentProp = fixableProps[idx]!;
183
+ const prevTrailing = getTrailingComments(prevProp, currentProp);
184
+ const prevEnd =
185
+ prevTrailing.length > 0
186
+ ? prevTrailing[prevTrailing.length - 1]!.range[1]
187
+ : prevProp.range[1];
188
+ // Find the comma after the previous property/comments
189
+ const allTokens = sourceCode.getTokensBetween(
190
+ prevProp,
191
+ currentProp,
192
+ {
193
+ includeComments: false
194
+ }
195
+ );
196
+ const tokenAfterPrev =
197
+ allTokens.find((tok) => tok.range[0] >= prevEnd) ?? null;
198
+ if (
199
+ tokenAfterPrev &&
200
+ tokenAfterPrev.value === "," &&
201
+ tokenAfterPrev.range[1] <= fullStart
202
+ ) {
203
+ return tokenAfterPrev.range[1];
204
+ }
205
+ return prevEnd;
206
+ };
203
207
 
204
208
  /**
205
209
  * Build the sorted text from the fixable properties while preserving
206
210
  * comments and formatting.
207
211
  */
208
- function buildSortedText(
212
+ const buildSortedText = (
209
213
  fixableProps: TSESTree.Property[],
210
214
  rangeStart: number
211
- ) {
215
+ ) => {
212
216
  // For each property, capture its "chunk": the property text plus
213
217
  // its associated comments (leading comments on separate lines,
214
218
  // trailing comments on the same line).
@@ -217,11 +221,13 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
217
221
  text: string;
218
222
  }[] = [];
219
223
 
220
- for (let i = 0; i < fixableProps.length; i++) {
221
- const prop = fixableProps[i]!;
222
- const prevProp = i > 0 ? fixableProps[i - 1]! : null;
224
+ for (let idx = 0; idx < fixableProps.length; idx++) {
225
+ const prop = fixableProps[idx]!;
226
+ const prevProp = idx > 0 ? fixableProps[idx - 1]! : null;
223
227
  const nextProp =
224
- i < fixableProps.length - 1 ? fixableProps[i + 1]! : null;
228
+ idx < fixableProps.length - 1
229
+ ? fixableProps[idx + 1]!
230
+ : null;
225
231
 
226
232
  const leading = getLeadingComments(prop, prevProp);
227
233
  const trailing = getTrailingComments(prop, nextProp);
@@ -233,52 +239,31 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
233
239
  ? trailing[trailing.length - 1]!.range[1]
234
240
  : prop.range[1];
235
241
 
236
- // Find the chunk start (after previous property's separator)
237
- let chunkStart: number;
238
- if (i === 0) {
239
- chunkStart = rangeStart;
240
- } else {
241
- const prevTrailing = getTrailingComments(prevProp!, prop);
242
- const prevEnd =
243
- prevTrailing.length > 0
244
- ? prevTrailing[prevTrailing.length - 1]!.range[1]
245
- : prevProp!.range[1];
246
- // Find the comma after the previous property/comments
247
- const tokenAfterPrev = sourceCode.getTokenAfter(
248
- {
249
- range: [prevEnd, prevEnd]
250
- } as TSESTree.Node,
251
- { includeComments: false }
252
- );
253
- if (
254
- tokenAfterPrev &&
255
- tokenAfterPrev.value === "," &&
256
- tokenAfterPrev.range[1] <= fullStart
257
- ) {
258
- chunkStart = tokenAfterPrev.range[1];
259
- } else {
260
- chunkStart = prevEnd;
261
- }
262
- }
242
+ const chunkStart = getChunkStart(
243
+ idx,
244
+ fixableProps,
245
+ rangeStart,
246
+ fullStart
247
+ );
263
248
 
264
249
  const text = sourceCode.text.slice(chunkStart, fullEnd);
265
250
  chunks.push({ prop, text });
266
251
  }
267
252
 
268
253
  // Sort the chunks
269
- const sorted = chunks.slice().sort((a, b) => {
254
+ const sorted = chunks.slice().sort((left, right) => {
270
255
  if (variablesBeforeFunctions) {
271
- const aIsFunc = isFunctionProperty(a.prop);
272
- const bIsFunc = isFunctionProperty(b.prop);
273
- if (aIsFunc !== bIsFunc) {
274
- return aIsFunc ? 1 : -1;
256
+ const leftIsFunc = isFunctionProperty(left.prop);
257
+ const rightIsFunc = isFunctionProperty(right.prop);
258
+ if (leftIsFunc !== rightIsFunc) {
259
+ return leftIsFunc ? 1 : SORT_BEFORE;
275
260
  }
276
261
  }
277
262
 
278
- const aKey = getPropertyKeyName(a.prop);
279
- const bKey = getPropertyKeyName(b.prop);
263
+ const leftKey = getPropertyKeyName(left.prop);
264
+ const rightKey = getPropertyKeyName(right.prop);
280
265
 
281
- let res = compareKeys(aKey, bKey);
266
+ let res = compareKeys(leftKey, rightKey);
282
267
  if (order === "desc") {
283
268
  res = -res;
284
269
  }
@@ -300,7 +285,7 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
300
285
  fixableProps[0]!.range[0] - col,
301
286
  fixableProps[0]!.range[0]
302
287
  );
303
- separator = ",\n" + indent;
288
+ separator = `,\n${indent}`;
304
289
  } else {
305
290
  separator = ", ";
306
291
  }
@@ -308,8 +293,8 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
308
293
  // Rebuild: first chunk keeps original leading whitespace,
309
294
  // subsequent chunks use the detected separator
310
295
  return sorted
311
- .map((chunk, i) => {
312
- if (i === 0) {
296
+ .map((chunk, idx) => {
297
+ if (idx === 0) {
313
298
  const originalFirstChunk = chunks[0]!;
314
299
  const originalLeadingWs =
315
300
  originalFirstChunk.text.match(/^(\s*)/)?.[1] ?? "";
@@ -320,7 +305,16 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
320
305
  return separator + stripped;
321
306
  })
322
307
  .join("");
323
- }
308
+ };
309
+
310
+ const getFixableProps = (node: TSESTree.ObjectExpression) =>
311
+ node.properties.filter(
312
+ (prop): prop is TSESTree.Property =>
313
+ prop.type === "Property" &&
314
+ !prop.computed &&
315
+ (prop.key.type === "Identifier" ||
316
+ prop.key.type === "Literal")
317
+ );
324
318
 
325
319
  /**
326
320
  * Checks an ObjectExpression node for unsorted keys.
@@ -329,7 +323,7 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
329
323
  * For auto-fix purposes, only simple properties are considered fixable.
330
324
  * (Computed keys, spread elements, or non-Identifier/Literal keys disable the fix.)
331
325
  */
332
- function checkObjectExpression(node: TSESTree.ObjectExpression) {
326
+ const checkObjectExpression = (node: TSESTree.ObjectExpression) => {
333
327
  if (node.properties.length < minKeys) {
334
328
  return;
335
329
  }
@@ -340,251 +334,244 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
340
334
  let keyName: string | null = null;
341
335
  let isFunc = false;
342
336
 
343
- if (prop.type === "Property") {
344
- if (prop.computed) {
345
- autoFixable = false;
346
- }
337
+ if (prop.type !== "Property") {
338
+ autoFixable = false;
339
+ return {
340
+ isFunction: isFunc,
341
+ keyName,
342
+ node: prop
343
+ };
344
+ }
347
345
 
348
- if (prop.key.type === "Identifier") {
349
- keyName = prop.key.name;
350
- } else if (prop.key.type === "Literal") {
351
- const value = prop.key.value;
352
- keyName =
353
- typeof value === "string" ? value : String(value);
354
- } else {
355
- autoFixable = false;
356
- }
346
+ if (prop.computed) {
347
+ autoFixable = false;
348
+ }
357
349
 
358
- if (isFunctionProperty(prop)) {
359
- isFunc = true;
360
- }
350
+ if (prop.key.type === "Identifier") {
351
+ keyName = prop.key.name;
352
+ } else if (prop.key.type === "Literal") {
353
+ const { value } = prop.key;
354
+ keyName = typeof value === "string" ? value : String(value);
361
355
  } else {
362
- // Spread elements or other non-Property nodes.
363
356
  autoFixable = false;
364
357
  }
365
358
 
359
+ if (isFunctionProperty(prop)) {
360
+ isFunc = true;
361
+ }
362
+
366
363
  return {
364
+ isFunction: isFunc,
367
365
  keyName,
368
- node: prop,
369
- isFunction: isFunc
366
+ node: prop
370
367
  };
371
368
  });
372
369
 
373
- const getFixableProps = () => {
374
- const props: TSESTree.Property[] = [];
375
- for (const prop of node.properties) {
376
- if (prop.type !== "Property") {
377
- continue;
378
- }
379
- if (prop.computed) {
380
- continue;
381
- }
382
- if (
383
- prop.key.type !== "Identifier" &&
384
- prop.key.type !== "Literal"
385
- ) {
386
- continue;
387
- }
388
- props.push(prop);
389
- }
390
- return props;
391
- };
392
-
393
370
  let fixProvided = false;
394
371
 
395
- for (let i = 1; i < keys.length; i++) {
396
- const prev = keys[i - 1];
397
- const curr = keys[i];
372
+ const createReportWithFix = (curr: KeyInfo, shouldFix: boolean) => {
373
+ context.report({
374
+ fix: shouldFix
375
+ ? (fixer) => {
376
+ const fixableProps = getFixableProps(node);
377
+ if (fixableProps.length < minKeys) {
378
+ return null;
379
+ }
380
+
381
+ const [firstProp] = fixableProps;
382
+ const lastProp =
383
+ fixableProps[fixableProps.length - 1];
398
384
 
399
- if (!prev || !curr) {
400
- continue;
401
- }
385
+ if (!firstProp || !lastProp) {
386
+ return null;
387
+ }
402
388
 
403
- if (prev.keyName === null || curr.keyName === null) {
404
- continue;
389
+ const firstLeading = getLeadingComments(
390
+ firstProp,
391
+ null
392
+ );
393
+ const [firstLeadingComment] = firstLeading;
394
+ const rangeStart = firstLeadingComment
395
+ ? firstLeadingComment.range[0]
396
+ : firstProp.range[0];
397
+ const lastTrailing = getTrailingComments(
398
+ lastProp,
399
+ null
400
+ );
401
+ const rangeEnd =
402
+ lastTrailing.length > 0
403
+ ? lastTrailing[lastTrailing.length - 1]!
404
+ .range[1]
405
+ : lastProp.range[1];
406
+ const sortedText = buildSortedText(
407
+ fixableProps,
408
+ rangeStart
409
+ );
410
+
411
+ return fixer.replaceTextRange(
412
+ [rangeStart, rangeEnd],
413
+ sortedText
414
+ );
415
+ }
416
+ : null,
417
+ messageId: "unsorted",
418
+ node:
419
+ curr.node.type === "Property"
420
+ ? curr.node.key
421
+ : curr.node
422
+ });
423
+ fixProvided = true;
424
+ };
425
+
426
+ keys.forEach((curr, idx) => {
427
+ if (idx === 0) {
428
+ return;
429
+ }
430
+ const prev = keys[idx - 1];
431
+
432
+ if (
433
+ !prev ||
434
+ !curr ||
435
+ prev.keyName === null ||
436
+ curr.keyName === null
437
+ ) {
438
+ return;
405
439
  }
406
440
 
407
441
  const shouldFix = !fixProvided && autoFixable;
408
442
 
409
- const reportWithFix = () => {
410
- context.report({
411
- node:
412
- curr.node.type === "Property"
413
- ? curr.node.key
414
- : curr.node,
415
- messageId: "unsorted",
416
- fix: shouldFix
417
- ? (fixer) => {
418
- const fixableProps = getFixableProps();
419
- if (fixableProps.length < minKeys) {
420
- return null;
421
- }
422
-
423
- const firstProp = fixableProps[0];
424
- const lastProp =
425
- fixableProps[fixableProps.length - 1];
426
-
427
- if (!firstProp || !lastProp) {
428
- return null;
429
- }
430
-
431
- const firstLeading = getLeadingComments(
432
- firstProp,
433
- null
434
- );
435
- const rangeStart =
436
- firstLeading.length > 0
437
- ? firstLeading[0]!.range[0]
438
- : firstProp.range[0];
439
- const lastTrailing = getTrailingComments(
440
- lastProp,
441
- null
442
- );
443
- const rangeEnd =
444
- lastTrailing.length > 0
445
- ? lastTrailing[
446
- lastTrailing.length - 1
447
- ]!.range[1]
448
- : lastProp.range[1];
449
- const sortedText = buildSortedText(
450
- fixableProps,
451
- rangeStart
452
- );
453
-
454
- return fixer.replaceTextRange(
455
- [rangeStart, rangeEnd],
456
- sortedText
457
- );
458
- }
459
- : null
460
- });
461
- fixProvided = true;
462
- };
443
+ if (
444
+ variablesBeforeFunctions &&
445
+ prev.isFunction &&
446
+ !curr.isFunction
447
+ ) {
448
+ createReportWithFix(curr, shouldFix);
449
+ return;
450
+ }
463
451
 
464
- if (variablesBeforeFunctions) {
465
- if (prev.isFunction && !curr.isFunction) {
466
- reportWithFix();
467
- continue;
468
- }
452
+ if (
453
+ variablesBeforeFunctions &&
454
+ prev.isFunction === curr.isFunction &&
455
+ compareKeys(prev.keyName, curr.keyName) > 0
456
+ ) {
457
+ createReportWithFix(curr, shouldFix);
458
+ return;
459
+ }
469
460
 
470
- if (
471
- prev.isFunction === curr.isFunction &&
472
- compareKeys(prev.keyName, curr.keyName) > 0
473
- ) {
474
- reportWithFix();
475
- }
476
- } else {
477
- if (compareKeys(prev.keyName, curr.keyName) > 0) {
478
- reportWithFix();
479
- }
461
+ if (
462
+ !variablesBeforeFunctions &&
463
+ compareKeys(prev.keyName, curr.keyName) > 0
464
+ ) {
465
+ createReportWithFix(curr, shouldFix);
480
466
  }
481
- }
482
- }
467
+ });
468
+ };
483
469
 
484
470
  // Also check object literals inside JSX prop expressions
485
- function checkJSXAttributeObject(attr: TSESTree.JSXAttribute) {
486
- const value = attr.value;
487
- if (value && value.type === "JSXExpressionContainer") {
488
- const expr = value.expression;
489
- if (expr && expr.type === "ObjectExpression") {
490
- checkObjectExpression(expr);
491
- }
471
+ const checkJSXAttributeObject = (attr: TSESTree.JSXAttribute) => {
472
+ const { value } = attr;
473
+ if (
474
+ value &&
475
+ value.type === "JSXExpressionContainer" &&
476
+ value.expression &&
477
+ value.expression.type === "ObjectExpression"
478
+ ) {
479
+ checkObjectExpression(value.expression);
492
480
  }
493
- }
481
+ };
482
+
483
+ const getAttrName = (
484
+ attr: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute
485
+ ) => {
486
+ if (
487
+ attr.type !== "JSXAttribute" ||
488
+ attr.name.type !== "JSXIdentifier"
489
+ ) {
490
+ return "";
491
+ }
492
+ return attr.name.name;
493
+ };
494
+
495
+ const compareAttrNames = (nameLeft: string, nameRight: string) => {
496
+ let res = compareKeys(nameLeft, nameRight);
497
+ if (order === "desc") {
498
+ res = -res;
499
+ }
500
+ return res;
501
+ };
502
+
503
+ const isOutOfOrder = (names: string[]) =>
504
+ names.some((currName, idx) => {
505
+ if (idx === 0 || !currName) {
506
+ return false;
507
+ }
508
+ const prevName = names[idx - 1];
509
+ return (
510
+ prevName !== undefined &&
511
+ compareAttrNames(prevName, currName) > 0
512
+ );
513
+ });
494
514
 
495
515
  // Also sort JSX attributes on elements
496
- function checkJSXOpeningElement(node: TSESTree.JSXOpeningElement) {
516
+ const checkJSXOpeningElement = (node: TSESTree.JSXOpeningElement) => {
497
517
  const attrs = node.attributes;
498
518
  if (attrs.length < minKeys) {
499
519
  return;
500
520
  }
501
521
 
502
- if (attrs.some((a) => a.type !== "JSXAttribute")) {
522
+ if (attrs.some((attr) => attr.type !== "JSXAttribute")) {
503
523
  return;
504
524
  }
505
525
  if (
506
526
  attrs.some(
507
- (a) =>
508
- a.type === "JSXAttribute" &&
509
- a.name.type !== "JSXIdentifier"
527
+ (attr) =>
528
+ attr.type === "JSXAttribute" &&
529
+ attr.name.type !== "JSXIdentifier"
510
530
  )
511
531
  ) {
512
532
  return;
513
533
  }
514
534
 
515
- const names = attrs.map((a) => {
516
- if (a.type !== "JSXAttribute") {
517
- return "";
518
- }
519
- if (a.name.type !== "JSXIdentifier") {
520
- return "";
521
- }
522
- return a.name.name;
523
- });
524
-
525
- const cmp = (a: string, b: string) => {
526
- let res = compareKeys(a, b);
527
- if (order === "desc") {
528
- res = -res;
529
- }
530
- return res;
531
- };
535
+ const names = attrs.map((attr) => getAttrName(attr));
532
536
 
533
- let outOfOrder = false;
534
- for (let i = 1; i < names.length; i++) {
535
- const prevName = names[i - 1];
536
- const currName = names[i];
537
- if (!prevName || !currName) {
538
- continue;
539
- }
540
- if (cmp(prevName, currName) > 0) {
541
- outOfOrder = true;
542
- break;
543
- }
544
- }
545
-
546
- if (!outOfOrder) {
537
+ if (!isOutOfOrder(names)) {
547
538
  return;
548
539
  }
549
540
 
550
541
  // Be conservative: only fix if there are no JSX comments/braces between attributes.
551
- for (let i = 1; i < attrs.length; i++) {
552
- const prevAttr = attrs[i - 1];
553
- const currAttr = attrs[i];
554
-
555
- if (!prevAttr || !currAttr) {
556
- continue;
542
+ const braceConflict = attrs.find((currAttr, idx) => {
543
+ if (idx === 0) {
544
+ return false;
545
+ }
546
+ const prevAttr = attrs[idx - 1];
547
+ if (!prevAttr) {
548
+ return false;
557
549
  }
558
-
559
550
  const between = sourceCode.text.slice(
560
551
  prevAttr.range[1],
561
552
  currAttr.range[0]
562
553
  );
563
- if (between.includes("{")) {
564
- context.report({
565
- node:
566
- currAttr.type === "JSXAttribute"
567
- ? currAttr.name
568
- : currAttr,
569
- messageId: "unsorted"
570
- });
571
- return;
572
- }
554
+ return between.includes("{");
555
+ });
556
+
557
+ if (braceConflict) {
558
+ context.report({
559
+ messageId: "unsorted",
560
+ node:
561
+ braceConflict.type === "JSXAttribute"
562
+ ? braceConflict.name
563
+ : braceConflict
564
+ });
565
+ return;
573
566
  }
574
567
 
575
- const sortedAttrs = attrs.slice().sort((a, b) => {
576
- const aName =
577
- a.type === "JSXAttribute" && a.name.type === "JSXIdentifier"
578
- ? a.name.name
579
- : "";
580
- const bName =
581
- b.type === "JSXAttribute" && b.name.type === "JSXIdentifier"
582
- ? b.name.name
583
- : "";
584
- return cmp(aName, bName);
585
- });
568
+ const sortedAttrs = attrs
569
+ .slice()
570
+ .sort((left, right) =>
571
+ compareAttrNames(getAttrName(left), getAttrName(right))
572
+ );
586
573
 
587
- const firstAttr = attrs[0];
574
+ const [firstAttr] = attrs;
588
575
  const lastAttr = attrs[attrs.length - 1];
589
576
 
590
577
  if (!firstAttr || !lastAttr) {
@@ -592,30 +579,69 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
592
579
  }
593
580
 
594
581
  const replacement = sortedAttrs
595
- .map((a) => sourceCode.getText(a))
582
+ .map((attr) => sourceCode.getText(attr))
596
583
  .join(" ");
597
584
 
598
585
  context.report({
599
- node:
600
- firstAttr.type === "JSXAttribute"
601
- ? firstAttr.name
602
- : firstAttr,
603
- messageId: "unsorted",
604
586
  fix(fixer) {
605
587
  return fixer.replaceTextRange(
606
588
  [firstAttr.range[0], lastAttr.range[1]],
607
589
  replacement
608
590
  );
609
- }
591
+ },
592
+ messageId: "unsorted",
593
+ node:
594
+ firstAttr.type === "JSXAttribute"
595
+ ? firstAttr.name
596
+ : firstAttr
610
597
  });
611
- }
598
+ };
612
599
 
613
600
  return {
614
- ObjectExpression: checkObjectExpression,
615
601
  JSXAttribute(node: TSESTree.JSXAttribute) {
616
602
  checkJSXAttributeObject(node);
617
603
  },
618
- JSXOpeningElement: checkJSXOpeningElement
604
+ JSXOpeningElement: checkJSXOpeningElement,
605
+ ObjectExpression: checkObjectExpression
619
606
  };
607
+ },
608
+ defaultOptions: [{}],
609
+ meta: {
610
+ docs: {
611
+ description:
612
+ "enforce sorted keys in object literals with auto-fix (limited to simple cases, preserving comments)"
613
+ },
614
+ fixable: "code",
615
+ messages: {
616
+ unsorted: "Object keys are not sorted."
617
+ },
618
+ // The schema supports the same options as the built-in sort-keys rule plus:
619
+ // variablesBeforeFunctions: boolean (when true, non-function properties come before function properties)
620
+ schema: [
621
+ {
622
+ additionalProperties: false,
623
+ properties: {
624
+ caseSensitive: {
625
+ type: "boolean"
626
+ },
627
+ minKeys: {
628
+ minimum: 2,
629
+ type: "integer"
630
+ },
631
+ natural: {
632
+ type: "boolean"
633
+ },
634
+ order: {
635
+ enum: ["asc", "desc"],
636
+ type: "string"
637
+ },
638
+ variablesBeforeFunctions: {
639
+ type: "boolean"
640
+ }
641
+ },
642
+ type: "object"
643
+ }
644
+ ],
645
+ type: "suggestion"
620
646
  }
621
647
  };