@onrails/codemod 0.2.0 → 0.2.2
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 +4 -4
- package/src/ast.ts +12 -1
- package/src/boundary-spec.ts +153 -0
- package/src/chains.ts +22 -18
- package/src/cli.ts +2 -2
- package/src/constants.ts +5 -5
- package/src/file-change.ts +11 -11
- package/src/index.ts +14 -0
- package/src/tersify-utils.ts +177 -0
- package/src/tersify.ts +36 -142
- package/src/types.ts +2 -2
- package/src/warnings.ts +24 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onrails/codemod",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Codemods for migrating onto @onrails/result (neverthrow → compat shim, and beyond)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alan R. Soares",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
".": "./src/index.ts"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@onrails/maybe": "0.
|
|
46
|
-
"@onrails/pattern": "0.
|
|
47
|
-
"@onrails/result": "0.
|
|
45
|
+
"@onrails/maybe": "0.2.0",
|
|
46
|
+
"@onrails/pattern": "0.2.0",
|
|
47
|
+
"@onrails/result": "0.2.0",
|
|
48
48
|
"typescript": "^6.0.3"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
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);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* boundary-spec.ts — the single owning module for the boundary idioms
|
|
3
|
+
* onrails enforces on consumers:
|
|
4
|
+
*
|
|
5
|
+
* 1. no `Promise<Result<…>>` in public signatures (`no-promise-result`)
|
|
6
|
+
* 2. no unsafe unwraps outside tests (`no-unsafe-unwrap`)
|
|
7
|
+
* 3. no deprecated neverthrow-era synonyms (`no-deprecated-synonyms`)
|
|
8
|
+
*
|
|
9
|
+
* The ESLint plugin (plain JS, `packages/eslint-plugin/src/index.js`) and the
|
|
10
|
+
* Biome plugin (GritQL, `packages/biome-plugin/rules/*.grit`) cannot import
|
|
11
|
+
* this module — cross-package imports are forbidden — so they hand-copy the
|
|
12
|
+
* names. `packages/codemod/test/boundary-conformance.spec.ts` reads those
|
|
13
|
+
* artifacts off disk and asserts they stay in sync with this spec.
|
|
14
|
+
*
|
|
15
|
+
* Deliberate per-engine divergences are declared in `ENGINE_DIVERGENCES`;
|
|
16
|
+
* anything not declared there is drift.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Unsafe unwraps
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* neverthrow-compat unsafe-unwrap methods → native named equivalents.
|
|
25
|
+
* The codemod rewrites `r._unsafeUnwrap()` to `unwrapOk(r)`; the natives are
|
|
26
|
+
* still unsafe (throw on the wrong branch) and stay lint-flagged.
|
|
27
|
+
*/
|
|
28
|
+
export const UNSAFE_UNWRAP_MEMBER_RENAMES: ReadonlyMap<string, string> = new Map([
|
|
29
|
+
["_unsafeUnwrap", "unwrapOk"],
|
|
30
|
+
["_unsafeUnwrapErr", "unwrapErr"],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** Free-function unwraps that throw on the unhappy branch. */
|
|
34
|
+
export const UNSAFE_UNWRAP_CALL_NAMES = ["unwrapOk", "unwrapErr", "unwrap"] as const;
|
|
35
|
+
|
|
36
|
+
/** Every identifier the `no-unsafe-unwrap` rule flags, across engines. */
|
|
37
|
+
export const UNSAFE_UNWRAP_NAMES: ReadonlySet<string> = new Set([
|
|
38
|
+
...UNSAFE_UNWRAP_MEMBER_RENAMES.keys(),
|
|
39
|
+
...UNSAFE_UNWRAP_CALL_NAMES,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Matches compat unsafe-unwrap member calls (`._unsafeUnwrap(` /
|
|
44
|
+
* `._unsafeUnwrapErr(`) in raw source. Derived from the rename map so the
|
|
45
|
+
* codemod's compat-only detection cannot drift from the spec.
|
|
46
|
+
*/
|
|
47
|
+
export const UNSAFE_UNWRAP_MEMBER_CALL_RE = new RegExp(
|
|
48
|
+
String.raw`\.(?:${[...UNSAFE_UNWRAP_MEMBER_RENAMES.keys()].join("|")})\s*\(`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Deprecated synonyms
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export interface DeprecatedSynonym {
|
|
56
|
+
/** The deprecated identifier as it appears in consumer code. */
|
|
57
|
+
readonly name: string;
|
|
58
|
+
/** `method` — flagged as `x.name(…)`; `call` — flagged as `name(…)`. */
|
|
59
|
+
readonly form: "method" | "call";
|
|
60
|
+
/** Canonical replacement identifier. */
|
|
61
|
+
readonly canonical: string;
|
|
62
|
+
/** Extra guidance when the rename is not a plain identifier swap. */
|
|
63
|
+
readonly hint?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Deprecated neverthrow/fp-era synonyms → canonical onrails names.
|
|
68
|
+
* Matching is name-based in every engine — neither GritQL nor the ESLint
|
|
69
|
+
* rules scope by import, so same-named calls from other libraries (RxJS
|
|
70
|
+
* `of`, fp-ts `fold`) are flagged too.
|
|
71
|
+
*/
|
|
72
|
+
export const DEPRECATED_SYNONYMS: readonly DeprecatedSynonym[] = [
|
|
73
|
+
{ name: "chain", form: "method", canonical: "flatMap" },
|
|
74
|
+
{
|
|
75
|
+
name: "isOk",
|
|
76
|
+
form: "method",
|
|
77
|
+
canonical: "isOk",
|
|
78
|
+
hint: "await the ResultAsync, then narrow with the isOk() free function",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "isErr",
|
|
82
|
+
form: "method",
|
|
83
|
+
canonical: "isErr",
|
|
84
|
+
hint: "await the ResultAsync, then narrow with the isErr() free function",
|
|
85
|
+
},
|
|
86
|
+
{ name: "fold", form: "call", canonical: "match" },
|
|
87
|
+
{ name: "matchResult", form: "call", canonical: "match" },
|
|
88
|
+
{ name: "matchMaybe", form: "call", canonical: "match" },
|
|
89
|
+
{ name: "getOrElse", form: "call", canonical: "unwrapOr" },
|
|
90
|
+
{ name: "sequenceTupleAsync", form: "call", canonical: "ResultAsync.combineTuple" },
|
|
91
|
+
{ name: "collect", form: "call", canonical: "combine" },
|
|
92
|
+
{ name: "of", form: "call", canonical: "ok", hint: "or some() for Maybe" },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** Flat old-name → canonical-name view of {@link DEPRECATED_SYNONYMS}. */
|
|
96
|
+
export const DEPRECATED_SYNONYM_RENAMES: ReadonlyMap<string, string> = new Map(
|
|
97
|
+
DEPRECATED_SYNONYMS.map((s) => [s.name, s.canonical]),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Rule descriptors
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/** `Promise<Result<…>>` in a type position → `ResultAsync<T, E>`. */
|
|
105
|
+
export const NO_PROMISE_RESULT_RULE = {
|
|
106
|
+
id: "no-promise-result",
|
|
107
|
+
replacement: "ResultAsync",
|
|
108
|
+
boundaryHelpers: ["fromAsync", "tryAsync"],
|
|
109
|
+
message:
|
|
110
|
+
"Promise<Result<…>> in a type position — use ResultAsync<T, E> at the boundary (fromAsync / tryAsync).",
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
/** Unsafe unwraps outside tests. Message is shared verbatim by both engines. */
|
|
114
|
+
export const NO_UNSAFE_UNWRAP_RULE = {
|
|
115
|
+
id: "no-unsafe-unwrap",
|
|
116
|
+
message:
|
|
117
|
+
"Avoid _unsafeUnwrap* and unwrap* outside tests — use match(), resolve(), or yieldResult() in tryGen.",
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
/** Deprecated synonym identifiers. Per-engine message wording differs. */
|
|
121
|
+
export const NO_DEPRECATED_SYNONYMS_RULE = {
|
|
122
|
+
id: "no-deprecated-synonyms",
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Declared divergences
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface EngineDivergence {
|
|
130
|
+
readonly rule: string;
|
|
131
|
+
/** Engine whose behaviour differs from the other(s). */
|
|
132
|
+
readonly engine: "eslint" | "biome" | "codemod";
|
|
133
|
+
/** Stable id the conformance test keys on. */
|
|
134
|
+
readonly divergence: string;
|
|
135
|
+
readonly detail: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deliberate cross-engine behaviour differences. The conformance test treats
|
|
140
|
+
* these as declared (allowed); any other difference is accidental drift.
|
|
141
|
+
*/
|
|
142
|
+
export const ENGINE_DIVERGENCES: readonly EngineDivergence[] = [
|
|
143
|
+
{
|
|
144
|
+
rule: "no-unsafe-unwrap",
|
|
145
|
+
engine: "biome",
|
|
146
|
+
divergence: "no-test-file-exemption",
|
|
147
|
+
detail:
|
|
148
|
+
"The ESLint rule skips *.spec.* / *.test.* files where unsafe unwraps are " +
|
|
149
|
+
"acceptable; GritQL patterns cannot read the file path, so the Biome rule " +
|
|
150
|
+
"flags test code too. Suppress with a biome-ignore comment or a linter " +
|
|
151
|
+
"override for test globs.",
|
|
152
|
+
},
|
|
153
|
+
];
|
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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { UNSAFE_UNWRAP_MEMBER_CALL_RE, UNSAFE_UNWRAP_MEMBER_RENAMES } from "./boundary-spec.js";
|
|
2
|
+
|
|
1
3
|
export const SKIP = new Set([
|
|
2
4
|
"node_modules",
|
|
3
5
|
"dist",
|
|
@@ -8,6 +10,7 @@ export const SKIP = new Set([
|
|
|
8
10
|
"build",
|
|
9
11
|
]);
|
|
10
12
|
export const CODE_EXT = /\.(ts|tsx|mts|cts)$/;
|
|
13
|
+
export const JSX_EXT = /\.(tsx|jsx)$/;
|
|
11
14
|
export const IMPORT_RE = /(from\s+|import\s*\(\s*)(['"])neverthrow\2/g;
|
|
12
15
|
export const COMPAT_SPEC = "@onrails/result/compat/neverthrow";
|
|
13
16
|
export const NATIVE_SPEC = "@onrails/result";
|
|
@@ -15,7 +18,7 @@ export const DEP_KEYS = ["dependencies", "devDependencies", "peerDependencies"]
|
|
|
15
18
|
export const TYPE_ONLY_NATIVE = new Set(["Result", "Ok", "Err", "UnexpectedError"]);
|
|
16
19
|
export const COMPAT_ONLY_PATTERNS = [
|
|
17
20
|
{ pattern: /\bResult\.(combine|fromThrowable)\b/, label: "Result static helper" },
|
|
18
|
-
{ pattern:
|
|
21
|
+
{ pattern: UNSAFE_UNWRAP_MEMBER_CALL_RE, label: "unsafe compat unwrap" },
|
|
19
22
|
] as const;
|
|
20
23
|
export const RESULT_SPECIFIC_CHAIN_METHODS = new Set([
|
|
21
24
|
"andThen",
|
|
@@ -56,8 +59,5 @@ export const TEE_METHODS = new Map([
|
|
|
56
59
|
["orTee", "tapErr"],
|
|
57
60
|
]);
|
|
58
61
|
export const PREDICATE_METHODS = new Set(["isOk", "isErr"]);
|
|
59
|
-
export const UNSAFE_UNWRAP_METHODS = new Map(
|
|
60
|
-
["_unsafeUnwrap", "unwrapOk"],
|
|
61
|
-
["_unsafeUnwrapErr", "unwrapErr"],
|
|
62
|
-
]);
|
|
62
|
+
export const UNSAFE_UNWRAP_METHODS = new Map(UNSAFE_UNWRAP_MEMBER_RENAMES);
|
|
63
63
|
export const SAFE_BASE_PATTERNS = [/^(ok|err)\s*\(/, /^Result\./, /^[a-zA-Z_$][\w$]*$/] as const;
|
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/index.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { main } from "./cli.js";
|
|
3
3
|
|
|
4
|
+
export {
|
|
5
|
+
DEPRECATED_SYNONYM_RENAMES,
|
|
6
|
+
DEPRECATED_SYNONYMS,
|
|
7
|
+
type DeprecatedSynonym,
|
|
8
|
+
ENGINE_DIVERGENCES,
|
|
9
|
+
type EngineDivergence,
|
|
10
|
+
NO_DEPRECATED_SYNONYMS_RULE,
|
|
11
|
+
NO_PROMISE_RESULT_RULE,
|
|
12
|
+
NO_UNSAFE_UNWRAP_RULE,
|
|
13
|
+
UNSAFE_UNWRAP_CALL_NAMES,
|
|
14
|
+
UNSAFE_UNWRAP_MEMBER_CALL_RE,
|
|
15
|
+
UNSAFE_UNWRAP_MEMBER_RENAMES,
|
|
16
|
+
UNSAFE_UNWRAP_NAMES,
|
|
17
|
+
} from "./boundary-spec.js";
|
|
4
18
|
export { rewriteCompatMethodChainsToNative } from "./chains.js";
|
|
5
19
|
export { main } from "./cli.js";
|
|
6
20
|
export { computeFileChange } from "./file-change.js";
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
// Helper to check for single statement return
|
|
4
|
+
export function getSingleReturnExpression(statement: ts.Statement): ts.Expression | undefined {
|
|
5
|
+
if (ts.isReturnStatement(statement)) {
|
|
6
|
+
return statement.expression;
|
|
7
|
+
}
|
|
8
|
+
if (ts.isBlock(statement)) {
|
|
9
|
+
if (statement.statements.length === 1) {
|
|
10
|
+
const single = statement.statements[0];
|
|
11
|
+
if (single && ts.isReturnStatement(single)) {
|
|
12
|
+
return single.expression;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Helper to check if body uses 'this' or 'arguments'
|
|
20
|
+
export function usesThisOrArguments(body: ts.Node): boolean {
|
|
21
|
+
let found = false;
|
|
22
|
+
const visit = (n: ts.Node) => {
|
|
23
|
+
if (found) return;
|
|
24
|
+
if (n.kind === ts.SyntaxKind.ThisKeyword) {
|
|
25
|
+
found = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (ts.isIdentifier(n) && n.text === "arguments") {
|
|
29
|
+
found = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
ts.isFunctionDeclaration(n) ||
|
|
34
|
+
ts.isFunctionExpression(n) ||
|
|
35
|
+
ts.isArrowFunction(n) ||
|
|
36
|
+
ts.isMethodDeclaration(n) ||
|
|
37
|
+
ts.isGetAccessorDeclaration(n) ||
|
|
38
|
+
ts.isSetAccessorDeclaration(n)
|
|
39
|
+
) {
|
|
40
|
+
return; // Stop recursion for nested scopes
|
|
41
|
+
}
|
|
42
|
+
ts.forEachChild(n, visit);
|
|
43
|
+
};
|
|
44
|
+
ts.forEachChild(body, visit);
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Helper to check if identifier referenced before pos
|
|
49
|
+
export function isIdentifierReferencedBefore(
|
|
50
|
+
name: string,
|
|
51
|
+
pos: number,
|
|
52
|
+
sf: ts.SourceFile,
|
|
53
|
+
): boolean {
|
|
54
|
+
let referenced = false;
|
|
55
|
+
const visit = (n: ts.Node) => {
|
|
56
|
+
if (referenced) return;
|
|
57
|
+
if (n.getStart(sf) >= pos) return;
|
|
58
|
+
if (ts.isIdentifier(n) && n.text === name) {
|
|
59
|
+
const parent = n.parent;
|
|
60
|
+
if (parent) {
|
|
61
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === n) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isPropertyAssignment(parent) && parent.name === n) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (ts.isMethodDeclaration(parent) && parent.name === n) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isPropertyDeclaration(parent) && parent.name === n) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (ts.isBindingElement(parent) && parent.propertyName === n) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
referenced = true;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
ts.forEachChild(n, visit);
|
|
81
|
+
};
|
|
82
|
+
ts.forEachChild(sf, visit);
|
|
83
|
+
return referenced;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getTypeParametersText(
|
|
87
|
+
node: ts.FunctionDeclaration | ts.FunctionExpression,
|
|
88
|
+
sf: ts.SourceFile,
|
|
89
|
+
): string {
|
|
90
|
+
if (node.typeParameters && node.typeParameters.length > 0) {
|
|
91
|
+
const params = node.typeParameters.map((p) => p.getText(sf));
|
|
92
|
+
return params.length === 1 ? `<${params[0]},>` : `<${params.join(", ")}>`;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const isSimpleExpression = (expr: ts.Expression): boolean =>
|
|
98
|
+
ts.isIdentifier(expr) ||
|
|
99
|
+
ts.isPropertyAccessExpression(expr) ||
|
|
100
|
+
ts.isElementAccessExpression(expr) ||
|
|
101
|
+
ts.isCallExpression(expr);
|
|
102
|
+
|
|
103
|
+
export function containsConditional(node: ts.Node): boolean {
|
|
104
|
+
if (ts.isConditionalExpression(node)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
let found = false;
|
|
108
|
+
ts.forEachChild(node, (child) => {
|
|
109
|
+
if (found) return;
|
|
110
|
+
if (containsConditional(child)) {
|
|
111
|
+
found = true;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A concise arrow body that is — or unwraps through `as` / `satisfies` to — an
|
|
118
|
+
// object literal must be parenthesized, else `{ … }` parses as a block body.
|
|
119
|
+
export function startsWithObjectLiteral(expr: ts.Expression): boolean {
|
|
120
|
+
let e: ts.Expression = expr;
|
|
121
|
+
while (ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) {
|
|
122
|
+
e = e.expression;
|
|
123
|
+
}
|
|
124
|
+
return ts.isObjectLiteralExpression(e);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getTernaryBranchText(node: ts.Expression, sf: ts.SourceFile): string {
|
|
128
|
+
let current = node;
|
|
129
|
+
let hasParens = false;
|
|
130
|
+
while (ts.isParenthesizedExpression(current)) {
|
|
131
|
+
current = current.expression;
|
|
132
|
+
hasParens = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let unwrapped: ts.Expression | undefined;
|
|
136
|
+
if (ts.isJsxFragment(current)) {
|
|
137
|
+
const children = current.children;
|
|
138
|
+
const nonTriviaChildren = children.filter((child) => {
|
|
139
|
+
if (ts.isJsxText(child)) {
|
|
140
|
+
return !child.containsOnlyTriviaWhiteSpaces;
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
if (nonTriviaChildren.length === 1) {
|
|
145
|
+
const soleChild = nonTriviaChildren[0];
|
|
146
|
+
if (soleChild && ts.isJsxExpression(soleChild) && soleChild.expression) {
|
|
147
|
+
unwrapped = soleChild.expression;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else if (ts.isJsxElement(current)) {
|
|
151
|
+
const tagName = current.openingElement.tagName.getText(sf);
|
|
152
|
+
if (tagName === "Fragment" || tagName === "React.Fragment" || tagName.endsWith(".Fragment")) {
|
|
153
|
+
if (current.openingElement.attributes.properties.length === 0) {
|
|
154
|
+
const children = current.children;
|
|
155
|
+
const nonTriviaChildren = children.filter((child) => {
|
|
156
|
+
if (ts.isJsxText(child)) {
|
|
157
|
+
return !child.containsOnlyTriviaWhiteSpaces;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
if (nonTriviaChildren.length === 1) {
|
|
162
|
+
const soleChild = nonTriviaChildren[0];
|
|
163
|
+
if (soleChild && ts.isJsxExpression(soleChild) && soleChild.expression) {
|
|
164
|
+
unwrapped = soleChild.expression;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (unwrapped) {
|
|
172
|
+
const unwrappedText = getTernaryBranchText(unwrapped, sf);
|
|
173
|
+
return hasParens ? `(${unwrappedText})` : unwrappedText;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return node.getText(sf);
|
|
177
|
+
}
|
package/src/tersify.ts
CHANGED
|
@@ -3,118 +3,18 @@ import ts from "typescript";
|
|
|
3
3
|
import { edit, spanEdit, walkSource } from "./ast.js";
|
|
4
4
|
import { applyEditStep, byStartDesc } from "./chains.js";
|
|
5
5
|
import { scanIfToSwitchSequences } from "./switch.js";
|
|
6
|
+
import {
|
|
7
|
+
containsConditional,
|
|
8
|
+
getSingleReturnExpression,
|
|
9
|
+
getTernaryBranchText,
|
|
10
|
+
getTypeParametersText,
|
|
11
|
+
isIdentifierReferencedBefore,
|
|
12
|
+
isSimpleExpression,
|
|
13
|
+
startsWithObjectLiteral,
|
|
14
|
+
usesThisOrArguments,
|
|
15
|
+
} from "./tersify-utils.js";
|
|
6
16
|
import type { Edit } from "./types.js";
|
|
7
17
|
|
|
8
|
-
// Helper to check for single statement return
|
|
9
|
-
function getSingleReturnExpression(statement: ts.Statement): ts.Expression | undefined {
|
|
10
|
-
if (ts.isReturnStatement(statement)) {
|
|
11
|
-
return statement.expression;
|
|
12
|
-
}
|
|
13
|
-
if (ts.isBlock(statement)) {
|
|
14
|
-
if (statement.statements.length === 1) {
|
|
15
|
-
const single = statement.statements[0];
|
|
16
|
-
if (single && ts.isReturnStatement(single)) {
|
|
17
|
-
return single.expression;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Helper to check if body uses 'this' or 'arguments'
|
|
25
|
-
function usesThisOrArguments(body: ts.Node): boolean {
|
|
26
|
-
let found = false;
|
|
27
|
-
const visit = (n: ts.Node) => {
|
|
28
|
-
if (found) return;
|
|
29
|
-
if (n.kind === ts.SyntaxKind.ThisKeyword) {
|
|
30
|
-
found = true;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (ts.isIdentifier(n) && n.text === "arguments") {
|
|
34
|
-
found = true;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (
|
|
38
|
-
ts.isFunctionDeclaration(n) ||
|
|
39
|
-
ts.isFunctionExpression(n) ||
|
|
40
|
-
ts.isArrowFunction(n) ||
|
|
41
|
-
ts.isMethodDeclaration(n) ||
|
|
42
|
-
ts.isGetAccessorDeclaration(n) ||
|
|
43
|
-
ts.isSetAccessorDeclaration(n)
|
|
44
|
-
) {
|
|
45
|
-
return; // Stop recursion for nested scopes
|
|
46
|
-
}
|
|
47
|
-
ts.forEachChild(n, visit);
|
|
48
|
-
};
|
|
49
|
-
ts.forEachChild(body, visit);
|
|
50
|
-
return found;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Helper to check if identifier referenced before pos
|
|
54
|
-
function isIdentifierReferencedBefore(name: string, pos: number, sf: ts.SourceFile): boolean {
|
|
55
|
-
let referenced = false;
|
|
56
|
-
const visit = (n: ts.Node) => {
|
|
57
|
-
if (referenced) return;
|
|
58
|
-
if (n.getStart(sf) >= pos) return;
|
|
59
|
-
if (ts.isIdentifier(n) && n.text === name) {
|
|
60
|
-
const parent = n.parent;
|
|
61
|
-
if (parent) {
|
|
62
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === n) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (ts.isPropertyAssignment(parent) && parent.name === n) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (ts.isMethodDeclaration(parent) && parent.name === n) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === n) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (ts.isBindingElement(parent) && parent.propertyName === n) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
referenced = true;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
ts.forEachChild(n, visit);
|
|
82
|
-
};
|
|
83
|
-
ts.forEachChild(sf, visit);
|
|
84
|
-
return referenced;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getTypeParametersText(
|
|
88
|
-
node: ts.FunctionDeclaration | ts.FunctionExpression,
|
|
89
|
-
sf: ts.SourceFile,
|
|
90
|
-
): string {
|
|
91
|
-
if (node.typeParameters && node.typeParameters.length > 0) {
|
|
92
|
-
const params = node.typeParameters.map((p) => p.getText(sf));
|
|
93
|
-
return params.length === 1 ? `<${params[0]},>` : `<${params.join(", ")}>`;
|
|
94
|
-
}
|
|
95
|
-
return "";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const isSimpleExpression = (expr: ts.Expression): boolean =>
|
|
99
|
-
ts.isIdentifier(expr) ||
|
|
100
|
-
ts.isPropertyAccessExpression(expr) ||
|
|
101
|
-
ts.isElementAccessExpression(expr) ||
|
|
102
|
-
ts.isCallExpression(expr);
|
|
103
|
-
|
|
104
|
-
function containsConditional(node: ts.Node): boolean {
|
|
105
|
-
if (ts.isConditionalExpression(node)) {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
let found = false;
|
|
109
|
-
ts.forEachChild(node, (child) => {
|
|
110
|
-
if (found) return;
|
|
111
|
-
if (containsConditional(child)) {
|
|
112
|
-
found = true;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return found;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
18
|
function tryTersifyIfElse(node: ts.Node, sf: ts.SourceFile): Maybe<Edit> {
|
|
119
19
|
if (ts.isIfStatement(node) && node.elseStatement) {
|
|
120
20
|
const x = getSingleReturnExpression(node.thenStatement);
|
|
@@ -138,24 +38,14 @@ function tryTersifyIfElse(node: ts.Node, sf: ts.SourceFile): Maybe<Edit> {
|
|
|
138
38
|
return some(spanEdit(node, sf, edit(text)));
|
|
139
39
|
}
|
|
140
40
|
|
|
141
|
-
const xText = x
|
|
142
|
-
const yText = y
|
|
41
|
+
const xText = getTernaryBranchText(x, sf);
|
|
42
|
+
const yText = getTernaryBranchText(y, sf);
|
|
143
43
|
return some(spanEdit(node, sf, edit(`return ${condText} ? ${xText} : ${yText};`)));
|
|
144
44
|
}
|
|
145
45
|
}
|
|
146
46
|
return none();
|
|
147
47
|
}
|
|
148
48
|
|
|
149
|
-
// A concise arrow body that is — or unwraps through `as` / `satisfies` to — an
|
|
150
|
-
// object literal must be parenthesized, else `{ … }` parses as a block body.
|
|
151
|
-
function startsWithObjectLiteral(expr: ts.Expression): boolean {
|
|
152
|
-
let e: ts.Expression = expr;
|
|
153
|
-
while (ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) {
|
|
154
|
-
e = e.expression;
|
|
155
|
-
}
|
|
156
|
-
return ts.isObjectLiteralExpression(e);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
49
|
function tryTersifyArrowBlock(node: ts.Node, sf: ts.SourceFile): Maybe<Edit> {
|
|
160
50
|
if (ts.isArrowFunction(node) && ts.isBlock(node.body)) {
|
|
161
51
|
const expr = getSingleReturnExpression(node.body);
|
|
@@ -353,8 +243,8 @@ function tryTersifyTernaryOrBooleanSequence(
|
|
|
353
243
|
const needsParens = !isSimpleExpression(stmt.expression);
|
|
354
244
|
text = needsParens ? `return !(${condText});` : `return !${condText};`;
|
|
355
245
|
} else {
|
|
356
|
-
const xText = x
|
|
357
|
-
const yText = y
|
|
246
|
+
const xText = getTernaryBranchText(x, sf);
|
|
247
|
+
const yText = getTernaryBranchText(y, sf);
|
|
358
248
|
text = `return ${condText} ? ${xText} : ${yText};`;
|
|
359
249
|
}
|
|
360
250
|
|
|
@@ -422,29 +312,33 @@ const TRANSFORMS: ReadonlyArray<(node: ts.Node, sf: ts.SourceFile) => Maybe<Edit
|
|
|
422
312
|
tryTersifyIdentityFilter,
|
|
423
313
|
];
|
|
424
314
|
|
|
425
|
-
function tersifyOnce(src: string): string {
|
|
315
|
+
function tersifyOnce(src: string, jsx: boolean): string {
|
|
426
316
|
const edits: Edit[] = [];
|
|
427
317
|
const skippedNodes = new Set<ts.Node>();
|
|
428
318
|
|
|
429
|
-
walkSource(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
319
|
+
walkSource(
|
|
320
|
+
src,
|
|
321
|
+
(node, sf) => {
|
|
322
|
+
if (skippedNodes.has(node)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
433
325
|
|
|
434
|
-
|
|
435
|
-
|
|
326
|
+
scanIfReturnSequences(node, sf, skippedNodes, edits);
|
|
327
|
+
scanIfToSwitchSequences(node, sf, skippedNodes, edits);
|
|
436
328
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
329
|
+
for (const transform of TRANSFORMS) {
|
|
330
|
+
const editResult = transform(node, sf);
|
|
331
|
+
if (editResult._tag === "Some") {
|
|
332
|
+
edits.push(editResult.value);
|
|
333
|
+
skippedNodes.add(node);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
443
336
|
}
|
|
444
|
-
}
|
|
445
337
|
|
|
446
|
-
|
|
447
|
-
|
|
338
|
+
return false;
|
|
339
|
+
},
|
|
340
|
+
jsx,
|
|
341
|
+
);
|
|
448
342
|
|
|
449
343
|
if (edits.length === 0) return src;
|
|
450
344
|
|
|
@@ -455,11 +349,11 @@ function tersifyOnce(src: string): string {
|
|
|
455
349
|
return next;
|
|
456
350
|
}
|
|
457
351
|
|
|
458
|
-
export function tersify(src: string): string {
|
|
352
|
+
export function tersify(src: string, jsx = false): string {
|
|
459
353
|
let current = src;
|
|
460
354
|
let iterations = 0;
|
|
461
355
|
while (iterations < 10) {
|
|
462
|
-
const next = tersifyOnce(current);
|
|
356
|
+
const next = tersifyOnce(current, jsx);
|
|
463
357
|
if (next === current) {
|
|
464
358
|
break;
|
|
465
359
|
}
|
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) =>
|