miniread 1.52.0 → 1.53.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 (26) hide show
  1. package/dist/core/is-type-only-global-reference.d.ts +2 -0
  2. package/dist/core/is-type-only-global-reference.js +33 -0
  3. package/dist/core/stable-naming.js +74 -5
  4. package/dist/transforms/_generated/manifest.js +5 -0
  5. package/dist/transforms/_generated/registry.js +2 -0
  6. package/dist/transforms/get-url-construction-kind.d.ts +5 -0
  7. package/dist/transforms/get-url-construction-kind.js +47 -0
  8. package/dist/transforms/is-allowed-url-constructor-in-scope.d.ts +2 -0
  9. package/dist/transforms/is-allowed-url-constructor-in-scope.js +70 -0
  10. package/dist/transforms/is-allowed-url-module-import-binding.d.ts +2 -0
  11. package/dist/transforms/is-allowed-url-module-import-binding.js +24 -0
  12. package/dist/transforms/is-allowed-url-namespace-binding-in-scope.d.ts +2 -0
  13. package/dist/transforms/is-allowed-url-namespace-binding-in-scope.js +35 -0
  14. package/dist/transforms/is-require-from-url-module.d.ts +15 -0
  15. package/dist/transforms/is-require-from-url-module.js +163 -0
  16. package/dist/transforms/rename-search-parameters-variables/get-member-expression-property-name.d.ts +2 -0
  17. package/dist/transforms/rename-search-parameters-variables/get-member-expression-property-name.js +20 -0
  18. package/dist/transforms/rename-search-parameters-variables/get-search-parameters-member-expression.d.ts +3 -0
  19. package/dist/transforms/rename-search-parameters-variables/get-search-parameters-member-expression.js +26 -0
  20. package/dist/transforms/rename-search-parameters-variables/manifest.json +20 -0
  21. package/dist/transforms/rename-search-parameters-variables/rename-search-parameters-variables-transform.d.ts +2 -0
  22. package/dist/transforms/rename-search-parameters-variables/rename-search-parameters-variables-transform.js +75 -0
  23. package/dist/transforms/rename-search-parameters-variables/search-parameters-usage-heuristics.d.ts +5 -0
  24. package/dist/transforms/rename-search-parameters-variables/search-parameters-usage-heuristics.js +151 -0
  25. package/dist/transforms/rename-url-variables/rename-url-variables-transform.js +13 -12
  26. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ export declare const isTypeOnlyGlobalReference: (path: NodePath) => boolean;
@@ -0,0 +1,33 @@
1
+ export const isTypeOnlyGlobalReference = (path) => {
2
+ // `typeof Foo` in a type position still resolves `Foo` from the value
3
+ // namespace, so it can be affected by local shadowing.
4
+ if (path.findParent((parent) => parent.node.type === "TSTypeQuery"))
5
+ return false;
6
+ // TypeScript keeps value and type namespaces separate, so introducing a local
7
+ // value binding will not affect a purely type-position reference like
8
+ // `type T = Foo`. We only reserve globals that are referenced from the value
9
+ // namespace.
10
+ return Boolean(path.findParent((parent) => {
11
+ const type = parent.node.type;
12
+ if (!type.startsWith("TS"))
13
+ return false;
14
+ // Some TS-prefixed nodes are runtime constructs (e.g. `enum`, `namespace`,
15
+ // `export = Foo`) and contain real value-position expressions. Treat those
16
+ // contexts as non-type-only unless we find a more specific type node
17
+ // closer to the identifier.
18
+ if (type === "TSEnumDeclaration" ||
19
+ type === "TSEnumMember" ||
20
+ type === "TSExportAssignment" ||
21
+ type === "TSImportEqualsDeclaration" ||
22
+ type === "TSExternalModuleReference" ||
23
+ type === "TSModuleBlock" ||
24
+ type === "TSModuleDeclaration" ||
25
+ type === "TSNamespaceExportDeclaration")
26
+ return false;
27
+ return (type !== "TSAsExpression" &&
28
+ type !== "TSNonNullExpression" &&
29
+ type !== "TSSatisfiesExpression" &&
30
+ type !== "TSInstantiationExpression" &&
31
+ type !== "TSTypeAssertion");
32
+ }));
33
+ };
@@ -7,6 +7,7 @@
7
7
  * - Stable names (`$timeoutId`): Readable AND deterministic across versions
8
8
  * - Readable names (`timeoutId`): Semantic but order-dependent
9
9
  */
10
+ import { isTypeOnlyGlobalReference } from "./is-type-only-global-reference.js";
10
11
  const STABLE_PREFIX = "$";
11
12
  /**
12
13
  * Returns true if the name has been stable-renamed (starts with $).
@@ -35,27 +36,95 @@ const hasShadowingRisk = (binding, bindingScope, targetName) => binding.referenc
35
36
  };
36
37
  return referencePath.scope.hasBinding(targetName, hasBindingOptions);
37
38
  });
39
+ // Cache is keyed by the program `Scope` instance produced by Babel traversal,
40
+ // so it won't leak across separate traversals / transform passes.
41
+ const programValueGlobalsCache = new WeakMap();
42
+ const getProgramValueGlobals = (scope) => {
43
+ const programScope = scope.getProgramParent();
44
+ const cached = programValueGlobalsCache.get(programScope);
45
+ if (cached)
46
+ return cached;
47
+ const programGlobals = programScope.globals;
48
+ const globalNames = Object.keys(programGlobals);
49
+ if (globalNames.length === 0) {
50
+ const empty = new Set();
51
+ programValueGlobalsCache.set(programScope, empty);
52
+ return empty;
53
+ }
54
+ const globalNameSet = new Set(globalNames);
55
+ const valueGlobals = new Set();
56
+ programScope.path.traverse({
57
+ Identifier(path) {
58
+ const name = path.node.name;
59
+ if (!globalNameSet.has(name))
60
+ return;
61
+ if (!path.isReferencedIdentifier())
62
+ return;
63
+ if (isTypeOnlyGlobalReference(path))
64
+ return;
65
+ valueGlobals.add(name);
66
+ },
67
+ });
68
+ // Cache is safe because we only ever use this set to prevent choosing rename
69
+ // targets that would shadow *existing* program-global references, and rename
70
+ // application should not introduce new free/global identifiers. If a future
71
+ // transform starts inserting new global references into the AST, this cache
72
+ // would need to be recomputed (or removed).
73
+ programValueGlobalsCache.set(programScope, valueGlobals);
74
+ return valueGlobals;
75
+ };
38
76
  /**
39
77
  * Finds the next available name slot in a scope.
40
78
  */
