miniread 1.48.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/transforms/_generated/manifest.js +15 -4
  2. package/dist/transforms/_generated/registry.js +4 -0
  3. package/dist/transforms/rename-object-keys-variables/manifest.json +2 -2
  4. package/dist/transforms/rename-promise-executor-parameters/manifest.json +3 -2
  5. package/dist/transforms/rename-promise-executor-parameters-v2/is-valid-binding-identifier.d.ts +1 -0
  6. package/dist/transforms/rename-promise-executor-parameters-v2/is-valid-binding-identifier.js +13 -0
  7. package/dist/transforms/rename-promise-executor-parameters-v2/manifest.json +20 -0
  8. package/dist/transforms/rename-promise-executor-parameters-v2/promise-executor-heuristics.d.ts +8 -0
  9. package/dist/transforms/rename-promise-executor-parameters-v2/promise-executor-heuristics.js +145 -0
  10. package/dist/transforms/rename-promise-executor-parameters-v2/rename-binding-if-safe.d.ts +2 -0
  11. package/dist/transforms/rename-promise-executor-parameters-v2/rename-binding-if-safe.js +43 -0
  12. package/dist/transforms/rename-promise-executor-parameters-v2/rename-promise-executor-parameters-v2-transform.d.ts +2 -0
  13. package/dist/transforms/rename-promise-executor-parameters-v2/rename-promise-executor-parameters-v2-transform.js +127 -0
  14. package/dist/transforms/rename-string-split-variables/get-split-helper-info.d.ts +8 -0
  15. package/dist/transforms/rename-string-split-variables/get-split-helper-info.js +40 -0
  16. package/dist/transforms/rename-string-split-variables/manifest.json +13 -0
  17. package/dist/transforms/rename-string-split-variables/rename-string-split-variables-transform.d.ts +2 -0
  18. package/dist/transforms/rename-string-split-variables/rename-string-split-variables-transform.js +69 -0
  19. package/dist/transforms/rename-string-split-variables/split-helper-declarations.d.ts +11 -0
  20. package/dist/transforms/rename-string-split-variables/split-helper-declarations.js +80 -0
  21. package/dist/transforms/rename-string-split-variables/split-helper-guards.d.ts +8 -0
  22. package/dist/transforms/rename-string-split-variables/split-helper-guards.js +73 -0
  23. package/dist/transforms/rename-string-split-variables/split-helper-loop.d.ts +3 -0
  24. package/dist/transforms/rename-string-split-variables/split-helper-loop.js +116 -0
  25. package/dist/transforms/rename-string-split-variables/split-helper-tail-expressions.d.ts +3 -0
  26. package/dist/transforms/rename-string-split-variables/split-helper-tail-expressions.js +86 -0
  27. package/dist/transforms/rename-string-split-variables/split-helper-tail-tests.d.ts +4 -0
  28. package/dist/transforms/rename-string-split-variables/split-helper-tail-tests.js +39 -0
  29. package/dist/transforms/rename-string-split-variables/split-helper-tail.d.ts +3 -0
  30. package/dist/transforms/rename-string-split-variables/split-helper-tail.js +49 -0
  31. package/dist/transforms/rename-use-reference-sets/manifest.json +2 -2
  32. package/package.json +1 -1
@@ -138,7 +138,7 @@ const manifestData = {
138
138
  "rename-object-keys-variables": {
139
139
  recommended: true,
140
140
  notes: "Measured with baseline none: 0.01%. Added to recommended for readability.",
141
- evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 99.99244056393393, "evaluatedAt": "2026-01-26T07:04:46.335Z", "changedLines": 602, "durationSeconds": 121.114895708, "stableNames": 1358 } },
141
+ evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 99.99244056393393, "evaluatedAt": "2026-01-27T06:42:06.193Z", "changedLines": 602, "durationSeconds": 165.514278459, "stableNames": 1358 } },
142
142
  },
143
143
  "rename-parameters-to-match-properties": {
144
144
  recommended: false,
@@ -157,10 +157,16 @@ const manifestData = {
157
157
  evaluations: { "legacy:evaluation": { "evaluatedAt": "2026-01-25T20:07:50.099Z", "changedLines": 8, "durationSeconds": 0, "stableNames": 0, "diffSizePercent": 100 }, "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-25T23:25:19.956Z", "changedLines": 8, "durationSeconds": 44.331720583, "stableNames": 1360 } },
158
158
  },
159
159
  "rename-promise-executor-parameters": {
160
- recommended: true,
161
- notes: "Auto-added by evaluation script.",
160
+ recommended: false,
161
+ notes: "Superseded by rename-promise-executor-parameters-v2. Known issue: can produce incorrect output for duplicate parameter names in non-strict scripts (e.g. `function (a, a) {}`).",
162
+ supersededBy: "rename-promise-executor-parameters-v2",
162
163
  evaluations: { "legacy:evaluation": { "evaluatedAt": "2026-01-25T20:08:45.593Z", "changedLines": 970, "durationSeconds": 0, "stableNames": 0, "diffSizePercent": 100 }, "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-25T23:26:26.589Z", "changedLines": 970, "durationSeconds": 63.099434166, "stableNames": 1359 } },
163
164
  },
