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 +360 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +360 -80
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
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.
|
|
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
|
-
|
|
67
|
-
verify: "prettier . --check && tsc --noEmit && tsup && eslint .
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
]
|
|
188
|
-
var
|
|
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({
|
|
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
|
-
|
|
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 (!
|
|
651
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
783
|
+
return ordered;
|
|
658
784
|
}
|
|
659
|
-
function
|
|
660
|
-
|
|
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
|
|
663
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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({
|
|
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
|
-
|
|
789
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
|
1081
|
+
function getRootName(node) {
|
|
832
1082
|
if (node.type === "Identifier") return node.name;
|
|
833
|
-
if (node.type === "MemberExpression") return
|
|
834
|
-
if (node.type === "CallExpression") return
|
|
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 =
|
|
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
|
|
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);
|