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.
- package/package.json +6 -1
- package/.absolutejs/eslint.cache.json +0 -49
- package/.absolutejs/prettier.cache.json +0 -49
- package/.absolutejs/tsconfig.tsbuildinfo +0 -1
- package/.claude/settings.local.json +0 -10
- package/.codex +0 -0
- package/.prettierignore +0 -4
- package/.prettierrc.json +0 -8
- package/eslint.config.mjs +0 -107
- package/src/index.ts +0 -45
- package/src/rules/explicit-object-types.ts +0 -75
- package/src/rules/inline-style-limit.ts +0 -88
- package/src/rules/localize-react-props.ts +0 -454
- package/src/rules/max-depth-extended.ts +0 -153
- package/src/rules/max-jsx-nesting.ts +0 -59
- package/src/rules/min-var-length.ts +0 -360
- package/src/rules/no-button-navigation.ts +0 -270
- package/src/rules/no-explicit-return-types.ts +0 -83
- package/src/rules/no-inline-prop-types.ts +0 -68
- package/src/rules/no-multi-style-objects.ts +0 -80
- package/src/rules/no-nested-jsx-return.ts +0 -205
- package/src/rules/no-or-none-component.ts +0 -63
- package/src/rules/no-transition-cssproperties.ts +0 -131
- package/src/rules/no-unnecessary-div.ts +0 -65
- package/src/rules/no-unnecessary-key.ts +0 -111
- package/src/rules/no-useless-function.ts +0 -56
- package/src/rules/seperate-style-files.ts +0 -79
- package/src/rules/sort-exports.ts +0 -581
- package/src/rules/sort-keys-fixable.ts +0 -1265
- package/src/rules/spring-naming-convention.ts +0 -160
- package/tsconfig.json +0 -17
|
@@ -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
|
-
};
|