eslint-plugin-unslop 0.1.5 → 0.2.0

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/dist/index.cjs CHANGED
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(index_exports);
37
37
  // package.json
38
38
  var package_default = {
39
39
  name: "eslint-plugin-unslop",
40
- version: "0.1.5",
40
+ version: "0.2.0",
41
41
  description: "ESLint plugin with rules for reducing AI-generated code smells",
42
42
  repository: {
43
43
  type: "git",
@@ -63,8 +63,8 @@ var package_default = {
63
63
  },
64
64
  scripts: {
65
65
  build: "tsup",
66
- format: "prettier . --write",
67
- verify: "prettier . --check && tsc --noEmit && tsup && eslint . && knip",
66
+ fix: "knip --fix && eslint . --fix && prettier . --write",
67
+ verify: "prettier . --check && knip && depcruise src && jscpd && tsc --noEmit && tsup && eslint .",
68
68
  test: "vitest run",
69
69
  "test:watch": "vitest",
70
70
  prepublishOnly: "npm run build"
@@ -92,8 +92,10 @@ var package_default = {
92
92
  "@types/estree": "^1.0.8",
93
93
  "@types/node": "^22.18.0",
94
94
  "@typescript-eslint/parser": "^8.57.2",
95
+ "dependency-cruiser": "^17.3.10",
95
96
  eslint: "^9.39.1",
96
97
  globals: "^17.4.0",
98
+ jscpd: "^4.0.8",
97
99
  knip: "^6.1.0",
98
100
  prettier: "^3.8.1",
99
101
  tsup: "^8.5.0",
@@ -117,6 +119,19 @@ function createStringLiteralListener(includeEscapedUnicode, visitLiteral) {
117
119
  TemplateLiteral: inspect
118
120
  };
119
121
  }
122
+ function extractContentAndWrapper(text, node) {
123
+ if (node.type === "Literal" && typeof node.value === "string" && typeof node.raw === "string") {
124
+ const quote = node.raw[0];
125
+ if (quote === '"' || quote === "'") {
126
+ return { content: node.raw.slice(1, -1), wrapper: quote };
127
+ }
128
+ return { content: null, wrapper: "" };
129
+ }
130
+ if (node.type === "TemplateLiteral") {
131
+ return { content: text, wrapper: "`" };
132
+ }
133
+ return { content: null, wrapper: "" };
134
+ }
120
135
  function getStringValue(node, includeEscapedUnicode) {
121
136
  if (node.type === "TemplateLiteral") {
122
137
  return node.quasis.map(
@@ -129,7 +144,7 @@ function getStringValue(node, includeEscapedUnicode) {
129
144
  return includeEscapedUnicode ? node.raw ?? node.value : node.value;
130
145
  }
131
146
 
132
- // src/rules/no-special-unicode.ts
147
+ // src/rules/no-special-unicode/index.ts
133
148
  var no_special_unicode_default = {
134
149
  meta: {
135
150
  type: "problem",
@@ -137,6 +152,7 @@ var no_special_unicode_default = {
137
152
  description: "Disallow special unicode punctuation and whitespace in strings",
138
153
  recommended: true
139
154
  },
155
+ fixable: "code",
140
156
  schema: [],
141
157
  messages: {
142
158
  bannedCharacter: "String contains {{name}} (U+{{code}}). Use the ASCII equivalent."
@@ -158,36 +174,68 @@ var no_special_unicode_default = {
158
174
  data: {
159
175
  name,
160
176
  code: code.toString(16).toUpperCase().padStart(4, "0")
177
+ },
178
+ fix(fixer) {
179
+ const fixedText = computeFixedText(text, node);
180
+ if (!fixedText) return null;
181
+ return fixer.replaceText(node, fixedText);
161
182
  }
162
183
  });
163
184
  }
164
185
  });
165
186
  }
166
187
  };
167
- var BANNED_CHARS = /* @__PURE__ */ new Map([
168
- ["\u201C", "left double quotation mark"],
169
- ["\u201D", "right double quotation mark"],
170
- ["\u2018", "left single quotation mark"],
171
- ["\u2019", "right single quotation mark"],
172
- ["\xA0", "non-breaking space"],
173
- ["\u202F", "narrow no-break space"],
174
- ["\u2007", "figure space"],
175
- ["\u2008", "punctuation space"],
176
- ["\u2009", "thin space"],
177
- ["\u200A", "hair space"],
178
- ["\u200B", "zero-width space"],
179
- ["\u2002", "en space"],
180
- ["\u2003", "em space"],
181
- ["\u205F", "medium mathematical space"],
182
- ["\u3000", "ideographic space"],
183
- ["\uFEFF", "zero-width no-break space"],
184
- ["\u2013", "en dash"],
185
- ["\u2014", "em dash"],
186
- ["\u2026", "horizontal ellipsis"]
187
- ]);
188
- var BANNED_CHARS_RE = new RegExp([...BANNED_CHARS.keys()].join("|"));
188
+ var CHAR_RULES = [
189
+ { char: "\u201C", name: "left double quotation mark", replacement: '"' },
190
+ { char: "\u201D", name: "right double quotation mark", replacement: '"' },
191
+ { char: "\u2018", name: "left single quotation mark", replacement: "'" },
192
+ { char: "\u2019", name: "right single quotation mark", replacement: "'" },
193
+ { char: "\xA0", name: "non-breaking space", replacement: " " },
194
+ { char: "\u202F", name: "narrow no-break space", replacement: " " },
195
+ { char: "\u2007", name: "figure space", replacement: " " },
196
+ { char: "\u2008", name: "punctuation space", replacement: " " },
197
+ { char: "\u2009", name: "thin space", replacement: " " },
198
+ { char: "\u200A", name: "hair space", replacement: " " },
199
+ { char: "\u200B", name: "zero-width space", replacement: "" },
200
+ { char: "\u2002", name: "en space", replacement: " " },
201
+ { char: "\u2003", name: "em space", replacement: " " },
202
+ { char: "\u205F", name: "medium mathematical space", replacement: " " },
203
+ { char: "\u3000", name: "ideographic space", replacement: " " },
204
+ { char: "\uFEFF", name: "zero-width no-break space", replacement: "" },
205
+ { char: "\u2013", name: "en dash", replacement: "-" },
206
+ { char: "\u2014", name: "em dash", replacement: "-" },
207
+ { char: "\u2026", name: "horizontal ellipsis", replacement: "..." }
208
+ ];
209
+ var BANNED_CHARS = new Map(CHAR_RULES.map(({ char, name }) => [char, name]));
210
+ var BANNED_CHARS_RE = new RegExp(CHAR_RULES.map(({ char }) => char).join("|"));
211
+ function computeFixedText(text, node) {
212
+ const { content, wrapper } = extractContentAndWrapper(text, node);
213
+ if (!content) return null;
214
+ const wrapperQuote = wrapper === "`" ? null : wrapper;
215
+ const result = applyReplacements(content, wrapperQuote);
216
+ if (!result) return null;
217
+ return wrapper + result + wrapper;
218
+ }
219
+ function applyReplacements(content, wrapperQuote) {
220
+ let result = content;
221
+ let madeReplacement = false;
222
+ for (const [char, replacement] of CHAR_REPLACEMENTS) {
223
+ if (!content.includes(char)) continue;
224
+ if (isUnsafeReplacement(wrapperQuote, replacement)) continue;
225
+ madeReplacement = true;
226
+ result = result.split(char).join(replacement);
227
+ }
228
+ return madeReplacement ? result : null;
229
+ }
230
+ var CHAR_REPLACEMENTS = new Map(CHAR_RULES.map(({ char, replacement }) => [char, replacement]));
231
+ function isUnsafeReplacement(wrapperQuote, replacement) {
232
+ if (!wrapperQuote) return false;
233
+ if (wrapperQuote === '"' && replacement.includes('"')) return true;
234
+ if (wrapperQuote === "'" && replacement.includes("'")) return true;
235
+ return false;
236
+ }
189
237
 
190
- // src/rules/no-unicode-escape.ts
238
+ // src/rules/no-unicode-escape/index.ts
191
239
  var no_unicode_escape_default = {
192
240
  meta: {
193
241
  type: "suggestion",
@@ -195,6 +243,7 @@ var no_unicode_escape_default = {
195
243
  description: "Prefer literal unicode characters over \\uXXXX escape sequences",
196
244
  recommended: true
197
245
  },
246
+ fixable: "code",
198
247
  schema: [],
199
248
  messages: {
200
249
  preferLiteral: "Use the actual character instead of a \\uXXXX escape sequence."
@@ -203,14 +252,49 @@ var no_unicode_escape_default = {
203
252
  create(context) {
204
253
  return createStringLiteralListener(true, (node, text) => {
205
254
  if (UNICODE_ESCAPE_RE.test(text)) {
206
- context.report({ node, messageId: "preferLiteral" });
255
+ context.report({
256
+ node,
257
+ messageId: "preferLiteral",
258
+ fix(fixer) {
259
+ const replacement = computeReplacement(text, node);
260
+ if (!replacement) return null;
261
+ return fixer.replaceText(node, replacement);
262
+ }
263
+ });
207
264
  }
208
265
  });
209
266
  }
210
267
  };
211
- var UNICODE_ESCAPE_RE = /\\u[0-9a-fA-F]{4}/;
268
+ function computeReplacement(text, node) {
269
+ const { content, wrapper } = extractContentAndWrapper(text, node);
270
+ if (!content) return null;
271
+ if (!allEscapesSafe(content)) return null;
272
+ const result = content.replace(UNICODE_ESCAPE_RE, toLiteralCharacter);
273
+ return wrapper + result + wrapper;
274
+ }
275
+ function allEscapesSafe(content) {
276
+ const escapes = content.match(UNICODE_ESCAPE_RE);
277
+ if (!escapes) return false;
278
+ for (const escape of escapes) {
279
+ const code = parseEscapeCode(escape);
280
+ if (isUnsafeChar(code)) return false;
281
+ }
282
+ return true;
283
+ }
284
+ function toLiteralCharacter(escape) {
285
+ return String.fromCharCode(parseEscapeCode(escape));
286
+ }
287
+ function parseEscapeCode(escape) {
288
+ return Number.parseInt(escape.slice(2), 16);
289
+ }
290
+ function isUnsafeChar(code) {
291
+ if (code <= 31) return true;
292
+ if (code === 34 || code === 39 || code === 92 || code === 96) return true;
293
+ return false;
294
+ }
295
+ var UNICODE_ESCAPE_RE = /\\u[0-9a-fA-F]{4}/g;
212
296
 
213
- // src/rules/no-deep-imports.ts
297
+ // src/rules/no-deep-imports/index.ts
214
298
  var import_node_fs2 = require("fs");
215
299
  var import_node_path3 = __toESM(require("path"), 1);
216
300
 
@@ -328,7 +412,7 @@ function buildContext(filename, projectRoot, sourceRoot) {
328
412
  };
329
413
  }
330
414
 
331
- // src/rules/no-deep-imports.ts
415
+ // src/rules/no-deep-imports/index.ts
332
416
  var NO_DEEP_IMPORTS_SCHEMA = [
333
417
  {
334
418
  type: "object",
@@ -397,7 +481,7 @@ function findViolation(specifier, filename, sourceRoot, sourceRelativePath) {
397
481
  };
398
482
  }
399
483
  function resolveImportSourceRelative(specifier, filename, sourceRoot) {
400
- const normalizedSpecifier = specifier.replace(/\.js$/, "");
484
+ const normalizedSpecifier = specifier.replace(/\.(js|ts|tsx|jsx)$/, "");
401
485
  if (normalizedSpecifier.startsWith("@/")) {
402
486
  return normalizedSpecifier.slice(2);
403
487
  }
@@ -582,7 +666,7 @@ function deriveEntity(consumerPath, mode) {
582
666
  }
583
667
  var MAX_DIR_DEPTH = 3;
584
668
 
585
- // src/rules/no-false-sharing.ts
669
+ // src/rules/no-false-sharing/index.ts
586
670
  var SCHEMA = [
587
671
  {
588
672
  type: "object",
@@ -647,30 +731,148 @@ var no_false_sharing_default = {
647
731
  };
648
732
  function extractTsProgram(context) {
649
733
  const services = context.sourceCode.parserServices;
650
- if (!isRecord2(services) || !("program" in services)) {
651
- return void 0;
734
+ if (!isRecord(services)) return void 0;
735
+ const program = "program" in services ? services.program : void 0;
736
+ return isTsProgram(program) ? program : void 0;
737
+ }
738
+ function isTsProgram(value) {
739
+ return isRecord(value) && "getTypeChecker" in value;
740
+ }
741
+
742
+ // src/rules/read-friendly-order/fixer-utils.ts
743
+ function stableTopologicalOrder(count, edges) {
744
+ const adjacency = createAdjacency(count);
745
+ const inDegree = new Array(count).fill(0);
746
+ applyEdges(adjacency, inDegree, edges);
747
+ const queue = buildInitialQueue(inDegree);
748
+ const ordered = consumeQueue(queue, adjacency, inDegree);
749
+ return ordered.length === count ? ordered : void 0;
750
+ }
751
+ function applyEdges(adjacency, inDegree, edges) {
752
+ for (const [from, to] of edges) {
753
+ if (isEdgeIgnored(adjacency, from, to)) continue;
754
+ adjacency[from].add(to);
755
+ inDegree[to] += 1;
756
+ }
757
+ }
758
+ function isEdgeIgnored(adjacency, from, to) {
759
+ if (from === to) return true;
760
+ return adjacency[from].has(to);
761
+ }
762
+ function createReplaceTextRangeFix(fixRange) {
763
+ return (fixer) => {
764
+ if (!fixRange) return null;
765
+ return fixer.replaceTextRange([fixRange[0], fixRange[1]], fixRange[2]);
766
+ };
767
+ }
768
+ function isSameIndexOrder(original, candidate) {
769
+ if (original.length !== candidate.length) return false;
770
+ for (let i = 0; i < original.length; i += 1) {
771
+ if (original[i]?.index !== candidate[i]?.index) return false;
652
772
  }
653
- const program = services.program;
654
- if (!isTsProgram(program)) {
655
- return void 0;
773
+ return true;
774
+ }
775
+ function consumeQueue(queue, adjacency, inDegree) {
776
+ const ordered = [];
777
+ while (queue.length > 0) {
778
+ const current = queue.shift();
779
+ if (current === void 0) break;
780
+ ordered.push(current);
781
+ visitNextNodes(adjacency[current] ?? [], inDegree, queue);
656
782
  }
657
- return program;
783
+ return ordered;
658
784
  }
659
- function isTsProgram(value) {
660
- return isRecord2(value) && "getTypeChecker" in value;
785
+ function visitNextNodes(nextNodes, inDegree, queue) {
786
+ for (const next of nextNodes) {
787
+ inDegree[next] -= 1;
788
+ if (inDegree[next] === 0) insertSorted(queue, next);
789
+ }
661
790
  }
662
- function isRecord2(value) {
663
- return value != void 0 && typeof value === "object";
791
+ function createAdjacency(count) {
792
+ const adjacency = [];
793
+ for (let i = 0; i < count; i += 1) adjacency.push(/* @__PURE__ */ new Set());
794
+ return adjacency;
795
+ }
796
+ function buildInitialQueue(inDegree) {
797
+ const queue = [];
798
+ for (const [index, degree] of inDegree.entries()) {
799
+ if (degree === 0) queue.push(index);
800
+ }
801
+ return queue;
802
+ }
803
+ function insertSorted(queue, value) {
804
+ const index = queue.findIndex((entry) => value < entry);
805
+ if (index < 0) {
806
+ queue.push(value);
807
+ return;
808
+ }
809
+ queue.splice(index, 0, value);
810
+ }
811
+ function createSafeReorderFix(sourceCode, originalNodes, orderedNodes, options) {
812
+ const originalRanges = extractSortedRanges(originalNodes);
813
+ if (!originalRanges) return void 0;
814
+ if (hasAmbiguousComments(sourceCode, originalRanges, originalNodes)) return void 0;
815
+ const [start] = originalRanges[0];
816
+ const [, end] = originalRanges[originalRanges.length - 1];
817
+ const text = orderedNodes.map((node) => formatNodeText(sourceCode.getText(node), options)).join("\n\n");
818
+ return [start, end, text];
819
+ }
820
+ function formatNodeText(text, options) {
821
+ if (!options?.leadingIndent) return text;
822
+ if (/^\s/.test(text)) return text;
823
+ return options.leadingIndent + text;
824
+ }
825
+ function extractSortedRanges(nodes) {
826
+ const ranges = [];
827
+ for (const node of nodes) {
828
+ if (!node.range) return void 0;
829
+ ranges.push(node.range);
830
+ }
831
+ const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
832
+ for (let i = 1; i < sorted.length; i += 1) {
833
+ if (sorted[i - 1][1] > sorted[i][0]) return void 0;
834
+ }
835
+ return sorted;
836
+ }
837
+ function hasAmbiguousComments(sourceCode, sortedRanges, nodes) {
838
+ const [start] = sortedRanges[0];
839
+ const [, end] = sortedRanges[sortedRanges.length - 1];
840
+ for (const comment of sourceCode.getAllComments()) {
841
+ const range = comment.range;
842
+ if (!range) continue;
843
+ if (range[0] < start || range[1] > end) continue;
844
+ if (!isInsideAnyNode(range, nodes)) return true;
845
+ }
846
+ return false;
847
+ }
848
+ function isInsideAnyNode(commentRange, nodes) {
849
+ for (const node of nodes) {
850
+ const range = node.range;
851
+ if (!range) continue;
852
+ if (commentRange[0] >= range[0] && commentRange[1] <= range[1]) return true;
853
+ }
854
+ return false;
664
855
  }
665
856
 
666
857
  // src/rules/read-friendly-order/class-order.ts
667
858
  function reportClassOrdering(program, context) {
668
859
  for (const classNode of collectClassDeclarations(program)) {
669
860
  const members = collectClassMembers(classNode);
670
- reportConstructorOrder(members, classNode, context);
671
- reportPublicFieldOrder(members, context);
672
- reportClassDependencyOrder(members, context);
861
+ const supportsFix = !hasUnsupportedClassMember(classNode);
862
+ const fixRange = supportsFix ? buildClassFixRange(members, context) : void 0;
863
+ reportConstructorOrder(members, classNode, context, fixRange);
864
+ reportPublicFieldOrder(members, context, fixRange);
865
+ reportClassDependencyOrder(members, context, fixRange);
866
+ }
867
+ }
868
+ function hasUnsupportedClassMember(classNode) {
869
+ for (const member of classNode.body.body) {
870
+ if ("computed" in member && member.computed) return true;
871
+ if ("decorators" in member && Array.isArray(member.decorators) && member.decorators.length > 0) {
872
+ return true;
873
+ }
673
874
  }
875
+ return false;
674
876
  }
675
877
  function collectClassDeclarations(program) {
676
878
  const classes = [];
@@ -690,16 +892,17 @@ function extractClassNode(statement) {
690
892
  }
691
893
  return void 0;
692
894
  }
693
- function reportConstructorOrder(members, classNode, context) {
895
+ function reportConstructorOrder(members, classNode, context, fixRange) {
694
896
  const ctor = members.find((m) => m.kind === "constructor");
695
897
  if (!ctor || ctor.index === 0) return;
696
898
  context.report({
697
899
  node: ctor.node,
698
900
  messageId: "constructorFirst",
699
- data: { className: classNode.id?.name ?? "anonymous class" }
901
+ data: { className: classNode.id?.name ?? "anonymous class" },
902
+ fix: createReplaceTextRangeFix(fixRange)
700
903
  });
701
904
  }
702
- function reportPublicFieldOrder(members, context) {
905
+ function reportPublicFieldOrder(members, context, fixRange) {
703
906
  const ctorIndex = members.find((m) => m.kind === "constructor")?.index ?? -1;
704
907
  const startIndex = ctorIndex >= 0 ? ctorIndex + 1 : 0;
705
908
  let seenOther = false;
@@ -710,7 +913,8 @@ function reportPublicFieldOrder(members, context) {
710
913
  context.report({
711
914
  node: member.node,
712
915
  messageId: "publicFieldOrder",
713
- data: { memberName: member.name }
916
+ data: { memberName: member.name },
917
+ fix: createReplaceTextRangeFix(fixRange)
714
918
  });
715
919
  }
716
920
  continue;
@@ -718,7 +922,7 @@ function reportPublicFieldOrder(members, context) {
718
922
  seenOther = true;
719
923
  }
720
924
  }
721
- function reportClassDependencyOrder(members, context) {
925
+ function reportClassDependencyOrder(members, context, fixRange) {
722
926
  const others = members.filter((m) => m.kind === "other");
723
927
  for (const member of others) {
724
928
  const consumer = findFirstClassConsumer(others, member, context);
@@ -726,16 +930,56 @@ function reportClassDependencyOrder(members, context) {
726
930
  context.report({
727
931
  node: member.node,
728
932
  messageId: "moveMemberBelow",
729
- data: { memberName: member.name, consumerName: consumer.name }
933
+ data: { memberName: member.name, consumerName: consumer.name },
934
+ fix: createReplaceTextRangeFix(fixRange)
730
935
  });
731
936
  }
732
937
  }
938
+ function buildClassFixRange(members, context) {
939
+ if (members.length < 2) return void 0;
940
+ const orderedMembers = getCanonicalClassMembers(members, context);
941
+ if (!orderedMembers || isSameIndexOrder(members, orderedMembers)) return void 0;
942
+ const originalNodes = members.map((member) => member.node);
943
+ const orderedNodes = orderedMembers.map((member) => member.node);
944
+ return createSafeReorderFix(context.sourceCode, originalNodes, orderedNodes);
945
+ }
946
+ function getCanonicalClassMembers(members, context) {
947
+ const constructorMembers = members.filter((member) => member.kind === "constructor");
948
+ const publicFields = members.filter((member) => member.kind === "public-field");
949
+ const others = members.filter((member) => member.kind === "other");
950
+ const orderedOthers = orderOtherMembers(others, context);
951
+ if (!orderedOthers) return void 0;
952
+ return [...constructorMembers, ...publicFields, ...orderedOthers];
953
+ }
954
+ function orderOtherMembers(others, context) {
955
+ const indexedOthers = others.map((member, indexInGroup) => ({ ...member, indexInGroup }));
956
+ const edges = collectOtherMemberEdges(indexedOthers, context);
957
+ if (edges.length === 0) return [...others];
958
+ const order = stableTopologicalOrder(indexedOthers.length, edges);
959
+ if (!order) return void 0;
960
+ return order.map((index) => indexedOthers[index]);
961
+ }
962
+ function collectOtherMemberEdges(others, context) {
963
+ const edges = [];
964
+ for (const member of others) {
965
+ const consumer = findFirstClassConsumer(others, member, context);
966
+ if (!consumer) continue;
967
+ edges.push([consumer.indexInGroup, member.indexInGroup]);
968
+ }
969
+ return edges;
970
+ }
733
971
  function collectClassMembers(classNode) {
734
972
  const members = [];
735
973
  for (const [index, raw] of classNode.body.body.entries()) {
736
974
  const named = toNamedMember(raw);
737
975
  if (!named) continue;
738
- members.push({ name: named.name, node: named.node, index, kind: classifyMember(named.node) });
976
+ members.push({
977
+ name: named.name,
978
+ node: named.node,
979
+ index,
980
+ indexInGroup: -1,
981
+ kind: classifyMember(named.node)
982
+ });
739
983
  }
740
984
  return members;
741
985
  }
@@ -785,8 +1029,23 @@ function reportTestOrdering(program, context) {
785
1029
  if (!entries.some((entry) => entry.kind === "test")) {
786
1030
  return;
787
1031
  }
788
- reportSetupOrder(entries, context);
789
- reportTeardownOrder(entries, context);
1032
+ const fixRange = buildTestPhaseFixRange(entries, context);
1033
+ reportSetupOrder(entries, context, fixRange);
1034
+ reportTeardownOrder(entries, context, fixRange);
1035
+ }
1036
+ function buildTestPhaseFixRange(entries, context) {
1037
+ if (entries.length < 2) return void 0;
1038
+ const ordered = getCanonicalPhaseEntries(entries);
1039
+ if (isSameIndexOrder(entries, ordered)) return void 0;
1040
+ const originalNodes = entries.map((entry) => entry.node);
1041
+ const orderedNodes = ordered.map((entry) => entry.node);
1042
+ return createSafeReorderFix(context.sourceCode, originalNodes, orderedNodes);
1043
+ }
1044
+ function getCanonicalPhaseEntries(entries) {
1045
+ const setup = entries.filter((entry) => entry.kind === "setup");
1046
+ const teardown = entries.filter((entry) => entry.kind === "teardown");
1047
+ const tests = entries.filter((entry) => entry.kind === "test");
1048
+ return [...setup, ...teardown, ...tests];
790
1049
  }
791
1050
  function collectTestPhaseEntries(program) {
792
1051
  const entries = [];
@@ -803,13 +1062,8 @@ function toTestPhaseEntry(statement, index) {
803
1062
  return void 0;
804
1063
  }
805
1064
  const rootName = getCallRootName(statement.expression);
806
- if (!rootName) {
807
- return void 0;
808
- }
809
- const kind = classifyHookName(rootName);
810
- if (!kind) {
811
- return void 0;
812
- }
1065
+ const kind = rootName ? classifyHookName(rootName) : void 0;
1066
+ if (!kind || !rootName) return void 0;
813
1067
  return { index, kind, hookName: rootName, node: statement };
814
1068
  }
815
1069
  function classifyHookName(name) {
@@ -822,19 +1076,15 @@ var SETUP_HOOKS = /* @__PURE__ */ new Set(["beforeAll", "beforeEach", "before"])
822
1076
  var TEARDOWN_HOOKS = /* @__PURE__ */ new Set(["afterAll", "afterEach", "after"]);
823
1077
  var TEST_CALLS = /* @__PURE__ */ new Set(["test", "it"]);
824
1078
  function getCallRootName(call) {
825
- const callee = call.callee;
826
- if (callee.type === "Identifier") return callee.name;
827
- if (callee.type === "MemberExpression") return readObjectRoot(callee.object);
828
- if (callee.type === "CallExpression") return getCallRootName(callee);
829
- return void 0;
1079
+ return getRootName(call.callee);
830
1080
  }
831
- function readObjectRoot(node) {
1081
+ function getRootName(node) {
832
1082
  if (node.type === "Identifier") return node.name;
833
- if (node.type === "MemberExpression") return readObjectRoot(node.object);
834
- if (node.type === "CallExpression") return getCallRootName(node);
1083
+ if (node.type === "MemberExpression") return getRootName(node.object);
1084
+ if (node.type === "CallExpression") return getRootName(node.callee);
835
1085
  return void 0;
836
1086
  }
837
- function reportSetupOrder(entries, context) {
1087
+ function reportSetupOrder(entries, context, fixRange) {
838
1088
  const firstTeardown = findFirstIndex(entries, "teardown");
839
1089
  const firstTest = findFirstIndex(entries, "test");
840
1090
  for (const entry of entries) {
@@ -843,7 +1093,8 @@ function reportSetupOrder(entries, context) {
843
1093
  context.report({
844
1094
  node: entry.node,
845
1095
  messageId: "setupBeforeTeardown",
846
- data: { hookName: entry.hookName }
1096
+ data: { hookName: entry.hookName },
1097
+ fix: createReplaceTextRangeFix(fixRange)
847
1098
  });
848
1099
  continue;
849
1100
  }
@@ -851,12 +1102,13 @@ function reportSetupOrder(entries, context) {
851
1102
  context.report({
852
1103
  node: entry.node,
853
1104
  messageId: "setupBeforeTests",
854
- data: { hookName: entry.hookName }
1105
+ data: { hookName: entry.hookName },
1106
+ fix: createReplaceTextRangeFix(fixRange)
855
1107
  });
856
1108
  }
857
1109
  }
858
1110
  }
859
- function reportTeardownOrder(entries, context) {
1111
+ function reportTeardownOrder(entries, context, fixRange) {
860
1112
  const firstTest = findFirstIndex(entries, "test");
861
1113
  if (firstTest < 0) return;
862
1114
  for (const entry of entries) {
@@ -864,7 +1116,8 @@ function reportTeardownOrder(entries, context) {
864
1116
  context.report({
865
1117
  node: entry.node,
866
1118
  messageId: "teardownBeforeTests",
867
- data: { hookName: entry.hookName }
1119
+ data: { hookName: entry.hookName },
1120
+ fix: createReplaceTextRangeFix(fixRange)
868
1121
  });
869
1122
  }
870
1123
  }
@@ -873,7 +1126,7 @@ function findFirstIndex(entries, kind) {
873
1126
  return match ? match.index : -1;
874
1127
  }
875
1128
 
876
- // src/rules/read-friendly-order.ts
1129
+ // src/rules/read-friendly-order/index.ts
877
1130
  var READ_FRIENDLY_ORDER_MESSAGES = {
878
1131
  moveHelperBelow: 'Place helper "{{helperName}}" below the top-level symbol "{{symbolName}}" that depends on it.',
879
1132
  moveConstantBelow: 'Place constant "{{constantName}}" below the top-level symbol "{{symbolName}}" that uses it.',
@@ -892,6 +1145,7 @@ var read_friendly_order_default = {
892
1145
  recommended: false
893
1146
  },
894
1147
  schema: [],
1148
+ fixable: "code",
895
1149
  messages: READ_FRIENDLY_ORDER_MESSAGES
896
1150
  },
897
1151
  create(context) {
@@ -909,10 +1163,11 @@ function reportTopLevelOrdering(program, context) {
909
1163
  const helpers = collectHelpers(body);
910
1164
  const refs = collectReferences(context.sourceCode.scopeManager.globalScope);
911
1165
  const cyclicNames = findCyclicHelperNames(body, helpers, refs);
1166
+ const fixRange = buildTopLevelFixRange({ body, helpers, refs, cyclicNames, context });
912
1167
  for (const helper of helpers) {
913
1168
  if (cyclicNames.has(helper.name)) continue;
914
1169
  if (hasEagerReference(body, helper, refs)) continue;
915
- const consumer = findFirstConsumer(body, helper, refs);
1170
+ const consumer = findFirstConsumerEntry(body, helper, refs)?.statement;
916
1171
  if (!consumer) continue;
917
1172
  context.report({
918
1173
  node: helper.node,
@@ -921,10 +1176,34 @@ function reportTopLevelOrdering(program, context) {
921
1176
  helperName: helper.name,
922
1177
  constantName: helper.name,
923
1178
  symbolName: getSymbolName(consumer)
924
- }
1179
+ },
1180
+ fix: createReplaceTextRangeFix(fixRange)
925
1181
  });
926
1182
  }
927
1183
  }
1184
+ function buildTopLevelFixRange(input) {
1185
+ const { body, helpers, refs, cyclicNames, context } = input;
1186
+ if (body.length < 2) return void 0;
1187
+ const edges = collectTopLevelEdges(body, helpers, refs, cyclicNames);
1188
+ if (edges.length === 0) return void 0;
1189
+ const order = stableTopologicalOrder(body.length, edges);
1190
+ if (!order || isIdentityOrder(order)) return void 0;
1191
+ const orderedBody = order.map((index) => body[index]);
1192
+ return createSafeReorderFix(context.sourceCode, body, orderedBody);
1193
+ }
1194
+ function collectTopLevelEdges(body, helpers, refs, cyclicNames) {
1195
+ const edges = [];
1196
+ for (const helper of helpers) {
1197
+ if (cyclicNames.has(helper.name) || hasEagerReference(body, helper, refs)) continue;
1198
+ const consumer = findFirstConsumerEntry(body, helper, refs);
1199
+ if (!consumer) continue;
1200
+ edges.push([consumer.index, helper.index]);
1201
+ }
1202
+ return edges;
1203
+ }
1204
+ function isIdentityOrder(order) {
1205
+ return order.every((value, index) => index === value);
1206
+ }
928
1207
  function getTopLevelStatements(program) {
929
1208
  return program.body.filter((s) => s.type !== "ImportDeclaration");
930
1209
  }
@@ -986,11 +1265,11 @@ function collectVariableHelpers(decl, index) {
986
1265
  function isFunctionInit(node) {
987
1266
  return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
988
1267
  }
989
- function findFirstConsumer(body, helper, refs) {
1268
+ function findFirstConsumerEntry(body, helper, refs) {
990
1269
  for (let i = helper.index + 1; i < body.length; i += 1) {
991
1270
  const stmt = body[i];
992
1271
  if (stmt.type === "ExportNamedDeclaration" && !stmt.declaration) continue;
993
- if (statementUsesName(stmt, helper.name, refs)) return stmt;
1272
+ if (statementUsesName(stmt, helper.name, refs)) return { statement: stmt, index: i };
994
1273
  }
995
1274
  return void 0;
996
1275
  }
@@ -1027,6 +1306,7 @@ function canReachSelf(start, deps) {
1027
1306
  const queue = [...deps.get(start) ?? []];
1028
1307
  while (queue.length > 0) {
1029
1308
  const current = queue.shift();
1309
+ if (current === void 0) break;
1030
1310
  if (current === start) return true;
1031
1311
  if (visited.has(current)) continue;
1032
1312
  visited.add(current);