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.
- package/.absolutejs/eslint.cache.json +49 -0
- package/.absolutejs/prettier.cache.json +49 -0
- package/.absolutejs/tsconfig.tsbuildinfo +1 -0
- package/.claude/settings.local.json +8 -3
- package/dist/index.js +1321 -1419
- package/eslint.config.mjs +107 -0
- package/package.json +10 -8
- package/src/index.ts +15 -15
- package/src/rules/explicit-object-types.ts +42 -40
- package/src/rules/inline-style-limit.ts +56 -54
- package/src/rules/localize-react-props.ts +261 -266
- package/src/rules/max-depth-extended.ts +55 -66
- package/src/rules/max-jsx-nesting.ts +28 -36
- package/src/rules/min-var-length.ts +238 -208
- package/src/rules/no-button-navigation.ts +114 -156
- package/src/rules/no-explicit-return-types.ts +32 -36
- package/src/rules/no-inline-prop-types.ts +30 -30
- package/src/rules/no-multi-style-objects.ts +35 -42
- package/src/rules/no-nested-jsx-return.ts +100 -105
- package/src/rules/no-or-none-component.ts +17 -19
- package/src/rules/no-transition-cssproperties.ts +76 -70
- package/src/rules/no-unnecessary-div.ts +26 -34
- package/src/rules/no-unnecessary-key.ts +41 -75
- package/src/rules/no-useless-function.ts +18 -20
- package/src/rules/seperate-style-files.ts +19 -21
- package/src/rules/sort-exports.ts +252 -248
- package/src/rules/sort-keys-fixable.ts +354 -328
- package/src/rules/spring-naming-convention.ts +104 -89
- package/tsconfig.json +2 -0
|
@@ -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 ESLint
|
|
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
|
|
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 there
|
|
16
|
-
* a reason the original rule doesn
|
|
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
|
-
|
|
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
|
|
81
|
-
const option = context.options
|
|
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
|
-
|
|
109
|
-
let
|
|
110
|
-
let
|
|
69
|
+
const compareKeys = (keyLeft: string, keyRight: string) => {
|
|
70
|
+
let left = keyLeft;
|
|
71
|
+
let right = keyRight;
|
|
111
72
|
|
|
112
73
|
if (!caseSensitive) {
|
|
113
|
-
|
|
114
|
-
|
|
74
|
+
left = left.toLowerCase();
|
|
75
|
+
right = right.toLowerCase();
|
|
115
76
|
}
|
|
116
77
|
|
|
117
78
|
if (natural) {
|
|
118
|
-
return
|
|
79
|
+
return left.localeCompare(right, undefined, {
|
|
119
80
|
numeric: true
|
|
120
81
|
});
|
|
121
82
|
}
|
|
122
83
|
|
|
123
|
-
return
|
|
124
|
-
}
|
|
84
|
+
return left.localeCompare(right);
|
|
85
|
+
};
|
|
125
86
|
|
|
126
87
|
/**
|
|
127
88
|
* Determines if a property is a function property.
|
|
128
89
|
*/
|
|
129
|
-
|
|
130
|
-
const value = prop
|
|
90
|
+
const isFunctionProperty = (prop: TSESTree.Property) => {
|
|
91
|
+
const { value } = prop;
|
|
131
92
|
return (
|
|
132
|
-
|
|
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
|
-
|
|
144
|
-
const key = prop
|
|
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
|
|
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
|
-
|
|
124
|
+
const getLeadingComments = (
|
|
164
125
|
prop: TSESTree.Property,
|
|
165
126
|
prevProp: TSESTree.Property | null
|
|
166
|
-
)
|
|
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
|
-
(
|
|
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
|
-
|
|
143
|
+
const getTrailingComments = (
|
|
183
144
|
prop: TSESTree.Property,
|
|
184
145
|
nextProp: TSESTree.Property | null
|
|
185
|
-
)
|
|
146
|
+
) => {
|
|
186
147
|
const after = sourceCode
|
|
187
148
|
.getCommentsAfter(prop)
|
|
188
|
-
.filter(
|
|
189
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|
221
|
-
const prop = fixableProps[
|
|
222
|
-
const prevProp =
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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((
|
|
254
|
+
const sorted = chunks.slice().sort((left, right) => {
|
|
270
255
|
if (variablesBeforeFunctions) {
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
return
|
|
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
|
|
279
|
-
const
|
|
263
|
+
const leftKey = getPropertyKeyName(left.prop);
|
|
264
|
+
const rightKey = getPropertyKeyName(right.prop);
|
|
280
265
|
|
|
281
|
-
let res = compareKeys(
|
|
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 =
|
|
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,
|
|
312
|
-
if (
|
|
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
|
-
|
|
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
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
337
|
+
if (prop.type !== "Property") {
|
|
338
|
+
autoFixable = false;
|
|
339
|
+
return {
|
|
340
|
+
isFunction: isFunc,
|
|
341
|
+
keyName,
|
|
342
|
+
node: prop
|
|
343
|
+
};
|
|
344
|
+
}
|
|
347
345
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
385
|
+
if (!firstProp || !lastProp) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
402
388
|
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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 (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
486
|
-
const value = attr
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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((
|
|
522
|
+
if (attrs.some((attr) => attr.type !== "JSXAttribute")) {
|
|
503
523
|
return;
|
|
504
524
|
}
|
|
505
525
|
if (
|
|
506
526
|
attrs.some(
|
|
507
|
-
(
|
|
508
|
-
|
|
509
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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((
|
|
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
|
};
|