lintmax 0.1.15
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.
Potentially problematic release.
This version of lintmax might be problematic. Click here for more details.
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1569 -0
- package/dist/constants-Cjkf4mJh.mjs +105 -0
- package/dist/eslint.d.mts +9 -0
- package/dist/eslint.mjs +344 -0
- package/dist/ignores-BzTRqd-5.mjs +102 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +2 -0
- package/dist/lintmax-types-CJ7VY33l.d.mts +69 -0
- package/dist/path-Cu_Nf2ct.mjs +168 -0
- package/dist/src-C8jQ6tK0.mjs +784 -0
- package/oxlintrc.json +104 -0
- package/package.json +83 -0
- package/tsconfig.json +27 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1569 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { i as DEFAULT_SHARED_IGNORE_PATTERNS } from "./constants-Cjkf4mJh.mjs";
|
|
3
|
+
import { i as joinPath, n as fromFileUrl, t as dirnamePath } from "./path-Cu_Nf2ct.mjs";
|
|
4
|
+
import { _ as run, a as PRETTIER_MD_ARGS, b as writeJson, c as decodeText, d as lintmaxRoot, f as pathExists, g as resolveBin, h as readVersion, i as CliExitError, l as ensureDirectory, m as readRequiredJson, o as cacheDir, p as readJson, r as sync, s as cwd, u as ignoreEntries, v as runCapture, y as usage } from "./src-C8jQ6tK0.mjs";
|
|
5
|
+
import { Glob, env, file, spawnSync, write } from "bun";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
//#region src/init.ts
|
|
8
|
+
const initScripts = async ({ pkg, pkgPath }) => {
|
|
9
|
+
const scripts = pkg.scripts ?? {};
|
|
10
|
+
let changed = false;
|
|
11
|
+
if (!scripts.fix) {
|
|
12
|
+
scripts.fix = "lintmax fix";
|
|
13
|
+
changed = true;
|
|
14
|
+
}
|
|
15
|
+
if (!scripts.check) {
|
|
16
|
+
scripts.check = "lintmax check";
|
|
17
|
+
changed = true;
|
|
18
|
+
}
|
|
19
|
+
if (!changed) return;
|
|
20
|
+
pkg.scripts = scripts;
|
|
21
|
+
await writeJson({
|
|
22
|
+
data: pkg,
|
|
23
|
+
path: pkgPath
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const initTsconfig = async ({ configFiles }) => {
|
|
27
|
+
const tsconfigPath = joinPath(cwd, "tsconfig.json");
|
|
28
|
+
if (!await pathExists({ path: tsconfigPath })) {
|
|
29
|
+
const tsconfig = { extends: "lintmax/tsconfig" };
|
|
30
|
+
if (configFiles.length > 0) tsconfig.include = configFiles;
|
|
31
|
+
await writeJson({
|
|
32
|
+
data: tsconfig,
|
|
33
|
+
path: tsconfigPath
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const tsconfig = await readRequiredJson({ path: tsconfigPath });
|
|
39
|
+
let changed = false;
|
|
40
|
+
if (tsconfig.extends !== "lintmax/tsconfig") {
|
|
41
|
+
tsconfig.extends = "lintmax/tsconfig";
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
const toAdd = configFiles.filter((f) => !(tsconfig.include ?? []).includes(f));
|
|
45
|
+
if (toAdd.length > 0) {
|
|
46
|
+
tsconfig.include = [...tsconfig.include ?? [], ...toAdd];
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
if (changed) await writeJson({
|
|
50
|
+
data: tsconfig,
|
|
51
|
+
path: tsconfigPath
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
process.stderr.write("tsconfig.json: could not parse, add \"extends\": \"lintmax/tsconfig\" manually\n");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const initGitignore = async () => {
|
|
58
|
+
const gitignorePath = joinPath(cwd, ".gitignore");
|
|
59
|
+
if (await pathExists({ path: gitignorePath })) {
|
|
60
|
+
const content = await file(gitignorePath).text();
|
|
61
|
+
const toAdd = [];
|
|
62
|
+
for (const entry of ignoreEntries) if (!content.includes(entry)) toAdd.push(entry);
|
|
63
|
+
if (toAdd.length > 0) await write(gitignorePath, `${content.trimEnd()}\n${toAdd.join("\n")}\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await write(gitignorePath, `${ignoreEntries.join("\n")}\n`);
|
|
67
|
+
};
|
|
68
|
+
const findLegacyConfigs = async () => {
|
|
69
|
+
const checks = [
|
|
70
|
+
".eslintrc",
|
|
71
|
+
".eslintrc.json",
|
|
72
|
+
".eslintrc.js",
|
|
73
|
+
".eslintrc.cjs",
|
|
74
|
+
".eslintrc.yml",
|
|
75
|
+
".eslintrc.yaml",
|
|
76
|
+
".prettierrc",
|
|
77
|
+
".prettierrc.json",
|
|
78
|
+
".prettierrc.js",
|
|
79
|
+
".prettierrc.yml",
|
|
80
|
+
".prettierrc.yaml",
|
|
81
|
+
".prettierrc.toml",
|
|
82
|
+
"biome.json",
|
|
83
|
+
"biome.jsonc",
|
|
84
|
+
".oxlintrc.json"
|
|
85
|
+
].map(async (configFile) => ({
|
|
86
|
+
configFile,
|
|
87
|
+
exists: await pathExists({ path: joinPath(cwd, configFile) })
|
|
88
|
+
}));
|
|
89
|
+
const resolved = await Promise.all(checks);
|
|
90
|
+
const found = [];
|
|
91
|
+
for (const item of resolved) if (item.exists) found.push(item.configFile);
|
|
92
|
+
return found;
|
|
93
|
+
};
|
|
94
|
+
const runInit = async () => {
|
|
95
|
+
const pkgPath = joinPath(cwd, "package.json");
|
|
96
|
+
if (!await pathExists({ path: pkgPath })) throw new CliExitError({
|
|
97
|
+
code: 1,
|
|
98
|
+
message: "No package.json found"
|
|
99
|
+
});
|
|
100
|
+
const pkg = await readRequiredJson({ path: pkgPath });
|
|
101
|
+
const configFiles = [];
|
|
102
|
+
if (await pathExists({ path: joinPath(cwd, "lintmax.config.ts") })) configFiles.push("lintmax.config.ts");
|
|
103
|
+
await initScripts({
|
|
104
|
+
pkg,
|
|
105
|
+
pkgPath
|
|
106
|
+
});
|
|
107
|
+
await initTsconfig({ configFiles });
|
|
108
|
+
await initGitignore();
|
|
109
|
+
const foundLegacy = await findLegacyConfigs();
|
|
110
|
+
process.stdout.write("tsconfig.json extends lintmax/tsconfig");
|
|
111
|
+
if (configFiles.length > 0) process.stdout.write(`, include: ${configFiles.join(", ")}`);
|
|
112
|
+
process.stdout.write("\n");
|
|
113
|
+
process.stdout.write("package.json \"fix\": \"lintmax fix\", \"check\": \"lintmax check\"\n");
|
|
114
|
+
process.stdout.write(`.gitignore ${ignoreEntries.join(", ")}\n`);
|
|
115
|
+
if (foundLegacy.length > 0) process.stdout.write(`\nLegacy configs found (can be removed): ${foundLegacy.join(", ")}\n`);
|
|
116
|
+
process.stdout.write("\nRun: bun fix\n");
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/rule-equivalence.ts
|
|
120
|
+
const equivalenceGroups = [
|
|
121
|
+
[
|
|
122
|
+
"lint/suspicious/noExplicitAny",
|
|
123
|
+
"@typescript-eslint/no-explicit-any",
|
|
124
|
+
"typescript-eslint(no-explicit-any)"
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"lint/correctness/noUnusedVariables",
|
|
128
|
+
"@typescript-eslint/no-unused-vars",
|
|
129
|
+
"eslint(no-unused-vars)"
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
"lint/suspicious/noDebugger",
|
|
133
|
+
"no-debugger",
|
|
134
|
+
"eslint(no-debugger)"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"lint/correctness/noUnreachable",
|
|
138
|
+
"no-unreachable",
|
|
139
|
+
"eslint(no-unreachable)"
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"lint/suspicious/noDoubleEquals",
|
|
143
|
+
"eqeqeq",
|
|
144
|
+
"eslint(eqeqeq)"
|
|
145
|
+
],
|
|
146
|
+
[
|
|
147
|
+
"lint/suspicious/noDuplicateCase",
|
|
148
|
+
"no-duplicate-case",
|
|
149
|
+
"eslint(no-duplicate-case)"
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
"lint/suspicious/noFallthroughSwitchClause",
|
|
153
|
+
"no-fallthrough",
|
|
154
|
+
"eslint(no-fallthrough)"
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
"lint/suspicious/noRedeclare",
|
|
158
|
+
"@typescript-eslint/no-redeclare",
|
|
159
|
+
"typescript-eslint(no-redeclare)"
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"lint/suspicious/noShadowRestrictedNames",
|
|
163
|
+
"no-shadow-restricted-names",
|
|
164
|
+
"eslint(no-shadow-restricted-names)"
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"lint/correctness/useIsNan",
|
|
168
|
+
"use-isnan",
|
|
169
|
+
"eslint(use-isnan)"
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"lint/correctness/noConstAssign",
|
|
173
|
+
"no-const-assign",
|
|
174
|
+
"eslint(no-const-assign)"
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"lint/correctness/noNewSymbol",
|
|
178
|
+
"no-new-symbol",
|
|
179
|
+
"eslint(no-new-symbol)"
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"lint/correctness/noUndeclaredVariables",
|
|
183
|
+
"no-undef",
|
|
184
|
+
"eslint(no-undef)"
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"lint/suspicious/noEmptyBlockStatements",
|
|
188
|
+
"no-empty",
|
|
189
|
+
"eslint(no-empty)"
|
|
190
|
+
],
|
|
191
|
+
[
|
|
192
|
+
"lint/suspicious/noSelfCompare",
|
|
193
|
+
"no-self-compare",
|
|
194
|
+
"eslint(no-self-compare)"
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
"lint/complexity/noUselessConstructor",
|
|
198
|
+
"@typescript-eslint/no-useless-constructor",
|
|
199
|
+
"eslint(no-useless-constructor)"
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
"lint/suspicious/noArrayIndexKey",
|
|
203
|
+
"react/no-array-index-key",
|
|
204
|
+
"eslint-plugin-react(no-array-index-key)"
|
|
205
|
+
],
|
|
206
|
+
[
|
|
207
|
+
"lint/correctness/useExhaustiveDependencies",
|
|
208
|
+
"react-hooks/exhaustive-deps",
|
|
209
|
+
"react-hooks(exhaustive-deps)"
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"lint/correctness/useHookAtTopLevel",
|
|
213
|
+
"react-hooks/rules-of-hooks",
|
|
214
|
+
"react-hooks(rules-of-hooks)"
|
|
215
|
+
]
|
|
216
|
+
];
|
|
217
|
+
const ruleToCanonical = /* @__PURE__ */ new Map();
|
|
218
|
+
for (const group of equivalenceGroups) {
|
|
219
|
+
const canonical = group[0] ?? "";
|
|
220
|
+
for (const rule of group) ruleToCanonical.set(rule, canonical);
|
|
221
|
+
}
|
|
222
|
+
const getCanonicalRule = (rule) => ruleToCanonical.get(rule) ?? rule;
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/aggregate.ts
|
|
225
|
+
const LINTER_PRIORITY = {
|
|
226
|
+
biome: 0,
|
|
227
|
+
eslint: 2,
|
|
228
|
+
oxlint: 1,
|
|
229
|
+
prettier: 3,
|
|
230
|
+
"sort-package-json": 4
|
|
231
|
+
};
|
|
232
|
+
const cwdPrefix = `${process.cwd()}/`;
|
|
233
|
+
const normalizePath = (filePath) => {
|
|
234
|
+
if (filePath.startsWith(cwdPrefix)) return filePath.slice(cwdPrefix.length);
|
|
235
|
+
return filePath;
|
|
236
|
+
};
|
|
237
|
+
const parseBiomeDiagnostics = ({ stdout }) => {
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = JSON.parse(stdout);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
if (!Array.isArray(parsed.diagnostics)) return [];
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const d of parsed.diagnostics) {
|
|
247
|
+
const filePath = d.location?.path;
|
|
248
|
+
const { category } = d;
|
|
249
|
+
if (filePath && category) {
|
|
250
|
+
const line = d.location?.start?.line ?? 0;
|
|
251
|
+
results.push({
|
|
252
|
+
file: normalizePath(filePath),
|
|
253
|
+
line,
|
|
254
|
+
linter: "biome",
|
|
255
|
+
rule: category
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return results;
|
|
260
|
+
};
|
|
261
|
+
const parseOxlintDiagnostics = ({ stdout }) => {
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = JSON.parse(stdout);
|
|
265
|
+
} catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
if (!Array.isArray(parsed.diagnostics)) return [];
|
|
269
|
+
const results = [];
|
|
270
|
+
for (const d of parsed.diagnostics) {
|
|
271
|
+
const filePath = d.filename;
|
|
272
|
+
const rule = d.code;
|
|
273
|
+
if (filePath && rule) {
|
|
274
|
+
const line = d.labels?.[0]?.span?.line ?? 0;
|
|
275
|
+
results.push({
|
|
276
|
+
file: normalizePath(filePath),
|
|
277
|
+
line,
|
|
278
|
+
linter: "oxlint",
|
|
279
|
+
rule
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
};
|
|
285
|
+
const parseEslintDiagnostics = ({ stdout }) => {
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(stdout);
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(parsed)) return [];
|
|
293
|
+
const results = [];
|
|
294
|
+
for (const fileEntry of parsed) {
|
|
295
|
+
const { filePath } = fileEntry;
|
|
296
|
+
if (filePath && Array.isArray(fileEntry.messages)) for (const msg of fileEntry.messages) {
|
|
297
|
+
const rule = msg.ruleId;
|
|
298
|
+
if (rule) results.push({
|
|
299
|
+
file: normalizePath(filePath),
|
|
300
|
+
line: msg.line ?? 0,
|
|
301
|
+
linter: "eslint",
|
|
302
|
+
rule
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return results;
|
|
307
|
+
};
|
|
308
|
+
const parsePrettierOutput = ({ stdout }) => {
|
|
309
|
+
const results = [];
|
|
310
|
+
const lines = stdout.trim().split("\n");
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
const filePath = line.trim();
|
|
313
|
+
if (filePath.length > 0) results.push({
|
|
314
|
+
file: normalizePath(filePath),
|
|
315
|
+
line: 0,
|
|
316
|
+
linter: "prettier",
|
|
317
|
+
rule: "unformatted"
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return results;
|
|
321
|
+
};
|
|
322
|
+
const parseSortPackageJsonOutput = ({ exitCode, stdout }) => {
|
|
323
|
+
if (exitCode === 0) return [];
|
|
324
|
+
const results = [];
|
|
325
|
+
const lines = stdout.trim().split("\n");
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
const filePath = line.trim();
|
|
328
|
+
if (filePath.length > 0 && filePath.endsWith(".json")) results.push({
|
|
329
|
+
file: normalizePath(filePath),
|
|
330
|
+
line: 0,
|
|
331
|
+
linter: "sort-package-json",
|
|
332
|
+
rule: "unsorted"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return results;
|
|
336
|
+
};
|
|
337
|
+
const dedup = (diagnostics) => {
|
|
338
|
+
const seen = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const d of diagnostics) if (d.line === 0) {
|
|
340
|
+
const key = `${d.file}\0${d.linter}\0${d.rule}`;
|
|
341
|
+
seen.set(key, d);
|
|
342
|
+
} else {
|
|
343
|
+
const canonical = getCanonicalRule(d.rule);
|
|
344
|
+
const key = `${d.file}\0${d.line}\0${canonical}`;
|
|
345
|
+
const existing = seen.get(key);
|
|
346
|
+
if (!existing || (LINTER_PRIORITY[d.linter] ?? 99) < (LINTER_PRIORITY[existing.linter] ?? 99)) seen.set(key, d);
|
|
347
|
+
}
|
|
348
|
+
return [...seen.values()];
|
|
349
|
+
};
|
|
350
|
+
const linterSort = (a, b) => (LINTER_PRIORITY[a] ?? 99) - (LINTER_PRIORITY[b] ?? 99);
|
|
351
|
+
const aggregate = ({ diagnostics }) => {
|
|
352
|
+
const deduped = dedup(diagnostics);
|
|
353
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
354
|
+
for (const d of deduped) {
|
|
355
|
+
let linterMap = fileMap.get(d.file);
|
|
356
|
+
if (!linterMap) {
|
|
357
|
+
linterMap = /* @__PURE__ */ new Map();
|
|
358
|
+
fileMap.set(d.file, linterMap);
|
|
359
|
+
}
|
|
360
|
+
let ruleMap = linterMap.get(d.linter);
|
|
361
|
+
if (!ruleMap) {
|
|
362
|
+
ruleMap = /* @__PURE__ */ new Map();
|
|
363
|
+
linterMap.set(d.linter, ruleMap);
|
|
364
|
+
}
|
|
365
|
+
let lines = ruleMap.get(d.rule);
|
|
366
|
+
if (!lines) {
|
|
367
|
+
lines = [];
|
|
368
|
+
ruleMap.set(d.rule, lines);
|
|
369
|
+
}
|
|
370
|
+
if (d.line > 0) lines.push(d.line);
|
|
371
|
+
}
|
|
372
|
+
const files = [];
|
|
373
|
+
const sortedFiles = [...fileMap.keys()].toSorted();
|
|
374
|
+
for (const filePath of sortedFiles) {
|
|
375
|
+
const linterMap = fileMap.get(filePath) ?? /* @__PURE__ */ new Map();
|
|
376
|
+
const linters = [];
|
|
377
|
+
const sortedLinters = [...linterMap.keys()].toSorted(linterSort);
|
|
378
|
+
for (const linterName of sortedLinters) {
|
|
379
|
+
const ruleMap = linterMap.get(linterName) ?? /* @__PURE__ */ new Map();
|
|
380
|
+
const rules = [];
|
|
381
|
+
const sortedRules = [...ruleMap.keys()].toSorted();
|
|
382
|
+
for (const ruleName of sortedRules) {
|
|
383
|
+
const lines = ruleMap.get(ruleName) ?? [];
|
|
384
|
+
const uniqueLines = [...new Set(lines)].toSorted((a, b) => a - b);
|
|
385
|
+
rules.push({
|
|
386
|
+
lines: uniqueLines,
|
|
387
|
+
rule: ruleName
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
linters.push({
|
|
391
|
+
linter: linterName,
|
|
392
|
+
rules
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
files.push({
|
|
396
|
+
file: filePath,
|
|
397
|
+
linters
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return files;
|
|
401
|
+
};
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/class-name.ts
|
|
404
|
+
/** biome-ignore-all lint/nursery/noContinue: AST traversal requires continue */
|
|
405
|
+
const CN_NAMES = new Set(["cn"]);
|
|
406
|
+
const BANNED_CALLEE_NAMES = new Set([
|
|
407
|
+
"classnames",
|
|
408
|
+
"clsx",
|
|
409
|
+
"cx",
|
|
410
|
+
"twMerge"
|
|
411
|
+
]);
|
|
412
|
+
const isJsxClassName = (node) => ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className";
|
|
413
|
+
const isCallToCn = (node) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && CN_NAMES.has(node.expression.text);
|
|
414
|
+
const isJoinCall = (node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "join";
|
|
415
|
+
const isBannedCallee = (node) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && BANNED_CALLEE_NAMES.has(node.expression.text);
|
|
416
|
+
const isStringLiteral = (node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
|
|
417
|
+
const findClassNameViolations = ({ sourceText }) => {
|
|
418
|
+
const sourceFile = ts.createSourceFile("file.tsx", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
419
|
+
const violations = [];
|
|
420
|
+
const visit = (node) => {
|
|
421
|
+
if (isJsxClassName(node)) {
|
|
422
|
+
const init = node.initializer;
|
|
423
|
+
if (init && ts.isJsxExpression(init) && init.expression) {
|
|
424
|
+
const expr = init.expression;
|
|
425
|
+
if (!(isStringLiteral(expr) || isCallToCn(expr))) {
|
|
426
|
+
const line = sourceFile.getLineAndCharacterOfPosition(expr.getStart()).line + 1;
|
|
427
|
+
if (ts.isTemplateLiteral(expr)) violations.push({
|
|
428
|
+
line,
|
|
429
|
+
rule: "cn/no-template-literal"
|
|
430
|
+
});
|
|
431
|
+
else if (ts.isConditionalExpression(expr)) violations.push({
|
|
432
|
+
line,
|
|
433
|
+
rule: "cn/no-ternary"
|
|
434
|
+
});
|
|
435
|
+
else if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) violations.push({
|
|
436
|
+
line,
|
|
437
|
+
rule: "cn/no-concatenation"
|
|
438
|
+
});
|
|
439
|
+
else if (isBannedCallee(expr)) violations.push({
|
|
440
|
+
line,
|
|
441
|
+
rule: "cn/no-banned-callee"
|
|
442
|
+
});
|
|
443
|
+
else if (isJoinCall(expr)) violations.push({
|
|
444
|
+
line,
|
|
445
|
+
rule: "cn/no-join"
|
|
446
|
+
});
|
|
447
|
+
else if (ts.isCallExpression(expr) && !isCallToCn(expr)) {
|
|
448
|
+
const callee = expr.expression;
|
|
449
|
+
if (ts.isIdentifier(callee) && BANNED_CALLEE_NAMES.has(callee.text)) violations.push({
|
|
450
|
+
line,
|
|
451
|
+
rule: "cn/no-banned-callee"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (isBannedCallee(node)) {
|
|
458
|
+
const call = node;
|
|
459
|
+
const { parent } = call;
|
|
460
|
+
if (!(ts.isJsxExpression(parent) && isJsxClassName(parent.parent))) {
|
|
461
|
+
const line = sourceFile.getLineAndCharacterOfPosition(call.getStart()).line + 1;
|
|
462
|
+
violations.push({
|
|
463
|
+
line,
|
|
464
|
+
rule: "cn/no-banned-callee"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
ts.forEachChild(node, visit);
|
|
469
|
+
};
|
|
470
|
+
visit(sourceFile);
|
|
471
|
+
return violations;
|
|
472
|
+
};
|
|
473
|
+
const TSX_EXTENSIONS = new Set([".tsx"]);
|
|
474
|
+
const isTsxFile = (path) => {
|
|
475
|
+
const dot = path.lastIndexOf(".");
|
|
476
|
+
return dot > path.lastIndexOf("/") && TSX_EXTENSIONS.has(path.slice(dot));
|
|
477
|
+
};
|
|
478
|
+
const checkClassNameFile = async (filePath) => {
|
|
479
|
+
if (!isTsxFile(filePath)) return [];
|
|
480
|
+
const f = file(filePath);
|
|
481
|
+
if (!await f.exists()) return [];
|
|
482
|
+
return findClassNameViolations({ sourceText: await f.text() }).map((v) => ({
|
|
483
|
+
file: filePath,
|
|
484
|
+
line: v.line,
|
|
485
|
+
linter: "cn",
|
|
486
|
+
rule: v.rule
|
|
487
|
+
}));
|
|
488
|
+
};
|
|
489
|
+
const checkClassName = async ({ root }) => {
|
|
490
|
+
const glob = new Glob("**/*.tsx");
|
|
491
|
+
const allDiagnostics = [];
|
|
492
|
+
for await (const path of glob.scan({
|
|
493
|
+
absolute: true,
|
|
494
|
+
cwd: root,
|
|
495
|
+
dot: false
|
|
496
|
+
})) {
|
|
497
|
+
if (path.includes("node_modules") || path.includes("readonly") || path.includes(".next") || path.includes("dist")) continue;
|
|
498
|
+
const diagnostics = await checkClassNameFile(path);
|
|
499
|
+
allDiagnostics.push(...diagnostics);
|
|
500
|
+
}
|
|
501
|
+
return allDiagnostics;
|
|
502
|
+
};
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/rules.ts
|
|
505
|
+
const extractBiomeRules = async () => {
|
|
506
|
+
const pkgPath = fromFileUrl(import.meta.resolve("@biomejs/biome/configuration_schema.json"));
|
|
507
|
+
const defs = JSON.parse(await file(pkgPath).text()).$defs ?? {};
|
|
508
|
+
const categories = [
|
|
509
|
+
"a11y",
|
|
510
|
+
"complexity",
|
|
511
|
+
"correctness",
|
|
512
|
+
"nursery",
|
|
513
|
+
"performance",
|
|
514
|
+
"security",
|
|
515
|
+
"style",
|
|
516
|
+
"suspicious"
|
|
517
|
+
];
|
|
518
|
+
const results = [];
|
|
519
|
+
for (const cat of categories) {
|
|
520
|
+
const key = cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
521
|
+
const rules = Object.keys(defs[key]?.properties ?? {});
|
|
522
|
+
for (const rule of rules) results.push({
|
|
523
|
+
fixable: false,
|
|
524
|
+
linter: "biome",
|
|
525
|
+
rule: `lint/${cat}/${rule}`
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return results;
|
|
529
|
+
};
|
|
530
|
+
const OXLINT_FIX_MARKERS = new Set([
|
|
531
|
+
"⚠️🛠️️",
|
|
532
|
+
"💡",
|
|
533
|
+
"🛠️",
|
|
534
|
+
"🛠️💡"
|
|
535
|
+
]);
|
|
536
|
+
const extractOxlintRules = () => {
|
|
537
|
+
const output = decodeText(spawnSync({
|
|
538
|
+
cmd: [
|
|
539
|
+
"bun",
|
|
540
|
+
"node_modules/.bin/oxlint",
|
|
541
|
+
"-c",
|
|
542
|
+
joinPath(cwd, "node_modules/.cache/lintmax/.oxlintrc.json"),
|
|
543
|
+
"--rules"
|
|
544
|
+
],
|
|
545
|
+
cwd,
|
|
546
|
+
stderr: "pipe",
|
|
547
|
+
stdout: "pipe"
|
|
548
|
+
}).stdout);
|
|
549
|
+
const results = [];
|
|
550
|
+
const lines = output.split("\n");
|
|
551
|
+
for (const line of lines) if (line.startsWith("|") && !line.includes("Rule name")) {
|
|
552
|
+
const cols = line.split("|").map((c) => c.trim());
|
|
553
|
+
const ruleName = cols[1];
|
|
554
|
+
const source = cols[2];
|
|
555
|
+
const fixCol = cols[5] ?? "";
|
|
556
|
+
const enabledCol = cols[4] ?? "";
|
|
557
|
+
if (ruleName && source && !ruleName.startsWith("---") && enabledCol.includes("✅")) results.push({
|
|
558
|
+
fixable: OXLINT_FIX_MARKERS.has(fixCol.trim()),
|
|
559
|
+
linter: "oxlint",
|
|
560
|
+
rule: source === "oxc" ? ruleName : `${source}(${ruleName})`
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return results;
|
|
564
|
+
};
|
|
565
|
+
const extractEslintRules = async () => {
|
|
566
|
+
const eslintBin = await resolveBin({
|
|
567
|
+
bin: "eslint",
|
|
568
|
+
pkg: "eslint"
|
|
569
|
+
});
|
|
570
|
+
const configPath = joinPath(cwd, "node_modules/.cache/lintmax/eslint.generated.mjs");
|
|
571
|
+
const dummyFile = joinPath(cwd, "_lintmax_dummy.ts");
|
|
572
|
+
await write(dummyFile, "export {}\n");
|
|
573
|
+
const result = spawnSync({
|
|
574
|
+
cmd: [
|
|
575
|
+
"bun",
|
|
576
|
+
eslintBin,
|
|
577
|
+
"--config",
|
|
578
|
+
configPath,
|
|
579
|
+
"--print-config",
|
|
580
|
+
dummyFile
|
|
581
|
+
],
|
|
582
|
+
cwd,
|
|
583
|
+
stderr: "pipe",
|
|
584
|
+
stdout: "pipe"
|
|
585
|
+
});
|
|
586
|
+
spawnSync({
|
|
587
|
+
cmd: [
|
|
588
|
+
"rm",
|
|
589
|
+
"-f",
|
|
590
|
+
dummyFile
|
|
591
|
+
],
|
|
592
|
+
stderr: "pipe",
|
|
593
|
+
stdout: "pipe"
|
|
594
|
+
});
|
|
595
|
+
if (result.exitCode !== 0) return [];
|
|
596
|
+
let parsed;
|
|
597
|
+
try {
|
|
598
|
+
parsed = JSON.parse(decodeText(result.stdout));
|
|
599
|
+
} catch {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
const allRules = parsed.rules ?? {};
|
|
603
|
+
const results = [];
|
|
604
|
+
for (const [rule, config] of Object.entries(allRules)) {
|
|
605
|
+
const level = Array.isArray(config) ? config[0] : config;
|
|
606
|
+
if (level !== 0 && level !== "off") results.push({
|
|
607
|
+
fixable: false,
|
|
608
|
+
linter: "eslint",
|
|
609
|
+
rule
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return results;
|
|
613
|
+
};
|
|
614
|
+
const extractAllRules = async () => {
|
|
615
|
+
const oxlint = extractOxlintRules();
|
|
616
|
+
const [biome, eslint] = await Promise.all([extractBiomeRules(), extractEslintRules()]);
|
|
617
|
+
return [
|
|
618
|
+
...biome,
|
|
619
|
+
...oxlint,
|
|
620
|
+
...eslint
|
|
621
|
+
].toSorted((a, b) => {
|
|
622
|
+
const linterCmp = a.linter.localeCompare(b.linter);
|
|
623
|
+
if (linterCmp !== 0) return linterCmp;
|
|
624
|
+
return a.rule.localeCompare(b.rule);
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
const formatRulesCompact = (rules) => {
|
|
628
|
+
const byLinter = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const r of rules) {
|
|
630
|
+
let list = byLinter.get(r.linter);
|
|
631
|
+
if (!list) {
|
|
632
|
+
list = [];
|
|
633
|
+
byLinter.set(r.linter, list);
|
|
634
|
+
}
|
|
635
|
+
list.push(r.rule);
|
|
636
|
+
}
|
|
637
|
+
const parts = [];
|
|
638
|
+
for (const [linter, ruleList] of byLinter) {
|
|
639
|
+
parts.push(`${linter} (${ruleList.length})`);
|
|
640
|
+
for (const rule of ruleList) parts.push(` ${rule}`);
|
|
641
|
+
}
|
|
642
|
+
return parts.join("\n");
|
|
643
|
+
};
|
|
644
|
+
const formatRulesHuman = (rules) => {
|
|
645
|
+
const header = "Linter Rule";
|
|
646
|
+
const separator = "─".repeat(60);
|
|
647
|
+
const lines = [header, separator];
|
|
648
|
+
for (const r of rules) lines.push(`${r.linter.padEnd(16)}${r.rule}`);
|
|
649
|
+
lines.push(separator);
|
|
650
|
+
lines.push(`Total: ${rules.length} rules`);
|
|
651
|
+
return lines.join("\n");
|
|
652
|
+
};
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/clean-ignores.ts
|
|
655
|
+
/** biome-ignore-all lint/performance/noAwaitInLoops: sequential file writes */
|
|
656
|
+
/** biome-ignore-all lint/nursery/useNamedCaptureGroup: not needed */
|
|
657
|
+
const eslintLineRe = /^(\s*(?:\/\/|\/\*)\s*(?:eslint-disable(?:-next-line)?)\s+)(.+?)(\s*\*\/)?$/u;
|
|
658
|
+
const oxlintLineRe = /^(\s*(?:\/\/|\/\*)\s*(?:oxlint-disable(?:-next-line)?)\s+)(.+?)(\s*\*\/)?$/u;
|
|
659
|
+
const biomeLineRe = /^(\s*(?:\/\/|\/\*\*)\s*biome-ignore(?:-all)?\s+)([\w/]+)(.*?)$/u;
|
|
660
|
+
const trailingCommentRe = /\s*--.*$/u;
|
|
661
|
+
const trailingCloseRe = /\s*\*\/$/u;
|
|
662
|
+
const oxlintPrefixRe = /^(?:oxc|eslint|typescript-eslint|typescript_eslint|react|react-hooks|react_hooks|jsx-a11y|jsx_a11y|import|nextjs|next|jsdoc|promise|unicorn|vitest|jest|eslint-plugin-react-perf|eslint-plugin-jsx-a11y|eslint-plugin-react|eslint-plugin-promise|eslint-plugin-unicorn|react-perf|react_perf|@next\/next|@typescript-eslint|@eslint-react)[\\/(/]/u;
|
|
663
|
+
const trailingParenRe = /\)$/u;
|
|
664
|
+
const trailingSepRe = /[\\/(/]$/u;
|
|
665
|
+
const eslintPluginPrefixRe = /^eslint-plugin-/u;
|
|
666
|
+
const normalizeRule = (rule) => {
|
|
667
|
+
const variants = [rule];
|
|
668
|
+
const oxMatch = oxlintPrefixRe.exec(rule);
|
|
669
|
+
if (oxMatch) {
|
|
670
|
+
const bare = rule.slice(oxMatch[0].length).replace(trailingParenRe, "");
|
|
671
|
+
variants.push(bare);
|
|
672
|
+
const prefix = oxMatch[0].replace(trailingSepRe, "");
|
|
673
|
+
variants.push(`${prefix}/${bare}`);
|
|
674
|
+
variants.push(`${prefix}(${bare})`);
|
|
675
|
+
if (prefix === "eslint") variants.push(bare);
|
|
676
|
+
if (prefix === "typescript-eslint") variants.push(`@typescript-eslint/${bare}`);
|
|
677
|
+
if (prefix.startsWith("eslint-plugin-")) {
|
|
678
|
+
const short = prefix.replace(eslintPluginPrefixRe, "");
|
|
679
|
+
variants.push(`${short}/${bare}`);
|
|
680
|
+
variants.push(`${short}(${bare})`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (rule.startsWith("@typescript-eslint/")) variants.push(`typescript-eslint(${rule.slice(19)})`);
|
|
684
|
+
if (rule.startsWith("@next/next/")) variants.push(`nextjs(${rule.slice(11)})`);
|
|
685
|
+
if (rule.startsWith("@eslint-react/")) variants.push(`react(${rule.slice(14)})`);
|
|
686
|
+
const extra = [];
|
|
687
|
+
for (const v of variants) {
|
|
688
|
+
if (v.includes("_")) extra.push(v.replaceAll("_", "-"));
|
|
689
|
+
if (v.includes("-")) extra.push(v.replaceAll("-", "_"));
|
|
690
|
+
}
|
|
691
|
+
for (const e of extra) variants.push(e);
|
|
692
|
+
return variants;
|
|
693
|
+
};
|
|
694
|
+
const buildActiveRuleSet = async () => {
|
|
695
|
+
const rules = await extractAllRules();
|
|
696
|
+
const active = /* @__PURE__ */ new Set();
|
|
697
|
+
for (const r of rules) {
|
|
698
|
+
active.add(r.rule);
|
|
699
|
+
for (const v of normalizeRule(r.rule)) active.add(v);
|
|
700
|
+
}
|
|
701
|
+
return active;
|
|
702
|
+
};
|
|
703
|
+
const isRuleActive = (rule, active) => {
|
|
704
|
+
if (active.has(rule)) return true;
|
|
705
|
+
for (const v of normalizeRule(rule)) if (active.has(v)) return true;
|
|
706
|
+
return false;
|
|
707
|
+
};
|
|
708
|
+
const splitRules = (str) => str.split(",").map((r) => r.trim().replace(trailingCommentRe, "").replace(trailingCloseRe, "")).filter(Boolean);
|
|
709
|
+
const processMultiRuleLine = ({ active, line, match, result }) => {
|
|
710
|
+
const prefix = match[1] ?? "";
|
|
711
|
+
const rulesStr = match[2] ?? "";
|
|
712
|
+
const suffix = match[3] ?? "";
|
|
713
|
+
const rules = splitRules(rulesStr);
|
|
714
|
+
const kept = rules.filter((r) => isRuleActive(r, active));
|
|
715
|
+
if (kept.length === 0) return rules.length;
|
|
716
|
+
if (kept.length < rules.length) {
|
|
717
|
+
result.push(`${prefix}${kept.join(", ")}${suffix}`);
|
|
718
|
+
return rules.length - kept.length;
|
|
719
|
+
}
|
|
720
|
+
result.push(line);
|
|
721
|
+
return 0;
|
|
722
|
+
};
|
|
723
|
+
const cleanFileIgnores = async (filePath, active) => {
|
|
724
|
+
const lines = (await file(filePath).text()).split("\n");
|
|
725
|
+
const result = [];
|
|
726
|
+
let removed = 0;
|
|
727
|
+
for (const line of lines) {
|
|
728
|
+
eslintLineRe.lastIndex = 0;
|
|
729
|
+
oxlintLineRe.lastIndex = 0;
|
|
730
|
+
biomeLineRe.lastIndex = 0;
|
|
731
|
+
const eslintMatch = eslintLineRe.exec(line);
|
|
732
|
+
if (eslintMatch) removed += processMultiRuleLine({
|
|
733
|
+
active,
|
|
734
|
+
line,
|
|
735
|
+
match: eslintMatch,
|
|
736
|
+
result
|
|
737
|
+
});
|
|
738
|
+
else {
|
|
739
|
+
const oxlintMatch = oxlintLineRe.exec(line);
|
|
740
|
+
if (oxlintMatch) removed += processMultiRuleLine({
|
|
741
|
+
active,
|
|
742
|
+
line,
|
|
743
|
+
match: oxlintMatch,
|
|
744
|
+
result
|
|
745
|
+
});
|
|
746
|
+
else {
|
|
747
|
+
const biomeMatch = biomeLineRe.exec(line);
|
|
748
|
+
if (biomeMatch && !isRuleActive(biomeMatch[2] ?? "", active)) removed += 1;
|
|
749
|
+
else result.push(line);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (removed > 0) await write(filePath, result.join("\n"));
|
|
754
|
+
return removed;
|
|
755
|
+
};
|
|
756
|
+
const cleanIgnores = async (filePaths) => {
|
|
757
|
+
const active = await buildActiveRuleSet();
|
|
758
|
+
let cleaned = 0;
|
|
759
|
+
const files = [];
|
|
760
|
+
for (const fp of filePaths) {
|
|
761
|
+
const count = await cleanFileIgnores(fp, active);
|
|
762
|
+
if (count > 0) {
|
|
763
|
+
cleaned += count;
|
|
764
|
+
files.push(fp);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
cleaned,
|
|
769
|
+
files
|
|
770
|
+
};
|
|
771
|
+
};
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/comments.ts
|
|
774
|
+
const KEEP_PATTERN = /eslint-disable|biome-ignore|oxlint-disable|@ts-nocheck|@ts-expect-error|@ts-ignore|@refresh|@flow|istanbul ignore|c8 ignore|webpackChunkName|prettier-ignore|noinspection|nolint|@jsx|@jsxImportSource|@jsxFrag|@license|@preserve|type-coverage:ignore/u;
|
|
775
|
+
const WHITESPACE_ONLY = /^\s*$/u;
|
|
776
|
+
const COMMENT_EXTENSIONS = new Set([
|
|
777
|
+
".cjs",
|
|
778
|
+
".js",
|
|
779
|
+
".jsx",
|
|
780
|
+
".mjs",
|
|
781
|
+
".mts",
|
|
782
|
+
".ts",
|
|
783
|
+
".tsx"
|
|
784
|
+
]);
|
|
785
|
+
const extOf = (path) => {
|
|
786
|
+
const dot = path.lastIndexOf(".");
|
|
787
|
+
return dot > path.lastIndexOf("/") ? path.slice(dot) : "";
|
|
788
|
+
};
|
|
789
|
+
const isCommentCandidate = (path) => COMMENT_EXTENSIONS.has(extOf(path));
|
|
790
|
+
const findDeletableComments = ({ sourceText }) => {
|
|
791
|
+
const sourceFile = ts.createSourceFile("file.ts", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
792
|
+
const seen = /* @__PURE__ */ new Set();
|
|
793
|
+
const deletable = [];
|
|
794
|
+
const visit = (node) => {
|
|
795
|
+
const leading = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
|
|
796
|
+
const trailing = ts.getTrailingCommentRanges(sourceText, node.getEnd());
|
|
797
|
+
const ranges = [...leading ?? [], ...trailing ?? []];
|
|
798
|
+
for (const range of ranges) if (!seen.has(range.pos)) {
|
|
799
|
+
seen.add(range.pos);
|
|
800
|
+
const text = sourceText.slice(range.pos, range.end);
|
|
801
|
+
if (!(text.startsWith("#!") || text.startsWith("/**") || text.startsWith("/// <") || KEEP_PATTERN.test(text))) {
|
|
802
|
+
const line = sourceFile.getLineAndCharacterOfPosition(range.pos).line + 1;
|
|
803
|
+
deletable.push({
|
|
804
|
+
end: range.end,
|
|
805
|
+
line,
|
|
806
|
+
start: range.pos
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
ts.forEachChild(node, visit);
|
|
811
|
+
};
|
|
812
|
+
visit(sourceFile);
|
|
813
|
+
deletable.sort((a, b) => a.start - b.start);
|
|
814
|
+
return deletable;
|
|
815
|
+
};
|
|
816
|
+
const deleteComments = ({ sourceText }) => {
|
|
817
|
+
const comments = findDeletableComments({ sourceText });
|
|
818
|
+
if (comments.length === 0) return sourceText;
|
|
819
|
+
const parts = [];
|
|
820
|
+
let cursor = 0;
|
|
821
|
+
for (const c of comments) {
|
|
822
|
+
const beforeChunk = sourceText.slice(cursor, c.start);
|
|
823
|
+
const lineStart = beforeChunk.lastIndexOf("\n") + 1;
|
|
824
|
+
const indent = beforeChunk.slice(lineStart);
|
|
825
|
+
let afterEnd = c.end;
|
|
826
|
+
if (sourceText[afterEnd] === "\n") afterEnd += 1;
|
|
827
|
+
else if (sourceText[afterEnd] === "\r" && sourceText[afterEnd + 1] === "\n") afterEnd += 2;
|
|
828
|
+
if (WHITESPACE_ONLY.test(indent) && afterEnd <= sourceText.length) {
|
|
829
|
+
parts.push(beforeChunk.slice(0, lineStart));
|
|
830
|
+
cursor = afterEnd;
|
|
831
|
+
} else {
|
|
832
|
+
parts.push(beforeChunk);
|
|
833
|
+
cursor = c.end;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
parts.push(sourceText.slice(cursor));
|
|
837
|
+
return parts.join("");
|
|
838
|
+
};
|
|
839
|
+
const processFile = async (filePath) => {
|
|
840
|
+
if (!isCommentCandidate(filePath)) return {
|
|
841
|
+
diagnostics: [],
|
|
842
|
+
modified: false
|
|
843
|
+
};
|
|
844
|
+
const f = file(filePath);
|
|
845
|
+
if (!await f.exists()) return {
|
|
846
|
+
diagnostics: [],
|
|
847
|
+
modified: false
|
|
848
|
+
};
|
|
849
|
+
const comments = findDeletableComments({ sourceText: await f.text() });
|
|
850
|
+
const diagnostics = [];
|
|
851
|
+
for (const c of comments) diagnostics.push({
|
|
852
|
+
file: filePath,
|
|
853
|
+
line: c.line,
|
|
854
|
+
linter: "comments",
|
|
855
|
+
rule: "deletable"
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
diagnostics,
|
|
859
|
+
modified: false
|
|
860
|
+
};
|
|
861
|
+
};
|
|
862
|
+
const processFileForFix = async (filePath) => {
|
|
863
|
+
if (!isCommentCandidate(filePath)) return false;
|
|
864
|
+
const f = file(filePath);
|
|
865
|
+
if (!await f.exists()) return false;
|
|
866
|
+
const sourceText = await f.text();
|
|
867
|
+
const result = deleteComments({ sourceText });
|
|
868
|
+
if (result !== sourceText) {
|
|
869
|
+
await write(filePath, result);
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
return false;
|
|
873
|
+
};
|
|
874
|
+
const checkComments = async ({ files }) => {
|
|
875
|
+
return (await Promise.all(files.map(async (f) => processFile(f)))).flatMap((r) => r.diagnostics);
|
|
876
|
+
};
|
|
877
|
+
const fixComments = async ({ files }) => {
|
|
878
|
+
return (await Promise.all(files.map(async (f) => processFileForFix(f)))).filter(Boolean).length;
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/compact.ts
|
|
882
|
+
const COMPACT_REGEX = /(?:\r?\n){2,}/gu;
|
|
883
|
+
const compactBasenames = new Set([
|
|
884
|
+
".env.example",
|
|
885
|
+
".gitignore",
|
|
886
|
+
".npmrc",
|
|
887
|
+
".prettierignore",
|
|
888
|
+
"Dockerfile",
|
|
889
|
+
"Makefile"
|
|
890
|
+
]);
|
|
891
|
+
const compactExtensions = new Set([
|
|
892
|
+
".cjs",
|
|
893
|
+
".css",
|
|
894
|
+
".gql",
|
|
895
|
+
".graphql",
|
|
896
|
+
".html",
|
|
897
|
+
".js",
|
|
898
|
+
".json",
|
|
899
|
+
".jsonc",
|
|
900
|
+
".jsx",
|
|
901
|
+
".mjs",
|
|
902
|
+
".mts",
|
|
903
|
+
".scss",
|
|
904
|
+
".sql",
|
|
905
|
+
".ts",
|
|
906
|
+
".tsx",
|
|
907
|
+
".txt",
|
|
908
|
+
".yaml",
|
|
909
|
+
".yml"
|
|
910
|
+
]);
|
|
911
|
+
const basename = ({ path }) => {
|
|
912
|
+
const index = path.lastIndexOf("/");
|
|
913
|
+
if (index === -1) return path;
|
|
914
|
+
return path.slice(index + 1);
|
|
915
|
+
};
|
|
916
|
+
const extension = ({ path }) => {
|
|
917
|
+
const slashIndex = path.lastIndexOf("/");
|
|
918
|
+
const dotIndex = path.lastIndexOf(".");
|
|
919
|
+
return dotIndex > slashIndex ? path.slice(dotIndex) : "";
|
|
920
|
+
};
|
|
921
|
+
const compactContent = ({ content }) => content.replace(COMPACT_REGEX, "\n");
|
|
922
|
+
const isCompactCandidate = ({ relativePath }) => {
|
|
923
|
+
const fileName = basename({ path: relativePath });
|
|
924
|
+
if (compactBasenames.has(fileName)) return true;
|
|
925
|
+
return compactExtensions.has(extension({ path: relativePath }));
|
|
926
|
+
};
|
|
927
|
+
const isBinary = ({ bytes }) => {
|
|
928
|
+
for (const byte of bytes) if (byte === 0) return true;
|
|
929
|
+
return false;
|
|
930
|
+
};
|
|
931
|
+
const listCompactFiles = ({ env, root }) => {
|
|
932
|
+
const result = spawnSync({
|
|
933
|
+
cmd: [
|
|
934
|
+
"git",
|
|
935
|
+
"-C",
|
|
936
|
+
root,
|
|
937
|
+
"ls-files",
|
|
938
|
+
"-z",
|
|
939
|
+
"--cached",
|
|
940
|
+
"--others",
|
|
941
|
+
"--exclude-standard"
|
|
942
|
+
],
|
|
943
|
+
env,
|
|
944
|
+
stderr: "pipe",
|
|
945
|
+
stdout: "pipe"
|
|
946
|
+
});
|
|
947
|
+
if (result.exitCode !== 0) {
|
|
948
|
+
const stderr = decodeText(result.stderr).trim();
|
|
949
|
+
if (stderr.toLowerCase().includes("not a git repository")) return [];
|
|
950
|
+
throw new CliExitError({
|
|
951
|
+
code: result.exitCode,
|
|
952
|
+
message: stderr.length > 0 ? stderr : "Failed to list files for compact step"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
const entries = decodeText(result.stdout).split("\0");
|
|
956
|
+
const files = [];
|
|
957
|
+
for (const entry of entries) if (entry.length > 0 && entry !== "bun.lock") files.push(entry);
|
|
958
|
+
return files;
|
|
959
|
+
};
|
|
960
|
+
const runCompact = async ({ env, human = false, mode, root }) => {
|
|
961
|
+
const files = listCompactFiles({
|
|
962
|
+
env,
|
|
963
|
+
root
|
|
964
|
+
});
|
|
965
|
+
const results = await Promise.all(files.map(async (relativePath) => {
|
|
966
|
+
if (!isCompactCandidate({ relativePath })) return {
|
|
967
|
+
changed: false,
|
|
968
|
+
relativePath,
|
|
969
|
+
scanned: false
|
|
970
|
+
};
|
|
971
|
+
const absolutePath = joinPath(root, relativePath);
|
|
972
|
+
const source = file(absolutePath);
|
|
973
|
+
if (!await source.exists()) return {
|
|
974
|
+
changed: false,
|
|
975
|
+
relativePath,
|
|
976
|
+
scanned: false
|
|
977
|
+
};
|
|
978
|
+
const bytes = new Uint8Array(await source.arrayBuffer());
|
|
979
|
+
if (isBinary({ bytes })) return {
|
|
980
|
+
changed: false,
|
|
981
|
+
relativePath,
|
|
982
|
+
scanned: true
|
|
983
|
+
};
|
|
984
|
+
const content = decodeText(bytes);
|
|
985
|
+
const compacted = compactContent({ content });
|
|
986
|
+
if (content === compacted) return {
|
|
987
|
+
changed: false,
|
|
988
|
+
relativePath,
|
|
989
|
+
scanned: true
|
|
990
|
+
};
|
|
991
|
+
if (mode === "fix") await write(absolutePath, compacted);
|
|
992
|
+
return {
|
|
993
|
+
changed: true,
|
|
994
|
+
relativePath,
|
|
995
|
+
scanned: true
|
|
996
|
+
};
|
|
997
|
+
}));
|
|
998
|
+
const changed = [];
|
|
999
|
+
let scanned = 0;
|
|
1000
|
+
for (const result of results) {
|
|
1001
|
+
if (result.scanned) scanned += 1;
|
|
1002
|
+
if (result.changed) changed.push(result.relativePath);
|
|
1003
|
+
}
|
|
1004
|
+
if (mode === "fix") {
|
|
1005
|
+
if (human) {
|
|
1006
|
+
process.stdout.write(`[compact] Scanned ${scanned} files\n`);
|
|
1007
|
+
process.stdout.write(`[compact] Updated ${changed.length} files\n`);
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (changed.length === 0) return;
|
|
1012
|
+
const shown = changed.slice(0, 10);
|
|
1013
|
+
const suffix = changed.length > shown.length ? `\n...and ${changed.length - shown.length} more` : "";
|
|
1014
|
+
throw new CliExitError({
|
|
1015
|
+
code: 1,
|
|
1016
|
+
message: `[compact]\nFiles requiring compaction:\n${shown.join("\n")}${suffix}\nRun: lintmax fix`
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
//#endregion
|
|
1020
|
+
//#region src/format.ts
|
|
1021
|
+
const formatGrouped = ({ files }) => {
|
|
1022
|
+
if (files.length === 0) return "";
|
|
1023
|
+
const parts = [];
|
|
1024
|
+
for (const f of files) {
|
|
1025
|
+
parts.push(f.file);
|
|
1026
|
+
for (const l of f.linters) {
|
|
1027
|
+
parts.push(` ${l.linter}`);
|
|
1028
|
+
for (const r of l.rules) {
|
|
1029
|
+
const lineStr = r.lines.length > 0 ? r.lines.join(",") : "";
|
|
1030
|
+
parts.push(` ${lineStr}${lineStr.length > 0 ? " " : ""}${r.rule}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return parts.join("\n");
|
|
1035
|
+
};
|
|
1036
|
+
//#endregion
|
|
1037
|
+
//#region src/pipeline.ts
|
|
1038
|
+
const createStepExecutor = ({ env, failures, root }) => {
|
|
1039
|
+
const runContinue = (opts) => {
|
|
1040
|
+
try {
|
|
1041
|
+
run(opts);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (error instanceof CliExitError) {
|
|
1044
|
+
failures.push({
|
|
1045
|
+
code: error.code,
|
|
1046
|
+
label: opts.label,
|
|
1047
|
+
message: error.message.length > 0 ? error.message : void 0
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
const runCompactContinue = async ({ human = false, mode }) => {
|
|
1055
|
+
try {
|
|
1056
|
+
await runCompact({
|
|
1057
|
+
env,
|
|
1058
|
+
human,
|
|
1059
|
+
mode,
|
|
1060
|
+
root
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (error instanceof CliExitError) {
|
|
1064
|
+
failures.push({
|
|
1065
|
+
code: error.code,
|
|
1066
|
+
label: "compact",
|
|
1067
|
+
message: error.message.length > 0 ? error.message : void 0
|
|
1068
|
+
});
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const runSteps = ({ steps }) => {
|
|
1075
|
+
for (const step of steps) runContinue({
|
|
1076
|
+
args: step.args,
|
|
1077
|
+
command: step.command ?? "bun",
|
|
1078
|
+
env,
|
|
1079
|
+
label: step.label,
|
|
1080
|
+
silent: step.silent
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
1083
|
+
const runStepsSilent = ({ steps }) => {
|
|
1084
|
+
for (const step of steps) {
|
|
1085
|
+
const result = runCapture({
|
|
1086
|
+
args: step.args,
|
|
1087
|
+
command: step.command ?? "bun",
|
|
1088
|
+
env,
|
|
1089
|
+
label: step.label
|
|
1090
|
+
});
|
|
1091
|
+
if (result.exitCode !== 0) failures.push({
|
|
1092
|
+
code: result.exitCode,
|
|
1093
|
+
label: step.label,
|
|
1094
|
+
message: result.stderr.length > 0 ? result.stderr.trim() : void 0
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
const clearFailures = () => {
|
|
1099
|
+
failures.length = 0;
|
|
1100
|
+
};
|
|
1101
|
+
const throwIfFailures = () => {
|
|
1102
|
+
if (failures.length === 0) return;
|
|
1103
|
+
const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
|
|
1104
|
+
throw new CliExitError({
|
|
1105
|
+
code: failures[0]?.code ?? 1,
|
|
1106
|
+
message: `One or more steps failed:\n${details}`
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
return {
|
|
1110
|
+
clearFailures,
|
|
1111
|
+
runCompactContinue,
|
|
1112
|
+
runSteps,
|
|
1113
|
+
runStepsSilent,
|
|
1114
|
+
throwIfFailures
|
|
1115
|
+
};
|
|
1116
|
+
};
|
|
1117
|
+
const createCheckSteps = ({ biomeBin, dir, eslintArgs, eslintBin, oxlintBin, prettierBin, sortPkgJson }) => [
|
|
1118
|
+
{
|
|
1119
|
+
args: [
|
|
1120
|
+
sortPkgJson,
|
|
1121
|
+
"--check",
|
|
1122
|
+
"**/package.json",
|
|
1123
|
+
"--ignore",
|
|
1124
|
+
"**/node_modules/**"
|
|
1125
|
+
],
|
|
1126
|
+
label: "sort-package-json"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
args: [
|
|
1130
|
+
biomeBin,
|
|
1131
|
+
"ci",
|
|
1132
|
+
"--config-path",
|
|
1133
|
+
dir,
|
|
1134
|
+
"--diagnostic-level=error"
|
|
1135
|
+
],
|
|
1136
|
+
label: "biome"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
args: [
|
|
1140
|
+
oxlintBin,
|
|
1141
|
+
"-c",
|
|
1142
|
+
joinPath(dir, ".oxlintrc.json"),
|
|
1143
|
+
"--quiet"
|
|
1144
|
+
],
|
|
1145
|
+
label: "oxlint"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
args: [
|
|
1149
|
+
eslintBin,
|
|
1150
|
+
"--no-error-on-unmatched-pattern",
|
|
1151
|
+
...eslintArgs
|
|
1152
|
+
],
|
|
1153
|
+
label: "eslint"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
args: [
|
|
1157
|
+
prettierBin,
|
|
1158
|
+
...PRETTIER_MD_ARGS,
|
|
1159
|
+
"--check",
|
|
1160
|
+
"--no-error-on-unmatched-pattern",
|
|
1161
|
+
"**/*.md"
|
|
1162
|
+
],
|
|
1163
|
+
label: "prettier"
|
|
1164
|
+
}
|
|
1165
|
+
];
|
|
1166
|
+
const createFixSteps = ({ biomeBin, dir, eslintArgs, eslintBin, hasFlowmark, oxlintBin, prettierBin, sortPkgJson }) => {
|
|
1167
|
+
const steps = [
|
|
1168
|
+
{
|
|
1169
|
+
args: [
|
|
1170
|
+
sortPkgJson,
|
|
1171
|
+
"**/package.json",
|
|
1172
|
+
"--ignore",
|
|
1173
|
+
"**/node_modules/**"
|
|
1174
|
+
],
|
|
1175
|
+
label: "sort-package-json",
|
|
1176
|
+
silent: true
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
args: [
|
|
1180
|
+
biomeBin,
|
|
1181
|
+
"check",
|
|
1182
|
+
"--config-path",
|
|
1183
|
+
dir,
|
|
1184
|
+
"--fix",
|
|
1185
|
+
"--diagnostic-level=error"
|
|
1186
|
+
],
|
|
1187
|
+
label: "biome",
|
|
1188
|
+
silent: true
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
args: [
|
|
1192
|
+
oxlintBin,
|
|
1193
|
+
"-c",
|
|
1194
|
+
joinPath(dir, ".oxlintrc.json"),
|
|
1195
|
+
"--fix",
|
|
1196
|
+
"--fix-suggestions",
|
|
1197
|
+
"--quiet"
|
|
1198
|
+
],
|
|
1199
|
+
label: "oxlint",
|
|
1200
|
+
silent: true
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
args: [
|
|
1204
|
+
eslintBin,
|
|
1205
|
+
...eslintArgs,
|
|
1206
|
+
"--fix"
|
|
1207
|
+
],
|
|
1208
|
+
label: "eslint",
|
|
1209
|
+
silent: true
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
args: [
|
|
1213
|
+
biomeBin,
|
|
1214
|
+
"check",
|
|
1215
|
+
"--config-path",
|
|
1216
|
+
dir,
|
|
1217
|
+
"--fix",
|
|
1218
|
+
"--diagnostic-level=error"
|
|
1219
|
+
],
|
|
1220
|
+
label: "biome",
|
|
1221
|
+
silent: true
|
|
1222
|
+
}
|
|
1223
|
+
];
|
|
1224
|
+
if (hasFlowmark) steps.push({
|
|
1225
|
+
args: [
|
|
1226
|
+
"-w",
|
|
1227
|
+
"0",
|
|
1228
|
+
"--auto",
|
|
1229
|
+
"."
|
|
1230
|
+
],
|
|
1231
|
+
command: "flowmark",
|
|
1232
|
+
label: "flowmark",
|
|
1233
|
+
silent: true
|
|
1234
|
+
});
|
|
1235
|
+
steps.push({
|
|
1236
|
+
args: [
|
|
1237
|
+
prettierBin,
|
|
1238
|
+
...PRETTIER_MD_ARGS,
|
|
1239
|
+
"--write",
|
|
1240
|
+
"--no-error-on-unmatched-pattern",
|
|
1241
|
+
"**/*.md"
|
|
1242
|
+
],
|
|
1243
|
+
label: "prettier",
|
|
1244
|
+
silent: true
|
|
1245
|
+
});
|
|
1246
|
+
return steps;
|
|
1247
|
+
};
|
|
1248
|
+
const captureAndParse = ({ env, failures, label, opts, parser }) => {
|
|
1249
|
+
const result = runCapture({
|
|
1250
|
+
args: opts.args,
|
|
1251
|
+
command: opts.command,
|
|
1252
|
+
env,
|
|
1253
|
+
label
|
|
1254
|
+
});
|
|
1255
|
+
if (result.exitCode === 0) return [];
|
|
1256
|
+
const diagnostics = parser(result);
|
|
1257
|
+
if (diagnostics.length > 0) return diagnostics;
|
|
1258
|
+
failures.push({
|
|
1259
|
+
code: result.exitCode,
|
|
1260
|
+
label,
|
|
1261
|
+
message: result.stderr.length > 0 ? result.stderr.trim() : void 0
|
|
1262
|
+
});
|
|
1263
|
+
return [];
|
|
1264
|
+
};
|
|
1265
|
+
const runAgentCheck = ({ biomeBin, dir, env, eslintArgs, eslintBin, failures, oxlintBin, prettierBin, sortPkgJson }) => {
|
|
1266
|
+
const allDiagnostics = [];
|
|
1267
|
+
const push = (d) => {
|
|
1268
|
+
if (d.length > 0) allDiagnostics.push(...d);
|
|
1269
|
+
};
|
|
1270
|
+
push(captureAndParse({
|
|
1271
|
+
env,
|
|
1272
|
+
failures,
|
|
1273
|
+
label: "sort-package-json",
|
|
1274
|
+
opts: {
|
|
1275
|
+
args: [
|
|
1276
|
+
sortPkgJson,
|
|
1277
|
+
"--check",
|
|
1278
|
+
"**/package.json",
|
|
1279
|
+
"--ignore",
|
|
1280
|
+
"**/node_modules/**"
|
|
1281
|
+
],
|
|
1282
|
+
command: "bun"
|
|
1283
|
+
},
|
|
1284
|
+
parser: parseSortPackageJsonOutput
|
|
1285
|
+
}));
|
|
1286
|
+
push(captureAndParse({
|
|
1287
|
+
env,
|
|
1288
|
+
failures,
|
|
1289
|
+
label: "biome",
|
|
1290
|
+
opts: {
|
|
1291
|
+
args: [
|
|
1292
|
+
biomeBin,
|
|
1293
|
+
"check",
|
|
1294
|
+
"--config-path",
|
|
1295
|
+
dir,
|
|
1296
|
+
"--reporter=json"
|
|
1297
|
+
],
|
|
1298
|
+
command: "bun"
|
|
1299
|
+
},
|
|
1300
|
+
parser: ({ stdout }) => parseBiomeDiagnostics({ stdout })
|
|
1301
|
+
}));
|
|
1302
|
+
push(captureAndParse({
|
|
1303
|
+
env,
|
|
1304
|
+
failures,
|
|
1305
|
+
label: "oxlint",
|
|
1306
|
+
opts: {
|
|
1307
|
+
args: [
|
|
1308
|
+
oxlintBin,
|
|
1309
|
+
"-c",
|
|
1310
|
+
joinPath(dir, ".oxlintrc.json"),
|
|
1311
|
+
"--quiet",
|
|
1312
|
+
"-f",
|
|
1313
|
+
"json"
|
|
1314
|
+
],
|
|
1315
|
+
command: "bun"
|
|
1316
|
+
},
|
|
1317
|
+
parser: ({ stdout }) => parseOxlintDiagnostics({ stdout })
|
|
1318
|
+
}));
|
|
1319
|
+
push(captureAndParse({
|
|
1320
|
+
env,
|
|
1321
|
+
failures,
|
|
1322
|
+
label: "eslint",
|
|
1323
|
+
opts: {
|
|
1324
|
+
args: [
|
|
1325
|
+
eslintBin,
|
|
1326
|
+
"--no-error-on-unmatched-pattern",
|
|
1327
|
+
...eslintArgs,
|
|
1328
|
+
"-f",
|
|
1329
|
+
"json"
|
|
1330
|
+
],
|
|
1331
|
+
command: "bun"
|
|
1332
|
+
},
|
|
1333
|
+
parser: ({ stdout }) => parseEslintDiagnostics({ stdout })
|
|
1334
|
+
}));
|
|
1335
|
+
push(captureAndParse({
|
|
1336
|
+
env,
|
|
1337
|
+
failures,
|
|
1338
|
+
label: "prettier",
|
|
1339
|
+
opts: {
|
|
1340
|
+
args: [
|
|
1341
|
+
prettierBin,
|
|
1342
|
+
...PRETTIER_MD_ARGS,
|
|
1343
|
+
"--list-different",
|
|
1344
|
+
"--no-error-on-unmatched-pattern",
|
|
1345
|
+
"**/*.md"
|
|
1346
|
+
],
|
|
1347
|
+
command: "bun"
|
|
1348
|
+
},
|
|
1349
|
+
parser: ({ stdout }) => parsePrettierOutput({ stdout })
|
|
1350
|
+
}));
|
|
1351
|
+
return allDiagnostics;
|
|
1352
|
+
};
|
|
1353
|
+
const throwAgentResults = ({ diagnostics, failures }) => {
|
|
1354
|
+
if (diagnostics.length === 0 && failures.length === 0) return;
|
|
1355
|
+
const output = formatGrouped({ files: aggregate({ diagnostics }) });
|
|
1356
|
+
if (output.length > 0) process.stdout.write(`${output}\n`);
|
|
1357
|
+
if (failures.length > 0) {
|
|
1358
|
+
const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
|
|
1359
|
+
process.stderr.write(`${details}\n`);
|
|
1360
|
+
}
|
|
1361
|
+
throw new CliExitError({ code: 1 });
|
|
1362
|
+
};
|
|
1363
|
+
const runLint = async ({ command, human = false }) => {
|
|
1364
|
+
const dir = joinPath(cwd, cacheDir);
|
|
1365
|
+
ensureDirectory({ directory: dir });
|
|
1366
|
+
const configPath = joinPath(cwd, "lintmax.config.ts");
|
|
1367
|
+
const hasConfig = await pathExists({ path: configPath });
|
|
1368
|
+
const bundledBinA = joinPath(lintmaxRoot, "node_modules", ".bin");
|
|
1369
|
+
const bundledBinB = joinPath(dirnamePath(lintmaxRoot), ".bin");
|
|
1370
|
+
const cwdBinDir = joinPath(cwd, "node_modules", ".bin");
|
|
1371
|
+
const runtimePath = joinPath(dir, "lintmax.json");
|
|
1372
|
+
const env$1 = {
|
|
1373
|
+
...env,
|
|
1374
|
+
PATH: `${bundledBinA}:${bundledBinB}:${cwdBinDir}:${env.PATH ?? ""}`
|
|
1375
|
+
};
|
|
1376
|
+
if (hasConfig) run({
|
|
1377
|
+
args: ["-e", `const m = await import('${configPath}'); const { sync: s } = await import('lintmax'); await s(m.default);`],
|
|
1378
|
+
command: "bun",
|
|
1379
|
+
env: env$1,
|
|
1380
|
+
label: "config",
|
|
1381
|
+
silent: true
|
|
1382
|
+
});
|
|
1383
|
+
else await sync();
|
|
1384
|
+
const runtime = await readJson({ path: runtimePath });
|
|
1385
|
+
const failures = [];
|
|
1386
|
+
const { clearFailures, runCompactContinue, runSteps, runStepsSilent, throwIfFailures } = createStepExecutor({
|
|
1387
|
+
env: env$1,
|
|
1388
|
+
failures,
|
|
1389
|
+
root: cwd
|
|
1390
|
+
});
|
|
1391
|
+
if (command === "fix" && runtime.compact === true) await runCompactContinue({
|
|
1392
|
+
human,
|
|
1393
|
+
mode: "fix"
|
|
1394
|
+
});
|
|
1395
|
+
const eslintArgs = ["--config", joinPath(dir, "eslint.generated.mjs")];
|
|
1396
|
+
const [sortPkgJson, biomeBin, oxlintBin, eslintBin, prettierBin] = await Promise.all([
|
|
1397
|
+
resolveBin({
|
|
1398
|
+
bin: "sort-package-json",
|
|
1399
|
+
pkg: "sort-package-json"
|
|
1400
|
+
}),
|
|
1401
|
+
resolveBin({
|
|
1402
|
+
bin: "biome",
|
|
1403
|
+
pkg: "@biomejs/biome"
|
|
1404
|
+
}),
|
|
1405
|
+
resolveBin({
|
|
1406
|
+
bin: "oxlint",
|
|
1407
|
+
pkg: "oxlint"
|
|
1408
|
+
}),
|
|
1409
|
+
resolveBin({
|
|
1410
|
+
bin: "eslint",
|
|
1411
|
+
pkg: "eslint"
|
|
1412
|
+
}),
|
|
1413
|
+
resolveBin({
|
|
1414
|
+
bin: "prettier",
|
|
1415
|
+
pkg: "prettier"
|
|
1416
|
+
})
|
|
1417
|
+
]);
|
|
1418
|
+
const hasFlowmark = spawnSync({
|
|
1419
|
+
cmd: ["which", "flowmark"],
|
|
1420
|
+
env: env$1,
|
|
1421
|
+
stderr: "pipe",
|
|
1422
|
+
stdout: "pipe"
|
|
1423
|
+
}).exitCode === 0;
|
|
1424
|
+
const checkSteps = createCheckSteps({
|
|
1425
|
+
biomeBin,
|
|
1426
|
+
dir,
|
|
1427
|
+
eslintArgs,
|
|
1428
|
+
eslintBin,
|
|
1429
|
+
oxlintBin,
|
|
1430
|
+
prettierBin,
|
|
1431
|
+
sortPkgJson
|
|
1432
|
+
});
|
|
1433
|
+
const shouldComments = runtime.comments !== false;
|
|
1434
|
+
const ignoreGlobs = DEFAULT_SHARED_IGNORE_PATTERNS.map((p) => new Glob(p));
|
|
1435
|
+
const isIgnored = (filePath) => ignoreGlobs.some((g) => g.match(filePath));
|
|
1436
|
+
const gitFiles = shouldComments ? listCompactFiles({
|
|
1437
|
+
env: env$1,
|
|
1438
|
+
root: cwd
|
|
1439
|
+
}).filter((f) => !isIgnored(f)) : [];
|
|
1440
|
+
if (command === "fix") {
|
|
1441
|
+
if (shouldComments) await fixComments({ files: gitFiles });
|
|
1442
|
+
if (gitFiles.length > 0) await cleanIgnores(gitFiles.map((f) => joinPath(cwd, f)));
|
|
1443
|
+
const fixSteps = createFixSteps({
|
|
1444
|
+
biomeBin,
|
|
1445
|
+
dir,
|
|
1446
|
+
eslintArgs,
|
|
1447
|
+
eslintBin,
|
|
1448
|
+
hasFlowmark,
|
|
1449
|
+
oxlintBin,
|
|
1450
|
+
prettierBin,
|
|
1451
|
+
sortPkgJson
|
|
1452
|
+
});
|
|
1453
|
+
if (human) runSteps({ steps: fixSteps });
|
|
1454
|
+
else runStepsSilent({ steps: fixSteps });
|
|
1455
|
+
clearFailures();
|
|
1456
|
+
if (human) {
|
|
1457
|
+
runSteps({ steps: checkSteps });
|
|
1458
|
+
throwIfFailures();
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const allDiagnostics = runAgentCheck({
|
|
1462
|
+
biomeBin,
|
|
1463
|
+
dir,
|
|
1464
|
+
env: env$1,
|
|
1465
|
+
eslintArgs,
|
|
1466
|
+
eslintBin,
|
|
1467
|
+
failures,
|
|
1468
|
+
oxlintBin,
|
|
1469
|
+
prettierBin,
|
|
1470
|
+
sortPkgJson
|
|
1471
|
+
});
|
|
1472
|
+
if (shouldComments) {
|
|
1473
|
+
const commentDiags = await checkComments({ files: gitFiles });
|
|
1474
|
+
allDiagnostics.push(...commentDiags);
|
|
1475
|
+
}
|
|
1476
|
+
const cnDiags = await checkClassName({ root: cwd });
|
|
1477
|
+
allDiagnostics.push(...cnDiags);
|
|
1478
|
+
throwAgentResults({
|
|
1479
|
+
diagnostics: allDiagnostics,
|
|
1480
|
+
failures
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (human) {
|
|
1485
|
+
runSteps({ steps: checkSteps });
|
|
1486
|
+
const cnDiagsHuman = await checkClassName({ root: cwd });
|
|
1487
|
+
if (cnDiagsHuman.length > 0) {
|
|
1488
|
+
const output = formatGrouped({ files: aggregate({ diagnostics: cnDiagsHuman }) });
|
|
1489
|
+
if (output.length > 0) process.stdout.write(`${output}\n`);
|
|
1490
|
+
failures.push({
|
|
1491
|
+
code: 1,
|
|
1492
|
+
label: "cn"
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
throwIfFailures();
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const allDiagnostics = runAgentCheck({
|
|
1499
|
+
biomeBin,
|
|
1500
|
+
dir,
|
|
1501
|
+
env: env$1,
|
|
1502
|
+
eslintArgs,
|
|
1503
|
+
eslintBin,
|
|
1504
|
+
failures,
|
|
1505
|
+
oxlintBin,
|
|
1506
|
+
prettierBin,
|
|
1507
|
+
sortPkgJson
|
|
1508
|
+
});
|
|
1509
|
+
if (shouldComments) {
|
|
1510
|
+
const commentDiags = await checkComments({ files: gitFiles });
|
|
1511
|
+
allDiagnostics.push(...commentDiags);
|
|
1512
|
+
}
|
|
1513
|
+
const cnDiags = await checkClassName({ root: cwd });
|
|
1514
|
+
allDiagnostics.push(...cnDiags);
|
|
1515
|
+
if (allDiagnostics.length > 0 || failures.length > 0) {
|
|
1516
|
+
const output = formatGrouped({ files: aggregate({ diagnostics: allDiagnostics }) });
|
|
1517
|
+
if (output.length > 0) process.stdout.write(`${output}\n`);
|
|
1518
|
+
if (failures.length > 0) {
|
|
1519
|
+
const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
|
|
1520
|
+
process.stderr.write(`${details}\n`);
|
|
1521
|
+
}
|
|
1522
|
+
throw new CliExitError({ code: 1 });
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/cli.ts
|
|
1527
|
+
const command = process.argv[2];
|
|
1528
|
+
const main = async () => {
|
|
1529
|
+
const version = await readVersion();
|
|
1530
|
+
if (command === "init") {
|
|
1531
|
+
await runInit();
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (command === "--version" || command === "-v") {
|
|
1535
|
+
process.stdout.write(`${version}\n`);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (command === "rules") {
|
|
1539
|
+
const human = process.argv.includes("--human");
|
|
1540
|
+
const rules = await extractAllRules();
|
|
1541
|
+
const output = human ? formatRulesHuman(rules) : formatRulesCompact(rules);
|
|
1542
|
+
process.stdout.write(`${output}\n`);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (command === "ignores") {
|
|
1546
|
+
const { runIgnores } = await import("./ignores-BzTRqd-5.mjs");
|
|
1547
|
+
await runIgnores(process.argv.includes("--verbose"));
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (command !== "fix" && command !== "check") {
|
|
1551
|
+
usage({ version });
|
|
1552
|
+
if (command === "--help" || command === "-h") return;
|
|
1553
|
+
throw new CliExitError({ code: 1 });
|
|
1554
|
+
}
|
|
1555
|
+
await runLint({
|
|
1556
|
+
command,
|
|
1557
|
+
human: process.argv.includes("--human")
|
|
1558
|
+
});
|
|
1559
|
+
};
|
|
1560
|
+
try {
|
|
1561
|
+
await main();
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
if (error instanceof CliExitError) {
|
|
1564
|
+
if (error.message.length > 0) process.stderr.write(`${error.message}\n`);
|
|
1565
|
+
process.exitCode = error.code;
|
|
1566
|
+
} else throw error;
|
|
1567
|
+
}
|
|
1568
|
+
//#endregion
|
|
1569
|
+
export {};
|