@onrails/codemod 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/ast.ts +12 -1
- package/src/chains.ts +22 -18
- package/src/cli.ts +2 -2
- package/src/constants.ts +1 -0
- package/src/file-change.ts +11 -11
- package/src/tersify.ts +22 -18
- package/src/types.ts +2 -2
- package/src/warnings.ts +24 -20
package/package.json
CHANGED
package/src/ast.ts
CHANGED
|
@@ -21,8 +21,19 @@ export const concatCollectors =
|
|
|
21
21
|
export const walkSource = (
|
|
22
22
|
src: string,
|
|
23
23
|
visit: (node: ts.Node, sf: ts.SourceFile) => unknown,
|
|
24
|
+
jsx = false,
|
|
24
25
|
): void => {
|
|
25
|
-
|
|
26
|
+
// Parse JSX files in TSX mode — otherwise `<`/`>` are read as comparison
|
|
27
|
+
// operators and `<T>expr` as a cast, which truncates node boundaries and
|
|
28
|
+
// corrupts span-based edits. The scriptKind is set explicitly (not just via
|
|
29
|
+
// the filename) so it can't drift from the extension.
|
|
30
|
+
const sf = ts.createSourceFile(
|
|
31
|
+
jsx ? "codemod.tsx" : "codemod.ts",
|
|
32
|
+
src,
|
|
33
|
+
ts.ScriptTarget.Latest,
|
|
34
|
+
true,
|
|
35
|
+
jsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
36
|
+
);
|
|
26
37
|
const go = (n: ts.Node) => {
|
|
27
38
|
if (visit(n, sf)) return;
|
|
28
39
|
ts.forEachChild(n, go);
|
package/src/chains.ts
CHANGED
|
@@ -213,28 +213,32 @@ export const applyEditStep = (acc: EditAcc, e: Edit): EditAcc => ({
|
|
|
213
213
|
imports: new Set([...acc.imports, ...e.imports]),
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
export function rewriteCompatMethodChainsToNative(src: string): string {
|
|
216
|
+
export function rewriteCompatMethodChainsToNative(src: string, jsx = false): string {
|
|
217
217
|
const edits: Edit[] = [];
|
|
218
218
|
|
|
219
|
-
walkSource(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
walkSource(
|
|
220
|
+
src,
|
|
221
|
+
(node, sf) => {
|
|
222
|
+
if (ts.isCallExpression(node)) {
|
|
223
|
+
const helper = helperCallToNative(node);
|
|
224
|
+
if (isSome(helper)) {
|
|
225
|
+
edits.push(spanEdit(node, sf, helper.value));
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
225
228
|
}
|
|
226
|
-
}
|
|
227
229
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
if (
|
|
231
|
+
isSupportedChainCall(node) &&
|
|
232
|
+
!isNestedInSupportedChain(node) &&
|
|
233
|
+
!ts.isPropertyAccessExpression(node.parent)
|
|
234
|
+
) {
|
|
235
|
+
const chain = collectChain(node);
|
|
236
|
+
const chained = chainToNative(chain.base, chain.steps);
|
|
237
|
+
if (isSome(chained)) edits.push(spanEdit(node, sf, chained.value));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
jsx,
|
|
241
|
+
);
|
|
238
242
|
|
|
239
243
|
if (edits.length === 0) return src;
|
|
240
244
|
|
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as Maybe from "@onrails/maybe";
|
|
|
3
3
|
import { match } from "@onrails/pattern";
|
|
4
4
|
import { err, isErr, ok, type Result, ResultAsync } from "@onrails/result";
|
|
5
5
|
import { Glob } from "bun";
|
|
6
|
-
import { CODE_EXT, SKIP } from "./constants.js";
|
|
6
|
+
import { CODE_EXT, JSX_EXT, SKIP } from "./constants.js";
|
|
7
7
|
import { computeFileChange, toFileChange } from "./file-change.js";
|
|
8
8
|
import { readFileText, writeFileText } from "./io.js";
|
|
9
9
|
import { rewritePkg } from "./pkg.js";
|
|
@@ -64,7 +64,7 @@ const rewriteCode = (
|
|
|
64
64
|
): ResultAsync<Maybe.Maybe<FileChange>, Error> =>
|
|
65
65
|
readFileText(path).flatMap((src) =>
|
|
66
66
|
Maybe.match(
|
|
67
|
-
computeFileChange(src, mode),
|
|
67
|
+
computeFileChange(src, mode, JSX_EXT.test(path)),
|
|
68
68
|
(c) => {
|
|
69
69
|
const change = toFileChange(path, c);
|
|
70
70
|
return c.changed && !dry
|
package/src/constants.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const SKIP = new Set([
|
|
|
8
8
|
"build",
|
|
9
9
|
]);
|
|
10
10
|
export const CODE_EXT = /\.(ts|tsx|mts|cts)$/;
|
|
11
|
+
export const JSX_EXT = /\.(tsx|jsx)$/;
|
|
11
12
|
export const IMPORT_RE = /(from\s+|import\s*\(\s*)(['"])neverthrow\2/g;
|
|
12
13
|
export const COMPAT_SPEC = "@onrails/result/compat/neverthrow";
|
|
13
14
|
export const NATIVE_SPEC = "@onrails/result";
|
package/src/file-change.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { type Maybe, none, some } from "@onrails/maybe";
|
|
2
|
-
import {
|
|
3
|
-
import { concatCollectors, countOccurrences } from "./ast.js";
|
|
2
|
+
import { countOccurrences } from "./ast.js";
|
|
4
3
|
import { rewriteCompatMethodChainsToNative } from "./chains.js";
|
|
5
4
|
import { COMPAT_SPEC, IMPORT_RE, NATIVE_SPEC } from "./constants.js";
|
|
6
5
|
import { rewriteCompatImportsToNative } from "./imports.js";
|
|
7
6
|
import { tersify } from "./tersify.js";
|
|
8
|
-
import type { ComputedChange, FileChange, Mode, ModeStrategy } from "./types.js";
|
|
7
|
+
import type { ComputedChange, FileChange, Mode, ModeStrategy, Warning } from "./types.js";
|
|
9
8
|
import {
|
|
10
9
|
collectNativeMigrationWarnings,
|
|
11
10
|
collectUnsupportedCompatImportWarnings,
|
|
12
11
|
} from "./warnings.js";
|
|
13
12
|
|
|
14
|
-
const rewriteCompatToNative =
|
|
13
|
+
const rewriteCompatToNative = (src: string, jsx: boolean): string =>
|
|
14
|
+
rewriteCompatMethodChainsToNative(rewriteCompatImportsToNative(src), jsx);
|
|
15
15
|
|
|
16
16
|
const rewriteNeverthrowToCompat = (src: string): string =>
|
|
17
17
|
src.replace(IMPORT_RE, (_, lead, quote) => `${lead}${quote}${COMPAT_SPEC}${quote}`);
|
|
18
18
|
|
|
19
|
-
const collectAllNativeWarnings =
|
|
20
|
-
collectUnsupportedCompatImportWarnings,
|
|
21
|
-
collectNativeMigrationWarnings,
|
|
22
|
-
|
|
19
|
+
const collectAllNativeWarnings = (next: string, jsx: boolean): readonly Warning[] => [
|
|
20
|
+
...collectUnsupportedCompatImportWarnings(next),
|
|
21
|
+
...collectNativeMigrationWarnings(next, jsx),
|
|
22
|
+
];
|
|
23
23
|
|
|
24
24
|
const MODES: Record<Mode, ModeStrategy> = {
|
|
25
25
|
compat: {
|
|
@@ -45,13 +45,13 @@ const MODES: Record<Mode, ModeStrategy> = {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
export const computeFileChange = (src: string, mode: Mode): Maybe<ComputedChange> => {
|
|
48
|
+
export const computeFileChange = (src: string, mode: Mode, jsx = false): Maybe<ComputedChange> => {
|
|
49
49
|
const strat = MODES[mode];
|
|
50
50
|
const before = strat.countBefore(src);
|
|
51
51
|
if (strat.earlyExit(src, before)) return none();
|
|
52
|
-
const next = strat.transform(src);
|
|
52
|
+
const next = strat.transform(src, jsx);
|
|
53
53
|
const changed = next !== src;
|
|
54
|
-
const warnings = strat.warnings(next);
|
|
54
|
+
const warnings = strat.warnings(next, jsx);
|
|
55
55
|
return !changed && warnings.length === 0
|
|
56
56
|
? none()
|
|
57
57
|
: some({ next, before, after: strat.countAfter(next), changed, warnings });
|
package/src/tersify.ts
CHANGED
|
@@ -422,29 +422,33 @@ const TRANSFORMS: ReadonlyArray<(node: ts.Node, sf: ts.SourceFile) => Maybe<Edit
|
|
|
422
422
|
tryTersifyIdentityFilter,
|
|
423
423
|
];
|
|
424
424
|
|
|
425
|
-
function tersifyOnce(src: string): string {
|
|
425
|
+
function tersifyOnce(src: string, jsx: boolean): string {
|
|
426
426
|
const edits: Edit[] = [];
|
|
427
427
|
const skippedNodes = new Set<ts.Node>();
|
|
428
428
|
|
|
429
|
-
walkSource(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
429
|
+
walkSource(
|
|
430
|
+
src,
|
|
431
|
+
(node, sf) => {
|
|
432
|
+
if (skippedNodes.has(node)) {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
433
435
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
+
scanIfReturnSequences(node, sf, skippedNodes, edits);
|
|
437
|
+
scanIfToSwitchSequences(node, sf, skippedNodes, edits);
|
|
436
438
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
439
|
+
for (const transform of TRANSFORMS) {
|
|
440
|
+
const editResult = transform(node, sf);
|
|
441
|
+
if (editResult._tag === "Some") {
|
|
442
|
+
edits.push(editResult.value);
|
|
443
|
+
skippedNodes.add(node);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
443
446
|
}
|
|
444
|
-
}
|
|
445
447
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
+
return false;
|
|
449
|
+
},
|
|
450
|
+
jsx,
|
|
451
|
+
);
|
|
448
452
|
|
|
449
453
|
if (edits.length === 0) return src;
|
|
450
454
|
|
|
@@ -455,11 +459,11 @@ function tersifyOnce(src: string): string {
|
|
|
455
459
|
return next;
|
|
456
460
|
}
|
|
457
461
|
|
|
458
|
-
export function tersify(src: string): string {
|
|
462
|
+
export function tersify(src: string, jsx = false): string {
|
|
459
463
|
let current = src;
|
|
460
464
|
let iterations = 0;
|
|
461
465
|
while (iterations < 10) {
|
|
462
|
-
const next = tersifyOnce(current);
|
|
466
|
+
const next = tersifyOnce(current, jsx);
|
|
463
467
|
if (next === current) {
|
|
464
468
|
break;
|
|
465
469
|
}
|
package/src/types.ts
CHANGED
|
@@ -24,8 +24,8 @@ export type ComputedChange = {
|
|
|
24
24
|
export type ModeStrategy = {
|
|
25
25
|
countBefore: (src: string) => number;
|
|
26
26
|
earlyExit: (src: string, before: number) => boolean;
|
|
27
|
-
transform: (src: string) => string;
|
|
28
|
-
warnings: (next: string) => readonly Warning[];
|
|
27
|
+
transform: (src: string, jsx: boolean) => string;
|
|
28
|
+
warnings: (next: string, jsx: boolean) => readonly Warning[];
|
|
29
29
|
countAfter: (next: string) => number;
|
|
30
30
|
};
|
|
31
31
|
|
package/src/warnings.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import {
|
|
2
|
+
import { walkSource } from "./ast.js";
|
|
3
3
|
import { COMPAT_ONLY_PATTERNS, COMPAT_SPEC } from "./constants.js";
|
|
4
4
|
import type { Warning } from "./types.js";
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ export const collectRegexLineWarnings = (src: string): readonly Warning[] =>
|
|
|
12
12
|
),
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
export const collectAstCompatWarnings = (src: string): readonly Warning[] => {
|
|
15
|
+
export const collectAstCompatWarnings = (src: string, jsx = false): readonly Warning[] => {
|
|
16
16
|
const warnings: Warning[] = [];
|
|
17
17
|
const lines = src.split(/\r?\n/);
|
|
18
18
|
const lineTextAt = (sf: ts.SourceFile, node: ts.Node) => {
|
|
@@ -20,32 +20,36 @@ export const collectAstCompatWarnings = (src: string): readonly Warning[] => {
|
|
|
20
20
|
return { line, text: lines[line - 1]?.trim() ?? node.getText(sf) };
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
walkSource(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
23
|
+
walkSource(
|
|
24
|
+
src,
|
|
25
|
+
(node, sf) => {
|
|
26
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
27
|
+
const method = node.expression.name.text;
|
|
28
|
+
if (method === "isOk" || method === "isErr") {
|
|
29
|
+
warnings.push({ ...lineTextAt(sf, node), label: "compat predicate method" });
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
|
-
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
34
|
+
const property = node.name.text;
|
|
35
|
+
if (property === "value" || property === "error") {
|
|
36
|
+
const receiver = node.expression.getText(sf);
|
|
37
|
+
if (/result$/i.test(receiver) || /^result$/i.test(receiver)) {
|
|
38
|
+
warnings.push({ ...lineTextAt(sf, node), label: "compat value/error property" });
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
42
|
+
},
|
|
43
|
+
jsx,
|
|
44
|
+
);
|
|
41
45
|
|
|
42
46
|
return warnings;
|
|
43
47
|
};
|
|
44
48
|
|
|
45
|
-
export const collectNativeMigrationWarnings =
|
|
46
|
-
collectRegexLineWarnings,
|
|
47
|
-
collectAstCompatWarnings,
|
|
48
|
-
|
|
49
|
+
export const collectNativeMigrationWarnings = (src: string, jsx = false): readonly Warning[] => [
|
|
50
|
+
...collectRegexLineWarnings(src),
|
|
51
|
+
...collectAstCompatWarnings(src, jsx),
|
|
52
|
+
];
|
|
49
53
|
|
|
50
54
|
export const collectUnsupportedCompatImportWarnings = (src: string): readonly Warning[] =>
|
|
51
55
|
src.split(/\r?\n/).flatMap((line, i) =>
|