eslint-plugin-absolute 0.2.0 → 0.2.2
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/.codex +0 -0
- package/dist/index.js +1500 -1421
- package/eslint.config.mjs +107 -0
- package/package.json +22 -20
- 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 +382 -256
- package/src/rules/sort-keys-fixable.ts +486 -322
- package/src/rules/spring-naming-convention.ts +104 -89
- package/tsconfig.json +3 -1
|
@@ -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,107 @@ type KeyInfo = {
|
|
|
34
34
|
isFunction: boolean;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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."
|
|
37
|
+
const SORT_BEFORE = -1;
|
|
38
|
+
|
|
39
|
+
const hasDuplicateNames = (names: Array<string | null>) => {
|
|
40
|
+
const seen = new Set<string>();
|
|
41
|
+
const nonNullNames = names.flatMap((name) => (name === null ? [] : [name]));
|
|
42
|
+
|
|
43
|
+
for (const name of nonNullNames) {
|
|
44
|
+
if (seen.has(name)) {
|
|
45
|
+
return true;
|
|
74
46
|
}
|
|
75
|
-
|
|
47
|
+
seen.add(name);
|
|
48
|
+
}
|
|
76
49
|
|
|
77
|
-
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const isSafeStaticTemplate = (node: TSESTree.TemplateLiteral) =>
|
|
54
|
+
node.expressions.length === 0;
|
|
55
|
+
|
|
56
|
+
const isSafeArrayElement: (node: TSESTree.Node | null) => boolean = (node) => {
|
|
57
|
+
if (!node || node.type === "SpreadElement") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return isSafeToReorderExpression(node);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const isSafeObjectProperty: (
|
|
65
|
+
property: TSESTree.ObjectExpression["properties"][number]
|
|
66
|
+
) => boolean = (property) => {
|
|
67
|
+
if (
|
|
68
|
+
property.type !== "Property" ||
|
|
69
|
+
property.computed ||
|
|
70
|
+
property.kind !== "init"
|
|
71
|
+
) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (property.key.type !== "Identifier" && property.key.type !== "Literal") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (property.method) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return isSafeToReorderExpression(property.value);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isSafeToReorderExpression: (node: TSESTree.Node | null) => boolean = (
|
|
87
|
+
node
|
|
88
|
+
) => {
|
|
89
|
+
if (!node || node.type === "PrivateIdentifier") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
switch (node.type) {
|
|
94
|
+
case "Identifier":
|
|
95
|
+
case "Literal":
|
|
96
|
+
case "ThisExpression":
|
|
97
|
+
case "FunctionExpression":
|
|
98
|
+
case "ArrowFunctionExpression":
|
|
99
|
+
case "ClassExpression":
|
|
100
|
+
return true;
|
|
101
|
+
case "TemplateLiteral":
|
|
102
|
+
return isSafeStaticTemplate(node);
|
|
103
|
+
case "UnaryExpression":
|
|
104
|
+
return isSafeToReorderExpression(node.argument);
|
|
105
|
+
case "ArrayExpression":
|
|
106
|
+
return node.elements.every(isSafeArrayElement);
|
|
107
|
+
case "ObjectExpression":
|
|
108
|
+
return node.properties.every(isSafeObjectProperty);
|
|
109
|
+
default:
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const isSafeJSXAttributeValue = (value: TSESTree.JSXAttribute["value"]) => {
|
|
115
|
+
if (value === null) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (value.type === "Literal") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (value.type !== "JSXExpressionContainer") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (value.expression.type === "JSXEmptyExpression") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return isSafeToReorderExpression(value.expression);
|
|
132
|
+
};
|
|
78
133
|
|
|
134
|
+
export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
79
135
|
create(context) {
|
|
80
|
-
const sourceCode = context
|
|
81
|
-
const option = context.options
|
|
136
|
+
const { sourceCode } = context;
|
|
137
|
+
const [option] = context.options;
|
|
82
138
|
|
|
83
139
|
const order: "asc" | "desc" =
|
|
84
140
|
option && option.order ? option.order : "asc";
|
|
@@ -105,110 +161,153 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
105
161
|
* Compare two key strings based on the provided options.
|
|
106
162
|
* This function mimics the behavior of the built-in rule.
|
|
107
163
|
*/
|
|
108
|
-
|
|
109
|
-
let
|
|
110
|
-
let
|
|
164
|
+
const compareKeys = (keyLeft: string, keyRight: string) => {
|
|
165
|
+
let left = keyLeft;
|
|
166
|
+
let right = keyRight;
|
|
111
167
|
|
|
112
168
|
if (!caseSensitive) {
|
|
113
|
-
|
|
114
|
-
|
|
169
|
+
left = left.toLowerCase();
|
|
170
|
+
right = right.toLowerCase();
|
|
115
171
|
}
|
|
116
172
|
|
|
117
173
|
if (natural) {
|
|
118
|
-
return
|
|
174
|
+
return left.localeCompare(right, undefined, {
|
|
119
175
|
numeric: true
|
|
120
176
|
});
|
|
121
177
|
}
|
|
122
178
|
|
|
123
|
-
return
|
|
124
|
-
}
|
|
179
|
+
return left.localeCompare(right);
|
|
180
|
+
};
|
|
125
181
|
|
|
126
182
|
/**
|
|
127
183
|
* Determines if a property is a function property.
|
|
128
184
|
*/
|
|
129
|
-
|
|
130
|
-
const value = prop
|
|
185
|
+
const isFunctionProperty = (prop: TSESTree.Property) => {
|
|
186
|
+
const { value } = prop;
|
|
131
187
|
return (
|
|
132
|
-
|
|
188
|
+
Boolean(value) &&
|
|
133
189
|
(value.type === "FunctionExpression" ||
|
|
134
190
|
value.type === "ArrowFunctionExpression" ||
|
|
135
191
|
prop.method === true)
|
|
136
192
|
);
|
|
137
|
-
}
|
|
193
|
+
};
|
|
138
194
|
|
|
139
195
|
/**
|
|
140
196
|
* Safely extracts a key name from a Property that we already know
|
|
141
197
|
* only uses Identifier or Literal keys in the fixer.
|
|
142
198
|
*/
|
|
143
|
-
|
|
144
|
-
const key = prop
|
|
199
|
+
const getPropertyKeyName = (prop: TSESTree.Property) => {
|
|
200
|
+
const { key } = prop;
|
|
145
201
|
if (key.type === "Identifier") {
|
|
146
202
|
return key.name;
|
|
147
203
|
}
|
|
148
204
|
if (key.type === "Literal") {
|
|
149
|
-
const value = key
|
|
205
|
+
const { value } = key;
|
|
150
206
|
if (typeof value === "string") {
|
|
151
207
|
return value;
|
|
152
208
|
}
|
|
153
209
|
return String(value);
|
|
154
210
|
}
|
|
155
211
|
return "";
|
|
156
|
-
}
|
|
212
|
+
};
|
|
157
213
|
|
|
158
214
|
/**
|
|
159
215
|
* Get leading comments for a property, excluding any comments that
|
|
160
216
|
* are on the same line as the previous property (those are trailing
|
|
161
217
|
* comments of the previous property).
|
|
162
218
|
*/
|
|
163
|
-
|
|
219
|
+
const getLeadingComments = (
|
|
164
220
|
prop: TSESTree.Property,
|
|
165
221
|
prevProp: TSESTree.Property | null
|
|
166
|
-
)
|
|
222
|
+
) => {
|
|
167
223
|
const comments = sourceCode.getCommentsBefore(prop);
|
|
168
224
|
if (!prevProp || comments.length === 0) {
|
|
169
225
|
return comments;
|
|
170
226
|
}
|
|
171
227
|
// Filter out comments on the same line as the previous property
|
|
172
228
|
return comments.filter(
|
|
173
|
-
(
|
|
229
|
+
(comment) => comment.loc.start.line !== prevProp.loc.end.line
|
|
174
230
|
);
|
|
175
|
-
}
|
|
231
|
+
};
|
|
176
232
|
|
|
177
233
|
/**
|
|
178
234
|
* Get trailing comments for a property that are on the same line.
|
|
179
235
|
* This includes both getCommentsAfter AND any getCommentsBefore of the
|
|
180
236
|
* next property that are on the same line as this property.
|
|
181
237
|
*/
|
|
182
|
-
|
|
238
|
+
const getTrailingComments = (
|
|
183
239
|
prop: TSESTree.Property,
|
|
184
240
|
nextProp: TSESTree.Property | null
|
|
185
|
-
)
|
|
241
|
+
) => {
|
|
186
242
|
const after = sourceCode
|
|
187
243
|
.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
|
|
244
|
+
.filter(
|
|
245
|
+
(comment) => comment.loc.start.line === prop.loc.end.line
|
|
193
246
|
);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!after.some((a) => a.range[0] === c.range[0])) {
|
|
197
|
-
after.push(c);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
247
|
+
if (!nextProp) {
|
|
248
|
+
return after;
|
|
200
249
|
}
|
|
250
|
+
|
|
251
|
+
const beforeNext = sourceCode.getCommentsBefore(nextProp);
|
|
252
|
+
const trailingOfPrev = beforeNext.filter(
|
|
253
|
+
(comment) => comment.loc.start.line === prop.loc.end.line
|
|
254
|
+
);
|
|
255
|
+
// Merge, avoiding duplicates
|
|
256
|
+
const newComments = trailingOfPrev.filter(
|
|
257
|
+
(comment) =>
|
|
258
|
+
!after.some(
|
|
259
|
+
(existing) => existing.range[0] === comment.range[0]
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
after.push(...newComments);
|
|
201
263
|
return after;
|
|
202
|
-
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const getChunkStart = (
|
|
267
|
+
idx: number,
|
|
268
|
+
fixableProps: TSESTree.Property[],
|
|
269
|
+
rangeStart: number,
|
|
270
|
+
fullStart: number
|
|
271
|
+
) => {
|
|
272
|
+
if (idx === 0) {
|
|
273
|
+
return rangeStart;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const prevProp = fixableProps[idx - 1]!;
|
|
277
|
+
const currentProp = fixableProps[idx]!;
|
|
278
|
+
const prevTrailing = getTrailingComments(prevProp, currentProp);
|
|
279
|
+
const prevEnd =
|
|
280
|
+
prevTrailing.length > 0
|
|
281
|
+
? prevTrailing[prevTrailing.length - 1]!.range[1]
|
|
282
|
+
: prevProp.range[1];
|
|
283
|
+
// Find the comma after the previous property/comments
|
|
284
|
+
const allTokens = sourceCode.getTokensBetween(
|
|
285
|
+
prevProp,
|
|
286
|
+
currentProp,
|
|
287
|
+
{
|
|
288
|
+
includeComments: false
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
const tokenAfterPrev =
|
|
292
|
+
allTokens.find((tok) => tok.range[0] >= prevEnd) ?? null;
|
|
293
|
+
if (
|
|
294
|
+
tokenAfterPrev &&
|
|
295
|
+
tokenAfterPrev.value === "," &&
|
|
296
|
+
tokenAfterPrev.range[1] <= fullStart
|
|
297
|
+
) {
|
|
298
|
+
return tokenAfterPrev.range[1];
|
|
299
|
+
}
|
|
300
|
+
return prevEnd;
|
|
301
|
+
};
|
|
203
302
|
|
|
204
303
|
/**
|
|
205
304
|
* Build the sorted text from the fixable properties while preserving
|
|
206
305
|
* comments and formatting.
|
|
207
306
|
*/
|
|
208
|
-
|
|
307
|
+
const buildSortedText = (
|
|
209
308
|
fixableProps: TSESTree.Property[],
|
|
210
309
|
rangeStart: number
|
|
211
|
-
) {
|
|
310
|
+
) => {
|
|
212
311
|
// For each property, capture its "chunk": the property text plus
|
|
213
312
|
// its associated comments (leading comments on separate lines,
|
|
214
313
|
// trailing comments on the same line).
|
|
@@ -217,11 +316,13 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
217
316
|
text: string;
|
|
218
317
|
}[] = [];
|
|
219
318
|
|
|
220
|
-
for (let
|
|
221
|
-
const prop = fixableProps[
|
|
222
|
-
const prevProp =
|
|
319
|
+
for (let idx = 0; idx < fixableProps.length; idx++) {
|
|
320
|
+
const prop = fixableProps[idx]!;
|
|
321
|
+
const prevProp = idx > 0 ? fixableProps[idx - 1]! : null;
|
|
223
322
|
const nextProp =
|
|
224
|
-
|
|
323
|
+
idx < fixableProps.length - 1
|
|
324
|
+
? fixableProps[idx + 1]!
|
|
325
|
+
: null;
|
|
225
326
|
|
|
226
327
|
const leading = getLeadingComments(prop, prevProp);
|
|
227
328
|
const trailing = getTrailingComments(prop, nextProp);
|
|
@@ -233,52 +334,31 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
233
334
|
? trailing[trailing.length - 1]!.range[1]
|
|
234
335
|
: prop.range[1];
|
|
235
336
|
|
|
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
|
-
}
|
|
337
|
+
const chunkStart = getChunkStart(
|
|
338
|
+
idx,
|
|
339
|
+
fixableProps,
|
|
340
|
+
rangeStart,
|
|
341
|
+
fullStart
|
|
342
|
+
);
|
|
263
343
|
|
|
264
344
|
const text = sourceCode.text.slice(chunkStart, fullEnd);
|
|
265
345
|
chunks.push({ prop, text });
|
|
266
346
|
}
|
|
267
347
|
|
|
268
348
|
// Sort the chunks
|
|
269
|
-
const sorted = chunks.slice().sort((
|
|
349
|
+
const sorted = chunks.slice().sort((left, right) => {
|
|
270
350
|
if (variablesBeforeFunctions) {
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
return
|
|
351
|
+
const leftIsFunc = isFunctionProperty(left.prop);
|
|
352
|
+
const rightIsFunc = isFunctionProperty(right.prop);
|
|
353
|
+
if (leftIsFunc !== rightIsFunc) {
|
|
354
|
+
return leftIsFunc ? 1 : SORT_BEFORE;
|
|
275
355
|
}
|
|
276
356
|
}
|
|
277
357
|
|
|
278
|
-
const
|
|
279
|
-
const
|
|
358
|
+
const leftKey = getPropertyKeyName(left.prop);
|
|
359
|
+
const rightKey = getPropertyKeyName(right.prop);
|
|
280
360
|
|
|
281
|
-
let res = compareKeys(
|
|
361
|
+
let res = compareKeys(leftKey, rightKey);
|
|
282
362
|
if (order === "desc") {
|
|
283
363
|
res = -res;
|
|
284
364
|
}
|
|
@@ -300,7 +380,7 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
300
380
|
fixableProps[0]!.range[0] - col,
|
|
301
381
|
fixableProps[0]!.range[0]
|
|
302
382
|
);
|
|
303
|
-
separator =
|
|
383
|
+
separator = `,\n${indent}`;
|
|
304
384
|
} else {
|
|
305
385
|
separator = ", ";
|
|
306
386
|
}
|
|
@@ -308,8 +388,8 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
308
388
|
// Rebuild: first chunk keeps original leading whitespace,
|
|
309
389
|
// subsequent chunks use the detected separator
|
|
310
390
|
return sorted
|
|
311
|
-
.map((chunk,
|
|
312
|
-
if (
|
|
391
|
+
.map((chunk, idx) => {
|
|
392
|
+
if (idx === 0) {
|
|
313
393
|
const originalFirstChunk = chunks[0]!;
|
|
314
394
|
const originalLeadingWs =
|
|
315
395
|
originalFirstChunk.text.match(/^(\s*)/)?.[1] ?? "";
|
|
@@ -320,7 +400,16 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
320
400
|
return separator + stripped;
|
|
321
401
|
})
|
|
322
402
|
.join("");
|
|
323
|
-
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const getFixableProps = (node: TSESTree.ObjectExpression) =>
|
|
406
|
+
node.properties.filter(
|
|
407
|
+
(prop): prop is TSESTree.Property =>
|
|
408
|
+
prop.type === "Property" &&
|
|
409
|
+
!prop.computed &&
|
|
410
|
+
(prop.key.type === "Identifier" ||
|
|
411
|
+
prop.key.type === "Literal")
|
|
412
|
+
);
|
|
324
413
|
|
|
325
414
|
/**
|
|
326
415
|
* Checks an ObjectExpression node for unsorted keys.
|
|
@@ -329,7 +418,7 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
329
418
|
* For auto-fix purposes, only simple properties are considered fixable.
|
|
330
419
|
* (Computed keys, spread elements, or non-Identifier/Literal keys disable the fix.)
|
|
331
420
|
*/
|
|
332
|
-
|
|
421
|
+
const checkObjectExpression = (node: TSESTree.ObjectExpression) => {
|
|
333
422
|
if (node.properties.length < minKeys) {
|
|
334
423
|
return;
|
|
335
424
|
}
|
|
@@ -340,251 +429,287 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
340
429
|
let keyName: string | null = null;
|
|
341
430
|
let isFunc = false;
|
|
342
431
|
|
|
343
|
-
if (prop.type
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
432
|
+
if (prop.type !== "Property") {
|
|
433
|
+
autoFixable = false;
|
|
434
|
+
return {
|
|
435
|
+
isFunction: isFunc,
|
|
436
|
+
keyName,
|
|
437
|
+
node: prop
|
|
438
|
+
};
|
|
439
|
+
}
|
|
347
440
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const value = prop.key.value;
|
|
352
|
-
keyName =
|
|
353
|
-
typeof value === "string" ? value : String(value);
|
|
354
|
-
} else {
|
|
355
|
-
autoFixable = false;
|
|
356
|
-
}
|
|
441
|
+
if (prop.computed) {
|
|
442
|
+
autoFixable = false;
|
|
443
|
+
}
|
|
357
444
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
445
|
+
if (prop.key.type === "Identifier") {
|
|
446
|
+
keyName = prop.key.name;
|
|
447
|
+
} else if (prop.key.type === "Literal") {
|
|
448
|
+
const { value } = prop.key;
|
|
449
|
+
keyName = typeof value === "string" ? value : String(value);
|
|
361
450
|
} else {
|
|
362
|
-
// Spread elements or other non-Property nodes.
|
|
363
451
|
autoFixable = false;
|
|
364
452
|
}
|
|
365
453
|
|
|
454
|
+
if (isFunctionProperty(prop)) {
|
|
455
|
+
isFunc = true;
|
|
456
|
+
}
|
|
457
|
+
|
|
366
458
|
return {
|
|
459
|
+
isFunction: isFunc,
|
|
367
460
|
keyName,
|
|
368
|
-
node: prop
|
|
369
|
-
isFunction: isFunc
|
|
461
|
+
node: prop
|
|
370
462
|
};
|
|
371
463
|
});
|
|
372
464
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
props.push(prop);
|
|
389
|
-
}
|
|
390
|
-
return props;
|
|
391
|
-
};
|
|
465
|
+
if (hasDuplicateNames(keys.map((key) => key.keyName))) {
|
|
466
|
+
autoFixable = false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (
|
|
470
|
+
autoFixable &&
|
|
471
|
+
keys.some(
|
|
472
|
+
(key) =>
|
|
473
|
+
key.node.type === "Property" &&
|
|
474
|
+
!isSafeToReorderExpression(key.node.value)
|
|
475
|
+
)
|
|
476
|
+
) {
|
|
477
|
+
autoFixable = false;
|
|
478
|
+
}
|
|
392
479
|
|
|
393
480
|
let fixProvided = false;
|
|
394
481
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
482
|
+
const createReportWithFix = (curr: KeyInfo, shouldFix: boolean) => {
|
|
483
|
+
context.report({
|
|
484
|
+
fix: shouldFix
|
|
485
|
+
? (fixer) => {
|
|
486
|
+
const fixableProps = getFixableProps(node);
|
|
487
|
+
if (fixableProps.length < minKeys) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const [firstProp] = fixableProps;
|
|
492
|
+
const lastProp =
|
|
493
|
+
fixableProps[fixableProps.length - 1];
|
|
398
494
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
495
|
+
if (!firstProp || !lastProp) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const firstLeading = getLeadingComments(
|
|
500
|
+
firstProp,
|
|
501
|
+
null
|
|
502
|
+
);
|
|
503
|
+
const [firstLeadingComment] = firstLeading;
|
|
504
|
+
const rangeStart = firstLeadingComment
|
|
505
|
+
? firstLeadingComment.range[0]
|
|
506
|
+
: firstProp.range[0];
|
|
507
|
+
const lastTrailing = getTrailingComments(
|
|
508
|
+
lastProp,
|
|
509
|
+
null
|
|
510
|
+
);
|
|
511
|
+
const rangeEnd =
|
|
512
|
+
lastTrailing.length > 0
|
|
513
|
+
? lastTrailing[lastTrailing.length - 1]!
|
|
514
|
+
.range[1]
|
|
515
|
+
: lastProp.range[1];
|
|
516
|
+
const sortedText = buildSortedText(
|
|
517
|
+
fixableProps,
|
|
518
|
+
rangeStart
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
return fixer.replaceTextRange(
|
|
522
|
+
[rangeStart, rangeEnd],
|
|
523
|
+
sortedText
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
: null,
|
|
527
|
+
messageId: "unsorted",
|
|
528
|
+
node:
|
|
529
|
+
curr.node.type === "Property"
|
|
530
|
+
? curr.node.key
|
|
531
|
+
: curr.node
|
|
532
|
+
});
|
|
533
|
+
fixProvided = true;
|
|
534
|
+
};
|
|
402
535
|
|
|
403
|
-
|
|
404
|
-
|
|
536
|
+
keys.forEach((curr, idx) => {
|
|
537
|
+
if (idx === 0) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const prev = keys[idx - 1];
|
|
541
|
+
|
|
542
|
+
if (
|
|
543
|
+
!prev ||
|
|
544
|
+
!curr ||
|
|
545
|
+
prev.keyName === null ||
|
|
546
|
+
curr.keyName === null
|
|
547
|
+
) {
|
|
548
|
+
return;
|
|
405
549
|
}
|
|
406
550
|
|
|
407
551
|
const shouldFix = !fixProvided && autoFixable;
|
|
408
552
|
|
|
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
|
-
};
|
|
553
|
+
if (
|
|
554
|
+
variablesBeforeFunctions &&
|
|
555
|
+
prev.isFunction &&
|
|
556
|
+
!curr.isFunction
|
|
557
|
+
) {
|
|
558
|
+
createReportWithFix(curr, shouldFix);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
463
561
|
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
562
|
+
if (
|
|
563
|
+
variablesBeforeFunctions &&
|
|
564
|
+
prev.isFunction === curr.isFunction &&
|
|
565
|
+
compareKeys(prev.keyName, curr.keyName) > 0
|
|
566
|
+
) {
|
|
567
|
+
createReportWithFix(curr, shouldFix);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
469
570
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
} else {
|
|
477
|
-
if (compareKeys(prev.keyName, curr.keyName) > 0) {
|
|
478
|
-
reportWithFix();
|
|
479
|
-
}
|
|
571
|
+
if (
|
|
572
|
+
!variablesBeforeFunctions &&
|
|
573
|
+
compareKeys(prev.keyName, curr.keyName) > 0
|
|
574
|
+
) {
|
|
575
|
+
createReportWithFix(curr, shouldFix);
|
|
480
576
|
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
577
|
+
});
|
|
578
|
+
};
|
|
483
579
|
|
|
484
580
|
// Also check object literals inside JSX prop expressions
|
|
485
|
-
|
|
486
|
-
const value = attr
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
581
|
+
const checkJSXAttributeObject = (attr: TSESTree.JSXAttribute) => {
|
|
582
|
+
const { value } = attr;
|
|
583
|
+
if (
|
|
584
|
+
value &&
|
|
585
|
+
value.type === "JSXExpressionContainer" &&
|
|
586
|
+
value.expression &&
|
|
587
|
+
value.expression.type === "ObjectExpression"
|
|
588
|
+
) {
|
|
589
|
+
checkObjectExpression(value.expression);
|
|
492
590
|
}
|
|
493
|
-
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const getAttrName = (
|
|
594
|
+
attr: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute
|
|
595
|
+
) => {
|
|
596
|
+
if (
|
|
597
|
+
attr.type !== "JSXAttribute" ||
|
|
598
|
+
attr.name.type !== "JSXIdentifier"
|
|
599
|
+
) {
|
|
600
|
+
return "";
|
|
601
|
+
}
|
|
602
|
+
return attr.name.name;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const compareAttrNames = (nameLeft: string, nameRight: string) => {
|
|
606
|
+
let res = compareKeys(nameLeft, nameRight);
|
|
607
|
+
if (order === "desc") {
|
|
608
|
+
res = -res;
|
|
609
|
+
}
|
|
610
|
+
return res;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const isOutOfOrder = (names: string[]) =>
|
|
614
|
+
names.some((currName, idx) => {
|
|
615
|
+
if (idx === 0 || !currName) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const prevName = names[idx - 1];
|
|
619
|
+
return (
|
|
620
|
+
prevName !== undefined &&
|
|
621
|
+
compareAttrNames(prevName, currName) > 0
|
|
622
|
+
);
|
|
623
|
+
});
|
|
494
624
|
|
|
495
625
|
// Also sort JSX attributes on elements
|
|
496
|
-
|
|
626
|
+
const checkJSXOpeningElement = (node: TSESTree.JSXOpeningElement) => {
|
|
497
627
|
const attrs = node.attributes;
|
|
498
628
|
if (attrs.length < minKeys) {
|
|
499
629
|
return;
|
|
500
630
|
}
|
|
501
631
|
|
|
502
|
-
if (attrs.some((
|
|
632
|
+
if (attrs.some((attr) => attr.type !== "JSXAttribute")) {
|
|
503
633
|
return;
|
|
504
634
|
}
|
|
505
635
|
if (
|
|
506
636
|
attrs.some(
|
|
507
|
-
(
|
|
508
|
-
|
|
509
|
-
|
|
637
|
+
(attr) =>
|
|
638
|
+
attr.type === "JSXAttribute" &&
|
|
639
|
+
attr.name.type !== "JSXIdentifier"
|
|
510
640
|
)
|
|
511
641
|
) {
|
|
512
642
|
return;
|
|
513
643
|
}
|
|
514
644
|
|
|
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
|
-
});
|
|
645
|
+
const names = attrs.map((attr) => getAttrName(attr));
|
|
524
646
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
res = -res;
|
|
529
|
-
}
|
|
530
|
-
return res;
|
|
531
|
-
};
|
|
647
|
+
if (!isOutOfOrder(names)) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
532
650
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
break;
|
|
543
|
-
}
|
|
651
|
+
if (hasDuplicateNames(names)) {
|
|
652
|
+
context.report({
|
|
653
|
+
messageId: "unsorted",
|
|
654
|
+
node:
|
|
655
|
+
attrs[0]!.type === "JSXAttribute"
|
|
656
|
+
? attrs[0]!.name
|
|
657
|
+
: attrs[0]!
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
544
660
|
}
|
|
545
661
|
|
|
546
|
-
if (
|
|
662
|
+
if (
|
|
663
|
+
attrs.some(
|
|
664
|
+
(attr) =>
|
|
665
|
+
attr.type === "JSXAttribute" &&
|
|
666
|
+
!isSafeJSXAttributeValue(attr.value)
|
|
667
|
+
)
|
|
668
|
+
) {
|
|
669
|
+
context.report({
|
|
670
|
+
messageId: "unsorted",
|
|
671
|
+
node:
|
|
672
|
+
attrs[0]!.type === "JSXAttribute"
|
|
673
|
+
? attrs[0]!.name
|
|
674
|
+
: attrs[0]!
|
|
675
|
+
});
|
|
547
676
|
return;
|
|
548
677
|
}
|
|
549
678
|
|
|
550
679
|
// Be conservative: only fix if there are no JSX comments/braces between attributes.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
680
|
+
const braceConflict = attrs.find((currAttr, idx) => {
|
|
681
|
+
if (idx === 0) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
const prevAttr = attrs[idx - 1];
|
|
685
|
+
if (!prevAttr) {
|
|
686
|
+
return false;
|
|
557
687
|
}
|
|
558
|
-
|
|
559
688
|
const between = sourceCode.text.slice(
|
|
560
689
|
prevAttr.range[1],
|
|
561
690
|
currAttr.range[0]
|
|
562
691
|
);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
692
|
+
return between.includes("{");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (braceConflict) {
|
|
696
|
+
context.report({
|
|
697
|
+
messageId: "unsorted",
|
|
698
|
+
node:
|
|
699
|
+
braceConflict.type === "JSXAttribute"
|
|
700
|
+
? braceConflict.name
|
|
701
|
+
: braceConflict
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
573
704
|
}
|
|
574
705
|
|
|
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
|
-
});
|
|
706
|
+
const sortedAttrs = attrs
|
|
707
|
+
.slice()
|
|
708
|
+
.sort((left, right) =>
|
|
709
|
+
compareAttrNames(getAttrName(left), getAttrName(right))
|
|
710
|
+
);
|
|
586
711
|
|
|
587
|
-
const firstAttr = attrs
|
|
712
|
+
const [firstAttr] = attrs;
|
|
588
713
|
const lastAttr = attrs[attrs.length - 1];
|
|
589
714
|
|
|
590
715
|
if (!firstAttr || !lastAttr) {
|
|
@@ -592,30 +717,69 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
592
717
|
}
|
|
593
718
|
|
|
594
719
|
const replacement = sortedAttrs
|
|
595
|
-
.map((
|
|
720
|
+
.map((attr) => sourceCode.getText(attr))
|
|
596
721
|
.join(" ");
|
|
597
722
|
|
|
598
723
|
context.report({
|
|
599
|
-
node:
|
|
600
|
-
firstAttr.type === "JSXAttribute"
|
|
601
|
-
? firstAttr.name
|
|
602
|
-
: firstAttr,
|
|
603
|
-
messageId: "unsorted",
|
|
604
724
|
fix(fixer) {
|
|
605
725
|
return fixer.replaceTextRange(
|
|
606
726
|
[firstAttr.range[0], lastAttr.range[1]],
|
|
607
727
|
replacement
|
|
608
728
|
);
|
|
609
|
-
}
|
|
729
|
+
},
|
|
730
|
+
messageId: "unsorted",
|
|
731
|
+
node:
|
|
732
|
+
firstAttr.type === "JSXAttribute"
|
|
733
|
+
? firstAttr.name
|
|
734
|
+
: firstAttr
|
|
610
735
|
});
|
|
611
|
-
}
|
|
736
|
+
};
|
|
612
737
|
|
|
613
738
|
return {
|
|
614
|
-
ObjectExpression: checkObjectExpression,
|
|
615
739
|
JSXAttribute(node: TSESTree.JSXAttribute) {
|
|
616
740
|
checkJSXAttributeObject(node);
|
|
617
741
|
},
|
|
618
|
-
JSXOpeningElement: checkJSXOpeningElement
|
|
742
|
+
JSXOpeningElement: checkJSXOpeningElement,
|
|
743
|
+
ObjectExpression: checkObjectExpression
|
|
619
744
|
};
|
|
745
|
+
},
|
|
746
|
+
defaultOptions: [{}],
|
|
747
|
+
meta: {
|
|
748
|
+
docs: {
|
|
749
|
+
description:
|
|
750
|
+
"enforce sorted keys in object literals with auto-fix (limited to simple cases, preserving comments)"
|
|
751
|
+
},
|
|
752
|
+
fixable: "code",
|
|
753
|
+
messages: {
|
|
754
|
+
unsorted: "Object keys are not sorted."
|
|
755
|
+
},
|
|
756
|
+
// The schema supports the same options as the built-in sort-keys rule plus:
|
|
757
|
+
// variablesBeforeFunctions: boolean (when true, non-function properties come before function properties)
|
|
758
|
+
schema: [
|
|
759
|
+
{
|
|
760
|
+
additionalProperties: false,
|
|
761
|
+
properties: {
|
|
762
|
+
caseSensitive: {
|
|
763
|
+
type: "boolean"
|
|
764
|
+
},
|
|
765
|
+
minKeys: {
|
|
766
|
+
minimum: 2,
|
|
767
|
+
type: "integer"
|
|
768
|
+
},
|
|
769
|
+
natural: {
|
|
770
|
+
type: "boolean"
|
|
771
|
+
},
|
|
772
|
+
order: {
|
|
773
|
+
enum: ["asc", "desc"],
|
|
774
|
+
type: "string"
|
|
775
|
+
},
|
|
776
|
+
variablesBeforeFunctions: {
|
|
777
|
+
type: "boolean"
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
type: "object"
|
|
781
|
+
}
|
|
782
|
+
],
|
|
783
|
+
type: "suggestion"
|
|
620
784
|
}
|
|
621
785
|
};
|