@onrails/codemod 0.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onrails/codemod",
3
- "version": "0.2.1",
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.1.4",
46
- "@onrails/pattern": "0.1.4",
47
- "@onrails/result": "0.1.4",
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": {
@@ -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/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",
@@ -16,7 +18,7 @@ export const DEP_KEYS = ["dependencies", "devDependencies", "peerDependencies"]
16
18
  export const TYPE_ONLY_NATIVE = new Set(["Result", "Ok", "Err", "UnexpectedError"]);
17
19
  export const COMPAT_ONLY_PATTERNS = [
18
20
  { pattern: /\bResult\.(combine|fromThrowable)\b/, label: "Result static helper" },
19
- { pattern: /\._unsafeUnwrap(Err)?\s*\(/, label: "unsafe compat unwrap" },
21
+ { pattern: UNSAFE_UNWRAP_MEMBER_CALL_RE, label: "unsafe compat unwrap" },
20
22
  ] as const;
21
23
  export const RESULT_SPECIFIC_CHAIN_METHODS = new Set([
22
24
  "andThen",
@@ -57,8 +59,5 @@ export const TEE_METHODS = new Map([
57
59
  ["orTee", "tapErr"],
58
60
  ]);
59
61
  export const PREDICATE_METHODS = new Set(["isOk", "isErr"]);
60
- export const UNSAFE_UNWRAP_METHODS = new Map([
61
- ["_unsafeUnwrap", "unwrapOk"],
62
- ["_unsafeUnwrapErr", "unwrapErr"],
63
- ]);
62
+ export const UNSAFE_UNWRAP_METHODS = new Map(UNSAFE_UNWRAP_MEMBER_RENAMES);
64
63
  export const SAFE_BASE_PATTERNS = [/^(ok|err)\s*\(/, /^Result\./, /^[a-zA-Z_$][\w$]*$/] as const;
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.getText(sf);
142
- const yText = y.getText(sf);
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.getText(sf);
357
- const yText = y.getText(sf);
246
+ const xText = getTernaryBranchText(x, sf);
247
+ const yText = getTernaryBranchText(y, sf);
358
248
  text = `return ${condText} ? ${xText} : ${yText};`;
359
249
  }
360
250