@zgeoff/format-codemod 0.0.1 → 0.0.3
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/README.md +13 -4
- package/dist/cli/expand-inputs.d.ts +4 -2
- package/dist/cli/parse-cli-args.d.ts +1 -0
- package/dist/cli.js +176 -126
- package/dist/index.js +156 -117
- package/dist/transform/collect-ast-nodes.d.ts +6 -0
- package/dist/transform/collect-child-nodes.d.ts +6 -0
- package/dist/transform/is-ast-node.d.ts +2 -0
- package/dist/transform/needs-blank-line.d.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,11 +19,18 @@ no "never" rules.
|
|
|
19
19
|
- after a `function` or `class` declaration
|
|
20
20
|
- on both sides of a control-flow block: `if`, `for`, `for…in`, `for…of`, `while`, `switch`, `try`
|
|
21
21
|
- on both sides of any statement that spans multiple lines
|
|
22
|
-
- at the boundary between statement kinds — a `const`/`let`/`var` declaration, a
|
|
23
|
-
assignment/increment statement — in any
|
|
22
|
+
- at the boundary between statement kinds — a `const`/`let`/`var` declaration, a
|
|
23
|
+
`using`/`await using` declaration, a call statement, an assignment/increment statement — in any
|
|
24
|
+
order; runs of one kind stay tight
|
|
25
|
+
- after a `using`/`await using` block, before any statement of another kind
|
|
24
26
|
- at the boundary between an instantiation-headed statement (`new` at the head of its expression
|
|
25
27
|
chain, as in `new X(…)` or `new X(…).method()`) and any other statement kind — `new` in argument
|
|
26
28
|
position doesn't count, and runs of instantiations stay glued
|
|
29
|
+
- at the boundary between an awaited and a non-awaited statement — awaited means an `await using`
|
|
30
|
+
declaration, or an `await` in the head of the statement's value chain (`await f()`,
|
|
31
|
+
`const x = await f()`, `x = (await f()).prop`); an `await` in argument position (`use(await f())`)
|
|
32
|
+
or under a non-chain head (`flag ? await f() : g()`) doesn't count, and runs of awaited statements
|
|
33
|
+
stay glued
|
|
27
34
|
|
|
28
35
|
**`@stylistic/lines-between-class-members`** — one blank line between class members.
|
|
29
36
|
|
|
@@ -32,8 +39,10 @@ no "never" rules.
|
|
|
32
39
|
- **Consecutive single-line `const`/`let`/`var` declarations stay glued** (no blank between them); a
|
|
33
40
|
multiline declaration is separated like any other multiline statement.
|
|
34
41
|
- **Bare declarations only.** `export const`, `export function`, and `export class` are not matched
|
|
35
|
-
— ESLint's selectors do not look through `export`, and neither does this. `await using`
|
|
36
|
-
|
|
42
|
+
— ESLint's selectors do not look through `export`, and neither does this. `using`/`await using`
|
|
43
|
+
declarations are not var declarations; they form their own statement kind.
|
|
44
|
+
- **The `using` and awaited-boundary rules go beyond ESLint** — `padding-line-between-statements`
|
|
45
|
+
cannot express either, so on these the codemod is the spec.
|
|
37
46
|
- **`.d.ts` files are skipped.**
|
|
38
47
|
|
|
39
48
|
## CLI
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deduped: overlapping patterns (a `src/**` glob plus a file inside src/) must
|
|
3
|
-
* not process — or report — the same file twice.
|
|
3
|
+
* not process — or report — the same file twice. Ignore globs filter the final
|
|
4
|
+
* list uniformly, so a file is skipped whether it arrived via a directory,
|
|
5
|
+
* a glob, or an explicit path argument.
|
|
4
6
|
*/
|
|
5
|
-
export declare function expandInputs(patterns: readonly string[]): Promise<string[]>;
|
|
7
|
+
export declare function expandInputs(patterns: readonly string[], ignore?: readonly string[]): Promise<string[]>;
|
package/dist/cli.js
CHANGED
|
@@ -20,9 +20,9 @@ function buildBenchStatsFromReports(reports, ms) {
|
|
|
20
20
|
// src/cli/expand-inputs.ts
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
|
-
async function expandInputs(patterns) {
|
|
23
|
+
async function expandInputs(patterns, ignore = []) {
|
|
24
24
|
const lists = await Promise.all(patterns.map((p) => expandPattern(p)));
|
|
25
|
-
return [...new Set(lists.flat())];
|
|
25
|
+
return [...new Set(lists.flat())].filter((file) => !isIgnored(file, ignore));
|
|
26
26
|
}
|
|
27
27
|
function expandPattern(p) {
|
|
28
28
|
if (fs.existsSync(p)) {
|
|
@@ -41,6 +41,13 @@ function isExcluded(entry) {
|
|
|
41
41
|
const p = typeof entry === "string" ? entry : path.join(entry.parentPath ?? "", entry.name);
|
|
42
42
|
return p.split(/[\\/]/u).some((seg) => seg === "node_modules" || seg === ".git");
|
|
43
43
|
}
|
|
44
|
+
function isIgnored(file, patterns) {
|
|
45
|
+
if (patterns.length === 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const relative = path.relative(process.cwd(), file);
|
|
49
|
+
return patterns.some((pattern) => path.matchesGlob(file, pattern) || path.matchesGlob(relative, pattern));
|
|
50
|
+
}
|
|
44
51
|
|
|
45
52
|
// src/cli/parse-cli-args.ts
|
|
46
53
|
import { parseArgs } from "node:util";
|
|
@@ -54,7 +61,8 @@ function parseCLIArgs(argv) {
|
|
|
54
61
|
bench: { type: "boolean", default: false },
|
|
55
62
|
quiet: { type: "boolean", default: false },
|
|
56
63
|
version: { type: "boolean", default: false },
|
|
57
|
-
help: { type: "boolean", default: false }
|
|
64
|
+
help: { type: "boolean", default: false },
|
|
65
|
+
ignore: { type: "string", multiple: true, default: [] }
|
|
58
66
|
},
|
|
59
67
|
allowPositionals: true,
|
|
60
68
|
strict: true
|
|
@@ -65,6 +73,7 @@ function parseCLIArgs(argv) {
|
|
|
65
73
|
bench: values.bench,
|
|
66
74
|
help: values.help,
|
|
67
75
|
version: values.version,
|
|
76
|
+
ignore: values.ignore,
|
|
68
77
|
inputs: positionals
|
|
69
78
|
};
|
|
70
79
|
} catch (error) {
|
|
@@ -86,10 +95,11 @@ usage:
|
|
|
86
95
|
format-codemod [options] <file|dir|glob> ...
|
|
87
96
|
|
|
88
97
|
options:
|
|
89
|
-
--check
|
|
90
|
-
--dry
|
|
91
|
-
--bench
|
|
92
|
-
--quiet
|
|
98
|
+
--check exit 1 if any file would change (no writes)
|
|
99
|
+
--dry print unified diff to stdout (no writes)
|
|
100
|
+
--bench print parse stats as JSON to stderr
|
|
101
|
+
--quiet only print files that would change
|
|
102
|
+
--ignore <glob> skip files matching the glob (repeatable)
|
|
93
103
|
--version
|
|
94
104
|
--help
|
|
95
105
|
|
|
@@ -102,6 +112,7 @@ examples:
|
|
|
102
112
|
format-codemod 'src/**/*.ts'
|
|
103
113
|
format-codemod --check 'src/**/*.{ts,tsx}'
|
|
104
114
|
format-codemod --dry path/to/file.ts
|
|
115
|
+
format-codemod --ignore 'src/generated/**' .
|
|
105
116
|
`);
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -149,6 +160,158 @@ function applyEdits(src, edits) {
|
|
|
149
160
|
return segments.toReversed().join("");
|
|
150
161
|
}
|
|
151
162
|
|
|
163
|
+
// src/transform/is-ast-node.ts
|
|
164
|
+
function isASTNode(value) {
|
|
165
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/transform/collect-ast-nodes.ts
|
|
169
|
+
function collectASTNodes(value) {
|
|
170
|
+
if (!Array.isArray(value)) {
|
|
171
|
+
return isASTNode(value) ? [value] : [];
|
|
172
|
+
}
|
|
173
|
+
const nodes = [];
|
|
174
|
+
for (const item of value) {
|
|
175
|
+
if (isASTNode(item)) {
|
|
176
|
+
nodes.push(item);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return nodes;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/transform/collect-child-nodes.ts
|
|
183
|
+
function collectChildNodes(node) {
|
|
184
|
+
const children = [];
|
|
185
|
+
for (const key of Object.keys(node)) {
|
|
186
|
+
if (key !== "loc" && key !== "parent") {
|
|
187
|
+
children.push(...collectASTNodes(node[key]));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return children;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/transform/needs-blank-line.ts
|
|
194
|
+
var CONTROL_FLOW_TYPES = new Set([
|
|
195
|
+
"IfStatement",
|
|
196
|
+
"ForStatement",
|
|
197
|
+
"ForInStatement",
|
|
198
|
+
"ForOfStatement",
|
|
199
|
+
"WhileStatement",
|
|
200
|
+
"SwitchStatement",
|
|
201
|
+
"TryStatement"
|
|
202
|
+
]);
|
|
203
|
+
function needsBlankLine(container, prev, next) {
|
|
204
|
+
if (container.type === "ClassBody") {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (isVarDecl(prev) && !isVarDecl(next) || isUsingDecl(prev) && !isUsingDecl(next)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (next.type === "ReturnStatement") {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (isFnOrClassDecl(prev)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return isNewHeaded(prev) !== isNewHeaded(next) || isAwaitHeaded(prev) !== isAwaitHeaded(next) || isKindBoundary(prev, next) || CONTROL_FLOW_TYPES.has(next.type) || CONTROL_FLOW_TYPES.has(prev.type);
|
|
217
|
+
}
|
|
218
|
+
function isNewHeaded(node) {
|
|
219
|
+
return getStatementValues(node).some((value) => getExpressionHead(value).type === "NewExpression");
|
|
220
|
+
}
|
|
221
|
+
function getStatementValues(node) {
|
|
222
|
+
if (node.type === "VariableDeclaration") {
|
|
223
|
+
return collectASTNodes(node["declarations"]).map((declarator) => declarator["init"]).filter((init) => isASTNode(init));
|
|
224
|
+
}
|
|
225
|
+
const { expression } = node;
|
|
226
|
+
if (node.type !== "ExpressionStatement" || !isASTNode(expression)) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const { right } = expression;
|
|
230
|
+
if (expression.type === "AssignmentExpression" && isASTNode(right)) {
|
|
231
|
+
return [right];
|
|
232
|
+
}
|
|
233
|
+
return [expression];
|
|
234
|
+
}
|
|
235
|
+
var HEAD_PROPERTY = {
|
|
236
|
+
CallExpression: "callee",
|
|
237
|
+
MemberExpression: "object",
|
|
238
|
+
ChainExpression: "expression",
|
|
239
|
+
AwaitExpression: "argument",
|
|
240
|
+
TSNonNullExpression: "expression",
|
|
241
|
+
ParenthesizedExpression: "expression"
|
|
242
|
+
};
|
|
243
|
+
function getExpressionHead(node) {
|
|
244
|
+
let current = node;
|
|
245
|
+
let property = HEAD_PROPERTY[current.type];
|
|
246
|
+
while (property !== undefined) {
|
|
247
|
+
const inner = current[property];
|
|
248
|
+
if (!isASTNode(inner)) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
current = inner;
|
|
252
|
+
property = HEAD_PROPERTY[current.type];
|
|
253
|
+
}
|
|
254
|
+
return current;
|
|
255
|
+
}
|
|
256
|
+
function isAwaitHeaded(node) {
|
|
257
|
+
if (node.type === "VariableDeclaration" && node.kind === "await using") {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return getStatementValues(node).some((value) => hasAwaitInHeadChain(value));
|
|
261
|
+
}
|
|
262
|
+
function hasAwaitInHeadChain(node) {
|
|
263
|
+
let current = node;
|
|
264
|
+
while (current !== undefined) {
|
|
265
|
+
if (current.type === "AwaitExpression") {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
const property = HEAD_PROPERTY[current.type];
|
|
269
|
+
const inner = property === undefined ? undefined : current[property];
|
|
270
|
+
current = isASTNode(inner) ? inner : undefined;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
var CALL_TYPES = ["CallExpression", "AwaitExpression"];
|
|
275
|
+
var MUTATION_TYPES = ["AssignmentExpression", "UpdateExpression"];
|
|
276
|
+
function isKindBoundary(prev, next) {
|
|
277
|
+
const prevKind = getStatementKind(prev);
|
|
278
|
+
const nextKind = getStatementKind(next);
|
|
279
|
+
return prevKind !== null && nextKind !== null && prevKind !== nextKind;
|
|
280
|
+
}
|
|
281
|
+
function getStatementKind(node) {
|
|
282
|
+
if (isUsingDecl(node)) {
|
|
283
|
+
return "using";
|
|
284
|
+
}
|
|
285
|
+
if (isVarDecl(node)) {
|
|
286
|
+
return "declaration";
|
|
287
|
+
}
|
|
288
|
+
if (isExpressionStatementOf(node, CALL_TYPES)) {
|
|
289
|
+
return "call";
|
|
290
|
+
}
|
|
291
|
+
return isExpressionStatementOf(node, MUTATION_TYPES) ? "mutation" : null;
|
|
292
|
+
}
|
|
293
|
+
function isExpressionStatementOf(node, types) {
|
|
294
|
+
const { expression } = node;
|
|
295
|
+
return node.type === "ExpressionStatement" && isASTNode(expression) && types.includes(expression.type);
|
|
296
|
+
}
|
|
297
|
+
var VAR_DECL_KINDS = new Set(["const", "let", "var"]);
|
|
298
|
+
function isVarDecl(node) {
|
|
299
|
+
if (node.type === "VariableDeclaration") {
|
|
300
|
+
return node.kind !== undefined && VAR_DECL_KINDS.has(node.kind);
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
var USING_DECL_KINDS = new Set(["using", "await using"]);
|
|
305
|
+
function isUsingDecl(node) {
|
|
306
|
+
if (node.type === "VariableDeclaration") {
|
|
307
|
+
return node.kind !== undefined && USING_DECL_KINDS.has(node.kind);
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
function isFnOrClassDecl(node) {
|
|
312
|
+
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
|
|
313
|
+
}
|
|
314
|
+
|
|
152
315
|
// src/transform/plan-gap-edit.ts
|
|
153
316
|
function planGapEdit(file, prev, next) {
|
|
154
317
|
const gap = buildGap(file, prev, next);
|
|
@@ -269,7 +432,7 @@ function buildPairEdits(file, container, body) {
|
|
|
269
432
|
for (let i = 0;i < body.length - 1; i++) {
|
|
270
433
|
const prev = body[i];
|
|
271
434
|
const next = body[i + 1];
|
|
272
|
-
if (prev !== undefined && next !== undefined && (needsBlankLine(container, prev, next) || isMultiline(file.src, prev) || isMultiline(file.src, next))) {
|
|
435
|
+
if (prev !== undefined && next !== undefined && !isImportPair(prev, next) && (needsBlankLine(container, prev, next) || isMultiline(file.src, prev) || isMultiline(file.src, next))) {
|
|
273
436
|
const edit = planGapEdit(file, prev, next);
|
|
274
437
|
if (edit !== null) {
|
|
275
438
|
edits.push(edit);
|
|
@@ -278,126 +441,13 @@ function buildPairEdits(file, container, body) {
|
|
|
278
441
|
}
|
|
279
442
|
return edits;
|
|
280
443
|
}
|
|
444
|
+
function isImportPair(prev, next) {
|
|
445
|
+
return prev.type === "ImportDeclaration" && next.type === "ImportDeclaration";
|
|
446
|
+
}
|
|
281
447
|
function isMultiline(src, node) {
|
|
282
448
|
return typeof node.start === "number" && typeof node.end === "number" && src.slice(node.start, node.end).includes(`
|
|
283
449
|
`);
|
|
284
450
|
}
|
|
285
|
-
var CONTROL_FLOW_TYPES = new Set([
|
|
286
|
-
"IfStatement",
|
|
287
|
-
"ForStatement",
|
|
288
|
-
"ForInStatement",
|
|
289
|
-
"ForOfStatement",
|
|
290
|
-
"WhileStatement",
|
|
291
|
-
"SwitchStatement",
|
|
292
|
-
"TryStatement"
|
|
293
|
-
]);
|
|
294
|
-
function needsBlankLine(container, prev, next) {
|
|
295
|
-
if (container.type === "ClassBody") {
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
if (isVarDecl(prev) && !isVarDecl(next)) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
if (next.type === "ReturnStatement") {
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
if (isFnOrClassDecl(prev)) {
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
return isNewHeaded(prev) !== isNewHeaded(next) || isKindBoundary(prev, next) || CONTROL_FLOW_TYPES.has(next.type) || CONTROL_FLOW_TYPES.has(prev.type);
|
|
308
|
-
}
|
|
309
|
-
function isNewHeaded(node) {
|
|
310
|
-
return getStatementValues(node).some((value) => getExpressionHead(value).type === "NewExpression");
|
|
311
|
-
}
|
|
312
|
-
function getStatementValues(node) {
|
|
313
|
-
if (node.type === "VariableDeclaration") {
|
|
314
|
-
return collectASTNodes(node["declarations"]).map((declarator) => declarator["init"]).filter((init) => isASTNode(init));
|
|
315
|
-
}
|
|
316
|
-
const { expression } = node;
|
|
317
|
-
if (node.type !== "ExpressionStatement" || !isASTNode(expression)) {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
const { right } = expression;
|
|
321
|
-
if (expression.type === "AssignmentExpression" && isASTNode(right)) {
|
|
322
|
-
return [right];
|
|
323
|
-
}
|
|
324
|
-
return [expression];
|
|
325
|
-
}
|
|
326
|
-
var HEAD_PROPERTY = {
|
|
327
|
-
CallExpression: "callee",
|
|
328
|
-
MemberExpression: "object",
|
|
329
|
-
ChainExpression: "expression",
|
|
330
|
-
AwaitExpression: "argument",
|
|
331
|
-
TSNonNullExpression: "expression",
|
|
332
|
-
ParenthesizedExpression: "expression"
|
|
333
|
-
};
|
|
334
|
-
function getExpressionHead(node) {
|
|
335
|
-
let current = node;
|
|
336
|
-
let property = HEAD_PROPERTY[current.type];
|
|
337
|
-
while (property !== undefined) {
|
|
338
|
-
const inner = current[property];
|
|
339
|
-
if (!isASTNode(inner)) {
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
current = inner;
|
|
343
|
-
property = HEAD_PROPERTY[current.type];
|
|
344
|
-
}
|
|
345
|
-
return current;
|
|
346
|
-
}
|
|
347
|
-
var CALL_TYPES = ["CallExpression", "AwaitExpression"];
|
|
348
|
-
var MUTATION_TYPES = ["AssignmentExpression", "UpdateExpression"];
|
|
349
|
-
function isKindBoundary(prev, next) {
|
|
350
|
-
const prevKind = getStatementKind(prev);
|
|
351
|
-
const nextKind = getStatementKind(next);
|
|
352
|
-
return prevKind !== null && nextKind !== null && prevKind !== nextKind;
|
|
353
|
-
}
|
|
354
|
-
function getStatementKind(node) {
|
|
355
|
-
if (isVarDecl(node)) {
|
|
356
|
-
return "declaration";
|
|
357
|
-
}
|
|
358
|
-
if (isExpressionStatementOf(node, CALL_TYPES)) {
|
|
359
|
-
return "call";
|
|
360
|
-
}
|
|
361
|
-
return isExpressionStatementOf(node, MUTATION_TYPES) ? "mutation" : null;
|
|
362
|
-
}
|
|
363
|
-
function isExpressionStatementOf(node, types) {
|
|
364
|
-
const { expression } = node;
|
|
365
|
-
return node.type === "ExpressionStatement" && isASTNode(expression) && types.includes(expression.type);
|
|
366
|
-
}
|
|
367
|
-
var VAR_DECL_KINDS = new Set(["const", "let", "var"]);
|
|
368
|
-
function isVarDecl(node) {
|
|
369
|
-
if (node.type === "VariableDeclaration") {
|
|
370
|
-
return node.kind !== undefined && VAR_DECL_KINDS.has(node.kind);
|
|
371
|
-
}
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
function isFnOrClassDecl(node) {
|
|
375
|
-
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
|
|
376
|
-
}
|
|
377
|
-
function collectChildNodes(node) {
|
|
378
|
-
const children = [];
|
|
379
|
-
for (const key of Object.keys(node)) {
|
|
380
|
-
if (key !== "loc" && key !== "parent") {
|
|
381
|
-
children.push(...collectASTNodes(node[key]));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return children;
|
|
385
|
-
}
|
|
386
|
-
function collectASTNodes(value) {
|
|
387
|
-
if (!Array.isArray(value)) {
|
|
388
|
-
return isASTNode(value) ? [value] : [];
|
|
389
|
-
}
|
|
390
|
-
const nodes = [];
|
|
391
|
-
for (const item of value) {
|
|
392
|
-
if (isASTNode(item)) {
|
|
393
|
-
nodes.push(item);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return nodes;
|
|
397
|
-
}
|
|
398
|
-
function isASTNode(value) {
|
|
399
|
-
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
400
|
-
}
|
|
401
451
|
|
|
402
452
|
// src/transform/parse-source.ts
|
|
403
453
|
import { parseSync } from "oxc-parser";
|
|
@@ -633,7 +683,7 @@ if (typeof parsedArgs === "string") {
|
|
|
633
683
|
console.error(parsedArgs);
|
|
634
684
|
process.exit(2);
|
|
635
685
|
}
|
|
636
|
-
var { mode, quiet, bench, help, version, inputs } = parsedArgs;
|
|
686
|
+
var { mode, quiet, bench, help, version, ignore, inputs } = parsedArgs;
|
|
637
687
|
if (help || inputs.length === 0 && !version) {
|
|
638
688
|
const exitCode = inputs.length === 0 ? 2 : 0;
|
|
639
689
|
printHelp();
|
|
@@ -646,7 +696,7 @@ if (version) {
|
|
|
646
696
|
process.exit(0);
|
|
647
697
|
}
|
|
648
698
|
var t0 = Date.now();
|
|
649
|
-
var files = await expandInputs(inputs);
|
|
699
|
+
var files = await expandInputs(inputs, ignore);
|
|
650
700
|
if (files.length === 0) {
|
|
651
701
|
console.error("no files matched");
|
|
652
702
|
process.exit(2);
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,158 @@ function applyEdits(src, edits) {
|
|
|
10
10
|
return segments.toReversed().join("");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// src/transform/is-ast-node.ts
|
|
14
|
+
function isASTNode(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/transform/collect-ast-nodes.ts
|
|
19
|
+
function collectASTNodes(value) {
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
return isASTNode(value) ? [value] : [];
|
|
22
|
+
}
|
|
23
|
+
const nodes = [];
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
if (isASTNode(item)) {
|
|
26
|
+
nodes.push(item);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return nodes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/transform/collect-child-nodes.ts
|
|
33
|
+
function collectChildNodes(node) {
|
|
34
|
+
const children = [];
|
|
35
|
+
for (const key of Object.keys(node)) {
|
|
36
|
+
if (key !== "loc" && key !== "parent") {
|
|
37
|
+
children.push(...collectASTNodes(node[key]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return children;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/transform/needs-blank-line.ts
|
|
44
|
+
var CONTROL_FLOW_TYPES = new Set([
|
|
45
|
+
"IfStatement",
|
|
46
|
+
"ForStatement",
|
|
47
|
+
"ForInStatement",
|
|
48
|
+
"ForOfStatement",
|
|
49
|
+
"WhileStatement",
|
|
50
|
+
"SwitchStatement",
|
|
51
|
+
"TryStatement"
|
|
52
|
+
]);
|
|
53
|
+
function needsBlankLine(container, prev, next) {
|
|
54
|
+
if (container.type === "ClassBody") {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (isVarDecl(prev) && !isVarDecl(next) || isUsingDecl(prev) && !isUsingDecl(next)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (next.type === "ReturnStatement") {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (isFnOrClassDecl(prev)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return isNewHeaded(prev) !== isNewHeaded(next) || isAwaitHeaded(prev) !== isAwaitHeaded(next) || isKindBoundary(prev, next) || CONTROL_FLOW_TYPES.has(next.type) || CONTROL_FLOW_TYPES.has(prev.type);
|
|
67
|
+
}
|
|
68
|
+
function isNewHeaded(node) {
|
|
69
|
+
return getStatementValues(node).some((value) => getExpressionHead(value).type === "NewExpression");
|
|
70
|
+
}
|
|
71
|
+
function getStatementValues(node) {
|
|
72
|
+
if (node.type === "VariableDeclaration") {
|
|
73
|
+
return collectASTNodes(node["declarations"]).map((declarator) => declarator["init"]).filter((init) => isASTNode(init));
|
|
74
|
+
}
|
|
75
|
+
const { expression } = node;
|
|
76
|
+
if (node.type !== "ExpressionStatement" || !isASTNode(expression)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const { right } = expression;
|
|
80
|
+
if (expression.type === "AssignmentExpression" && isASTNode(right)) {
|
|
81
|
+
return [right];
|
|
82
|
+
}
|
|
83
|
+
return [expression];
|
|
84
|
+
}
|
|
85
|
+
var HEAD_PROPERTY = {
|
|
86
|
+
CallExpression: "callee",
|
|
87
|
+
MemberExpression: "object",
|
|
88
|
+
ChainExpression: "expression",
|
|
89
|
+
AwaitExpression: "argument",
|
|
90
|
+
TSNonNullExpression: "expression",
|
|
91
|
+
ParenthesizedExpression: "expression"
|
|
92
|
+
};
|
|
93
|
+
function getExpressionHead(node) {
|
|
94
|
+
let current = node;
|
|
95
|
+
let property = HEAD_PROPERTY[current.type];
|
|
96
|
+
while (property !== undefined) {
|
|
97
|
+
const inner = current[property];
|
|
98
|
+
if (!isASTNode(inner)) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
current = inner;
|
|
102
|
+
property = HEAD_PROPERTY[current.type];
|
|
103
|
+
}
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
function isAwaitHeaded(node) {
|
|
107
|
+
if (node.type === "VariableDeclaration" && node.kind === "await using") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return getStatementValues(node).some((value) => hasAwaitInHeadChain(value));
|
|
111
|
+
}
|
|
112
|
+
function hasAwaitInHeadChain(node) {
|
|
113
|
+
let current = node;
|
|
114
|
+
while (current !== undefined) {
|
|
115
|
+
if (current.type === "AwaitExpression") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const property = HEAD_PROPERTY[current.type];
|
|
119
|
+
const inner = property === undefined ? undefined : current[property];
|
|
120
|
+
current = isASTNode(inner) ? inner : undefined;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
var CALL_TYPES = ["CallExpression", "AwaitExpression"];
|
|
125
|
+
var MUTATION_TYPES = ["AssignmentExpression", "UpdateExpression"];
|
|
126
|
+
function isKindBoundary(prev, next) {
|
|
127
|
+
const prevKind = getStatementKind(prev);
|
|
128
|
+
const nextKind = getStatementKind(next);
|
|
129
|
+
return prevKind !== null && nextKind !== null && prevKind !== nextKind;
|
|
130
|
+
}
|
|
131
|
+
function getStatementKind(node) {
|
|
132
|
+
if (isUsingDecl(node)) {
|
|
133
|
+
return "using";
|
|
134
|
+
}
|
|
135
|
+
if (isVarDecl(node)) {
|
|
136
|
+
return "declaration";
|
|
137
|
+
}
|
|
138
|
+
if (isExpressionStatementOf(node, CALL_TYPES)) {
|
|
139
|
+
return "call";
|
|
140
|
+
}
|
|
141
|
+
return isExpressionStatementOf(node, MUTATION_TYPES) ? "mutation" : null;
|
|
142
|
+
}
|
|
143
|
+
function isExpressionStatementOf(node, types) {
|
|
144
|
+
const { expression } = node;
|
|
145
|
+
return node.type === "ExpressionStatement" && isASTNode(expression) && types.includes(expression.type);
|
|
146
|
+
}
|
|
147
|
+
var VAR_DECL_KINDS = new Set(["const", "let", "var"]);
|
|
148
|
+
function isVarDecl(node) {
|
|
149
|
+
if (node.type === "VariableDeclaration") {
|
|
150
|
+
return node.kind !== undefined && VAR_DECL_KINDS.has(node.kind);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
var USING_DECL_KINDS = new Set(["using", "await using"]);
|
|
155
|
+
function isUsingDecl(node) {
|
|
156
|
+
if (node.type === "VariableDeclaration") {
|
|
157
|
+
return node.kind !== undefined && USING_DECL_KINDS.has(node.kind);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function isFnOrClassDecl(node) {
|
|
162
|
+
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
|
|
163
|
+
}
|
|
164
|
+
|
|
13
165
|
// src/transform/plan-gap-edit.ts
|
|
14
166
|
function planGapEdit(file, prev, next) {
|
|
15
167
|
const gap = buildGap(file, prev, next);
|
|
@@ -130,7 +282,7 @@ function buildPairEdits(file, container, body) {
|
|
|
130
282
|
for (let i = 0;i < body.length - 1; i++) {
|
|
131
283
|
const prev = body[i];
|
|
132
284
|
const next = body[i + 1];
|
|
133
|
-
if (prev !== undefined && next !== undefined && (needsBlankLine(container, prev, next) || isMultiline(file.src, prev) || isMultiline(file.src, next))) {
|
|
285
|
+
if (prev !== undefined && next !== undefined && !isImportPair(prev, next) && (needsBlankLine(container, prev, next) || isMultiline(file.src, prev) || isMultiline(file.src, next))) {
|
|
134
286
|
const edit = planGapEdit(file, prev, next);
|
|
135
287
|
if (edit !== null) {
|
|
136
288
|
edits.push(edit);
|
|
@@ -139,126 +291,13 @@ function buildPairEdits(file, container, body) {
|
|
|
139
291
|
}
|
|
140
292
|
return edits;
|
|
141
293
|
}
|
|
294
|
+
function isImportPair(prev, next) {
|
|
295
|
+
return prev.type === "ImportDeclaration" && next.type === "ImportDeclaration";
|
|
296
|
+
}
|
|
142
297
|
function isMultiline(src, node) {
|
|
143
298
|
return typeof node.start === "number" && typeof node.end === "number" && src.slice(node.start, node.end).includes(`
|
|
144
299
|
`);
|
|
145
300
|
}
|
|
146
|
-
var CONTROL_FLOW_TYPES = new Set([
|
|
147
|
-
"IfStatement",
|
|
148
|
-
"ForStatement",
|
|
149
|
-
"ForInStatement",
|
|
150
|
-
"ForOfStatement",
|
|
151
|
-
"WhileStatement",
|
|
152
|
-
"SwitchStatement",
|
|
153
|
-
"TryStatement"
|
|
154
|
-
]);
|
|
155
|
-
function needsBlankLine(container, prev, next) {
|
|
156
|
-
if (container.type === "ClassBody") {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
if (isVarDecl(prev) && !isVarDecl(next)) {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
if (next.type === "ReturnStatement") {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
if (isFnOrClassDecl(prev)) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
return isNewHeaded(prev) !== isNewHeaded(next) || isKindBoundary(prev, next) || CONTROL_FLOW_TYPES.has(next.type) || CONTROL_FLOW_TYPES.has(prev.type);
|
|
169
|
-
}
|
|
170
|
-
function isNewHeaded(node) {
|
|
171
|
-
return getStatementValues(node).some((value) => getExpressionHead(value).type === "NewExpression");
|
|
172
|
-
}
|
|
173
|
-
function getStatementValues(node) {
|
|
174
|
-
if (node.type === "VariableDeclaration") {
|
|
175
|
-
return collectASTNodes(node["declarations"]).map((declarator) => declarator["init"]).filter((init) => isASTNode(init));
|
|
176
|
-
}
|
|
177
|
-
const { expression } = node;
|
|
178
|
-
if (node.type !== "ExpressionStatement" || !isASTNode(expression)) {
|
|
179
|
-
return [];
|
|
180
|
-
}
|
|
181
|
-
const { right } = expression;
|
|
182
|
-
if (expression.type === "AssignmentExpression" && isASTNode(right)) {
|
|
183
|
-
return [right];
|
|
184
|
-
}
|
|
185
|
-
return [expression];
|
|
186
|
-
}
|
|
187
|
-
var HEAD_PROPERTY = {
|
|
188
|
-
CallExpression: "callee",
|
|
189
|
-
MemberExpression: "object",
|
|
190
|
-
ChainExpression: "expression",
|
|
191
|
-
AwaitExpression: "argument",
|
|
192
|
-
TSNonNullExpression: "expression",
|
|
193
|
-
ParenthesizedExpression: "expression"
|
|
194
|
-
};
|
|
195
|
-
function getExpressionHead(node) {
|
|
196
|
-
let current = node;
|
|
197
|
-
let property = HEAD_PROPERTY[current.type];
|
|
198
|
-
while (property !== undefined) {
|
|
199
|
-
const inner = current[property];
|
|
200
|
-
if (!isASTNode(inner)) {
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
current = inner;
|
|
204
|
-
property = HEAD_PROPERTY[current.type];
|
|
205
|
-
}
|
|
206
|
-
return current;
|
|
207
|
-
}
|
|
208
|
-
var CALL_TYPES = ["CallExpression", "AwaitExpression"];
|
|
209
|
-
var MUTATION_TYPES = ["AssignmentExpression", "UpdateExpression"];
|
|
210
|
-
function isKindBoundary(prev, next) {
|
|
211
|
-
const prevKind = getStatementKind(prev);
|
|
212
|
-
const nextKind = getStatementKind(next);
|
|
213
|
-
return prevKind !== null && nextKind !== null && prevKind !== nextKind;
|
|
214
|
-
}
|
|
215
|
-
function getStatementKind(node) {
|
|
216
|
-
if (isVarDecl(node)) {
|
|
217
|
-
return "declaration";
|
|
218
|
-
}
|
|
219
|
-
if (isExpressionStatementOf(node, CALL_TYPES)) {
|
|
220
|
-
return "call";
|
|
221
|
-
}
|
|
222
|
-
return isExpressionStatementOf(node, MUTATION_TYPES) ? "mutation" : null;
|
|
223
|
-
}
|
|
224
|
-
function isExpressionStatementOf(node, types) {
|
|
225
|
-
const { expression } = node;
|
|
226
|
-
return node.type === "ExpressionStatement" && isASTNode(expression) && types.includes(expression.type);
|
|
227
|
-
}
|
|
228
|
-
var VAR_DECL_KINDS = new Set(["const", "let", "var"]);
|
|
229
|
-
function isVarDecl(node) {
|
|
230
|
-
if (node.type === "VariableDeclaration") {
|
|
231
|
-
return node.kind !== undefined && VAR_DECL_KINDS.has(node.kind);
|
|
232
|
-
}
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
function isFnOrClassDecl(node) {
|
|
236
|
-
return node.type === "FunctionDeclaration" || node.type === "ClassDeclaration";
|
|
237
|
-
}
|
|
238
|
-
function collectChildNodes(node) {
|
|
239
|
-
const children = [];
|
|
240
|
-
for (const key of Object.keys(node)) {
|
|
241
|
-
if (key !== "loc" && key !== "parent") {
|
|
242
|
-
children.push(...collectASTNodes(node[key]));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return children;
|
|
246
|
-
}
|
|
247
|
-
function collectASTNodes(value) {
|
|
248
|
-
if (!Array.isArray(value)) {
|
|
249
|
-
return isASTNode(value) ? [value] : [];
|
|
250
|
-
}
|
|
251
|
-
const nodes = [];
|
|
252
|
-
for (const item of value) {
|
|
253
|
-
if (isASTNode(item)) {
|
|
254
|
-
nodes.push(item);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return nodes;
|
|
258
|
-
}
|
|
259
|
-
function isASTNode(value) {
|
|
260
|
-
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
261
|
-
}
|
|
262
301
|
|
|
263
302
|
// src/transform/parse-source.ts
|
|
264
303
|
import { parseSync } from "oxc-parser";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ASTNode } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The "always" rules: a blank line between class members, after a var or
|
|
4
|
+
* using block before a statement of any other kind, before a return, after a
|
|
5
|
+
* function/class declaration, on both sides of a control-flow block — its
|
|
6
|
+
* closing brace ends a visual unit just like its opening keyword starts one —
|
|
7
|
+
* and at the boundary between statement kinds: call vs mutation,
|
|
8
|
+
* instantiation vs anything else, and awaited vs non-awaited. There are no
|
|
9
|
+
* "never" rules, so any match means one blank.
|
|
10
|
+
*/
|
|
11
|
+
export declare function needsBlankLine(container: ASTNode, prev: ASTNode, next: ASTNode): boolean;
|