From a54f46f603756e0bce1f5187518eab79fdc7c407 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 22 May 2026 21:44:54 +0100 Subject: [PATCH 1/4] feat: migrate `left-pad` and `pad-left` to ast-grep --- codemods/left-pad/index.js | 47 ++++++++++--------------- codemods/pad-left/index.js | 47 ++++++++++--------------- codemods/shared-ast-grep.js | 42 +++++++++++++++------- test/fixtures/left-pad/case-1/after.js | 1 + test/fixtures/left-pad/case-1/result.js | 1 + test/fixtures/pad-left/case-1/after.js | 1 + test/fixtures/pad-left/case-1/result.js | 1 + 7 files changed, 70 insertions(+), 70 deletions(-) diff --git a/codemods/left-pad/index.js b/codemods/left-pad/index.js index 77d102ea..91e899a3 100644 --- a/codemods/left-pad/index.js +++ b/codemods/left-pad/index.js @@ -1,5 +1,10 @@ -import jscodeshift from 'jscodeshift'; -import { removeImport } from '../shared.js'; +import { ts } from '@ast-grep/napi'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; + +const MODULE_NAME = 'left-pad'; /** * @typedef {import('../../types.js').Codemod} Codemod @@ -15,36 +20,20 @@ export default function (options) { name: 'left-pad', to: 'native', transform: ({ file }) => { - const j = jscodeshift; - const root = j(file.source); + const ast = ts.parse(file.source); + const root = ast.root(); + + const { edits, localNames } = removeImport(root, MODULE_NAME); - const { identifier } = removeImport('left-pad', root, j); - root - .find(j.CallExpression, { - callee: { - type: 'Identifier', - name: identifier, - }, - }) - .replaceWith(({ node }) => { - const [stringArg, ...otherArgs] = node.arguments; - return j.callExpression( - j.memberExpression( - j.callExpression( - j.memberExpression( - // @ts-ignore - j.parenthesizedExpression(stringArg), - j.identifier('toString'), - ), - [], - ), - j.identifier('padStart'), - ), - [...otherArgs], - ); + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + const [first, ...rest] = args; + return `(${first}).toString().padStart(${rest.join(', ')})`; }); + edits.push(...callEdits); + } - return root.toSource(options); + return edits.length > 0 ? root.commitEdits(edits) : file.source; }, }; } diff --git a/codemods/pad-left/index.js b/codemods/pad-left/index.js index 05ea82b2..c229eb8a 100644 --- a/codemods/pad-left/index.js +++ b/codemods/pad-left/index.js @@ -1,5 +1,10 @@ -import jscodeshift from 'jscodeshift'; -import { removeImport } from '../shared.js'; +import { ts } from '@ast-grep/napi'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; + +const MODULE_NAME = 'pad-left'; /** * @typedef {import('../../types.js').Codemod} Codemod @@ -15,36 +20,20 @@ export default function (options) { name: 'pad-left', to: 'native', transform: ({ file }) => { - const j = jscodeshift; - const root = j(file.source); + const ast = ts.parse(file.source); + const root = ast.root(); + + const { edits, localNames } = removeImport(root, MODULE_NAME); - const { identifier } = removeImport('pad-left', root, j); - root - .find(j.CallExpression, { - callee: { - type: 'Identifier', - name: identifier, - }, - }) - .replaceWith(({ node }) => { - const [stringArg, ...otherArgs] = node.arguments; - return j.callExpression( - j.memberExpression( - j.callExpression( - j.memberExpression( - // @ts-ignore - j.parenthesizedExpression(stringArg), - j.identifier('toString'), - ), - [], - ), - j.identifier('padStart'), - ), - [...otherArgs], - ); + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + const [first, ...rest] = args; + return `(${first}).toString().padStart(${rest.join(', ')})`; }); + edits.push(...callEdits); + } - return root.toSource(options); + return edits.length > 0 ? root.commitEdits(edits) : file.source; }, }; } diff --git a/codemods/shared-ast-grep.js b/codemods/shared-ast-grep.js index 1fc654ea..aee3a3df 100644 --- a/codemods/shared-ast-grep.js +++ b/codemods/shared-ast-grep.js @@ -249,18 +249,18 @@ export function findDefaultImportIdentifier(root, moduleName) { } /** - * Compute edits that replace every call of `fromIdentifier(...)` with - * `toCallee(...)`, preserving the original argument list verbatim. + * Low-level helper that finds all calls to `fromIdentifier(...)` and applies + * a custom format callback to produce the replacement text for each call. * * @param {SgNode} root - The root of the AST. * @param {string} fromIdentifier - The identifier currently being called. - * @param {string} toCallee - The replacement callee expression (e.g. `Array.of`). + * @param {(args: string[]) => string} formatReplacement - Receives the argument texts and returns the full replacement string. * @returns {Edit[]} */ -export function computeSimpleCallReplacementEdits( +export function computeCallReplacementEdits( root, fromIdentifier, - toCallee, + formatReplacement, ) { /** @type {Edit[]} */ const edits = []; @@ -271,17 +271,35 @@ export function computeSimpleCallReplacementEdits( }); for (const call of calls) { const argsMatch = call.getMultipleMatches('ARGS'); - const argsText = argsMatch - ? argsMatch - .filter((m) => m.kind() !== ',') - .map((m) => m.text()) - .join(', ') - : ''; - edits.push(call.replace(`${toCallee}(${argsText})`)); + const args = argsMatch + ? argsMatch.filter((m) => m.kind() !== ',').map((m) => m.text()) + : []; + edits.push(call.replace(formatReplacement(args))); } return edits; } +/** + * Compute edits that replace every call of `fromIdentifier(...)` with + * `toCallee(...)`, preserving the original argument list verbatim. + * + * @param {SgNode} root - The root of the AST. + * @param {string} fromIdentifier - The identifier currently being called. + * @param {string} toCallee - The replacement callee expression (e.g. `Array.of`). + * @returns {Edit[]} + */ +export function computeSimpleCallReplacementEdits( + root, + fromIdentifier, + toCallee, +) { + return computeCallReplacementEdits( + root, + fromIdentifier, + (args) => `${toCallee}(${args.join(', ')})`, + ); +} + /** * Iterate over call expressions of a specific identifier and generate edits. * diff --git a/test/fixtures/left-pad/case-1/after.js b/test/fixtures/left-pad/case-1/after.js index 9c78f35a..136c59b3 100644 --- a/test/fixtures/left-pad/case-1/after.js +++ b/test/fixtures/left-pad/case-1/after.js @@ -1,3 +1,4 @@ + var assert = require("assert"); assert.equal(("foo").toString().padStart(5), "foo "); diff --git a/test/fixtures/left-pad/case-1/result.js b/test/fixtures/left-pad/case-1/result.js index 9c78f35a..136c59b3 100644 --- a/test/fixtures/left-pad/case-1/result.js +++ b/test/fixtures/left-pad/case-1/result.js @@ -1,3 +1,4 @@ + var assert = require("assert"); assert.equal(("foo").toString().padStart(5), "foo "); diff --git a/test/fixtures/pad-left/case-1/after.js b/test/fixtures/pad-left/case-1/after.js index 9c78f35a..136c59b3 100644 --- a/test/fixtures/pad-left/case-1/after.js +++ b/test/fixtures/pad-left/case-1/after.js @@ -1,3 +1,4 @@ + var assert = require("assert"); assert.equal(("foo").toString().padStart(5), "foo "); diff --git a/test/fixtures/pad-left/case-1/result.js b/test/fixtures/pad-left/case-1/result.js index 9c78f35a..136c59b3 100644 --- a/test/fixtures/pad-left/case-1/result.js +++ b/test/fixtures/pad-left/case-1/result.js @@ -1,3 +1,4 @@ + var assert = require("assert"); assert.equal(("foo").toString().padStart(5), "foo "); From 72c675041a6c041182bc631c19ea5dfd181b0b83 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 22 May 2026 21:48:19 +0100 Subject: [PATCH 2/4] chore: use `computeCallReplacementEdits` in other codemods --- codemods/array-buffer-byte-length/index.js | 46 +++++++--------------- codemods/es-define-property/index.js | 32 ++++++--------- codemods/is-array-buffer/index.js | 31 +++++---------- codemods/is-boolean-object/index.js | 31 +++++---------- codemods/is-date-object/index.js | 31 +++++---------- codemods/is-even/index.js | 31 +++++---------- codemods/is-negative-zero/index.js | 26 ++++++------ codemods/is-number/index.js | 31 +++++---------- codemods/is-odd/index.js | 31 +++++---------- codemods/is-regexp/index.js | 31 +++++---------- codemods/is-string/index.js | 31 +++++---------- codemods/is-whitespace/index.js | 28 +++++-------- codemods/shared-ast-grep.js | 7 +++- 13 files changed, 123 insertions(+), 264 deletions(-) diff --git a/codemods/array-buffer-byte-length/index.js b/codemods/array-buffer-byte-length/index.js index 429390cc..11a37e5b 100644 --- a/codemods/array-buffer-byte-length/index.js +++ b/codemods/array-buffer-byte-length/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'array-buffer-byte-length'; @@ -22,37 +25,16 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - const identifierName = localNames[0]; - - const callExpressions = root.findAll({ - rule: { - pattern: `${identifierName}($$$ARG)`, - }, - }); - - for (const call of callExpressions) { - const argsMatch = call.getMultipleMatches('ARG'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - - if (args.length !== 1) continue; - - const argNode = args[0]; - const argText = argNode.text(); - - const isIdentifier = argNode.kind() === 'identifier'; - const isNewArrayBuffer = - argNode.kind() === 'new_expression' && - argText.startsWith('new ArrayBuffer'); - - if (isIdentifier || isNewArrayBuffer) { - edits.push(call.replace(`${argText}.byteLength`)); - } + for (const identifierName of localNames) { + const callEdits = computeCallReplacementEdits( + root, + identifierName, + (args) => { + if (args.length !== 1) return null; + return `${args[0]}.byteLength`; + }, + ); + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/es-define-property/index.js b/codemods/es-define-property/index.js index 1b26c4cc..85fbef65 100644 --- a/codemods/es-define-property/index.js +++ b/codemods/es-define-property/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'es-define-property'; @@ -21,27 +24,14 @@ export default function (options) { const root = ast.root(); const { edits, localNames } = removeImport(root, MODULE_NAME); - const identifierName = localNames[0]; - if (!identifierName) { - return edits.length > 0 ? root.commitEdits(edits) : file.source; - } - - const calls = root.findAll({ - rule: { - pattern: `${identifierName}($$$ARGS)`, - }, - }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - const argsText = argsMatch - ? argsMatch - .filter((m) => m.kind() !== ',') - .map((m) => m.text()) - .join(', ') - : ''; - edits.push(call.replace(`Object.defineProperty(${argsText})`)); + for (const identifierName of localNames) { + const callEdits = computeCallReplacementEdits( + root, + identifierName, + (args) => `Object.defineProperty(${args.join(', ')})`, + ); + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-array-buffer/index.js b/codemods/is-array-buffer/index.js index 97382936..d94bf923 100644 --- a/codemods/is-array-buffer/index.js +++ b/codemods/is-array-buffer/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-array-buffer'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `(${args[0]} instanceof ArrayBuffer)`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `(${argText} instanceof ArrayBuffer)`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-boolean-object/index.js b/codemods/is-boolean-object/index.js index 1756700d..1cfcf0fe 100644 --- a/codemods/is-boolean-object/index.js +++ b/codemods/is-boolean-object/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-boolean-object'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `Object.prototype.toString.call(${args[0]}) === '[object Boolean]'`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `Object.prototype.toString.call(${argText}) === '[object Boolean]'`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-date-object/index.js b/codemods/is-date-object/index.js index e9edfe24..04f073e1 100644 --- a/codemods/is-date-object/index.js +++ b/codemods/is-date-object/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-date-object'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `Object.prototype.toString.call(${args[0]}) === '[object Date]'`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `Object.prototype.toString.call(${argText}) === '[object Date]'`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-even/index.js b/codemods/is-even/index.js index 169ec95b..de9554cf 100644 --- a/codemods/is-even/index.js +++ b/codemods/is-even/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-even'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `(${args[0]} % 2 === 0)`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `(${argText} % 2 === 0)`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-negative-zero/index.js b/codemods/is-negative-zero/index.js index 4ba0f8b2..e0b7190c 100644 --- a/codemods/is-negative-zero/index.js +++ b/codemods/is-negative-zero/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-negative-zero'; @@ -42,20 +45,15 @@ export default function (options) { } // Find remaining localName(...) calls and replace with Object.is(arg, -0) - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, + const callEdits = computeCallReplacementEdits( + root, + localName, + (args) => { + if (args.length !== 1) return null; + return `Object.is(${args[0]}, -0)`; }, - }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - const argText = args[0].text(); - edits.push(call.replace(`Object.is(${argText}, -0)`)); - } + ); + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-number/index.js b/codemods/is-number/index.js index a5733bf7..f4549b35 100644 --- a/codemods/is-number/index.js +++ b/codemods/is-number/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-number'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `(typeof ${args[0]} === 'number' || typeof ${args[0]} === 'string' && Number.isFinite(+${args[0]}))`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `(typeof ${argText} === 'number' || typeof ${argText} === 'string' && Number.isFinite(+${argText}))`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-odd/index.js b/codemods/is-odd/index.js index 377945fd..da41b692 100644 --- a/codemods/is-odd/index.js +++ b/codemods/is-odd/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-odd'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `(${args[0]} % 2 !== 0)`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `(${argText} % 2 !== 0)`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-regexp/index.js b/codemods/is-regexp/index.js index 1e3922f8..c8e7797c 100644 --- a/codemods/is-regexp/index.js +++ b/codemods/is-regexp/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-regexp'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `(${args[0]} instanceof RegExp)`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `(${argText} instanceof RegExp)`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-string/index.js b/codemods/is-string/index.js index 8e30ddd3..e43eaae5 100644 --- a/codemods/is-string/index.js +++ b/codemods/is-string/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-string'; @@ -22,28 +25,12 @@ export default function (options) { const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `Object.prototype.toString.call(${args[0]}) === '[object String]'`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - - const argText = args[0].text(); - const replacement = `Object.prototype.toString.call(${argText}) === '[object String]'`; - edits.push(call.replace(replacement)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/is-whitespace/index.js b/codemods/is-whitespace/index.js index 7c5740be..6c3618cd 100644 --- a/codemods/is-whitespace/index.js +++ b/codemods/is-whitespace/index.js @@ -1,5 +1,8 @@ import { ts } from '@ast-grep/napi'; -import { removeImport } from '../shared-ast-grep.js'; +import { + computeCallReplacementEdits, + removeImport, +} from '../shared-ast-grep.js'; const MODULE_NAME = 'is-whitespace'; @@ -21,25 +24,12 @@ export default function (options) { const root = ast.root(); const { edits, localNames } = removeImport(root, MODULE_NAME); - if (localNames.length === 0) { - return file.source; - } - - for (const localName of localNames) { - const calls = root.findAll({ - rule: { - pattern: `${localName}($$$ARGS)`, - }, + for (const name of localNames) { + const callEdits = computeCallReplacementEdits(root, name, (args) => { + if (args.length !== 1) return null; + return `${args[0]}.trim() === ''`; }); - - for (const call of calls) { - const argsMatch = call.getMultipleMatches('ARGS'); - if (!argsMatch) continue; - const args = argsMatch.filter((m) => m.kind() !== ','); - if (args.length !== 1) continue; - const argText = args[0].text(); - edits.push(call.replace(`${argText}.trim() === ''`)); - } + edits.push(...callEdits); } return edits.length > 0 ? root.commitEdits(edits) : file.source; diff --git a/codemods/shared-ast-grep.js b/codemods/shared-ast-grep.js index aee3a3df..22b8ce3e 100644 --- a/codemods/shared-ast-grep.js +++ b/codemods/shared-ast-grep.js @@ -251,10 +251,11 @@ export function findDefaultImportIdentifier(root, moduleName) { /** * Low-level helper that finds all calls to `fromIdentifier(...)` and applies * a custom format callback to produce the replacement text for each call. + * Return `null` from the callback to skip a call without producing an edit. * * @param {SgNode} root - The root of the AST. * @param {string} fromIdentifier - The identifier currently being called. - * @param {(args: string[]) => string} formatReplacement - Receives the argument texts and returns the full replacement string. + * @param {(args: string[]) => string | null} formatReplacement - Receives the argument texts and returns the replacement string, or null to skip. * @returns {Edit[]} */ export function computeCallReplacementEdits( @@ -274,7 +275,9 @@ export function computeCallReplacementEdits( const args = argsMatch ? argsMatch.filter((m) => m.kind() !== ',').map((m) => m.text()) : []; - edits.push(call.replace(formatReplacement(args))); + const replacement = formatReplacement(args); + if (replacement === null) continue; + edits.push(call.replace(replacement)); } return edits; } From b6c77a63340112319f01ddfd02faad978fca8f9f Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 26 May 2026 01:04:42 +0100 Subject: [PATCH 3/4] Update shared-ast-grep.d.ts --- types/codemods/shared-ast-grep.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/types/codemods/shared-ast-grep.d.ts b/types/codemods/shared-ast-grep.d.ts index 7cf66fec..40432595 100644 --- a/types/codemods/shared-ast-grep.d.ts +++ b/types/codemods/shared-ast-grep.d.ts @@ -48,6 +48,17 @@ export function findDefaultImportIdentifier(root: SgNode, moduleName: string): { imports: SgNode[]; identifierName: string | null; }; +/** + * Low-level helper that finds all calls to `fromIdentifier(...)` and applies + * a custom format callback to produce the replacement text for each call. + * Return `null` from the callback to skip a call without producing an edit. + * + * @param {SgNode} root - The root of the AST. + * @param {string} fromIdentifier - The identifier currently being called. + * @param {(args: string[]) => string | null} formatReplacement - Receives the argument texts and returns the replacement string, or null to skip. + * @returns {Edit[]} + */ +export function computeCallReplacementEdits(root: SgNode, fromIdentifier: string, formatReplacement: (args: string[]) => string | null): Edit[]; /** * Compute edits that replace every call of `fromIdentifier(...)` with * `toCallee(...)`, preserving the original argument list verbatim. From c687b56c3bcd1abf26b7b352cdb047ba54f5b691 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 26 May 2026 15:04:05 +0100 Subject: [PATCH 4/4] Update shared-ast-grep.js --- codemods/shared-ast-grep.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/codemods/shared-ast-grep.js b/codemods/shared-ast-grep.js index 22b8ce3e..99f47450 100644 --- a/codemods/shared-ast-grep.js +++ b/codemods/shared-ast-grep.js @@ -441,18 +441,10 @@ export function computePolyfillPropertyReplacementEdits( identifierName, propertyName, ) { - /** @type {Edit[]} */ - const edits = []; - const calls = root.findAll({ - rule: { pattern: `${identifierName}($ARG)` }, + return computeCallReplacementEdits(root, identifierName, (args) => { + if (args.length !== 1) return null; + return `${args[0]}.${propertyName}`; }); - for (const call of calls) { - const arg = call.getMatch('ARG'); - if (arg) { - edits.push(call.replace(`${arg.text()}.${propertyName}`)); - } - } - return edits; } /**