eslint-plugin-absolute 0.1.6 → 0.2.1

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