eslint-plugin-absolute 0.2.3 → 0.2.5

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.
@@ -34,7 +34,33 @@ type KeyInfo = {
34
34
  isFunction: boolean;
35
35
  };
36
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
+
37
53
  const SORT_BEFORE = -1;
54
+ const PURE_CONSTRUCTORS = new Set(["Date"]);
55
+ const PURE_GLOBAL_FUNCTIONS = new Set(["Boolean", "Number", "String"]);
56
+ const PURE_MEMBER_METHODS = new Set([
57
+ "getDay",
58
+ "getHours",
59
+ "getMilliseconds",
60
+ "getMinutes",
61
+ "getSeconds",
62
+ "padStart"
63
+ ]);
38
64
 
39
65
  const hasDuplicateNames = (names: Array<string | null>) => {
40
66
  const seen = new Set<string>();
@@ -50,91 +76,13 @@ const hasDuplicateNames = (names: Array<string | null>) => {
50
76
  return false;
51
77
  };
52
78
 
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
- };
133
-
134
79
  export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
135
80
  create(context) {
136
81
  const { sourceCode } = context;
137
82
  const [option] = context.options;
83
+ const topLevelBindings = new Map<string, TopLevelBinding>();
84
+ const pureFunctionCache = new Map<TSESTree.Node, boolean>();
85
+ const pureFunctionInProgress = new Set<TSESTree.Node>();
138
86
 
139
87
  const order: "asc" | "desc" =
140
88
  option && option.order ? option.order : "asc";
@@ -157,6 +105,59 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
157
105
  ? option.variablesBeforeFunctions
158
106
  : false;
159
107
 
108
+ for (const statement of sourceCode.ast.body) {
109
+ if (
110
+ statement.type === "ImportDeclaration" &&
111
+ statement.specifiers.length > 0
112
+ ) {
113
+ for (const specifier of statement.specifiers) {
114
+ topLevelBindings.set(specifier.local.name, {
115
+ kind: "import"
116
+ });
117
+ }
118
+
119
+ continue;
120
+ }
121
+
122
+ if (statement.type === "FunctionDeclaration" && statement.id) {
123
+ topLevelBindings.set(statement.id.name, {
124
+ kind: "function",
125
+ node: statement
126
+ });
127
+
128
+ continue;
129
+ }
130
+
131
+ if (
132
+ statement.type !== "VariableDeclaration" ||
133
+ statement.kind !== "const"
134
+ ) {
135
+ continue;
136
+ }
137
+
138
+ for (const declaration of statement.declarations) {
139
+ if (declaration.id.type !== "Identifier" || !declaration.init) {
140
+ continue;
141
+ }
142
+
143
+ if (
144
+ declaration.init.type === "ArrowFunctionExpression" ||
145
+ declaration.init.type === "FunctionExpression"
146
+ ) {
147
+ topLevelBindings.set(declaration.id.name, {
148
+ kind: "function",
149
+ node: declaration.init
150
+ });
151
+ continue;
152
+ }
153
+
154
+ topLevelBindings.set(declaration.id.name, {
155
+ kind: "value",
156
+ node: declaration.init
157
+ });
158
+ }
159
+ }
160
+
160
161
  /**
161
162
  * Compare two key strings based on the provided options.
162
163
  * This function mimics the behavior of the built-in rule.
@@ -179,6 +180,387 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
179
180
  return left.localeCompare(right);
180
181
  };
181
182
 
183
+ const addBoundIdentifiers = (
184
+ node: TSESTree.Node | null,
185
+ stableLocals: Set<string>
186
+ ) => {
187
+ if (!node) {
188
+ return;
189
+ }
190
+
191
+ if (node.type === "Identifier") {
192
+ stableLocals.add(node.name);
193
+ return;
194
+ }
195
+
196
+ if (node.type === "AssignmentPattern") {
197
+ addBoundIdentifiers(node.left, stableLocals);
198
+ return;
199
+ }
200
+
201
+ if (node.type === "RestElement") {
202
+ addBoundIdentifiers(node.argument, stableLocals);
203
+ return;
204
+ }
205
+
206
+ if (node.type === "ArrayPattern") {
207
+ for (const element of node.elements) {
208
+ if (!element) {
209
+ continue;
210
+ }
211
+
212
+ addBoundIdentifiers(element, stableLocals);
213
+ }
214
+ return;
215
+ }
216
+
217
+ if (node.type === "ObjectPattern") {
218
+ for (const property of node.properties) {
219
+ if (property.type === "RestElement") {
220
+ addBoundIdentifiers(property.argument, stableLocals);
221
+ continue;
222
+ }
223
+
224
+ addBoundIdentifiers(property.value, stableLocals);
225
+ }
226
+ }
227
+ };
228
+
229
+ const getStableLocalsForNode = (node: TSESTree.Node) => {
230
+ const stableLocals = new Set<string>();
231
+ const ancestors = sourceCode.getAncestors(node);
232
+
233
+ for (const ancestor of ancestors) {
234
+ if (
235
+ ancestor.type === "FunctionDeclaration" ||
236
+ ancestor.type === "FunctionExpression" ||
237
+ ancestor.type === "ArrowFunctionExpression"
238
+ ) {
239
+ for (const parameter of ancestor.params) {
240
+ addBoundIdentifiers(parameter, stableLocals);
241
+ }
242
+ }
243
+ }
244
+
245
+ for (const ancestor of ancestors) {
246
+ if (
247
+ ancestor.type !== "Program" &&
248
+ ancestor.type !== "BlockStatement"
249
+ ) {
250
+ continue;
251
+ }
252
+
253
+ for (const statement of ancestor.body) {
254
+ if (
255
+ statement.range[0] >= node.range[0] ||
256
+ statement.type !== "VariableDeclaration" ||
257
+ statement.kind !== "const"
258
+ ) {
259
+ break;
260
+ }
261
+
262
+ for (const declaration of statement.declarations) {
263
+ addBoundIdentifiers(declaration.id, stableLocals);
264
+ }
265
+ }
266
+ }
267
+
268
+ return stableLocals;
269
+ };
270
+
271
+ const getStaticMemberName = (
272
+ memberExpression: TSESTree.MemberExpression
273
+ ) => {
274
+ if (
275
+ !memberExpression.computed &&
276
+ memberExpression.property.type === "Identifier"
277
+ ) {
278
+ return memberExpression.property.name;
279
+ }
280
+
281
+ if (
282
+ memberExpression.computed &&
283
+ memberExpression.property.type === "Literal" &&
284
+ typeof memberExpression.property.value === "string"
285
+ ) {
286
+ return memberExpression.property.value;
287
+ }
288
+
289
+ return null;
290
+ };
291
+
292
+ const isStableIdentifier = (
293
+ name: string,
294
+ stableLocals: ReadonlySet<string>
295
+ ) => {
296
+ if (stableLocals.has(name)) {
297
+ return true;
298
+ }
299
+
300
+ const binding = topLevelBindings.get(name);
301
+ if (!binding) {
302
+ return false;
303
+ }
304
+
305
+ if (binding.kind === "import") {
306
+ return true;
307
+ }
308
+
309
+ if (binding.kind === "value") {
310
+ return isPureRuntimeExpression(binding.node, stableLocals);
311
+ }
312
+
313
+ return false;
314
+ };
315
+
316
+ const isPureTopLevelFunction = (
317
+ functionNode:
318
+ | TSESTree.ArrowFunctionExpression
319
+ | TSESTree.FunctionDeclaration
320
+ | TSESTree.FunctionExpression
321
+ ): boolean => {
322
+ const cached = pureFunctionCache.get(functionNode);
323
+ if (cached !== undefined) {
324
+ return cached;
325
+ }
326
+
327
+ if (pureFunctionInProgress.has(functionNode)) {
328
+ return false;
329
+ }
330
+
331
+ pureFunctionInProgress.add(functionNode);
332
+
333
+ const stableLocals = new Set<string>();
334
+
335
+ for (const parameter of functionNode.params) {
336
+ if (parameter.type === "Identifier") {
337
+ stableLocals.add(parameter.name);
338
+ }
339
+ }
340
+
341
+ let isPure = true;
342
+ const checkExpression = (expression: TSESTree.Expression) =>
343
+ isPureRuntimeExpression(expression, stableLocals);
344
+
345
+ if (functionNode.body.type === "BlockStatement") {
346
+ for (const statement of functionNode.body.body) {
347
+ if (statement.type === "ReturnStatement") {
348
+ if (
349
+ statement.argument &&
350
+ !checkExpression(statement.argument)
351
+ ) {
352
+ isPure = false;
353
+ }
354
+ continue;
355
+ }
356
+
357
+ if (
358
+ statement.type === "VariableDeclaration" &&
359
+ statement.kind === "const"
360
+ ) {
361
+ for (const declaration of statement.declarations) {
362
+ if (
363
+ declaration.id.type !== "Identifier" ||
364
+ !declaration.init ||
365
+ !checkExpression(declaration.init)
366
+ ) {
367
+ isPure = false;
368
+ break;
369
+ }
370
+
371
+ stableLocals.add(declaration.id.name);
372
+ }
373
+
374
+ if (!isPure) {
375
+ break;
376
+ }
377
+
378
+ continue;
379
+ }
380
+
381
+ isPure = false;
382
+ break;
383
+ }
384
+ } else {
385
+ isPure = checkExpression(functionNode.body);
386
+ }
387
+
388
+ pureFunctionInProgress.delete(functionNode);
389
+ pureFunctionCache.set(functionNode, isPure);
390
+ return isPure;
391
+ };
392
+
393
+ const isPureRuntimeExpression: (
394
+ node: TSESTree.Node | null,
395
+ stableLocals: ReadonlySet<string>
396
+ ) => boolean = (node, stableLocals) => {
397
+ if (!node || node.type === "PrivateIdentifier") {
398
+ return false;
399
+ }
400
+
401
+ switch (node.type) {
402
+ case "Identifier":
403
+ return isStableIdentifier(node.name, stableLocals);
404
+ case "Literal":
405
+ case "FunctionExpression":
406
+ case "ArrowFunctionExpression":
407
+ case "ClassExpression":
408
+ return true;
409
+ case "ThisExpression":
410
+ return stableLocals.has("this");
411
+ case "TemplateLiteral":
412
+ return node.expressions.every((expression) =>
413
+ isPureRuntimeExpression(expression, stableLocals)
414
+ );
415
+ case "UnaryExpression":
416
+ return isPureRuntimeExpression(node.argument, stableLocals);
417
+ case "BinaryExpression":
418
+ case "LogicalExpression":
419
+ return (
420
+ isPureRuntimeExpression(node.left, stableLocals) &&
421
+ isPureRuntimeExpression(node.right, stableLocals)
422
+ );
423
+ case "ConditionalExpression":
424
+ return (
425
+ isPureRuntimeExpression(node.test, stableLocals) &&
426
+ isPureRuntimeExpression(
427
+ node.consequent,
428
+ stableLocals
429
+ ) &&
430
+ isPureRuntimeExpression(node.alternate, stableLocals)
431
+ );
432
+ case "ArrayExpression":
433
+ return node.elements.every((element) => {
434
+ if (!element || element.type === "SpreadElement") {
435
+ return false;
436
+ }
437
+
438
+ return isPureRuntimeExpression(element, stableLocals);
439
+ });
440
+ case "ObjectExpression":
441
+ return node.properties.every((property) => {
442
+ if (
443
+ property.type !== "Property" ||
444
+ property.computed ||
445
+ property.kind !== "init"
446
+ ) {
447
+ return false;
448
+ }
449
+
450
+ if (
451
+ property.key.type !== "Identifier" &&
452
+ property.key.type !== "Literal"
453
+ ) {
454
+ return false;
455
+ }
456
+
457
+ if (property.method) {
458
+ return true;
459
+ }
460
+
461
+ return isPureRuntimeExpression(
462
+ property.value,
463
+ stableLocals
464
+ );
465
+ });
466
+ case "MemberExpression":
467
+ return (
468
+ isPureRuntimeExpression(node.object, stableLocals) &&
469
+ (!node.computed ||
470
+ isPureRuntimeExpression(
471
+ node.property,
472
+ stableLocals
473
+ ))
474
+ );
475
+ case "NewExpression":
476
+ return (
477
+ node.callee.type === "Identifier" &&
478
+ PURE_CONSTRUCTORS.has(node.callee.name) &&
479
+ node.arguments.every((argument) => {
480
+ if (argument.type === "SpreadElement") {
481
+ return false;
482
+ }
483
+
484
+ return isPureRuntimeExpression(
485
+ argument,
486
+ stableLocals
487
+ );
488
+ })
489
+ );
490
+ case "CallExpression": {
491
+ const argsArePure = node.arguments.every((argument) => {
492
+ if (argument.type === "SpreadElement") {
493
+ return false;
494
+ }
495
+
496
+ return isPureRuntimeExpression(argument, stableLocals);
497
+ });
498
+
499
+ if (!argsArePure) {
500
+ return false;
501
+ }
502
+
503
+ if (node.callee.type === "Identifier") {
504
+ if (PURE_GLOBAL_FUNCTIONS.has(node.callee.name)) {
505
+ return true;
506
+ }
507
+
508
+ const binding = topLevelBindings.get(node.callee.name);
509
+ return (
510
+ binding?.kind === "function" &&
511
+ isPureTopLevelFunction(binding.node)
512
+ );
513
+ }
514
+
515
+ if (node.callee.type !== "MemberExpression") {
516
+ return false;
517
+ }
518
+
519
+ const memberName = getStaticMemberName(node.callee);
520
+ if (!memberName || !PURE_MEMBER_METHODS.has(memberName)) {
521
+ return false;
522
+ }
523
+
524
+ return isPureRuntimeExpression(
525
+ node.callee.object,
526
+ stableLocals
527
+ );
528
+ }
529
+ default:
530
+ return false;
531
+ }
532
+ };
533
+
534
+ const isSafeToReorderExpression: (
535
+ node: TSESTree.Node | null
536
+ ) => boolean = (node) => isPureRuntimeExpression(node, new Set());
537
+
538
+ const isSafeJSXAttributeValue = (
539
+ value: TSESTree.JSXAttribute["value"],
540
+ scopeNode: TSESTree.Node
541
+ ) => {
542
+ if (value === null) {
543
+ return true;
544
+ }
545
+
546
+ if (value.type === "Literal") {
547
+ return true;
548
+ }
549
+
550
+ if (value.type !== "JSXExpressionContainer") {
551
+ return false;
552
+ }
553
+
554
+ if (value.expression.type === "JSXEmptyExpression") {
555
+ return false;
556
+ }
557
+
558
+ return isPureRuntimeExpression(
559
+ value.expression,
560
+ getStableLocalsForNode(scopeNode)
561
+ );
562
+ };
563
+
182
564
  /**
183
565
  * Determines if a property is a function property.
184
566
  */
@@ -471,7 +853,10 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
471
853
  keys.some(
472
854
  (key) =>
473
855
  key.node.type === "Property" &&
474
- !isSafeToReorderExpression(key.node.value)
856
+ !isPureRuntimeExpression(
857
+ key.node.value,
858
+ getStableLocalsForNode(key.node)
859
+ )
475
860
  )
476
861
  ) {
477
862
  autoFixable = false;
@@ -663,7 +1048,7 @@ export const sortKeysFixable: TSESLint.RuleModule<MessageIds, Options> = {
663
1048
  attrs.some(
664
1049
  (attr) =>
665
1050
  attr.type === "JSXAttribute" &&
666
- !isSafeJSXAttributeValue(attr.value)
1051
+ !isSafeJSXAttributeValue(attr.value, attr)
667
1052
  )
668
1053
  ) {
669
1054
  context.report({