@pob/codemods 0.18.2 → 0.19.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.
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runRemoveUnusedOxlintDirectives } from "../lib/removeUnusedOxlintDirectives.js";
|
|
4
|
+
|
|
5
|
+
const files = runRemoveUnusedOxlintDirectives({ args: process.argv.slice(2) });
|
|
6
|
+
for (const file of files) console.log(file);
|
|
7
|
+
console.log(`Done! ${files.length} file(s) updated.`);
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import runJscodeshift from "./runJscodeshift.cjs";
|
|
3
3
|
|
|
4
|
+
export {
|
|
5
|
+
removeUnusedOxlintDirectives,
|
|
6
|
+
runRemoveUnusedOxlintDirectives,
|
|
7
|
+
} from "./removeUnusedOxlintDirectives.js";
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Runs a transform of lib/transforms on `paths`, like `pob-migrate`.
|
|
6
11
|
*
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ offset: number; length: number }} Span
|
|
8
|
+
* A span reported by oxlint, in utf-8 bytes: the whole directive comment when
|
|
9
|
+
* none of its rules is used, else one unused rule name of its list.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const UNUSED_DIRECTIVE_MESSAGE = "Unused oxlint-disable directive";
|
|
13
|
+
|
|
14
|
+
const DIRECTIVE_KEYWORD_REGEXP = /^\s*oxlint-disable(?:-next-line|-line)?\s/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Splits `// oxlint-disable-next-line a, b -- reason` (or its block form) into
|
|
18
|
+
* the rule names and the text around them.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} comment
|
|
21
|
+
*/
|
|
22
|
+
const parseDirective = (comment) => {
|
|
23
|
+
const isBlock = comment.startsWith("/*");
|
|
24
|
+
const body = isBlock ? comment.slice(2, -2) : comment.slice(2);
|
|
25
|
+
const keyword = DIRECTIVE_KEYWORD_REGEXP.exec(body);
|
|
26
|
+
if (!keyword) throw new Error(`Unexpected directive: ${comment}`);
|
|
27
|
+
const afterKeyword = body.slice(keyword[0].length);
|
|
28
|
+
const descriptionIndex = afterKeyword.indexOf(" --");
|
|
29
|
+
const rulesAndSpaces =
|
|
30
|
+
descriptionIndex === -1
|
|
31
|
+
? afterKeyword
|
|
32
|
+
: afterKeyword.slice(0, descriptionIndex);
|
|
33
|
+
const rulesText = rulesAndSpaces.trimEnd();
|
|
34
|
+
return {
|
|
35
|
+
prefix: comment.slice(0, 2) + keyword[0],
|
|
36
|
+
rules: rulesText.split(",").map((rule) => rule.trim()),
|
|
37
|
+
suffix:
|
|
38
|
+
rulesAndSpaces.slice(rulesText.length) +
|
|
39
|
+
(descriptionIndex === -1 ? "" : afterKeyword.slice(descriptionIndex)) +
|
|
40
|
+
(isBlock ? "*/" : ""),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} source
|
|
46
|
+
* @param {number} start
|
|
47
|
+
* @param {number} end
|
|
48
|
+
*/
|
|
49
|
+
const removeComment = (source, start, end) => {
|
|
50
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
51
|
+
const newLineIndex = source.indexOf("\n", end);
|
|
52
|
+
const lineEnd = newLineIndex === -1 ? source.length : newLineIndex;
|
|
53
|
+
const isAloneOnItsLine =
|
|
54
|
+
source.slice(lineStart, start).trim() === "" &&
|
|
55
|
+
source.slice(end, lineEnd).trim() === "";
|
|
56
|
+
if (isAloneOnItsLine) {
|
|
57
|
+
return source.slice(0, lineStart) + source.slice(lineEnd + 1);
|
|
58
|
+
}
|
|
59
|
+
return source.slice(0, start).replace(/[\t ]+$/, "") + source.slice(end);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} source
|
|
64
|
+
* @param {number} offset
|
|
65
|
+
*/
|
|
66
|
+
const findEnclosingComment = (source, offset) => {
|
|
67
|
+
const start = Math.max(
|
|
68
|
+
source.lastIndexOf("//", offset),
|
|
69
|
+
source.lastIndexOf("/*", offset),
|
|
70
|
+
);
|
|
71
|
+
if (source.startsWith("/*", start)) {
|
|
72
|
+
return { start, end: source.indexOf("*/", offset) + 2 };
|
|
73
|
+
}
|
|
74
|
+
const newLineIndex = source.indexOf("\n", offset);
|
|
75
|
+
return { start, end: newLineIndex === -1 ? source.length : newLineIndex };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Removes the directives, or the rule names inside a directive, that oxlint
|
|
80
|
+
* reported as unused.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} source
|
|
83
|
+
* @param {Span[]} spans
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export const removeUnusedOxlintDirectives = (source, spans) => {
|
|
87
|
+
const buffer = Buffer.from(source, "utf8");
|
|
88
|
+
/** @param {number} byteOffset */
|
|
89
|
+
const toCharOffset = (byteOffset) =>
|
|
90
|
+
buffer.subarray(0, byteOffset).toString("utf8").length;
|
|
91
|
+
|
|
92
|
+
/** @type {Map<number, { start: number; end: number; unusedRules?: string[] }>} */
|
|
93
|
+
const editsByStart = new Map();
|
|
94
|
+
for (const span of spans) {
|
|
95
|
+
const start = toCharOffset(span.offset);
|
|
96
|
+
const end = toCharOffset(span.offset + span.length);
|
|
97
|
+
const text = source.slice(start, end);
|
|
98
|
+
if (text.startsWith("//") || text.startsWith("/*")) {
|
|
99
|
+
editsByStart.set(start, { start, end });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const comment = findEnclosingComment(source, start);
|
|
103
|
+
const edit = editsByStart.get(comment.start) ?? {
|
|
104
|
+
...comment,
|
|
105
|
+
unusedRules: [],
|
|
106
|
+
};
|
|
107
|
+
edit.unusedRules?.push(text);
|
|
108
|
+
editsByStart.set(comment.start, edit);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let result = source;
|
|
112
|
+
// from the end, so that the offsets of the previous edits stay valid
|
|
113
|
+
const edits = [...editsByStart.values()].toSorted(
|
|
114
|
+
(a, b) => b.start - a.start,
|
|
115
|
+
);
|
|
116
|
+
for (const { start, end, unusedRules } of edits) {
|
|
117
|
+
if (!unusedRules) {
|
|
118
|
+
result = removeComment(result, start, end);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const comment = result.slice(start, end);
|
|
122
|
+
const { prefix, rules, suffix } = parseDirective(comment);
|
|
123
|
+
const remainingRules = rules.filter((rule) => !unusedRules.includes(rule));
|
|
124
|
+
if (rules.length - remainingRules.length !== unusedRules.length) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Unused rules ${unusedRules.join(", ")} not found in: ${comment}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
result =
|
|
130
|
+
remainingRules.length === 0
|
|
131
|
+
? removeComment(result, start, end)
|
|
132
|
+
: result.slice(0, start) +
|
|
133
|
+
prefix +
|
|
134
|
+
remainingRules.join(", ") +
|
|
135
|
+
suffix +
|
|
136
|
+
result.slice(end);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Runs the oxlint of the project in `cwd` and removes the directives it
|
|
143
|
+
* reports as unused. Generated files belong in the `ignorePatterns` of
|
|
144
|
+
* `.oxlintrc.json`, or their directives are removed too.
|
|
145
|
+
*
|
|
146
|
+
* @param {{ cwd?: string; args?: string[] }} [options] `args` are passed to
|
|
147
|
+
* oxlint, for example the paths to lint.
|
|
148
|
+
* @returns {string[]} the modified files
|
|
149
|
+
*/
|
|
150
|
+
export const runRemoveUnusedOxlintDirectives = ({
|
|
151
|
+
cwd = process.cwd(),
|
|
152
|
+
args = [],
|
|
153
|
+
} = {}) => {
|
|
154
|
+
const require = createRequire(path.join(cwd, "package.json"));
|
|
155
|
+
const oxlintBin = path.join(
|
|
156
|
+
path.dirname(require.resolve("oxlint/package.json")),
|
|
157
|
+
"bin/oxlint",
|
|
158
|
+
);
|
|
159
|
+
const { stdout, stderr, error } = spawnSync(
|
|
160
|
+
process.execPath,
|
|
161
|
+
[oxlintBin, "--quiet", "--format", "json", ...args],
|
|
162
|
+
{ cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 },
|
|
163
|
+
);
|
|
164
|
+
if (error) throw error;
|
|
165
|
+
|
|
166
|
+
/** @type {{ diagnostics: { message: string; filename: string; labels: { span: Span }[] }[] }} */
|
|
167
|
+
let report;
|
|
168
|
+
try {
|
|
169
|
+
report = JSON.parse(stdout);
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error(`oxlint did not output json:\n${stdout}\n${stderr}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @type {Map<string, Span[]>} */
|
|
175
|
+
const spansByFile = new Map();
|
|
176
|
+
for (const { message, filename, labels } of report.diagnostics) {
|
|
177
|
+
if (!message.startsWith(UNUSED_DIRECTIVE_MESSAGE)) continue;
|
|
178
|
+
const spans = spansByFile.get(filename) ?? [];
|
|
179
|
+
spans.push(labels[0].span);
|
|
180
|
+
spansByFile.set(filename, spans);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const [filename, spans] of spansByFile) {
|
|
184
|
+
const filePath = path.resolve(cwd, filename);
|
|
185
|
+
writeFileSync(
|
|
186
|
+
filePath,
|
|
187
|
+
removeUnusedOxlintDirectives(readFileSync(filePath, "utf8"), spans),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return [...spansByFile.keys()];
|
|
191
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pob/codemods",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "codemods for pob",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/christophehurpeau/pob",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"directory": "@pob/codemods"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"pob-migrate": "./bin/migrate.mjs"
|
|
18
|
+
"pob-migrate": "./bin/migrate.mjs",
|
|
19
|
+
"pob-remove-unused-oxlint-directives": "./bin/remove-unused-oxlint-directives.mjs"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
21
22
|
"bin",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"access": "public"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"@babel/parser": "7.29.
|
|
36
|
+
"@babel/parser": "7.29.13",
|
|
36
37
|
"jscodeshift": "17.4.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"@types/node": "24.19.1"
|
|
41
42
|
},
|
|
42
43
|
"engines": {
|
|
43
|
-
"node": "
|
|
44
|
+
"node": "^24.22.1 || >=26.13.0"
|
|
44
45
|
},
|
|
45
46
|
"pob": {},
|
|
46
47
|
"scripts": {
|