41
79
  const findAvailableName = (scope, binding, baseName, startIndex, canBeStable, currentName) => {
42
80
  let index = startIndex;
81
+ // Treat any identifier referenced as a program-global value as reserved.
82
+ //
83
+ // For readable renames we check both the readable and stable variants; this
84
+ // keeps the stable/readable "slot" aligned (e.g. an existing `$foo` blocks
85
+ // introducing `foo`) and prevents introducing a local binding that would
86
+ // shadow a global used anywhere in the file (even across sibling scopes).
87
+ //
88
+ // For stable-only renames we only need to ensure the stable identifier itself
89
+ // is free (stable names don't collide with readable ones).
90
+ // Safe to read once per call: `globals` are gathered during traversal; actual
91
+ // renames happen later in `RenameGroup.apply()`.
92
+ // Note: This is intentionally conservative and can reduce rename coverage in
93
+ // bundles with many free identifiers.
94
+ const programGlobals = getProgramValueGlobals(scope);
43
95
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
44
96
  while (true) {
45
97
  const suffix = index === 1 ? "" : String(index);
46
98
  const candidateBase = `${baseName}${suffix}`;
47
99
  const candidateStable = makeStableName(candidateBase);
48
100
  const candidateReadable = candidateBase;
49
- // Check if either form is already taken in this scope
101
+ // Always reserve the stable name, even when we won't emit it, to keep the
102
+ // "slot" aligned across stable/readable naming and avoid collisions with
103
+ // already-stable identifiers.
50
104
  const stableTaken = candidateStable === currentName
51
105
  ? false
52
- : scope.hasBinding(candidateStable);
106
+ : scope.hasBinding(candidateStable) ||
107
+ programGlobals.has(candidateStable);
108
+ if (canBeStable) {
109
+ if (stableTaken) {
110
+ index++;
111
+ continue;
112
+ }
113
+ // Stable names (`$foo`) don't collide with readable names (`foo`), so when
114
+ // we know we'll emit a stable name, we only need to check the stable slot.
115
+ const name = candidateStable;
116
+ if (binding && hasShadowingRisk(binding, scope, name)) {
117
+ index++;
118
+ continue;
119
+ }
120
+ return { name, nextIndex: index + 1 };
121
+ }
53
122
  const readableTaken = candidateReadable === currentName
54
123
  ? false
55
- : scope.hasBinding(candidateReadable);
124
+ : scope.hasBinding(candidateReadable) ||
125
+ programGlobals.has(candidateReadable);
56
126
  if (!stableTaken && !readableTaken) {
57
- // This slot is available
58
- const name = canBeStable ? candidateStable : candidateReadable;
127
+ const name = candidateReadable;
59
128
  if (binding && hasShadowingRisk(binding, scope, name)) {
60
129
  index++;
61
130
  continue;
@@ -201,6 +201,11 @@ const manifestData = {
201
201
  notes: "Negative diff reduction in evaluation; keep optional.",
202
202
  evaluations: { "legacy:evaluation": { "diffSizePercent": 100.00188483649043, "evaluatedAt": "2026-01-25T10:04:22.417Z", "changedLines": 0, "durationSeconds": 0, "stableNames": 0 } },
203
203
  },
204
+ "rename-search-parameters-variables": {
205
+ recommended: true,
206
+ notes: "Measured with baseline none: 100.00% of original diff. Added to recommended for readability.",
207
+ evaluations: { "legacy:evaluation": { "diffSizePercent": 100, "evaluatedAt": "2026-01-25T09:49:29.000Z", "changedLines": 0, "durationSeconds": 0, "stableNames": 0 }, "claude-code-2.1.10:claude-code-2.1.11": { "diffSizePercent": 100, "evaluatedAt": "2026-01-26T07:05:01.592Z", "changedLines": 32, "durationSeconds": 54.402595083, "stableNames": 1358 } },
208
+ },
204
209
  "rename-string-split-variables": {
205
210
  recommended: true,
206
211
  notes: "Measured with baseline none: 0.00%. Added to recommended for readability.",
@@ -39,6 +39,7 @@ import { renameRegexBuildersTransform } from "../rename-regex-builders/rename-re
39
39
  import { renameReplaceChildParametersTransform } from "../rename-replace-child-parameters/rename-replace-child-parameters-transform.js";
40
40
  import { renameRestParametersTransform } from "../rename-rest-parameters/rename-rest-parameters-transform.js";
41
41
  import { renameSafePropertyAccessorParametersTransform } from "../rename-safe-property-accessor-parameters/rename-safe-property-accessor-parameters-transform.js";
42
+ import { renameSearchParametersVariablesTransform } from "../rename-search-parameters-variables/rename-search-parameters-variables-transform.js";
42
43
  import { renameStringSplitVariablesTransform } from "../rename-string-split-variables/rename-string-split-variables-transform.js";
43
44
  import { renameThisAliasesTransform } from "../rename-this-aliases/rename-this-aliases-transform.js";
44
45
  import { renameTimeoutDurationParametersTransform } from "../rename-timeout-duration-parameters/rename-timeout-duration-parameters-transform.js";
@@ -96,6 +97,7 @@ export const transformRegistry = {
96
97
  [renameReplaceChildParametersTransform.id]: renameReplaceChildParametersTransform,
97
98
  [renameRestParametersTransform.id]: renameRestParametersTransform,
98
99
  [renameSafePropertyAccessorParametersTransform.id]: renameSafePropertyAccessorParametersTransform,
100
+ [renameSearchParametersVariablesTransform.id]: renameSearchParametersVariablesTransform,
99
101
  [renameStringSplitVariablesTransform.id]: renameStringSplitVariablesTransform,
100
102
  [renameThisAliasesTransform.id]: renameThisAliasesTransform,
101
103
  [renameTimeoutDurationParametersTransform.id]: renameTimeoutDurationParametersTransform,
@@ -0,0 +1,5 @@
1
+ import type { Scope } from "@babel/traverse";
2
+ import type { Expression } from "@babel/types";
3
+ type UrlConstructionKind = "identifier" | "member";
4
+ export declare const getUrlConstructionKind: (scope: Scope, node: Expression | null | undefined, constructorName: "URL" | "URLSearchParams") => UrlConstructionKind | undefined;
5
+ export {};
@@ -0,0 +1,47 @@
1
+ import { isAllowedUrlNamespaceBindingInScope } from "./is-allowed-url-namespace-binding-in-scope.js";
2
+ export const getUrlConstructionKind = (scope, node, constructorName) => {
3
+ if (node?.type !== "NewExpression")
4
+ return undefined;
5
+ const callee = node.callee;
6
+ if (callee.type === "Identifier") {
7
+ return callee.name === constructorName ? "identifier" : undefined;
8
+ }
9
+ if (callee.type !== "MemberExpression" &&
10
+ callee.type !== "OptionalMemberExpression") {
11
+ return undefined;
12
+ }
13
+ // `new url?.URL()` is a syntax error, but `new (url?.URL)(...)` is valid and
14
+ // produces an `OptionalMemberExpression` callee that Babel marks as
15
+ // parenthesized.
16
+ if (callee.type === "OptionalMemberExpression" &&
17
+ !callee.extra?.parenthesized) {
18
+ return undefined;
19
+ }
20
+ let propertyName = undefined;
21
+ if (callee.computed) {
22
+ if (callee.property.type === "StringLiteral") {
23
+ propertyName = callee.property.value;
24
+ }
25
+ else if (callee.property.type === "TemplateLiteral" &&
26
+ callee.property.expressions.length === 0 &&
27
+ callee.property.quasis.length === 1) {
28
+ const quasi = callee.property.quasis[0];
29
+ const cooked = quasi?.value.cooked;
30
+ propertyName = cooked ?? quasi?.value.raw;
31
+ }
32
+ }
33
+ else if (callee.property.type === "Identifier") {
34
+ propertyName = callee.property.name;
35
+ }
36
+ else if (callee.property.type === "StringLiteral") {
37
+ propertyName = callee.property.value;
38
+ }
39
+ if (propertyName !== constructorName)
40
+ return undefined;
41
+ if (callee.object.type !== "Identifier")
42
+ return undefined;
43
+ if (!isAllowedUrlNamespaceBindingInScope(scope, callee.object.name)) {
44
+ return undefined;
45
+ }
46
+ return "member";
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { Scope } from "@babel/traverse";
2
+ export declare const isAllowedUrlConstructorInScope: (scope: Scope, constructorName: "URL" | "URLSearchParams") => boolean;
@@ -0,0 +1,70 @@
1
+ import { isAllowedUrlModuleImportBinding } from "./is-allowed-url-module-import-binding.js";
2
+ import { isNodeRequireInScope, isRequireFromUrlModule, } from "./is-require-from-url-module.js";
3
+ const doesObjectPatternBindConstructorName = (pattern, constructorName) => {
4
+ for (const property of pattern.properties) {
5
+ if (property.type !== "ObjectProperty")
6
+ continue;
7
+ const keyName = property.computed
8
+ ? property.key.type === "StringLiteral"
9
+ ? property.key.value
10
+ : undefined
11
+ : property.key.type === "Identifier"
12
+ ? property.key.name
13
+ : property.key.type === "StringLiteral"
14
+ ? property.key.value
15
+ : undefined;
16
+ if (keyName !== constructorName)
17
+ continue;
18
+ const bindingName = property.value.type === "Identifier"
19
+ ? property.value.name
20
+ : property.value.type === "AssignmentPattern" &&
21
+ property.value.left.type === "Identifier"
22
+ ? property.value.left.name
23
+ : undefined;
24
+ if (bindingName !== constructorName)
25
+ continue;
26
+ return true;
27
+ }
28
+ return false;
29
+ };
30
+ export const isAllowedUrlConstructorInScope = (scope, constructorName) => {
31
+ const binding = scope.getBinding(constructorName);
32
+ if (!binding)
33
+ return true;
34
+ // Aliases like `const { URL: MyURL } = require("node:url")` do not create a
35
+ // binding for `URL`, so they don't affect this shadowing check.
36
+ if (binding.path.isImportSpecifier() &&
37
+ binding.path.parentPath.isImportDeclaration() &&
38
+ (binding.path.node.importKind === "type" ||
39
+ binding.path.parentPath.node.importKind === "type")) {
40
+ // `import type` doesn't introduce a runtime value binding. Treat it as if
41
+ // the constructor name wasn't bound locally (e.g. allow the global binding).
42
+ return true;
43
+ }
44
+ if (isAllowedUrlModuleImportBinding(binding.path, constructorName))
45
+ return true;
46
+ const declarator = binding.path.isVariableDeclarator()
47
+ ? binding.path
48
+ : binding.path.findParent((parent) => parent.isVariableDeclarator());
49
+ if (!declarator?.isVariableDeclarator())
50
+ return false;
51
+ const init = declarator.node.init;
52
+ if (declarator.node.id.type === "ObjectPattern") {
53
+ if (!doesObjectPatternBindConstructorName(declarator.node.id, constructorName)) {
54
+ return false;
55
+ }
56
+ if (!isRequireFromUrlModule(init))
57
+ return false;
58
+ if (!isNodeRequireInScope(declarator.scope))
59
+ return false;
60
+ return true;
61
+ }
62
+ if (declarator.node.id.type === "Identifier") {
63
+ if (!isRequireFromUrlModule(init, constructorName))
64
+ return false;
65
+ if (!isNodeRequireInScope(declarator.scope))
66
+ return false;
67
+ return true;
68
+ }
69
+ return false;
70
+ };
@@ -0,0 +1,2 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ export declare const isAllowedUrlModuleImportBinding: (bindingPath: NodePath, importedName: "URL" | "URLSearchParams") => boolean;
@@ -0,0 +1,24 @@
1
+ export const isAllowedUrlModuleImportBinding = (bindingPath, importedName) => {
2
+ // Only allow named imports (e.g. `import { URL } from "node:url"`).
3
+ // Default/namespace imports don't bind the constructor and often indicate
4
+ // interop or module-namespace values, so treating them as "safe stdlib" would
5
+ // create false positives.
6
+ if (!bindingPath.isImportSpecifier())
7
+ return false;
8
+ // `import type { URL } from "node:url"` (and TS 5.0 `import { type URL } ...`)
9
+ // doesn't introduce a runtime value binding, so it can't be treated as a safe
10
+ // stdlib constructor in scope.
11
+ if (bindingPath.node.importKind === "type")
12
+ return false;
13
+ const imported = bindingPath.node.imported;
14
+ const bindingImportedName = imported.type === "Identifier" ? imported.name : imported.value;
15
+ if (bindingImportedName !== importedName)
16
+ return false;
17
+ const importDeclaration = bindingPath.parentPath;
18
+ if (!importDeclaration.isImportDeclaration())
19
+ return false;
20
+ if (importDeclaration.node.importKind === "type")
21
+ return false;
22
+ const source = importDeclaration.node.source.value;
23
+ return source === "url" || source === "node:url";
24
+ };
@@ -0,0 +1,2 @@
1
+ import type { Scope } from "@babel/traverse";
2
+ export declare const isAllowedUrlNamespaceBindingInScope: (scope: Scope, bindingName: string) => boolean;
@@ -0,0 +1,35 @@
1
+ import { isNodeRequireInScope, isRequireFromUrlModule, } from "./is-require-from-url-module.js";
2
+ export const isAllowedUrlNamespaceBindingInScope = (scope, bindingName) => {
3
+ const binding = scope.getBinding(bindingName);
4
+ if (!binding)
5
+ return false;
6
+ if (binding.path.isImportNamespaceSpecifier() ||
7
+ binding.path.isImportDefaultSpecifier()) {
8
+ // Accept both:
9
+ // - `import * as url from "node:url"` (namespace import)
10
+ // - `import url from "node:url"` (CJS interop default import)
11
+ //
12
+ // In Node, `node:url` is a CommonJS module and default import resolves to
13
+ // the module exports object, so it supports `url.URL` / `url.URLSearchParams`.
14
+ // This also matches common transpiled/bundled output shapes.
15
+ const importDeclaration = binding.path.parentPath;
16
+ if (!importDeclaration.isImportDeclaration())
17
+ return false;
18
+ if (importDeclaration.node.importKind === "type")
19
+ return false;
20
+ const source = importDeclaration.node.source.value;
21
+ return source === "url" || source === "node:url";
22
+ }
23
+ const declarator = binding.path.isVariableDeclarator()
24
+ ? binding.path
25
+ : binding.path.findParent((parent) => parent.isVariableDeclarator());
26
+ if (!declarator?.isVariableDeclarator())
27
+ return false;
28
+ // No `propertyName` arg: ensure the binding is the module object
29
+ // (`require("node:url")`), not a specific export (`require("node:url").URL`).
30
+ if (!isRequireFromUrlModule(declarator.node.init))
31
+ return false;
32
+ if (!isNodeRequireInScope(declarator.scope))
33
+ return false;
34
+ return true;
35
+ };
@@ -0,0 +1,15 @@
1
+ import type { Scope } from "@babel/traverse";
2
+ import type { Expression } from "@babel/types";
3
+ /**
4
+ * Returns `true` for:
5
+ * - `require("url")` / `require("node:url")`
6
+ * - `require("url").URL` / `require("node:url").URL` (or any other `propertyName`),
7
+ * but only when the `propertyName` argument is provided
8
+ *
9
+ * Note: This helper only checks the shape of the AST. If `require` is locally
10
+ * bound in the current scope (e.g. by a bundler), callers should additionally
11
+ * verify that `require` refers to Node's `require()` (see
12
+ * {@link isNodeRequireInScope}).
13
+ */
14
+ export declare const isRequireFromUrlModule: (node: Expression | null | undefined, propertyName?: string) => boolean;
15
+ export declare const isNodeRequireInScope: (scope: Scope) => boolean;
@@ -0,0 +1,163 @@
1
+ const getRequireFromUrlModuleArgument = (expression) => {
2
+ if (expression.type !== "CallExpression")
3
+ return;
4
+ if (expression.callee.type !== "Identifier")
5
+ return;
6
+ if (expression.callee.name !== "require")
7
+ return;
8
+ if (expression.arguments.length !== 1)
9
+ return;
10
+ const argument = expression.arguments[0];
11
+ if (!argument)
12
+ return;
13
+ if (argument.type === "StringLiteral") {
14
+ if (argument.value === "url")
15
+ return "url";
16
+ if (argument.value === "node:url")
17
+ return "node:url";
18
+ return;
19
+ }
20
+ if (argument.type === "TemplateLiteral") {
21
+ if (argument.expressions.length > 0)
22
+ return;
23
+ const quasi = argument.quasis[0];
24
+ if (!quasi)
25
+ return;
26
+ const value = quasi.value.cooked ?? quasi.value.raw;
27
+ if (value === "url")
28
+ return "url";
29
+ if (value === "node:url")
30
+ return "node:url";
31
+ return;
32
+ }
33
+ };
34
+ /**
35
+ * Returns `true` for:
36
+ * - `require("url")` / `require("node:url")`
37
+ * - `require("url").URL` / `require("node:url").URL` (or any other `propertyName`),
38
+ * but only when the `propertyName` argument is provided
39
+ *
40
+ * Note: This helper only checks the shape of the AST. If `require` is locally
41
+ * bound in the current scope (e.g. by a bundler), callers should additionally
42
+ * verify that `require` refers to Node's `require()` (see
43
+ * {@link isNodeRequireInScope}).
44
+ */
45
+ export const isRequireFromUrlModule = (node, propertyName) => {
46
+ if (!node)
47
+ return false;
48
+ if (!propertyName)
49
+ return Boolean(getRequireFromUrlModuleArgument(node));
50
+ if (node.type !== "MemberExpression")
51
+ return false;
52
+ let memberPropertyName = undefined;
53
+ if (node.computed) {
54
+ if (node.property.type === "StringLiteral") {
55
+ memberPropertyName = node.property.value;
56
+ }
57
+ else if (node.property.type === "TemplateLiteral" &&
58
+ node.property.expressions.length === 0 &&
59
+ node.property.quasis.length === 1) {
60
+ const quasi = node.property.quasis[0];
61
+ memberPropertyName = quasi?.value.cooked ?? quasi?.value.raw;
62
+ }
63
+ }
64
+ else if (node.property.type === "Identifier") {
65
+ memberPropertyName = node.property.name;
66
+ }
67
+ const hasMatchingProperty = memberPropertyName === propertyName;
68
+ if (!hasMatchingProperty)
69
+ return false;
70
+ // We only accept a direct `require("url").X` (or `require("node:url").X`)
71
+ // member expression; deeper chains like `require("url").X.Y` are not relevant
72
+ // for our current usage and would fail the base `require("url")` check anyway.
73
+ if (node.object.type !== "CallExpression")
74
+ return false;
75
+ if (node.object.callee.type !== "Identifier")
76
+ return false;
77
+ if (node.object.callee.name !== "require")
78
+ return false;
79
+ return Boolean(getRequireFromUrlModuleArgument(node.object));
80
+ };
81
+ export const isNodeRequireInScope = (scope) => {
82
+ const requireBinding = scope.getBinding("require");
83
+ if (!requireBinding) {
84
+ const program = scope.getProgramParent().block;
85
+ if (program.type !== "Program")
86
+ return true;
87
+ // Ignore type-only module syntax: it's erased at runtime and commonly used
88
+ // in TS/CJS codebases that still rely on Node's `require`.
89
+ const hasModuleSyntax = program.body.some((statement) => {
90
+ if (statement.type === "ImportDeclaration") {
91
+ return statement.importKind !== "type";
92
+ }
93
+ if (statement.type === "ExportAllDeclaration") {
94
+ return statement.exportKind !== "type";
95
+ }
96
+ if (statement.type === "ExportNamedDeclaration") {
97
+ return statement.exportKind !== "type";
98
+ }
99
+ return statement.type === "ExportDefaultDeclaration";
100
+ });
101
+ // In value-level modules, an unbound `require` is unlikely to be Node's
102
+ // `require` (Node ESM doesn't provide it). Treat it as unsafe to avoid
103
+ // false positives from bundler shims.
104
+ if (hasModuleSyntax)
105
+ return false;
106
+ // In non-module files, assume an unbound `require` is Node's CJS `require`.
107
+ // This can be false for AMD/UMD/browser bundles, but tightening this would
108
+ // introduce false negatives for common Node/CJS inputs.
109
+ return true;
110
+ }
111
+ if (!requireBinding.path.isVariableDeclarator())
112
+ return false;
113
+ const init = requireBinding.path.node.init;
114
+ if (init?.type !== "CallExpression")
115
+ return false;
116
+ if (init.callee.type === "Identifier") {
117
+ const createRequireBinding = scope.getBinding(init.callee.name);
118
+ if (!createRequireBinding)
119
+ return false;
120
+ if (!createRequireBinding.path.isImportSpecifier())
121
+ return false;
122
+ if (createRequireBinding.path.node.importKind === "type")
123
+ return false;
124
+ const imported = createRequireBinding.path.node.imported;
125
+ const importedName = imported.type === "Identifier" ? imported.name : imported.value;
126
+ if (importedName !== "createRequire")
127
+ return false;
128
+ const importDeclaration = createRequireBinding.path.parentPath;
129
+ if (!importDeclaration.isImportDeclaration())
130
+ return false;
131
+ if (importDeclaration.node.importKind === "type")
132
+ return false;
133
+ const source = importDeclaration.node.source.value;
134
+ return source === "module" || source === "node:module";
135
+ }
136
+ if (init.callee.type !== "MemberExpression")
137
+ return false;
138
+ if (init.callee.object.type !== "Identifier")
139
+ return false;
140
+ const propertyName = init.callee.computed
141
+ ? init.callee.property.type === "StringLiteral"
142
+ ? init.callee.property.value
143
+ : undefined
144
+ : init.callee.property.type === "Identifier"
145
+ ? init.callee.property.name
146
+ : undefined;
147
+ if (propertyName !== "createRequire")
148
+ return false;
149
+ const moduleBindingName = init.callee.object.name;
150
+ const moduleBinding = scope.getBinding(moduleBindingName);
151
+ if (!moduleBinding)
152
+ return false;
153
+ const moduleBindingPath = moduleBinding.path;
154
+ if (!moduleBindingPath.isImportDefaultSpecifier() &&
155
+ !moduleBindingPath.isImportNamespaceSpecifier()) {
156
+ return false;
157
+ }
158
+ const importDeclaration = moduleBindingPath.parentPath;
159
+ if (!importDeclaration.isImportDeclaration())
160
+ return false;
161
+ const source = importDeclaration.node.source.value;
162
+ return source === "module" || source === "node:module";
163
+ };
@@ -0,0 +1,2 @@
1
+ import type { MemberExpression, OptionalMemberExpression } from "@babel/types";
2
+ export declare const getMemberExpressionPropertyName: (expression: MemberExpression | OptionalMemberExpression) => string | undefined;
@@ -0,0 +1,20 @@
1
+ export const getMemberExpressionPropertyName = (expression) => {
2
+ const key = expression.property;
3
+ if (!expression.computed) {
4
+ if (key.type === "Identifier")
5
+ return key.name;
6
+ if (key.type === "StringLiteral")
7
+ return key.value;
8
+ return undefined;
9
+ }
10
+ if (key.type === "StringLiteral")
11
+ return key.value;
12
+ if (key.type === "TemplateLiteral" &&
13
+ key.expressions.length === 0 &&
14
+ key.quasis.length === 1) {
15
+ const quasi = key.quasis[0];
16
+ const cooked = quasi?.value.cooked;
17
+ return cooked ?? quasi?.value.raw;
18
+ }
19
+ return undefined;
20
+ };
@@ -0,0 +1,3 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ import type { MemberExpression, OptionalMemberExpression } from "@babel/types";
3
+ export declare const getSearchParametersMemberExpression: (referencePath: NodePath) => NodePath<MemberExpression | OptionalMemberExpression> | undefined;
@@ -0,0 +1,26 @@
1
+ export const getSearchParametersMemberExpression = (referencePath) => {
2
+ const parent = referencePath.parentPath;
3
+ if (!parent)
4
+ return;
5
+ if (parent.isMemberExpression() &&
6
+ parent.node.object === referencePath.node) {
7
+ return parent;
8
+ }
9
+ if (parent.isOptionalMemberExpression() &&
10
+ parent.node.object === referencePath.node) {
11
+ return parent;
12
+ }
13
+ if (parent.isTSNonNullExpression() &&
14
+ parent.node.expression === referencePath.node) {
15
+ const outer = parent.parentPath;
16
+ if (!outer)
17
+ return;
18
+ if (outer.isMemberExpression() && outer.node.object === parent.node) {
19
+ return outer;
20
+ }
21
+ if (outer.isOptionalMemberExpression() &&
22
+ outer.node.object === parent.node) {
23
+ return outer;
24
+ }
25
+ }
26
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "recommended": true,
3
+ "notes": "Measured with baseline none: 100.00% of original diff. Added to recommended for readability.",
4
+ "evaluations": {
5
+ "legacy:evaluation": {
6
+ "diffSizePercent": 100,
7
+ "evaluatedAt": "2026-01-25T09:49:29.000Z",
8
+ "changedLines": 0,
9
+ "durationSeconds": 0,
10
+ "stableNames": 0
11
+ },
12
+ "claude-code-2.1.10:claude-code-2.1.11": {
13
+ "diffSizePercent": 100,
14
+ "evaluatedAt": "2026-01-26T07:05:01.592Z",
15
+ "changedLines": 32,
16
+ "durationSeconds": 54.402595083,
17
+ "stableNames": 1358
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type Transform } from "../../core/types.js";
2
+ export declare const renameSearchParametersVariablesTransform: Transform;
@@ -0,0 +1,75 @@
1
+ import { createRequire } from "node:module";
2
+ import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
+ import { getFilesToProcess, } from "../../core/types.js";
4
+ import { getUrlConstructionKind } from "../get-url-construction-kind.js";
5
+ import { isAllowedUrlConstructorInScope } from "../is-allowed-url-constructor-in-scope.js";
6
+ import { isSearchParametersDestructureUsage, isSearchParametersForOfUsage, isSearchParametersPropertyAccess, isSearchParametersSpreadUsage, } from "./search-parameters-usage-heuristics.js";
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 renameSearchParametersVariablesTransform = {
11
+ id: "rename-search-parameters-variables",
12
+ description: "Renames variables initialized with new URLSearchParams(...) when they’re used (member access, destructuring, for-of, spread)",
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
+ const group = new RenameGroup();
20
+ traverse(fileInfo.ast, {
21
+ VariableDeclarator(path) {
22
+ nodesVisited += 1;
23
+ // Only rename actual bindings. Patterns don't create a stable
24
+ // `URLSearchParams` variable to rename, and `URLSearchParams` is
25
+ // iterable (not a plain object), so destructuring doesn't map cleanly.
26
+ if (path.node.id.type !== "Identifier")
27
+ return;
28
+ const constructionKind = getUrlConstructionKind(path.scope, path.node.init, "URLSearchParams");
29
+ if (!constructionKind)
30
+ return;
31
+ if (constructionKind === "identifier" &&
32
+ !isAllowedUrlConstructorInScope(path.scope, "URLSearchParams")) {
33
+ return;
34
+ }
35
+ const currentName = path.node.id.name;
36
+ if (isStableRenamed(currentName))
37
+ return;
38
+ const binding = path.scope.getBinding(currentName);
39
+ if (!binding)
40
+ return;
41
+ if (!binding.constant)
42
+ return;
43
+ // Unlike `URL`, `URLSearchParams` is frequently used via a single
44
+ // method/property call. Construction is already a strong signal, so we
45
+ // rename on first evidence of usage.
46
+ let hasUsage = false;
47
+ for (const referencePath of binding.referencePaths) {
48
+ if (isSearchParametersPropertyAccess(referencePath) ||
49
+ isSearchParametersForOfUsage(referencePath) ||
50
+ isSearchParametersSpreadUsage(referencePath) ||
51
+ isSearchParametersDestructureUsage(referencePath)) {
52
+ hasUsage = true;
53
+ break;
54
+ }
55
+ }
56
+ if (!hasUsage)
57
+ return;
58
+ const candidateName = "searchParameters";
59
+ if (candidateName === currentName)
60
+ return;
61
+ group.add({
62
+ scope: path.scope,
63
+ currentName,
64
+ baseName: candidateName,
65
+ });
66
+ },
67
+ });
68
+ transformationsApplied += group.apply();
69
+ }
70
+ return Promise.resolve({
71
+ nodesVisited,
72
+ transformationsApplied,
73
+ });
74
+ },
75
+ };
@@ -0,0 +1,5 @@
1
+ import type { NodePath } from "@babel/traverse";
2
+ export declare const isSearchParametersPropertyAccess: (referencePath: NodePath) => boolean;
3
+ export declare const isSearchParametersForOfUsage: (referencePath: NodePath) => boolean;
4
+ export declare const isSearchParametersSpreadUsage: (referencePath: NodePath) => boolean;
5
+ export declare const isSearchParametersDestructureUsage: (referencePath: NodePath) => boolean;
@@ -0,0 +1,151 @@
1
+ import { getMemberExpressionPropertyName } from "./get-member-expression-property-name.js";
2
+ import { getSearchParametersMemberExpression } from "./get-search-parameters-member-expression.js";
3
+ const searchParametersPropertyNames = new Set([
4
+ "append",
5
+ "delete",
6
+ "entries",
7
+ "forEach",
8
+ "get",
9
+ "getAll",
10
+ "has",
11
+ "keys",
12
+ "set",
13
+ "size",
14
+ "sort",
15
+ "toString",
16
+ "values",
17
+ ]);
18
+ export const isSearchParametersPropertyAccess = (referencePath) => {
19
+ const memberExpression = getSearchParametersMemberExpression(referencePath);
20
+ if (!memberExpression)
21
+ return false;
22
+ const keyName = getMemberExpressionPropertyName(memberExpression.node);
23
+ if (!keyName)
24
+ return false;
25
+ return searchParametersPropertyNames.has(keyName);
26
+ };
27
+ export const isSearchParametersForOfUsage = (referencePath) => {
28
+ const parent = referencePath.parentPath;
29
+ if (!parent)
30
+ return false;
31
+ if (parent.isForOfStatement()) {
32
+ return parent.node.right === referencePath.node;
33
+ }
34
+ if (parent.isTSNonNullExpression() &&
35
+ parent.node.expression === referencePath.node) {
36
+ const forOf = parent.parentPath;
37
+ if (!forOf)
38
+ return false;
39
+ if (!forOf.isForOfStatement())
40
+ return false;
41
+ return forOf.node.right === parent.node;
42
+ }
43
+ return false;
44
+ };
45
+ export const isSearchParametersSpreadUsage = (referencePath) => {
46
+ const parent = referencePath.parentPath;
47
+ if (!parent)
48
+ return false;
49
+ if (parent.isSpreadElement()) {
50
+ const container = parent.parentPath;
51
+ if (!container)
52
+ return false;
53
+ if (!container.isArrayExpression() &&
54
+ !container.isCallExpression() &&
55
+ !container.isNewExpression()) {
56
+ return false;
57
+ }
58
+ return parent.node.argument === referencePath.node;
59
+ }
60
+ if (parent.isTSNonNullExpression() &&
61
+ parent.node.expression === referencePath.node) {
62
+ const spread = parent.parentPath;
63
+ if (!spread)
64
+ return false;
65
+ if (!spread.isSpreadElement())
66
+ return false;
67
+ const container = spread.parentPath;
68
+ if (!container)
69
+ return false;
70
+ if (!container.isArrayExpression() &&
71
+ !container.isCallExpression() &&
72
+ !container.isNewExpression()) {
73
+ return false;
74
+ }
75
+ return spread.node.argument === parent.node;
76
+ }
77
+ return false;
78
+ };
79
+ const isSearchParametersDestructurePattern = (pattern) => {
80
+ if (pattern.properties.length === 0)
81
+ return false;
82
+ for (const property of pattern.properties) {
83
+ // Rest properties suggest generic object destructuring, so we stay
84
+ // conservative and don't treat it as URLSearchParams usage evidence.
85
+ if (property.type === "RestElement")
86
+ return false;
87
+ const keyName = property.computed
88
+ ? property.key.type === "StringLiteral"
89
+ ? property.key.value
90
+ : undefined
91
+ : property.key.type === "Identifier"
92
+ ? property.key.name
93
+ : property.key.type === "StringLiteral"
94
+ ? property.key.value
95
+ : undefined;
96
+ if (!keyName)
97
+ return false;
98
+ // Be strict: `URLSearchParams` method names overlap with many other APIs, so
99
+ // we only treat destructuring as usage evidence when *every* key matches.
100
+ if (!searchParametersPropertyNames.has(keyName))
101
+ return false;
102
+ }
103
+ return true;
104
+ };
105
+ export const isSearchParametersDestructureUsage = (referencePath) => {
106
+ const parent = referencePath.parentPath;
107
+ if (parent?.isTSNonNullExpression() &&
108
+ parent.node.expression === referencePath.node) {
109
+ const outer = parent.parentPath;
110
+ if (!outer)
111
+ return false;
112
+ if (outer.isVariableDeclarator()) {
113
+ if (outer.node.init !== parent.node)
114
+ return false;
115
+ const id = outer.node.id;
116
+ if (id.type !== "ObjectPattern")
117
+ return false;
118
+ return isSearchParametersDestructurePattern(id);
119
+ }
120
+ if (outer.isAssignmentExpression()) {
121
+ if (outer.node.operator !== "=")
122
+ return false;
123
+ if (outer.node.right !== parent.node)
124
+ return false;
125
+ const left = outer.node.left;
126
+ if (left.type !== "ObjectPattern")
127
+ return false;
128
+ return isSearchParametersDestructurePattern(left);
129
+ }
130
+ return false;
131
+ }
132
+ if (parent?.isVariableDeclarator()) {
133
+ if (parent.node.init !== referencePath.node)
134
+ return false;
135
+ const id = parent.node.id;
136
+ if (id.type !== "ObjectPattern")
137
+ return false;
138
+ return isSearchParametersDestructurePattern(id);
139
+ }
140
+ if (parent?.isAssignmentExpression()) {
141
+ if (parent.node.operator !== "=")
142
+ return false;
143
+ if (parent.node.right !== referencePath.node)
144
+ return false;
145
+ const left = parent.node.left;
146
+ if (left.type !== "ObjectPattern")
147
+ return false;
148
+ return isSearchParametersDestructurePattern(left);
149
+ }
150
+ return false;
151
+ };
@@ -2,17 +2,11 @@ import { createRequire } from "node:module";
2
2
  import { isStableRenamed, RenameGroup } from "../../core/stable-naming.js";
3
3
  import { getFilesToProcess, } from "../../core/types.js";
4
4
  import { hasUrlDestructure, urlPropertyNames, } from "../url-usage-heuristics.js";
5
+ import { getUrlConstructionKind } from "../get-url-construction-kind.js";
6
+ import { isAllowedUrlConstructorInScope } from "../is-allowed-url-constructor-in-scope.js";
5
7
  const require = createRequire(import.meta.url);
6
8
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7
9
  const traverse = require("@babel/traverse").default;
8
- const isUrlConstruction = (node) => {
9
- if (node?.type !== "NewExpression")
10
- return false;
11
- if (node.callee.type !== "Identifier" || node.callee.name !== "URL") {
12
- return false;
13
- }
14
- return true;
15
- };
16
10
  const isUrlPropertyAccess = (referencePath) => {
17
11
  const parent = referencePath.parentPath;
18
12
  if (!parent)
@@ -48,14 +42,21 @@ export const renameUrlVariablesTransform = {
48
42
  nodesVisited += 1;
49
43
  if (path.node.id.type !== "Identifier")
50
44
  return;
51
- if (!isUrlConstruction(path.node.init))
45
+ const constructionKind = getUrlConstructionKind(path.scope, path.node.init, "URL");
46
+ if (!constructionKind)
47
+ return;
48
+ if (constructionKind === "identifier" &&
49
+ !isAllowedUrlConstructorInScope(path.scope, "URL")) {
52
50
  return;
51
+ }
53
52
  const currentName = path.node.id.name;
54
53
  if (isStableRenamed(currentName))
55
54
  return;
56
55
  const binding = path.scope.getBinding(currentName);
57
56
  if (!binding)
58
57
  return;
58
+ if (!binding.constant)
59
+ return;
59
60
  let sawUrlDestructure = false;
60
61
  let propertyAccesses = 0;
61
62
  for (const referencePath of binding.referencePaths) {
@@ -71,12 +72,12 @@ export const renameUrlVariablesTransform = {
71
72
  }
72
73
  if (!sawUrlDestructure && propertyAccesses < 2)
73
74
  return;
75
+ // Name collisions (including unresolved program globals) are handled by
76
+ // `RenameGroup`/`findAvailableName` so we don't need to bail out early
77
+ // when `url` is already referenced elsewhere in the file.
74
78
  const candidateName = "url";
75
79
  if (candidateName === currentName)
76
80
  return;
77
- const programScope = path.scope.getProgramParent();
78
- if (Object.hasOwn(programScope.globals, candidateName))
79
- return;
80
81
  group.add({
81
82
  scope: path.scope,
82
83
  currentName,
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.52.0",
5
+ "version": "1.53.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",