From 939f0a2d0957aec26fa37d2de595f68cf8f3ab9a Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:24:05 +0100 Subject: [PATCH 1/5] feat(`fs-access-mode-constants`): general stability --- recipes/fs-access-mode-constants/codemod.yaml | 2 +- .../fs-access-mode-constants/src/workflow.ts | 135 +++++++++++++----- .../tests/expected/file-08.js | 4 + .../tests/expected/file-09.js | 3 + .../tests/input/file-08.js | 4 + .../tests/input/file-09.js | 3 + 6 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 recipes/fs-access-mode-constants/tests/expected/file-08.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/file-09.js create mode 100644 recipes/fs-access-mode-constants/tests/input/file-08.js create mode 100644 recipes/fs-access-mode-constants/tests/input/file-09.js diff --git a/recipes/fs-access-mode-constants/codemod.yaml b/recipes/fs-access-mode-constants/codemod.yaml index ad85b5d4..93d8b377 100644 --- a/recipes/fs-access-mode-constants/codemod.yaml +++ b/recipes/fs-access-mode-constants/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" name: "@nodejs/fs-access-mode-constants" -version: "1.0.2" +version: "1.0.3" description: Handle DEP0176 via transforming imports of `fs.F_OK`, `fs.R_OK`, `fs.W_OK`, `fs.X_OK` from the root `fs` module to `fs.constants`. author: nekojanai (Jana) license: MIT diff --git a/recipes/fs-access-mode-constants/src/workflow.ts b/recipes/fs-access-mode-constants/src/workflow.ts index 5f72ff63..72b43396 100644 --- a/recipes/fs-access-mode-constants/src/workflow.ts +++ b/recipes/fs-access-mode-constants/src/workflow.ts @@ -1,13 +1,15 @@ import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; -import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; -const patterns = ['F_OK', 'R_OK', 'W_OK', 'X_OK']; +const PATTERNS = ['F_OK', 'R_OK', 'W_OK', 'X_OK']; export default function tranform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; + const replacementMap = new Map(); const requireStatements = getNodeRequireCalls(root, 'fs'); @@ -17,63 +19,128 @@ export default function tranform(root: SgRoot): string | null { }); if (objectPattern) { + const promisesBinding = getLocalPromisesBinding(statement); let objPatArr = objectPattern .findAll({ rule: { kind: 'shorthand_property_identifier_pattern' }, }) .map((v) => v.text()); - objPatArr = objPatArr.filter((v) => !patterns.includes(v)); - objPatArr.push('constants'); - edits.push(objectPattern.replace(`{ ${objPatArr.join(', ')} }`)); + + const removedBindings = objPatArr.filter((v) => PATTERNS.includes(v)); + if (removedBindings.length > 0) { + for (const binding of removedBindings) { + if (promisesBinding) { + replacementMap.set( + binding, + `${promisesBinding}.constants.${binding}`, + ); + } else { + replacementMap.set(binding, `constants.${binding}`); + } + } + + objPatArr = objPatArr.filter((v) => !PATTERNS.includes(v)); + if (!promisesBinding && !objPatArr.includes('constants')) { + objPatArr.push('constants'); + } + edits.push(objectPattern.replace(`{ ${objPatArr.join(', ')} }`)); + } } } const importStatements = getNodeImportStatements(root, 'fs'); - let promisesImportName = ''; for (const statement of importStatements) { + const promisesBinding = getLocalPromisesBinding(statement); const objectPattern = statement.find({ rule: { kind: 'named_imports' }, }); if (objectPattern) { - let objPatArr = objectPattern - .findAll({ - rule: { kind: 'import_specifier' }, - }) - .map((v) => v.text()); - objPatArr = objPatArr.filter((v) => !patterns.includes(v)); - const promisesImport = objPatArr.find((v) => v.startsWith('promises')); - if (promisesImport) { - if (promisesImport.includes('as')) { - const m = promisesImport.matchAll(/promises as (\w+)/g); - m.forEach((v) => { - promisesImportName = v[1] ?? 'promises'; - }); - } else { - promisesImportName = promisesImport; + const specifiers = objectPattern.findAll({ + rule: { kind: 'import_specifier' }, + }); + + const filteredImports: string[] = []; + let removedAny = false; + + for (const specifier of specifiers) { + const importedName = specifier.field('name')?.text() ?? ''; + const localName = specifier.field('alias')?.text() ?? importedName; + + if (PATTERNS.includes(importedName)) { + removedAny = true; + const replacementPrefix = promisesBinding + ? `${promisesBinding}.constants` + : 'constants'; + replacementMap.set(localName, `${replacementPrefix}.${importedName}`); + continue; + } + + filteredImports.push(specifier.text()); + } + + if (removedAny) { + if (!promisesBinding && !filteredImports.includes('constants')) { + filteredImports.push('constants'); } - promisesImportName = `${promisesImportName}.`; - } else { - objPatArr.push('constants'); + edits.push(objectPattern.replace(`{ ${filteredImports.join(', ')} }`)); } - edits.push(objectPattern.replace(`{ ${objPatArr.join(', ')} }`)); } } - for (const _OK of patterns) { - for (const [prefix, replacement] of [ - ['fs.', 'fs.constants.'], - ['', `${promisesImportName ? promisesImportName : ''}constants.`], - ]) { - const patterns = rootNode.findAll({ - rule: { pattern: `${prefix}${_OK}` }, + for (const statement of [...requireStatements, ...importStatements]) { + for (const _OK of PATTERNS) { + const local = resolveBindingPath(statement, `$.${_OK}`); + if (!local?.includes('.') || local.includes('.constants.')) { + continue; + } + + replacementMap.set(local, local.replace(`.${_OK}`, `.constants.${_OK}`)); + } + } + + for (const [local, replacement] of replacementMap) { + if (local.includes('.')) { + const nodes = rootNode.findAll({ + rule: { pattern: local }, }); - for (const pattern of patterns) { - edits.push(pattern.replace(`${replacement}${_OK}`)); + for (const node of nodes) { + edits.push(node.replace(replacement)); + } + continue; + } + + const refs = rootNode.findAll({ + rule: { + kind: 'identifier', + regex: `^${escapeRegExp(local)}$`, + }, + }); + + for (const ref of refs) { + if ( + ref.inside({ rule: { kind: 'named_imports' } }) || + ref.inside({ rule: { kind: 'object_pattern' } }) + ) { + continue; } + edits.push(ref.replace(replacement)); } } return rootNode.commitEdits(edits); } + +function getLocalPromisesBinding(statement: SgNode): string { + const resolved = resolveBindingPath(statement, '$.promises'); + if (!resolved || resolved.includes('.')) { + return ''; + } + + return resolved; +} + +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/recipes/fs-access-mode-constants/tests/expected/file-08.js b/recipes/fs-access-mode-constants/tests/expected/file-08.js new file mode 100644 index 00000000..e159c8bd --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/file-08.js @@ -0,0 +1,4 @@ +import { readFileSync } from 'node:fs'; + +const F_OK = 1; +console.log(readFileSync, F_OK); diff --git a/recipes/fs-access-mode-constants/tests/expected/file-09.js b/recipes/fs-access-mode-constants/tests/expected/file-09.js new file mode 100644 index 00000000..cbcd24a1 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/file-09.js @@ -0,0 +1,3 @@ +import { access, constants } from 'node:fs'; + +access('/path/to/file', constants.F_OK, callback); diff --git a/recipes/fs-access-mode-constants/tests/input/file-08.js b/recipes/fs-access-mode-constants/tests/input/file-08.js new file mode 100644 index 00000000..e159c8bd --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/file-08.js @@ -0,0 +1,4 @@ +import { readFileSync } from 'node:fs'; + +const F_OK = 1; +console.log(readFileSync, F_OK); diff --git a/recipes/fs-access-mode-constants/tests/input/file-09.js b/recipes/fs-access-mode-constants/tests/input/file-09.js new file mode 100644 index 00000000..cbcd24a1 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/file-09.js @@ -0,0 +1,3 @@ +import { access, constants } from 'node:fs'; + +access('/path/to/file', constants.F_OK, callback); From 0b27bcbef51377642a3db37c00321830752c2fec Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:25:29 +0100 Subject: [PATCH 2/5] test(`fs-access-mode-constants`): better naming --- .../tests/expected/{file-09.js => already-constants-import.js} | 0 .../tests/expected/{file-07.js => constants-destructured-only.js} | 0 .../tests/expected/{file-03.js => destructured-import.js} | 0 .../tests/expected/{file-02.js => destructured-require.js} | 0 .../tests/expected/{file-04.js => mixed-require-namespace.js} | 0 .../tests/expected/{file-01.js => namespace-import-basic.js} | 0 .../tests/expected/{file-06.js => namespace-multiple-uses.js} | 0 .../tests/expected/{file-00.js => namespace-require-basic.js} | 0 .../tests/expected/{file-05.js => promises-alias-import.js} | 0 .../tests/expected/{file-08.js => unrelated-f-ok-identifier.js} | 0 .../tests/input/{file-09.js => already-constants-import.js} | 0 .../tests/input/{file-07.js => constants-destructured-only.js} | 0 .../tests/input/{file-03.js => destructured-import.js} | 0 .../tests/input/{file-02.js => destructured-require.js} | 0 .../tests/input/{file-04.js => mixed-require-namespace.js} | 0 .../tests/input/{file-01.js => namespace-import-basic.js} | 0 .../tests/input/{file-06.js => namespace-multiple-uses.js} | 0 .../tests/input/{file-00.js => namespace-require-basic.js} | 0 .../tests/input/{file-05.js => promises-alias-import.js} | 0 .../tests/input/{file-08.js => unrelated-f-ok-identifier.js} | 0 20 files changed, 0 insertions(+), 0 deletions(-) rename recipes/fs-access-mode-constants/tests/expected/{file-09.js => already-constants-import.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-07.js => constants-destructured-only.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-03.js => destructured-import.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-02.js => destructured-require.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-04.js => mixed-require-namespace.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-01.js => namespace-import-basic.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-06.js => namespace-multiple-uses.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-00.js => namespace-require-basic.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-05.js => promises-alias-import.js} (100%) rename recipes/fs-access-mode-constants/tests/expected/{file-08.js => unrelated-f-ok-identifier.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-09.js => already-constants-import.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-07.js => constants-destructured-only.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-03.js => destructured-import.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-02.js => destructured-require.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-04.js => mixed-require-namespace.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-01.js => namespace-import-basic.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-06.js => namespace-multiple-uses.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-00.js => namespace-require-basic.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-05.js => promises-alias-import.js} (100%) rename recipes/fs-access-mode-constants/tests/input/{file-08.js => unrelated-f-ok-identifier.js} (100%) diff --git a/recipes/fs-access-mode-constants/tests/expected/file-09.js b/recipes/fs-access-mode-constants/tests/expected/already-constants-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-09.js rename to recipes/fs-access-mode-constants/tests/expected/already-constants-import.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-07.js b/recipes/fs-access-mode-constants/tests/expected/constants-destructured-only.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-07.js rename to recipes/fs-access-mode-constants/tests/expected/constants-destructured-only.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-03.js b/recipes/fs-access-mode-constants/tests/expected/destructured-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-03.js rename to recipes/fs-access-mode-constants/tests/expected/destructured-import.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-02.js b/recipes/fs-access-mode-constants/tests/expected/destructured-require.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-02.js rename to recipes/fs-access-mode-constants/tests/expected/destructured-require.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-04.js b/recipes/fs-access-mode-constants/tests/expected/mixed-require-namespace.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-04.js rename to recipes/fs-access-mode-constants/tests/expected/mixed-require-namespace.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-01.js b/recipes/fs-access-mode-constants/tests/expected/namespace-import-basic.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-01.js rename to recipes/fs-access-mode-constants/tests/expected/namespace-import-basic.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-06.js b/recipes/fs-access-mode-constants/tests/expected/namespace-multiple-uses.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-06.js rename to recipes/fs-access-mode-constants/tests/expected/namespace-multiple-uses.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-00.js b/recipes/fs-access-mode-constants/tests/expected/namespace-require-basic.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-00.js rename to recipes/fs-access-mode-constants/tests/expected/namespace-require-basic.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-05.js b/recipes/fs-access-mode-constants/tests/expected/promises-alias-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-05.js rename to recipes/fs-access-mode-constants/tests/expected/promises-alias-import.js diff --git a/recipes/fs-access-mode-constants/tests/expected/file-08.js b/recipes/fs-access-mode-constants/tests/expected/unrelated-f-ok-identifier.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/expected/file-08.js rename to recipes/fs-access-mode-constants/tests/expected/unrelated-f-ok-identifier.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-09.js b/recipes/fs-access-mode-constants/tests/input/already-constants-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-09.js rename to recipes/fs-access-mode-constants/tests/input/already-constants-import.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-07.js b/recipes/fs-access-mode-constants/tests/input/constants-destructured-only.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-07.js rename to recipes/fs-access-mode-constants/tests/input/constants-destructured-only.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-03.js b/recipes/fs-access-mode-constants/tests/input/destructured-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-03.js rename to recipes/fs-access-mode-constants/tests/input/destructured-import.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-02.js b/recipes/fs-access-mode-constants/tests/input/destructured-require.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-02.js rename to recipes/fs-access-mode-constants/tests/input/destructured-require.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-04.js b/recipes/fs-access-mode-constants/tests/input/mixed-require-namespace.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-04.js rename to recipes/fs-access-mode-constants/tests/input/mixed-require-namespace.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-01.js b/recipes/fs-access-mode-constants/tests/input/namespace-import-basic.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-01.js rename to recipes/fs-access-mode-constants/tests/input/namespace-import-basic.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-06.js b/recipes/fs-access-mode-constants/tests/input/namespace-multiple-uses.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-06.js rename to recipes/fs-access-mode-constants/tests/input/namespace-multiple-uses.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-00.js b/recipes/fs-access-mode-constants/tests/input/namespace-require-basic.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-00.js rename to recipes/fs-access-mode-constants/tests/input/namespace-require-basic.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-05.js b/recipes/fs-access-mode-constants/tests/input/promises-alias-import.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-05.js rename to recipes/fs-access-mode-constants/tests/input/promises-alias-import.js diff --git a/recipes/fs-access-mode-constants/tests/input/file-08.js b/recipes/fs-access-mode-constants/tests/input/unrelated-f-ok-identifier.js similarity index 100% rename from recipes/fs-access-mode-constants/tests/input/file-08.js rename to recipes/fs-access-mode-constants/tests/input/unrelated-f-ok-identifier.js From d0ce68c9ba4cb38cd418ac7de27e370c58c6a6ee Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:28:22 +0100 Subject: [PATCH 3/5] stability improvement --- package-lock.json | 2 +- recipes/fs-access-mode-constants/package.json | 2 +- .../fs-access-mode-constants/src/workflow.ts | 340 ++++++++++++------ .../tests/expected/aliased-import-bindings.js | 3 + .../expected/aliased-require-bindings.js | 3 + .../tests/expected/non-fs-import-f-ok.js | 3 + .../tests/expected/non-fs-require-f-ok.js | 3 + .../tests/input/aliased-import-bindings.js | 3 + .../tests/input/aliased-require-bindings.js | 3 + .../tests/input/non-fs-import-f-ok.js | 3 + .../tests/input/non-fs-require-f-ok.js | 3 + 11 files changed, 266 insertions(+), 102 deletions(-) create mode 100644 recipes/fs-access-mode-constants/tests/expected/aliased-import-bindings.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/aliased-require-bindings.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/non-fs-import-f-ok.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/non-fs-require-f-ok.js create mode 100644 recipes/fs-access-mode-constants/tests/input/aliased-import-bindings.js create mode 100644 recipes/fs-access-mode-constants/tests/input/aliased-require-bindings.js create mode 100644 recipes/fs-access-mode-constants/tests/input/non-fs-import-f-ok.js create mode 100644 recipes/fs-access-mode-constants/tests/input/non-fs-require-f-ok.js diff --git a/package-lock.json b/package-lock.json index 67157724..a5586a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4388,7 +4388,7 @@ }, "recipes/fs-access-mode-constants": { "name": "@nodejs/fs-access-mode-constants", - "version": "1.0.1", + "version": "1.0.3", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" diff --git a/recipes/fs-access-mode-constants/package.json b/recipes/fs-access-mode-constants/package.json index fd81fe9e..3d7687e1 100644 --- a/recipes/fs-access-mode-constants/package.json +++ b/recipes/fs-access-mode-constants/package.json @@ -1,6 +1,6 @@ { "name": "@nodejs/fs-access-mode-constants", - "version": "1.0.1", + "version": "1.0.3", "description": "Handle DEP0176 via transforming imports of `fs.F_OK`, `fs.R_OK`, `fs.W_OK`, `fs.X_OK` from the root `fs` module to `fs.constants`.", "type": "module", "scripts": { diff --git a/recipes/fs-access-mode-constants/src/workflow.ts b/recipes/fs-access-mode-constants/src/workflow.ts index 72b43396..b054dbae 100644 --- a/recipes/fs-access-mode-constants/src/workflow.ts +++ b/recipes/fs-access-mode-constants/src/workflow.ts @@ -1,146 +1,286 @@ -import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; -import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; +import { updateBinding } from '@nodejs/codemod-utils/ast-grep/update-binding'; import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; const PATTERNS = ['F_OK', 'R_OK', 'W_OK', 'X_OK']; -export default function tranform(root: SgRoot): string | null { +type BindingMapping = { + local: string; + replacement: string; +}; + +export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; - const replacementMap = new Map(); - - const requireStatements = getNodeRequireCalls(root, 'fs'); + const localBindings = new Map(); + const namespaceBindings = new Map(); - for (const statement of requireStatements) { - const objectPattern = statement.find({ - rule: { kind: 'object_pattern' }, - }); + const importStatements = getModuleDependencies(root, 'fs'); - if (objectPattern) { - const promisesBinding = getLocalPromisesBinding(statement); - let objPatArr = objectPattern - .findAll({ - rule: { kind: 'shorthand_property_identifier_pattern' }, - }) - .map((v) => v.text()); - - const removedBindings = objPatArr.filter((v) => PATTERNS.includes(v)); - if (removedBindings.length > 0) { - for (const binding of removedBindings) { - if (promisesBinding) { - replacementMap.set( - binding, - `${promisesBinding}.constants.${binding}`, - ); - } else { - replacementMap.set(binding, `constants.${binding}`); - } - } - - objPatArr = objPatArr.filter((v) => !PATTERNS.includes(v)); - if (!promisesBinding && !objPatArr.includes('constants')) { - objPatArr.push('constants'); - } - edits.push(objectPattern.replace(`{ ${objPatArr.join(', ')} }`)); - } - } - } - - const importStatements = getNodeImportStatements(root, 'fs'); + if (!importStatements) return null; for (const statement of importStatements) { - const promisesBinding = getLocalPromisesBinding(statement); - const objectPattern = statement.find({ - rule: { kind: 'named_imports' }, - }); - - if (objectPattern) { - const specifiers = objectPattern.findAll({ - rule: { kind: 'import_specifier' }, - }); - - const filteredImports: string[] = []; - let removedAny = false; + const promisesBinding = resolveBindingPath(statement, '$.promises'); + const rewritten = rewriteBindings(statement, promisesBinding); + edits.push(...rewritten.edits); - for (const specifier of specifiers) { - const importedName = specifier.field('name')?.text() ?? ''; - const localName = specifier.field('alias')?.text() ?? importedName; - - if (PATTERNS.includes(importedName)) { - removedAny = true; - const replacementPrefix = promisesBinding - ? `${promisesBinding}.constants` - : 'constants'; - replacementMap.set(localName, `${replacementPrefix}.${importedName}`); - continue; - } - - filteredImports.push(specifier.text()); - } - - if (removedAny) { - if (!promisesBinding && !filteredImports.includes('constants')) { - filteredImports.push('constants'); - } - edits.push(objectPattern.replace(`{ ${filteredImports.join(', ')} }`)); - } + for (const mapping of rewritten.mappings) { + localBindings.set(mapping.local, mapping.replacement); } - } - for (const statement of [...requireStatements, ...importStatements]) { - for (const _OK of PATTERNS) { - const local = resolveBindingPath(statement, `$.${_OK}`); - if (!local?.includes('.') || local.includes('.constants.')) { + for (const pattern of PATTERNS) { + const resolved = resolveBindingPath(statement, `$.${pattern}`); + if (!resolved?.includes('.') || resolved.includes('.constants.')) { continue; } - replacementMap.set(local, local.replace(`.${_OK}`, `.constants.${_OK}`)); + namespaceBindings.set( + resolved, + resolved.replace(`.${pattern}`, `.constants.${pattern}`), + ); } } - for (const [local, replacement] of replacementMap) { - if (local.includes('.')) { - const nodes = rootNode.findAll({ - rule: { pattern: local }, - }); - for (const node of nodes) { - edits.push(node.replace(replacement)); - } - continue; + for (const [path, replacement] of namespaceBindings) { + const nodes = rootNode.findAll({ + rule: { pattern: path }, + }); + + for (const node of nodes) { + edits.push(node.replace(replacement)); } + } - const refs = rootNode.findAll({ + for (const [local, replacement] of localBindings) { + const identifiers = rootNode.findAll({ rule: { kind: 'identifier', regex: `^${escapeRegExp(local)}$`, }, }); - for (const ref of refs) { + for (const identifier of identifiers) { if ( - ref.inside({ rule: { kind: 'named_imports' } }) || - ref.inside({ rule: { kind: 'object_pattern' } }) + identifier.inside({ rule: { kind: 'named_imports' } }) || + identifier.inside({ rule: { kind: 'object_pattern' } }) ) { continue; } - edits.push(ref.replace(replacement)); + + edits.push(identifier.replace(replacement)); } } + if (edits.length === 0) return null; + return rootNode.commitEdits(edits); } -function getLocalPromisesBinding(statement: SgNode): string { - const resolved = resolveBindingPath(statement, '$.promises'); - if (!resolved || resolved.includes('.')) { - return ''; +function rewriteBindings( + statement: SgNode, + promisesBinding: string, +): { edits: Edit[]; mappings: BindingMapping[] } { + const objectPattern = statement.find({ + rule: { kind: 'object_pattern' }, + }); + + if (objectPattern) { + return rewriteObjectPattern(statement, objectPattern, promisesBinding); + } + + const namedImports = statement.find({ + rule: { kind: 'named_imports' }, + }); + + if (namedImports) { + return rewriteNamedImports(statement, namedImports, promisesBinding); + } + + return { edits: [], mappings: [] }; +} + +function rewriteObjectPattern( + statement: SgNode, + pattern: SgNode, + promisesBinding: string, +): { edits: Edit[]; mappings: BindingMapping[] } { + const shorthandBindings = pattern + .findAll({ + rule: { kind: 'shorthand_property_identifier_pattern' }, + }) + .map((node) => node.text()); + + const aliasedBindings = pattern + .findAll({ + rule: { + kind: 'pair_pattern', + has: { + field: 'key', + kind: 'property_identifier', + }, + }, + }) + .map((pair) => { + const imported = pair.field('key')?.text() ?? ''; + const local = pair.field('value')?.text() ?? imported; + + return { + imported, + local, + text: pair.text(), + }; + }); + + const removedShorthand = shorthandBindings.filter((name) => + PATTERNS.includes(name), + ); + const removedAliased = aliasedBindings.filter((binding) => + PATTERNS.includes(binding.imported), + ); + + if (removedShorthand.length === 0 && removedAliased.length === 0) { + return { edits: [], mappings: [] }; + } + + const mappings: BindingMapping[] = []; + const replacementPrefix = promisesBinding + ? `${promisesBinding}.constants` + : 'constants'; + + for (const imported of removedShorthand) { + mappings.push({ + local: imported, + replacement: `${replacementPrefix}.${imported}`, + }); + } + + for (const binding of removedAliased) { + mappings.push({ + local: binding.local, + replacement: `${replacementPrefix}.${binding.imported}`, + }); + } + + const shouldAddConstants = + !promisesBinding && !shorthandBindings.includes('constants'); + + if (removedShorthand.length === 1 && removedAliased.length === 0) { + const singleBindingEdit = getSingleBindingEdit( + statement, + removedShorthand[0], + shouldAddConstants, + ); + + if (singleBindingEdit) { + return { edits: [singleBindingEdit], mappings }; + } + } + + const kept = [ + ...shorthandBindings.filter((name) => !PATTERNS.includes(name)), + ...aliasedBindings + .filter((binding) => !PATTERNS.includes(binding.imported)) + .map((binding) => binding.text), + ]; + + if (!promisesBinding && !kept.includes('constants')) { + kept.push('constants'); + } + + return { + edits: [pattern.replace(`{ ${kept.join(', ')} }`)], + mappings, + }; +} + +function rewriteNamedImports( + statement: SgNode, + pattern: SgNode, + promisesBinding: string, +): { edits: Edit[]; mappings: BindingMapping[] } { + const specifiers = pattern.findAll({ + rule: { kind: 'import_specifier' }, + }); + + const kept: string[] = []; + const mappings: BindingMapping[] = []; + const removedUnaliased: string[] = []; + const removedAliased: string[] = []; + const replacementPrefix = promisesBinding + ? `${promisesBinding}.constants` + : 'constants'; + + for (const specifier of specifiers) { + const imported = specifier.field('name')?.text() ?? ''; + const local = specifier.field('alias')?.text() ?? imported; + + if (PATTERNS.includes(imported)) { + mappings.push({ + local, + replacement: `${replacementPrefix}.${imported}`, + }); + + if (local === imported) { + removedUnaliased.push(imported); + } else { + removedAliased.push(local); + } + + continue; + } + + kept.push(specifier.text()); } - return resolved; + if (!mappings.length) return { edits: [], mappings: [] }; + + const shouldAddConstants = !promisesBinding && !kept.includes('constants'); + + if (removedUnaliased.length === 1 && removedAliased.length === 0) { + const singleBindingEdit = getSingleBindingEdit( + statement, + removedUnaliased[0], + shouldAddConstants, + ); + + if (singleBindingEdit) { + return { edits: [singleBindingEdit], mappings }; + } + } + + if (!promisesBinding && !kept.includes('constants')) { + kept.push('constants'); + } + + return { + edits: [pattern.replace(`{ ${kept.join(', ')} }`)], + mappings, + }; } function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + +function getSingleBindingEdit( + statement: SgNode, + oldBinding: string, + shouldAddConstants: boolean, +): Edit | null { + const update = updateBinding(statement, { + old: oldBinding, + new: shouldAddConstants ? 'constants' : undefined, + }); + + if (update?.edit) { + return update.edit; + } + + if (update?.lineToRemove) { + return statement.replace(''); + } + + return null; +} diff --git a/recipes/fs-access-mode-constants/tests/expected/aliased-import-bindings.js b/recipes/fs-access-mode-constants/tests/expected/aliased-import-bindings.js new file mode 100644 index 00000000..1551d984 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/aliased-import-bindings.js @@ -0,0 +1,3 @@ +import { access, constants } from 'node:fs'; + +access('/path/to/file', constants.F_OK | constants.X_OK, callback); diff --git a/recipes/fs-access-mode-constants/tests/expected/aliased-require-bindings.js b/recipes/fs-access-mode-constants/tests/expected/aliased-require-bindings.js new file mode 100644 index 00000000..8be1b289 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/aliased-require-bindings.js @@ -0,0 +1,3 @@ +const { access, constants } = require('node:fs'); + +access('/path/to/file', constants.F_OK | constants.R_OK, callback); diff --git a/recipes/fs-access-mode-constants/tests/expected/non-fs-import-f-ok.js b/recipes/fs-access-mode-constants/tests/expected/non-fs-import-f-ok.js new file mode 100644 index 00000000..cbcaa257 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/non-fs-import-f-ok.js @@ -0,0 +1,3 @@ +import { F_OK } from './local-constants.js'; + +console.log(F_OK); diff --git a/recipes/fs-access-mode-constants/tests/expected/non-fs-require-f-ok.js b/recipes/fs-access-mode-constants/tests/expected/non-fs-require-f-ok.js new file mode 100644 index 00000000..35ba65d6 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/non-fs-require-f-ok.js @@ -0,0 +1,3 @@ +const { F_OK } = require('./local-constants.cjs'); + +console.log(F_OK); diff --git a/recipes/fs-access-mode-constants/tests/input/aliased-import-bindings.js b/recipes/fs-access-mode-constants/tests/input/aliased-import-bindings.js new file mode 100644 index 00000000..0ab329dd --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/aliased-import-bindings.js @@ -0,0 +1,3 @@ +import { access, F_OK as fileExists, X_OK as canExec } from 'node:fs'; + +access('/path/to/file', fileExists | canExec, callback); diff --git a/recipes/fs-access-mode-constants/tests/input/aliased-require-bindings.js b/recipes/fs-access-mode-constants/tests/input/aliased-require-bindings.js new file mode 100644 index 00000000..7080934f --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/aliased-require-bindings.js @@ -0,0 +1,3 @@ +const { access, F_OK: fileExists, R_OK: canRead } = require('node:fs'); + +access('/path/to/file', fileExists | canRead, callback); diff --git a/recipes/fs-access-mode-constants/tests/input/non-fs-import-f-ok.js b/recipes/fs-access-mode-constants/tests/input/non-fs-import-f-ok.js new file mode 100644 index 00000000..cbcaa257 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/non-fs-import-f-ok.js @@ -0,0 +1,3 @@ +import { F_OK } from './local-constants.js'; + +console.log(F_OK); diff --git a/recipes/fs-access-mode-constants/tests/input/non-fs-require-f-ok.js b/recipes/fs-access-mode-constants/tests/input/non-fs-require-f-ok.js new file mode 100644 index 00000000..35ba65d6 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/non-fs-require-f-ok.js @@ -0,0 +1,3 @@ +const { F_OK } = require('./local-constants.cjs'); + +console.log(F_OK); From 274a2f535125e24ff20d8f54ee3585289d1cb6b6 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:08:22 +0100 Subject: [PATCH 4/5] add more tests case --- .../tests/expected/already-constants-namespace.js | 4 ++++ .../tests/expected/already-constants-require.js | 3 +++ .../tests/expected/destructured-from-constants.js | 4 ++++ .../tests/input/already-constants-namespace.js | 4 ++++ .../tests/input/already-constants-require.js | 3 +++ .../tests/input/destructured-from-constants.js | 4 ++++ 6 files changed, 22 insertions(+) create mode 100644 recipes/fs-access-mode-constants/tests/expected/already-constants-namespace.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/already-constants-require.js create mode 100644 recipes/fs-access-mode-constants/tests/expected/destructured-from-constants.js create mode 100644 recipes/fs-access-mode-constants/tests/input/already-constants-namespace.js create mode 100644 recipes/fs-access-mode-constants/tests/input/already-constants-require.js create mode 100644 recipes/fs-access-mode-constants/tests/input/destructured-from-constants.js diff --git a/recipes/fs-access-mode-constants/tests/expected/already-constants-namespace.js b/recipes/fs-access-mode-constants/tests/expected/already-constants-namespace.js new file mode 100644 index 00000000..7af7387c --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/already-constants-namespace.js @@ -0,0 +1,4 @@ +import * as fs from 'node:fs'; + +fs.access('/path/to/file', fs.constants.F_OK, callback); +fs.access('/path/to/file', fs.constants.R_OK | fs.constants.W_OK, callback); diff --git a/recipes/fs-access-mode-constants/tests/expected/already-constants-require.js b/recipes/fs-access-mode-constants/tests/expected/already-constants-require.js new file mode 100644 index 00000000..314cce18 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/already-constants-require.js @@ -0,0 +1,3 @@ +const fs = require('node:fs'); + +fs.accessSync('/path/to/file', fs.constants.X_OK); diff --git a/recipes/fs-access-mode-constants/tests/expected/destructured-from-constants.js b/recipes/fs-access-mode-constants/tests/expected/destructured-from-constants.js new file mode 100644 index 00000000..330d8a1c --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/expected/destructured-from-constants.js @@ -0,0 +1,4 @@ +const { constants } = require('node:fs'); +const { F_OK, R_OK } = constants; + +console.log(F_OK, R_OK); diff --git a/recipes/fs-access-mode-constants/tests/input/already-constants-namespace.js b/recipes/fs-access-mode-constants/tests/input/already-constants-namespace.js new file mode 100644 index 00000000..7af7387c --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/already-constants-namespace.js @@ -0,0 +1,4 @@ +import * as fs from 'node:fs'; + +fs.access('/path/to/file', fs.constants.F_OK, callback); +fs.access('/path/to/file', fs.constants.R_OK | fs.constants.W_OK, callback); diff --git a/recipes/fs-access-mode-constants/tests/input/already-constants-require.js b/recipes/fs-access-mode-constants/tests/input/already-constants-require.js new file mode 100644 index 00000000..314cce18 --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/already-constants-require.js @@ -0,0 +1,3 @@ +const fs = require('node:fs'); + +fs.accessSync('/path/to/file', fs.constants.X_OK); diff --git a/recipes/fs-access-mode-constants/tests/input/destructured-from-constants.js b/recipes/fs-access-mode-constants/tests/input/destructured-from-constants.js new file mode 100644 index 00000000..330d8a1c --- /dev/null +++ b/recipes/fs-access-mode-constants/tests/input/destructured-from-constants.js @@ -0,0 +1,4 @@ +const { constants } = require('node:fs'); +const { F_OK, R_OK } = constants; + +console.log(F_OK, R_OK); From fe1a8930d0ea8621038d1aa2e22a359818653af9 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:16:59 +0100 Subject: [PATCH 5/5] WIP --- .../fs-access-mode-constants/src/workflow.ts | 263 +++++++++--------- 1 file changed, 127 insertions(+), 136 deletions(-) diff --git a/recipes/fs-access-mode-constants/src/workflow.ts b/recipes/fs-access-mode-constants/src/workflow.ts index b054dbae..1c44ec9a 100644 --- a/recipes/fs-access-mode-constants/src/workflow.ts +++ b/recipes/fs-access-mode-constants/src/workflow.ts @@ -4,13 +4,18 @@ import { updateBinding } from '@nodejs/codemod-utils/ast-grep/update-binding'; import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; -const PATTERNS = ['F_OK', 'R_OK', 'W_OK', 'X_OK']; +const PATTERN_SET = new Set(['F_OK', 'R_OK', 'W_OK', 'X_OK']); type BindingMapping = { local: string; replacement: string; }; +type RemovedBinding = { + imported: string; + local: string; +}; + export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; @@ -30,7 +35,7 @@ export default function transform(root: SgRoot): string | null { localBindings.set(mapping.local, mapping.replacement); } - for (const pattern of PATTERNS) { + for (const pattern of PATTERN_SET) { const resolved = resolveBindingPath(statement, `$.${pattern}`); if (!resolved?.includes('.') || resolved.includes('.constants.')) { continue; @@ -43,35 +48,8 @@ export default function transform(root: SgRoot): string | null { } } - for (const [path, replacement] of namespaceBindings) { - const nodes = rootNode.findAll({ - rule: { pattern: path }, - }); - - for (const node of nodes) { - edits.push(node.replace(replacement)); - } - } - - for (const [local, replacement] of localBindings) { - const identifiers = rootNode.findAll({ - rule: { - kind: 'identifier', - regex: `^${escapeRegExp(local)}$`, - }, - }); - - for (const identifier of identifiers) { - if ( - identifier.inside({ rule: { kind: 'named_imports' } }) || - identifier.inside({ rule: { kind: 'object_pattern' } }) - ) { - continue; - } - - edits.push(identifier.replace(replacement)); - } - } + applyNamespaceReplacements(rootNode, edits, namespaceBindings); + applyLocalReplacements(rootNode, edits, localBindings); if (edits.length === 0) return null; @@ -86,17 +64,15 @@ function rewriteBindings( rule: { kind: 'object_pattern' }, }); - if (objectPattern) { + if (objectPattern) return rewriteObjectPattern(statement, objectPattern, promisesBinding); - } const namedImports = statement.find({ rule: { kind: 'named_imports' }, }); - if (namedImports) { + if (namedImports) return rewriteNamedImports(statement, namedImports, promisesBinding); - } return { edits: [], mappings: [] }; } @@ -133,66 +109,46 @@ function rewriteObjectPattern( }; }); - const removedShorthand = shorthandBindings.filter((name) => - PATTERNS.includes(name), - ); - const removedAliased = aliasedBindings.filter((binding) => - PATTERNS.includes(binding.imported), - ); - - if (removedShorthand.length === 0 && removedAliased.length === 0) { - return { edits: [], mappings: [] }; - } - - const mappings: BindingMapping[] = []; - const replacementPrefix = promisesBinding - ? `${promisesBinding}.constants` - : 'constants'; - - for (const imported of removedShorthand) { - mappings.push({ - local: imported, - replacement: `${replacementPrefix}.${imported}`, - }); - } + const kept: string[] = []; + const removed: RemovedBinding[] = []; + let removedShorthandCount = 0; + let removedAliasedCount = 0; + + for (const name of shorthandBindings) { + if (PATTERN_SET.has(name)) { + removedShorthandCount += 1; + removed.push({ + imported: name, + local: name, + }); + continue; + } - for (const binding of removedAliased) { - mappings.push({ - local: binding.local, - replacement: `${replacementPrefix}.${binding.imported}`, - }); + kept.push(name); } - const shouldAddConstants = - !promisesBinding && !shorthandBindings.includes('constants'); - - if (removedShorthand.length === 1 && removedAliased.length === 0) { - const singleBindingEdit = getSingleBindingEdit( - statement, - removedShorthand[0], - shouldAddConstants, - ); - - if (singleBindingEdit) { - return { edits: [singleBindingEdit], mappings }; + for (const binding of aliasedBindings) { + if (PATTERN_SET.has(binding.imported)) { + removedAliasedCount += 1; + removed.push({ + imported: binding.imported, + local: binding.local, + }); + continue; } - } - - const kept = [ - ...shorthandBindings.filter((name) => !PATTERNS.includes(name)), - ...aliasedBindings - .filter((binding) => !PATTERNS.includes(binding.imported)) - .map((binding) => binding.text), - ]; - if (!promisesBinding && !kept.includes('constants')) { - kept.push('constants'); + kept.push(binding.text); } - return { - edits: [pattern.replace(`{ ${kept.join(', ')} }`)], - mappings, - }; + return rewriteCollectedBindings({ + statement, + pattern, + promisesBinding, + kept, + removed, + allowSingleBindingOptimization: + removedShorthandCount === 1 && removedAliasedCount === 0, + }); } function rewriteNamedImports( @@ -205,55 +161,111 @@ function rewriteNamedImports( }); const kept: string[] = []; - const mappings: BindingMapping[] = []; - const removedUnaliased: string[] = []; - const removedAliased: string[] = []; - const replacementPrefix = promisesBinding - ? `${promisesBinding}.constants` - : 'constants'; + const removed: RemovedBinding[] = []; for (const specifier of specifiers) { const imported = specifier.field('name')?.text() ?? ''; const local = specifier.field('alias')?.text() ?? imported; - if (PATTERNS.includes(imported)) { - mappings.push({ + if (PATTERN_SET.has(imported)) { + removed.push({ + imported, local, - replacement: `${replacementPrefix}.${imported}`, }); - if (local === imported) { - removedUnaliased.push(imported); - } else { - removedAliased.push(local); - } - continue; } kept.push(specifier.text()); } - if (!mappings.length) return { edits: [], mappings: [] }; + return rewriteCollectedBindings({ + statement, + pattern, + promisesBinding, + kept, + removed, + allowSingleBindingOptimization: + removed.length === 1 && removed[0].local === removed[0].imported, + }); +} + +function applyNamespaceReplacements( + rootNode: SgNode, + edits: Edit[], + replacements: Map, +): void { + for (const [path, replacement] of replacements) { + const nodes = rootNode.findAll({ rule: { pattern: path } }); - const shouldAddConstants = !promisesBinding && !kept.includes('constants'); + for (const node of nodes) { + edits.push(node.replace(replacement)); + } + } +} - if (removedUnaliased.length === 1 && removedAliased.length === 0) { - const singleBindingEdit = getSingleBindingEdit( - statement, - removedUnaliased[0], - shouldAddConstants, - ); +function applyLocalReplacements( + rootNode: SgNode, + edits: Edit[], + replacements: Map, +): void { + for (const [local, replacement] of replacements) { + const identifiers = rootNode.findAll({ + rule: { + kind: 'identifier', + regex: `^${escapeRegExp(local)}$`, + }, + }); - if (singleBindingEdit) { - return { edits: [singleBindingEdit], mappings }; + for (const identifier of identifiers) { + if ( + !identifier.inside({ rule: { kind: 'named_imports' } }) || + !identifier.inside({ rule: { kind: 'object_pattern' } }) + ) { + edits.push(identifier.replace(replacement)); + } } } +} + +function rewriteCollectedBindings({ + statement, + pattern, + promisesBinding, + kept, + removed, + allowSingleBindingOptimization, +}: { + statement: SgNode; + pattern: SgNode; + promisesBinding: string; + kept: string[]; + removed: RemovedBinding[]; + allowSingleBindingOptimization: boolean; +}): { edits: Edit[]; mappings: BindingMapping[] } { + if (!removed.length) return { edits: [], mappings: [] }; + + const replacementPrefix = promisesBinding + ? `${promisesBinding}.constants` + : 'constants'; + const mappings = removed.map((binding) => ({ + local: binding.local, + replacement: `${replacementPrefix}.${binding.imported}`, + })); + + const shouldAddConstants = !promisesBinding && !kept.includes('constants'); + + if (allowSingleBindingOptimization && removed.length === 1) { + const singleBindingEdit = updateBinding(statement, { + old: removed[0].imported, + new: shouldAddConstants ? 'constants' : undefined, + }).edit; - if (!promisesBinding && !kept.includes('constants')) { - kept.push('constants'); + if (singleBindingEdit) return { edits: [singleBindingEdit], mappings }; } + if (shouldAddConstants) kept.push('constants'); + return { edits: [pattern.replace(`{ ${kept.join(', ')} }`)], mappings, @@ -263,24 +275,3 @@ function rewriteNamedImports( function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } - -function getSingleBindingEdit( - statement: SgNode, - oldBinding: string, - shouldAddConstants: boolean, -): Edit | null { - const update = updateBinding(statement, { - old: oldBinding, - new: shouldAddConstants ? 'constants' : undefined, - }); - - if (update?.edit) { - return update.edit; - } - - if (update?.lineToRemove) { - return statement.replace(''); - } - - return null; -}