165
+ "rename-promise-executor-parameters-v2": {
166
+ recommended: true,
167
+ notes: "Supersedes rename-promise-executor-parameters with improved error-handler detection. Intentionally self-contained (helpers duplicated) per docs/workflow.md §3.6.",
168
+ evaluations: { "legacy:evaluation": { "diffSizePercent": 100, "evaluatedAt": "2026-01-25T21:22:01.643Z", "changedLines": 1086, "durationSeconds": 0, "stableNames": 0 }, "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 99.99622028196697, "evaluatedAt": "2026-01-26T07:04:42.067Z", "changedLines": 1086, "durationSeconds": 129.947271542, "stableNames": 1359 } },
169
+ },
164
170
  "rename-range-parameters": {
165
171
  recommended: true,
166
172
  notes: "Renames range-style parameters when the count defaults to the start value and start resets to 0.",
@@ -185,6 +191,11 @@ const manifestData = {
185
191
  notes: "Negative diff reduction in evaluation; keep optional.",
186
192
  evaluations: { "legacy:evaluation": { "diffSizePercent": 100.00188483649043, "evaluatedAt": "2026-01-25T10:04:22.417Z", "changedLines": 0, "durationSeconds": 0, "stableNames": 0 } },
187
193
  },
194
+ "rename-string-split-variables": {
195
+ recommended: true,
196
+ notes: "Measured with baseline none: 0.00%. Added to recommended for readability.",
197
+ evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-27T06:41:49.624Z", "changedLines": 0, "durationSeconds": 149.03264720899998, "stableNames": 1357 } },
198
+ },
188
199
  "rename-this-aliases": {
189
200
  recommended: true,
190
201
  notes: "Measured with baseline none: 0.00%. Improves readability by stabilizing `this` aliases used for closures.",
@@ -236,7 +247,7 @@ const manifestData = {
236
247
  },
237
248
  "rename-use-reference-sets": {
238
249
  recommended: true,
239
- evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-26T08:03:28.285Z", "changedLines": 24, "durationSeconds": 146.202273464, "stableNames": 1358 } },
250
+ evaluations: { "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-27T06:41:50.482Z", "changedLines": 24, "durationSeconds": 149.98115670800001, "stableNames": 1358 } },
240
251
  },
241
252
  "simplify-boolean-negations": {
242
253
  recommended: true,
@@ -31,11 +31,13 @@ import { renameParametersToMatchPropertiesTransform } from "../rename-parameters
31
31
  import { renameParametersToMatchPropertiesV2Transform } from "../rename-parameters-to-match-properties-v2/rename-parameters-to-match-properties-v2-transform.js";
32
32
  import { renameProcessStdoutHandlersTransform } from "../rename-process-stdout-handlers/rename-process-stdout-handlers-transform.js";
33
33
  import { renamePromiseExecutorParametersTransform } from "../rename-promise-executor-parameters/rename-promise-executor-parameters-transform.js";
34
+ import { renamePromiseExecutorParametersV2Transform } from "../rename-promise-executor-parameters-v2/rename-promise-executor-parameters-v2-transform.js";
34
35
  import { renameRangeParametersTransform } from "../rename-range-parameters/rename-range-parameters-transform.js";
35
36
  import { renameRegexBuildersTransform } from "../rename-regex-builders/rename-regex-builders-transform.js";
36
37
  import { renameReplaceChildParametersTransform } from "../rename-replace-child-parameters/rename-replace-child-parameters-transform.js";
37
38
  import { renameRestParametersTransform } from "../rename-rest-parameters/rename-rest-parameters-transform.js";
38
39
  import { renameSafePropertyAccessorParametersTransform } from "../rename-safe-property-accessor-parameters/rename-safe-property-accessor-parameters-transform.js";
40
+ import { renameStringSplitVariablesTransform } from "../rename-string-split-variables/rename-string-split-variables-transform.js";
39
41
  import { renameThisAliasesTransform } from "../rename-this-aliases/rename-this-aliases-transform.js";
40
42
  import { renameTimeoutDurationParametersTransform } from "../rename-timeout-duration-parameters/rename-timeout-duration-parameters-transform.js";
41
43
  import { renameTimeoutIdsTransform } from "../rename-timeout-ids/rename-timeout-ids-transform.js";
@@ -84,11 +86,13 @@ export const transformRegistry = {
84
86
  [renameParametersToMatchPropertiesV2Transform.id]: renameParametersToMatchPropertiesV2Transform,
85
87
  [renameProcessStdoutHandlersTransform.id]: renameProcessStdoutHandlersTransform,
86
88
  [renamePromiseExecutorParametersTransform.id]: renamePromiseExecutorParametersTransform,
89
+ [renamePromiseExecutorParametersV2Transform.id]: renamePromiseExecutorParametersV2Transform,
87
90
  [renameRangeParametersTransform.id]: renameRangeParametersTransform,
88
91
  [renameRegexBuildersTransform.id]: renameRegexBuildersTransform,
89
92
  [renameReplaceChildParametersTransform.id]: renameReplaceChildParametersTransform,
90
93
  [renameRestParametersTransform.id]: renameRestParametersTransform,
91
94
  [renameSafePropertyAccessorParametersTransform.id]: renameSafePropertyAccessorParametersTransform,
95
+ [renameStringSplitVariablesTransform.id]: renameStringSplitVariablesTransform,
92
96
  [renameThisAliasesTransform.id]: renameThisAliasesTransform,
93
97
  [renameTimeoutDurationParametersTransform.id]: renameTimeoutDurationParametersTransform,
94
98
  [renameTimeoutIdsTransform.id]: renameTimeoutIdsTransform,
@@ -3,9 +3,9 @@
3
3
  "evaluations": {
4
4
  "claude-code-2.1.10:claude-code-2.1.11": {
5
5
  "diffSizePercent": 99.99244056393393,
6
- "evaluatedAt": "2026-01-26T07:04:46.335Z",
6
+ "evaluatedAt": "2026-01-27T06:42:06.193Z",
7
7
  "changedLines": 602,
8
- "durationSeconds": 121.114895708,
8
+ "durationSeconds": 165.514278459,
9
9
  "stableNames": 1358
10
10
  }
11
11
  },
@@ -1,5 +1,5 @@
1
1
  {
2
- "recommended": true,
2
+ "recommended": false,
3
3
  "evaluations": {
4
4
  "legacy:evaluation": {
5
5
  "evaluatedAt": "2026-01-25T20:08:45.593Z",
@@ -16,5 +16,6 @@
16
16
  "stableNames": 1359
17
17
  }
18
18
  },
19
- "notes": "Auto-added by evaluation script."
19
+ "notes": "Superseded by rename-promise-executor-parameters-v2. Known issue: can produce incorrect output for duplicate parameter names in non-strict scripts (e.g. `function (a, a) {}`).",
20
+ "supersededBy": "rename-promise-executor-parameters-v2"
20
21
  }
@@ -0,0 +1 @@
1
+ export declare const isValidBindingIdentifier: (name: string) => boolean;
@@ -0,0 +1,13 @@
1
+ import { isIdentifierName, isKeyword, isStrictBindReservedWord, } from "@babel/helper-validator-identifier";
2
+ export const isValidBindingIdentifier = (name) => {
3
+ if (!isIdentifierName(name))
4
+ return false;
5
+ if (isKeyword(name))
6
+ return false;
7
+ // Parse uses `sourceType: "unambiguous"` which often yields `script`; in that mode,
8
+ // some module-only reserved words (e.g. `await`) can be valid identifiers.
9
+ // When code is parsed as a module, invalid bindings are rejected by the parser anyway.
10
+ if (isStrictBindReservedWord(name, false))
11
+ return false;
12
+ return true;
13
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "recommended": true,
3
+ "notes": "Supersedes rename-promise-executor-parameters with improved error-handler detection. Intentionally self-contained (helpers duplicated) per docs/workflow.md §3.6.",
4
+ "evaluations": {
5
+ "legacy:evaluation": {
6
+ "diffSizePercent": 100,
7
+ "evaluatedAt": "2026-01-25T21:22:01.643Z",
8
+ "changedLines": 1086,
9
+ "durationSeconds": 0,
10
+ "stableNames": 0
11
+ },
12
+ "claude-code-2.1.10:claude-code-2.1.11": {
13
+ "diffSizePercent": 99.99622028196697,
14
+ "evaluatedAt": "2026-01-26T07:04:42.067Z",
15
+ "changedLines": 1086,
16
+ "durationSeconds": 129.947271542,
17
+ "stableNames": 1359
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,8 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { ArrowFunctionExpression, FunctionExpression, NewExpression } from "@babel/types";
3
+ export declare const getExecutorFunctionIfEligible: (path: NodePath<NewExpression>) => NodePath<FunctionExpression | ArrowFunctionExpression> | undefined;
4
+ export declare const isDirectCallOfBinding: (referencePath: NodePath) => boolean;
5
+ export declare const isAssignedToOnHandlerProperty: (referencePath: NodePath) => boolean;
6
+ export declare const isErrorEventHandlerArgument: (referencePath: NodePath) => boolean;
7
+ export declare const isErrorEventHandlerRemovalArgument: (referencePath: NodePath) => boolean;
8
+ export declare const wouldShadowReferencedOuterBinding: (executorPath: NodePath<FunctionExpression | ArrowFunctionExpression>, targetName: string) => boolean;
@@ -0,0 +1,145 @@
1
+ const allowedOnHandlerPropertyNames = new Set([
2
+ "onabort",
3
+ "onerror",
4
+ "ontimeout",
5
+ ]);
6
+ const allowedErrorEventNames = new Set(["error"]);
7
+ const allowedErrorListenerAddMethodNames = new Set([
8
+ "addListener",
9
+ "addEventListener",
10
+ "on",
11
+ "once",
12
+ "prependListener",
13
+ "prependOnceListener",
14
+ ]);
15
+ const allowedErrorListenerRemoveMethodNames = new Set([
16
+ "off",
17
+ "removeEventListener",
18
+ "removeListener",
19
+ ]);
20
+ export const getExecutorFunctionIfEligible = (path) => {
21
+ // Intentionally does NOT require the constructor callee to be `Promise`.
22
+ // Bundled/minified code frequently uses Promise polyfills like `new K((a,b)=>...)`,
23
+ // so we treat the executor signature + usage heuristics as the "promise-like" signal.
24
+ if (path.node.arguments.length !== 1)
25
+ return;
26
+ const executor = path.get("arguments.0");
27
+ if (!executor.isFunctionExpression() &&
28
+ !executor.isArrowFunctionExpression()) {
29
+ return;
30
+ }
31
+ const parameters = executor.node.params;
32
+ if (parameters.length !== 2)
33
+ return;
34
+ const [firstParameter, secondParameter] = parameters;
35
+ if (!firstParameter || !secondParameter)
36
+ return;
37
+ if (firstParameter.type !== "Identifier")
38
+ return;
39
+ if (secondParameter.type !== "Identifier")
40
+ return;
41
+ // v1 checked that the *source* parameter names are valid identifiers, but this is redundant:
42
+ // if Babel produced Identifier nodes under `sourceType: "unambiguous"` parsing, then the names
43
+ // are already valid for that parsed sourceType (often `script` for minified bundles).
44
+ // We only validate that the *target* stable names are safe to introduce (see canRenameBindingSafely).
45
+ return executor;
46
+ };
47
+ export const isDirectCallOfBinding = (referencePath) => {
48
+ const parentPath = referencePath.parentPath;
49
+ if (!parentPath?.isCallExpression())
50
+ return false;
51
+ return referencePath.key === "callee";
52
+ };
53
+ export const isAssignedToOnHandlerProperty = (referencePath) => {
54
+ const parentPath = referencePath.parentPath;
55
+ if (!parentPath?.isAssignmentExpression())
56
+ return false;
57
+ if (parentPath.node.operator !== "=")
58
+ return false;
59
+ if (referencePath.key !== "right")
60
+ return false;
61
+ const left = parentPath.node.left;
62
+ if (left.type !== "MemberExpression")
63
+ return false;
64
+ if (left.computed)
65
+ return false;
66
+ if (left.property.type !== "Identifier")
67
+ return false;
68
+ if (!allowedOnHandlerPropertyNames.has(left.property.name))
69
+ return false;
70
+ return true;
71
+ };
72
+ export const isErrorEventHandlerArgument = (referencePath) => {
73
+ const parentPath = referencePath.parentPath;
74
+ if (!parentPath?.isCallExpression())
75
+ return false;
76
+ if (referencePath.listKey !== "arguments")
77
+ return false;
78
+ if (typeof referencePath.key !== "number")
79
+ return false;
80
+ if (referencePath.key !== 1)
81
+ return false;
82
+ const firstArgument = parentPath.node.arguments[0];
83
+ if (firstArgument?.type !== "StringLiteral")
84
+ return false;
85
+ if (!allowedErrorEventNames.has(firstArgument.value))
86
+ return false;
87
+ const callee = parentPath.node.callee;
88
+ if (callee.type !== "MemberExpression")
89
+ return false;
90
+ if (callee.computed)
91
+ return false;
92
+ if (callee.property.type !== "Identifier")
93
+ return false;
94
+ if (!allowedErrorListenerAddMethodNames.has(callee.property.name))
95
+ return false;
96
+ return true;
97
+ };
98
+ export const isErrorEventHandlerRemovalArgument = (referencePath) => {
99
+ const parentPath = referencePath.parentPath;
100
+ if (!parentPath?.isCallExpression())
101
+ return false;
102
+ if (referencePath.listKey !== "arguments")
103
+ return false;
104
+ if (typeof referencePath.key !== "number")
105
+ return false;
106
+ if (referencePath.key !== 1)
107
+ return false;
108
+ const firstArgument = parentPath.node.arguments[0];
109
+ if (firstArgument?.type !== "StringLiteral")
110
+ return false;
111
+ if (!allowedErrorEventNames.has(firstArgument.value))
112
+ return false;
113
+ const callee = parentPath.node.callee;
114
+ if (callee.type !== "MemberExpression")
115
+ return false;
116
+ if (callee.computed)
117
+ return false;
118
+ if (callee.property.type !== "Identifier")
119
+ return false;
120
+ if (!allowedErrorListenerRemoveMethodNames.has(callee.property.name)) {
121
+ return false;
122
+ }
123
+ return true;
124
+ };
125
+ export const wouldShadowReferencedOuterBinding = (executorPath, targetName) => {
126
+ const parentBinding = executorPath.scope.getBinding(targetName);
127
+ if (!parentBinding)
128
+ return false;
129
+ let foundReference = false;
130
+ executorPath.traverse({
131
+ Identifier(identifierPath) {
132
+ if (foundReference)
133
+ return;
134
+ if (identifierPath.node.name !== targetName)
135
+ return;
136
+ if (!identifierPath.isReferencedIdentifier())
137
+ return;
138
+ if (identifierPath.scope.getBinding(targetName) !== parentBinding)
139
+ return;
140
+ foundReference = true;
141
+ identifierPath.stop();
142
+ },
143
+ });
144
+ return foundReference;
145
+ };
@@ -0,0 +1,2 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ export declare const canRenameBindingSafely: (bindingScope: NodePath["scope"], fromName: string, toName: string) => boolean;
@@ -0,0 +1,43 @@
1
+ import { isValidBindingIdentifier } from "./is-valid-binding-identifier.js";
2
+ const hasIntermediateBinding = (fromScope, upToScope, targetName) => {
3
+ const visited = new Set();
4
+ let scope = fromScope;
5
+ while (scope && scope !== upToScope) {
6
+ // Cycles should never happen in Babel scope chains; if one does, be conservative and
7
+ // assume shadowing risk so we don't apply an unsafe rename.
8
+ if (visited.has(scope))
9
+ return true;
10
+ visited.add(scope);
11
+ if (scope.hasOwnBinding(targetName))
12
+ return true;
13
+ scope = scope.parent;
14
+ }
15
+ return false;
16
+ };
17
+ const hasShadowingRisk = (binding, bindingScope, targetName) => binding.referencePaths.some((referencePath) => {
18
+ if (referencePath.scope === bindingScope)
19
+ return false;
20
+ // Only consider bindings between this reference’s scope and the executor’s binding scope.
21
+ // This avoids incorrectly blocking safe renames due to unrelated outer bindings (e.g., an
22
+ // outer `resolve` that is never referenced inside the executor).
23
+ return hasIntermediateBinding(referencePath.scope, bindingScope, targetName);
24
+ });
25
+ export const canRenameBindingSafely = (bindingScope, fromName, toName) => {
26
+ if (fromName === toName)
27
+ return false;
28
+ if (!isValidBindingIdentifier(toName))
29
+ return false;
30
+ if (bindingScope.hasOwnBinding(toName))
31
+ return false;
32
+ const programScope = bindingScope.getProgramParent();
33
+ // `globals` contains only referenced (unbound) globals for this file, so this does not block
34
+ // in files where the identifier is never used.
35
+ if (Object.hasOwn(programScope.globals, toName))
36
+ return false;
37
+ const binding = bindingScope.getBinding(fromName);
38
+ if (!binding)
39
+ return false;
40
+ if (hasShadowingRisk(binding, bindingScope, toName))
41
+ return false;
42
+ return true;
43
+ };
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renamePromiseExecutorParametersV2Transform: Transform;
@@ -0,0 +1,127 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ import { getExecutorFunctionIfEligible, isAssignedToOnHandlerProperty, isDirectCallOfBinding, isErrorEventHandlerArgument, isErrorEventHandlerRemovalArgument, wouldShadowReferencedOuterBinding, } from "./promise-executor-heuristics.js";
5
+ import { canRenameBindingSafely } from "./rename-binding-if-safe.js";
6
+ const STABLE_PREFIX = "$";
7
+ const require = createRequire(import.meta.url);
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
9
+ const traverse = require("@babel/traverse").default;
10
+ export const renamePromiseExecutorParametersV2Transform = {
11
+ id: "rename-promise-executor-parameters-v2",
12
+ description: "Renames promise-like executor parameters to $resolve/$reject when usage is high-confidence, including error event handlers",
13
+ scope: "file",
14
+ parallelizable: true,
15
+ transform(context) {
16
+ let nodesVisited = 0;
17
+ let transformationsApplied = 0;
18
+ for (const fileInfo of getFilesToProcess(context)) {
19
+ traverse(fileInfo.ast, {
20
+ NewExpression(path) {
21
+ nodesVisited++;
22
+ const executorPath = getExecutorFunctionIfEligible(path);
23
+ if (!executorPath)
24
+ return;
25
+ // Redundant with getExecutorFunctionIfEligible's narrowing, but keeps this visitor robust
26
+ // if eligibility rules ever change (and avoids relying on a tuple cast).
27
+ const parameters = executorPath.node.params;
28
+ if (parameters.length !== 2)
29
+ return;
30
+ const [firstParameter, secondParameter] = parameters;
31
+ if (!firstParameter || !secondParameter)
32
+ return;
33
+ if (firstParameter.type !== "Identifier")
34
+ return;
35
+ if (secondParameter.type !== "Identifier")
36
+ return;
37
+ const resolveParameter = firstParameter;
38
+ const rejectParameter = secondParameter;
39
+ // Non-strict scripts can allow duplicate parameter names like `function (a, a) {}`.
40
+ // Babel models these as a single binding, so we can't safely rename parameters independently.
41
+ if (resolveParameter.name === rejectParameter.name)
42
+ return;
43
+ const targetResolveName = `${STABLE_PREFIX}resolve`;
44
+ const targetRejectName = `${STABLE_PREFIX}reject`;
45
+ // Skip already-stable names, unless they already match the target stable names.
46
+ // This allows completing a partially-stable executor like `($resolve, e)` → `($resolve, $reject)`.
47
+ if (isStableRenamed(resolveParameter.name) &&
48
+ resolveParameter.name !== targetResolveName) {
49
+ return;
50
+ }
51
+ if (isStableRenamed(rejectParameter.name) &&
52
+ rejectParameter.name !== targetRejectName) {
53
+ return;
54
+ }
55
+ const bindingScope = executorPath.scope;
56
+ const resolveBinding = bindingScope.getBinding(resolveParameter.name);
57
+ const rejectBinding = bindingScope.getBinding(rejectParameter.name);
58
+ if (!resolveBinding || !rejectBinding)
59
+ return;
60
+ const resolveIsCalled = resolveBinding.referencePaths.some((referencePath) => isDirectCallOfBinding(referencePath));
61
+ if (!resolveIsCalled)
62
+ return;
63
+ const resolveIsOnlyCalled = resolveBinding.referencePaths.every((referencePath) => isDirectCallOfBinding(referencePath));
64
+ // Intentionally does not accept `resolve` being passed as a callback (e.g. `on("end", resolve)`):
65
+ // unlike `reject` as an `"error"` handler, those patterns are lower-confidence and easier to misclassify.
66
+ if (!resolveIsOnlyCalled)
67
+ return;
68
+ const rejectIsCalled = rejectBinding.referencePaths.some((referencePath) => isDirectCallOfBinding(referencePath));
69
+ const rejectIsOnHandler = rejectBinding.referencePaths.some((referencePath) => isAssignedToOnHandlerProperty(referencePath));
70
+ const rejectIsErrorHandler = rejectBinding.referencePaths.some((referencePath) => isErrorEventHandlerArgument(referencePath));
71
+ if (!rejectIsCalled && !rejectIsOnHandler && !rejectIsErrorHandler) {
72
+ return;
73
+ }
74
+ const rejectIsOnlyCalledOrHandlerAssigned = rejectBinding.referencePaths.every((referencePath) => isDirectCallOfBinding(referencePath) ||
75
+ isAssignedToOnHandlerProperty(referencePath) ||
76
+ isErrorEventHandlerArgument(referencePath) ||
77
+ isErrorEventHandlerRemovalArgument(referencePath));
78
+ if (!rejectIsOnlyCalledOrHandlerAssigned)
79
+ return;
80
+ const wantsResolveRename = resolveParameter.name !== targetResolveName;
81
+ const wantsRejectRename = rejectParameter.name !== targetRejectName;
82
+ // Nothing to do: both parameters are already the stable target names.
83
+ if (!wantsResolveRename && !wantsRejectRename)
84
+ return;
85
+ // Shadowing checks require a full traversal of the executor, so only do them for parameters
86
+ // that actually need renaming.
87
+ const resolveWouldShadow = wantsResolveRename &&
88
+ wouldShadowReferencedOuterBinding(executorPath, targetResolveName);
89
+ const rejectWouldShadow = wantsRejectRename &&
90
+ wouldShadowReferencedOuterBinding(executorPath, targetRejectName);
91
+ const fromResolveName = resolveParameter.name;
92
+ const fromRejectName = rejectParameter.name;
93
+ const canRenameResolve = !wantsResolveRename ||
94
+ (!resolveWouldShadow &&
95
+ canRenameBindingSafely(bindingScope, fromResolveName, targetResolveName));
96
+ const canRenameReject = !wantsRejectRename ||
97
+ (!rejectWouldShadow &&
98
+ canRenameBindingSafely(bindingScope, fromRejectName, targetRejectName));
99
+ // Avoid ending up with inconsistent executor signatures like `(WA, reject)` —
100
+ // only rename when both parameters are already correct or can be renamed safely.
101
+ if (!canRenameResolve || !canRenameReject)
102
+ return;
103
+ let renamedResolve = false;
104
+ let renamedReject = false;
105
+ // We already validated both renames against the same pre-rename scope state, so we can
106
+ // rename directly without re-checking and risking a partial rename.
107
+ //
108
+ // Note: Renaming `resolve` first cannot invalidate `reject`'s rename because:
109
+ // - we reject duplicate parameter names (`resolveParameter.name === rejectParameter.name`), and
110
+ // - we never allow a stable-but-wrong name like `$resolve`/`$reject` in the opposite slot.
111
+ if (wantsResolveRename) {
112
+ bindingScope.rename(fromResolveName, targetResolveName);
113
+ renamedResolve = true;
114
+ }
115
+ if (wantsRejectRename) {
116
+ bindingScope.rename(fromRejectName, targetRejectName);
117
+ renamedReject = true;
118
+ }
119
+ // Metrics represent renamed bindings, not "executors processed".
120
+ transformationsApplied +=
121
+ Number(renamedResolve) + Number(renamedReject);
122
+ },
123
+ });
124
+ }
125
+ return Promise.resolve({ nodesVisited, transformationsApplied });
126
+ },
127
+ };
@@ -0,0 +1,8 @@
1
+ import type { ArrowFunctionExpression, FunctionDeclaration, FunctionExpression } from "@babel/types";
2
+ import type { SplitHelperDeclarations } from "./split-helper-declarations.js";
3
+ type SplitHelperInfo = SplitHelperDeclarations & {
4
+ inputName: string;
5
+ delimiterName: string;
6
+ };
7
+ export declare const getSplitHelperInfo: (node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression) => SplitHelperInfo | undefined;
8
+ export {};
@@ -0,0 +1,40 @@
1
+ import { isIdentifierNode } from "./split-helper-guards.js";
2
+ import { getSplitHelperDeclarationsFromStatements } from "./split-helper-declarations.js";
3
+ import { isSplitHelperLoop } from "./split-helper-loop.js";
4
+ import { isSplitHelperTail } from "./split-helper-tail.js";
5
+ export const getSplitHelperInfo = (node) => {
6
+ if (node.params.length !== 2)
7
+ return undefined;
8
+ const inputParameter = node.params[0];
9
+ const delimiterParameter = node.params[1];
10
+ if (!isIdentifierNode(inputParameter) ||
11
+ !isIdentifierNode(delimiterParameter))
12
+ return undefined;
13
+ if (node.body.type !== "BlockStatement")
14
+ return undefined;
15
+ const statements = node.body.body;
16
+ const declarationsMatch = getSplitHelperDeclarationsFromStatements(statements, inputParameter.name, delimiterParameter.name);
17
+ if (!declarationsMatch)
18
+ return undefined;
19
+ const { nextStatementIndex, ...declarations } = declarationsMatch;
20
+ const loopStatement = statements[nextStatementIndex];
21
+ if (!isSplitHelperLoop(loopStatement, declarations, inputParameter.name, delimiterParameter.name))
22
+ return undefined;
23
+ const tailStatements = statements.slice(nextStatementIndex + 1);
24
+ if (!isSplitHelperTail(tailStatements, declarations, inputParameter.name))
25
+ return undefined;
26
+ const uniqueNames = new Set([
27
+ inputParameter.name,
28
+ delimiterParameter.name,
29
+ declarations.partsName,
30
+ declarations.startIndexName,
31
+ declarations.delimiterIndexName,
32
+ ]);
33
+ if (uniqueNames.size !== 5)
34
+ return undefined;
35
+ return {
36
+ inputName: inputParameter.name,
37
+ delimiterName: delimiterParameter.name,
38
+ ...declarations,
39
+ };
40
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "recommended": true,
3
+ "evaluations": {
4
+ "claude-code-2.1.10:claude-code-2.1.11": {
5
+ "diffSizePercent": 100,
6
+ "evaluatedAt": "2026-01-27T06:41:49.624Z",
7
+ "changedLines": 0,
8
+ "durationSeconds": 149.03264720899998,
9
+ "stableNames": 1357
10
+ }
11
+ },
12
+ "notes": "Measured with baseline none: 0.00%. Added to recommended for readability."
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { Transform } from "../../core/types.js";
2
+ export declare const renameStringSplitVariablesTransform: Transform;
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "node:module";
2
+ import { RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess } from "../../core/types.js";
4
+ import { getSplitHelperInfo } from "./get-split-helper-info.js";
5
+ const require = createRequire(import.meta.url);
6
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7
+ const traverse = require("@babel/traverse").default;
8
+ const collectSplitHelperRenames = (path, group) => {
9
+ // Intentionally no early-return based on stable-renamed params:
10
+ // parameters can be stable while helper locals are still minified.
11
+ const info = getSplitHelperInfo(path.node);
12
+ if (!info)
13
+ return;
14
+ const scope = path.scope;
15
+ group.add({
16
+ scope,
17
+ currentName: info.inputName,
18
+ baseName: "input",
19
+ });
20
+ group.add({
21
+ scope,
22
+ currentName: info.delimiterName,
23
+ baseName: "delimiter",
24
+ });
25
+ group.add({
26
+ scope,
27
+ currentName: info.partsName,
28
+ baseName: "parts",
29
+ });
30
+ group.add({
31
+ scope,
32
+ currentName: info.startIndexName,
33
+ baseName: "startIndex",
34
+ });
35
+ group.add({
36
+ scope,
37
+ currentName: info.delimiterIndexName,
38
+ baseName: "delimiterIndex",
39
+ });
40
+ };
41
+ export const renameStringSplitVariablesTransform = {
42
+ id: "rename-string-split-variables",
43
+ description: "Renames local variables in manual string split helpers (indexOf + slice loop) to stable $input-style names (prefers $input/$delimiter/$parts/$startIndex/$delimiterIndex)",
44
+ scope: "file",
45
+ parallelizable: true,
46
+ transform(context) {
47
+ let nodesVisited = 0;
48
+ let transformationsApplied = 0;
49
+ for (const fileInfo of getFilesToProcess(context)) {
50
+ const group = new RenameGroup();
51
+ traverse(fileInfo.ast, {
52
+ FunctionDeclaration(path) {
53
+ nodesVisited++;
54
+ collectSplitHelperRenames(path, group);
55
+ },
56
+ FunctionExpression(path) {
57
+ nodesVisited++;
58
+ collectSplitHelperRenames(path, group);
59
+ },
60
+ ArrowFunctionExpression(path) {
61
+ nodesVisited++;
62
+ collectSplitHelperRenames(path, group);
63
+ },
64
+ });
65
+ transformationsApplied += group.apply();
66
+ }
67
+ return Promise.resolve({ nodesVisited, transformationsApplied });
68
+ },
69
+ };
@@ -0,0 +1,11 @@
1
+ import type { Statement } from "@babel/types";
2
+ export type SplitHelperDeclarations = {
3
+ partsName: string;
4
+ startIndexName: string;
5
+ delimiterIndexName: string;
6
+ };
7
+ type SplitHelperDeclarationsMatch = SplitHelperDeclarations & {
8
+ nextStatementIndex: number;
9
+ };
10
+ export declare const getSplitHelperDeclarationsFromStatements: (statements: Statement[], inputName: string, delimiterName: string) => SplitHelperDeclarationsMatch | undefined;
11
+ export {};
@@ -0,0 +1,80 @@
1
+ import { isIdentifierNode, isMemberCall, isNumericLiteralNode, } from "./split-helper-guards.js";
2
+ const isDirectivePrologueStatement = (statement) => {
3
+ // In JavaScript, any leading sequence of `"..."`; expression statements is the
4
+ // "directive prologue" (most commonly `"use strict";`). Babel sometimes also
5
+ // marks these with `statement.directive`, but we intentionally only rely on
6
+ // the lexical shape so we still match after AST rewrites.
7
+ if (statement.type !== "ExpressionStatement")
8
+ return false;
9
+ return statement.expression.type === "StringLiteral";
10
+ };
11
+ const getVariableDeclarators = (statement) => {
12
+ if (statement.type !== "VariableDeclaration")
13
+ return undefined;
14
+ if (statement.declarations.length === 0)
15
+ return undefined;
16
+ return statement.declarations;
17
+ };
18
+ export const getSplitHelperDeclarationsFromStatements = (statements, inputName, delimiterName) => {
19
+ const declarators = [];
20
+ let statementIndex = 0;
21
+ while (statementIndex < statements.length) {
22
+ const statement = statements[statementIndex];
23
+ if (!statement)
24
+ return undefined;
25
+ if (!isDirectivePrologueStatement(statement))
26
+ break;
27
+ statementIndex++;
28
+ }
29
+ if (statementIndex >= statements.length)
30
+ return undefined;
31
+ let declarationStatementsConsumed = 0;
32
+ while (declarators.length < 3) {
33
+ if (statementIndex >= statements.length)
34
+ return undefined;
35
+ const statement = statements[statementIndex];
36
+ if (!statement)
37
+ return undefined;
38
+ const statementDeclarators = getVariableDeclarators(statement);
39
+ if (!statementDeclarators)
40
+ return undefined;
41
+ declarationStatementsConsumed++;
42
+ if (declarationStatementsConsumed > 3)
43
+ return undefined;
44
+ const totalDeclarators = declarators.length + statementDeclarators.length;
45
+ if (totalDeclarators > 3)
46
+ return undefined;
47
+ declarators.push(...statementDeclarators);
48
+ statementIndex++;
49
+ }
50
+ const identifierDeclarators = declarators.filter((declarator) => declarator.id.type === "Identifier");
51
+ if (identifierDeclarators.length !== declarators.length)
52
+ return undefined;
53
+ const partsDeclarator = identifierDeclarators.find((declarator) => {
54
+ if (declarator.init?.type !== "ArrayExpression")
55
+ return false;
56
+ return declarator.init.elements.length === 0;
57
+ });
58
+ const startDeclarator = identifierDeclarators.find((declarator) => isNumericLiteralNode(declarator.init ?? undefined, 0));
59
+ const indexDeclarator = identifierDeclarators.find((declarator) => {
60
+ if (declarator.init?.type !== "CallExpression")
61
+ return false;
62
+ if (!isMemberCall(declarator.init, inputName, "indexOf"))
63
+ return false;
64
+ if (!isIdentifierNode(declarator.init.arguments[0], delimiterName))
65
+ return false;
66
+ if (declarator.init.arguments.length === 1)
67
+ return true;
68
+ if (declarator.init.arguments.length !== 2)
69
+ return false;
70
+ return isNumericLiteralNode(declarator.init.arguments[1], 0);
71
+ });
72
+ if (!partsDeclarator || !startDeclarator || !indexDeclarator)
73
+ return undefined;
74
+ return {
75
+ partsName: partsDeclarator.id.name,
76
+ startIndexName: startDeclarator.id.name,
77
+ delimiterIndexName: indexDeclarator.id.name,
78
+ nextStatementIndex: statementIndex,
79
+ };
80
+ };
@@ -0,0 +1,8 @@
1
+ import type { BinaryExpression, CallExpression, Identifier, Node, NumericLiteral, Statement } from "@babel/types";
2
+ export declare const isIdentifierNode: (node: Node | undefined, name?: string) => node is Identifier;
3
+ export declare const isNumericLiteralNode: (node: Node | undefined, value?: number) => node is NumericLiteral;
4
+ export declare const getBlockStatements: (statement: Statement) => Statement[] | undefined;
5
+ export declare const isMemberCall: (expression: CallExpression, objectName: string, propertyName: string) => boolean;
6
+ export declare const isPushSliceStatement: (statement: Statement, partsName: string, inputName: string, startName: string, endName?: string) => boolean;
7
+ export declare const isAssignmentStatement: (statement: Statement, targetName: string, expectedExpression: (expression: Node) => boolean) => boolean;
8
+ export declare const isBinaryExpression: (expression: Node, operator: BinaryExpression["operator"], leftName: string, rightValue: number) => boolean;
@@ -0,0 +1,73 @@
1
+ export const isIdentifierNode = (node, name) => {
2
+ if (node?.type !== "Identifier")
3
+ return false;
4
+ return name ? node.name === name : true;
5
+ };
6
+ export const isNumericLiteralNode = (node, value) => {
7
+ if (node?.type !== "NumericLiteral")
8
+ return false;
9
+ return value === undefined ? true : node.value === value;
10
+ };
11
+ export const getBlockStatements = (statement) => {
12
+ if (statement.type !== "BlockStatement")
13
+ return undefined;
14
+ return statement.body;
15
+ };
16
+ export const isMemberCall = (expression, objectName, propertyName) => {
17
+ const callee = expression.callee;
18
+ if (callee.type !== "MemberExpression")
19
+ return false;
20
+ if (callee.computed)
21
+ return false;
22
+ if (!isIdentifierNode(callee.object, objectName))
23
+ return false;
24
+ return isIdentifierNode(callee.property, propertyName);
25
+ };
26
+ const isSliceCallWithArguments = (expression, inputName, startName, endName) => {
27
+ if (!isMemberCall(expression, inputName, "slice"))
28
+ return false;
29
+ if (!isIdentifierNode(expression.arguments[0], startName))
30
+ return false;
31
+ if (endName === undefined) {
32
+ return expression.arguments.length === 1;
33
+ }
34
+ if (!isIdentifierNode(expression.arguments[1], endName))
35
+ return false;
36
+ return expression.arguments.length === 2;
37
+ };
38
+ export const isPushSliceStatement = (statement, partsName, inputName, startName, endName) => {
39
+ if (statement.type !== "ExpressionStatement")
40
+ return false;
41
+ const expression = statement.expression;
42
+ if (expression.type !== "CallExpression")
43
+ return false;
44
+ if (!isMemberCall(expression, partsName, "push"))
45
+ return false;
46
+ if (expression.arguments.length !== 1)
47
+ return false;
48
+ const argument0 = expression.arguments[0];
49
+ if (argument0?.type !== "CallExpression")
50
+ return false;
51
+ return isSliceCallWithArguments(argument0, inputName, startName, endName);
52
+ };
53
+ export const isAssignmentStatement = (statement, targetName, expectedExpression) => {
54
+ if (statement.type !== "ExpressionStatement")
55
+ return false;
56
+ const expression = statement.expression;
57
+ if (expression.type !== "AssignmentExpression")
58
+ return false;
59
+ if (expression.operator !== "=")
60
+ return false;
61
+ if (!isIdentifierNode(expression.left, targetName))
62
+ return false;
63
+ return expectedExpression(expression.right);
64
+ };
65
+ export const isBinaryExpression = (expression, operator, leftName, rightValue) => {
66
+ if (expression.type !== "BinaryExpression")
67
+ return false;
68
+ if (expression.operator !== operator)
69
+ return false;
70
+ if (!isIdentifierNode(expression.left, leftName))
71
+ return false;
72
+ return isNumericLiteralNode(expression.right, rightValue);
73
+ };
@@ -0,0 +1,3 @@
1
+ import type { Statement } from "@babel/types";
2
+ import type { SplitHelperDeclarations } from "./split-helper-declarations.js";
3
+ export declare const isSplitHelperLoop: (statement: Statement | undefined, declarations: SplitHelperDeclarations, inputName: string, delimiterName: string) => boolean;
@@ -0,0 +1,116 @@
1
+ import { isAssignmentStatement, isBinaryExpression, isIdentifierNode, isMemberCall, isNumericLiteralNode, isPushSliceStatement, } from "./split-helper-guards.js";
2
+ const isNegativeOneLiteral = (expression) => {
3
+ if (expression.type === "NumericLiteral")
4
+ return expression.value === -1;
5
+ if (expression.type !== "UnaryExpression")
6
+ return false;
7
+ if (expression.operator !== "-")
8
+ return false;
9
+ if (expression.argument.type !== "NumericLiteral")
10
+ return false;
11
+ return expression.argument.value === 1;
12
+ };
13
+ const isNotFoundBinaryTest = (expression, name) => {
14
+ if (expression.type !== "BinaryExpression")
15
+ return false;
16
+ if (expression.operator !== "!==" && expression.operator !== "!=")
17
+ return false;
18
+ if (!isIdentifierNode(expression.left, name))
19
+ return false;
20
+ return isNegativeOneLiteral(expression.right);
21
+ };
22
+ const isFoundBinaryTest = (expression, name) => {
23
+ if (expression.type !== "BinaryExpression")
24
+ return false;
25
+ if (expression.operator !== ">")
26
+ return false;
27
+ if (!isIdentifierNode(expression.left, name))
28
+ return false;
29
+ return isNegativeOneLiteral(expression.right);
30
+ };
31
+ const isBitwiseNotTest = (expression, name) => {
32
+ if (expression.type !== "UnaryExpression")
33
+ return false;
34
+ if (expression.operator !== "~")
35
+ return false;
36
+ return isIdentifierNode(expression.argument, name);
37
+ };
38
+ export const isSplitHelperLoop = (statement, declarations, inputName, delimiterName) => {
39
+ if (statement?.type !== "WhileStatement")
40
+ return false;
41
+ const isNonNegativeTest = (expression) => {
42
+ if (isBinaryExpression(expression, ">=", declarations.delimiterIndexName, 0)) {
43
+ return true;
44
+ }
45
+ if (expression.type !== "BinaryExpression")
46
+ return false;
47
+ if (expression.operator !== "<=")
48
+ return false;
49
+ if (!isNumericLiteralNode(expression.left, 0))
50
+ return false;
51
+ return isIdentifierNode(expression.right, declarations.delimiterIndexName);
52
+ };
53
+ if (!isNonNegativeTest(statement.test) &&
54
+ !isNotFoundBinaryTest(statement.test, declarations.delimiterIndexName) &&
55
+ !isFoundBinaryTest(statement.test, declarations.delimiterIndexName) &&
56
+ !isBitwiseNotTest(statement.test, declarations.delimiterIndexName)) {
57
+ return false;
58
+ }
59
+ if (statement.body.type !== "BlockStatement")
60
+ return false;
61
+ if (statement.body.body.length !== 3)
62
+ return false;
63
+ const [pushStatement, advanceStatement, updateStatement] = statement.body.body;
64
+ if (!pushStatement || !advanceStatement || !updateStatement)
65
+ return false;
66
+ if (!isPushSliceStatement(pushStatement, declarations.partsName, inputName, declarations.startIndexName, declarations.delimiterIndexName))
67
+ return false;
68
+ const isAdvanceExpression = (expression) => {
69
+ if (isBinaryExpression(expression, "+", declarations.delimiterIndexName, 1)) {
70
+ return true;
71
+ }
72
+ if (expression.type === "BinaryExpression" && expression.operator === "+") {
73
+ if (isNumericLiteralNode(expression.left, 1) &&
74
+ isIdentifierNode(expression.right, declarations.delimiterIndexName)) {
75
+ return true;
76
+ }
77
+ const left = expression.left;
78
+ const right = expression.right;
79
+ if (left.type === "MemberExpression" &&
80
+ !left.computed &&
81
+ isIdentifierNode(left.object, delimiterName) &&
82
+ isIdentifierNode(left.property, "length") &&
83
+ isIdentifierNode(right, declarations.delimiterIndexName)) {
84
+ return true;
85
+ }
86
+ }
87
+ if (expression.type !== "BinaryExpression")
88
+ return false;
89
+ if (expression.operator !== "+")
90
+ return false;
91
+ if (!isIdentifierNode(expression.left, declarations.delimiterIndexName))
92
+ return false;
93
+ const right = expression.right;
94
+ if (right.type !== "MemberExpression")
95
+ return false;
96
+ if (right.computed)
97
+ return false;
98
+ if (!isIdentifierNode(right.object, delimiterName))
99
+ return false;
100
+ return isIdentifierNode(right.property, "length");
101
+ };
102
+ if (!isAssignmentStatement(advanceStatement, declarations.startIndexName, isAdvanceExpression))
103
+ return false;
104
+ const isUpdateExpression = (expression) => {
105
+ if (expression.type !== "CallExpression")
106
+ return false;
107
+ if (!isMemberCall(expression, inputName, "indexOf"))
108
+ return false;
109
+ if (expression.arguments.length !== 2)
110
+ return false;
111
+ if (!isIdentifierNode(expression.arguments[0], delimiterName))
112
+ return false;
113
+ return isIdentifierNode(expression.arguments[1], declarations.startIndexName);
114
+ };
115
+ return isAssignmentStatement(updateStatement, declarations.delimiterIndexName, isUpdateExpression);
116
+ };
@@ -0,0 +1,3 @@
1
+ import type { Node } from "@babel/types";
2
+ import type { SplitHelperDeclarations } from "./split-helper-declarations.js";
3
+ export declare const isReturnWithOptionalTailPush: (node: Node, declarations: SplitHelperDeclarations, inputName: string) => boolean;
@@ -0,0 +1,86 @@
1
+ import { isIdentifierNode } from "./split-helper-guards.js";
2
+ import { isAtEndTest, isShouldPushTailTest, } from "./split-helper-tail-tests.js";
3
+ const isPushSliceCallExpression = (node, declarations, inputName) => {
4
+ if (node.type !== "CallExpression")
5
+ return false;
6
+ const callee = node.callee;
7
+ if (callee.type !== "MemberExpression")
8
+ return false;
9
+ if (callee.computed)
10
+ return false;
11
+ if (!isIdentifierNode(callee.object, declarations.partsName))
12
+ return false;
13
+ if (!isIdentifierNode(callee.property, "push"))
14
+ return false;
15
+ if (node.arguments.length !== 1)
16
+ return false;
17
+ const argument0 = node.arguments[0];
18
+ if (argument0?.type !== "CallExpression")
19
+ return false;
20
+ const sliceCallee = argument0.callee;
21
+ if (sliceCallee.type !== "MemberExpression")
22
+ return false;
23
+ if (sliceCallee.computed)
24
+ return false;
25
+ if (!isIdentifierNode(sliceCallee.object, inputName))
26
+ return false;
27
+ if (!isIdentifierNode(sliceCallee.property, "slice"))
28
+ return false;
29
+ if (argument0.arguments.length !== 1)
30
+ return false;
31
+ return isIdentifierNode(argument0.arguments[0], declarations.startIndexName);
32
+ };
33
+ const isPushSequenceExpression = (node, declarations, inputName) => {
34
+ if (node.type !== "SequenceExpression")
35
+ return false;
36
+ if (node.expressions.length !== 2)
37
+ return false;
38
+ const [first, second] = node.expressions;
39
+ if (!first || !second)
40
+ return false;
41
+ if (!isPushSliceCallExpression(first, declarations, inputName))
42
+ return false;
43
+ return isIdentifierNode(second, declarations.partsName);
44
+ };
45
+ export const isReturnWithOptionalTailPush = (node, declarations, inputName) => {
46
+ if (node.type === "ConditionalExpression") {
47
+ const testIsShouldPush = isShouldPushTailTest(node.test, declarations, inputName);
48
+ const testIsAtEnd = isAtEndTest(node.test, declarations, inputName);
49
+ if (!testIsShouldPush && !testIsAtEnd)
50
+ return false;
51
+ const consequent = node.consequent;
52
+ const alternate = node.alternate;
53
+ const consequentIsParts = isIdentifierNode(consequent, declarations.partsName);
54
+ const alternateIsParts = isIdentifierNode(alternate, declarations.partsName);
55
+ const consequentIsPush = isPushSequenceExpression(consequent, declarations, inputName);
56
+ const alternateIsPush = isPushSequenceExpression(alternate, declarations, inputName);
57
+ if (testIsShouldPush) {
58
+ if (!consequentIsPush)
59
+ return false;
60
+ return alternateIsParts;
61
+ }
62
+ if (!testIsAtEnd)
63
+ return false;
64
+ if (!alternateIsPush)
65
+ return false;
66
+ return consequentIsParts;
67
+ }
68
+ if (node.type !== "SequenceExpression")
69
+ return false;
70
+ if (node.expressions.length !== 2)
71
+ return false;
72
+ const [first, second] = node.expressions;
73
+ if (!first || !second)
74
+ return false;
75
+ if (!isIdentifierNode(second, declarations.partsName))
76
+ return false;
77
+ if (isPushSliceCallExpression(first, declarations, inputName))
78
+ return true;
79
+ if (first.type === "LogicalExpression" &&
80
+ first.operator === "&&" &&
81
+ isShouldPushTailTest(first.left, declarations, inputName) &&
82
+ isPushSliceCallExpression(first.right, declarations, inputName)) {
83
+ return true;
84
+ }
85
+ return false;
86
+ };
@@ -0,0 +1,4 @@
1
+ import type { Node } from "@babel/types";
2
+ import type { SplitHelperDeclarations } from "./split-helper-declarations.js";
3
+ export declare const isShouldPushTailTest: (node: Node, declarations: SplitHelperDeclarations, inputName: string) => boolean;
4
+ export declare const isAtEndTest: (node: Node, declarations: SplitHelperDeclarations, inputName: string) => boolean;
@@ -0,0 +1,39 @@
1
+ import { isIdentifierNode } from "./split-helper-guards.js";
2
+ const isInputLengthMemberExpression = (node, inputName) => {
3
+ if (node.type !== "MemberExpression")
4
+ return false;
5
+ if (node.computed)
6
+ return false;
7
+ if (!isIdentifierNode(node.object, inputName))
8
+ return false;
9
+ return isIdentifierNode(node.property, "length");
10
+ };
11
+ export const isShouldPushTailTest = (node, declarations, inputName) => {
12
+ if (node.type !== "BinaryExpression")
13
+ return false;
14
+ const operator = node.operator;
15
+ if ((operator === "!==" || operator === "!=" || operator === "<") &&
16
+ isIdentifierNode(node.left, declarations.startIndexName) &&
17
+ isInputLengthMemberExpression(node.right, inputName)) {
18
+ return true;
19
+ }
20
+ if ((operator === "!==" || operator === "!=" || operator === ">") &&
21
+ isInputLengthMemberExpression(node.left, inputName) &&
22
+ isIdentifierNode(node.right, declarations.startIndexName)) {
23
+ return true;
24
+ }
25
+ return false;
26
+ };
27
+ export const isAtEndTest = (node, declarations, inputName) => {
28
+ if (node.type !== "BinaryExpression")
29
+ return false;
30
+ const operator = node.operator;
31
+ if (operator !== "===" && operator !== "==")
32
+ return false;
33
+ if (isIdentifierNode(node.left, declarations.startIndexName) &&
34
+ isInputLengthMemberExpression(node.right, inputName)) {
35
+ return true;
36
+ }
37
+ return (isInputLengthMemberExpression(node.left, inputName) &&
38
+ isIdentifierNode(node.right, declarations.startIndexName));
39
+ };
@@ -0,0 +1,3 @@
1
+ import type { Statement } from "@babel/types";
2
+ import type { SplitHelperDeclarations } from "./split-helper-declarations.js";
3
+ export declare const isSplitHelperTail: (tailStatements: Statement[], declarations: SplitHelperDeclarations, inputName: string) => boolean;
@@ -0,0 +1,49 @@
1
+ import { getBlockStatements, isIdentifierNode, isPushSliceStatement, } from "./split-helper-guards.js";
2
+ import { isReturnWithOptionalTailPush } from "./split-helper-tail-expressions.js";
3
+ import { isShouldPushTailTest } from "./split-helper-tail-tests.js";
4
+ const isReturnPartsStatement = (statement, declarations) => {
5
+ if (statement?.type !== "ReturnStatement")
6
+ return false;
7
+ return isIdentifierNode(statement.argument ?? undefined, declarations.partsName);
8
+ };
9
+ export const isSplitHelperTail = (tailStatements, declarations, inputName) => {
10
+ if (tailStatements.length === 2) {
11
+ const [firstTailStatement, secondTailStatement] = tailStatements;
12
+ if (!firstTailStatement || !secondTailStatement)
13
+ return false;
14
+ if (firstTailStatement.type === "IfStatement") {
15
+ const ifStatement = firstTailStatement;
16
+ const returnStatement = secondTailStatement;
17
+ if (ifStatement.alternate)
18
+ return false;
19
+ if (!isShouldPushTailTest(ifStatement.test, declarations, inputName))
20
+ return false;
21
+ const ifStatements = getBlockStatements(ifStatement.consequent) ?? [
22
+ ifStatement.consequent,
23
+ ];
24
+ if (ifStatements.length !== 1)
25
+ return false;
26
+ const ifConsequent = ifStatements[0];
27
+ if (!ifConsequent)
28
+ return false;
29
+ if (!isPushSliceStatement(ifConsequent, declarations.partsName, inputName, declarations.startIndexName))
30
+ return false;
31
+ return isReturnPartsStatement(returnStatement, declarations);
32
+ }
33
+ if (isPushSliceStatement(firstTailStatement, declarations.partsName, inputName, declarations.startIndexName)) {
34
+ return isReturnPartsStatement(secondTailStatement, declarations);
35
+ }
36
+ return false;
37
+ }
38
+ if (tailStatements.length !== 1)
39
+ return false;
40
+ const tailStatement = tailStatements[0];
41
+ if (!tailStatement)
42
+ return false;
43
+ if (tailStatement.type !== "ReturnStatement")
44
+ return false;
45
+ const argument = tailStatement.argument;
46
+ if (!argument)
47
+ return false;
48
+ return isReturnWithOptionalTailPush(argument, declarations, inputName);
49
+ };
@@ -3,9 +3,9 @@
3
3
  "evaluations": {
4
4
  "claude-code-2.1.10:claude-code-2.1.11": {
5
5
  "diffSizePercent": 100,
6
- "evaluatedAt": "2026-01-26T08:03:28.285Z",
6
+ "evaluatedAt": "2026-01-27T06:41:50.482Z",
7
7
  "changedLines": 24,
8
- "durationSeconds": 146.202273464,
8
+ "durationSeconds": 149.98115670800001,
9
9
  "stableNames": 1358
10
10
  }
11
11
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "miniread",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.48.0",
5
+ "version": "1.50.0",
6
6
  "description": "Transform minified JavaScript/TypeScript into a more readable form using deterministic AST-based transforms.",
7
7
  "repository": {
8
8
  "type": "git",