Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 135 additions & 120 deletions package-lock.json

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions recipes/util-is/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# `util.is*()`

This codemod replaces the following deprecated `util.is*()` methods with their modern equivalents:

- [DEP0044: `util.isArray()`](https://nodejs.org/docs/latest/api/deprecations.html#DEP0044)
- [DEP0045: `util.isBoolean()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0045-utilisboolean)
- [DEP0046: `util.isBuffer()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0046-utilisbuffer)
- [DEP0047: `util.isDate()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0047-utilisdate)
- [DEP0048: `util.isError()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0048-utiliserror)
- [DEP0049: `util.isFunction()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0049-utilisfunction)
- [DEP0050: `util.isNull()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0050-utilisnull)
- [DEP0051: `util.isNullOrUndefined()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0051-utilisnullorundefined)
- [DEP0052: `util.isNumber()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0052-utilisnumber)
- [DEP0053: `util.isObject()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0053-utilisobject)
- [DEP0054: `util.isPrimitive()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0054-utilisprimitive)
- [DEP0055: `util.isRegExp()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0055-utilisregexp)
- [DEP0056: `util.isString()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0056-utilisstring)
- [DEP0057: `util.isSymbol()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0057-utilissymbol)
- [DEP0058: `util.isUndefined()`](https://nodejs.org/docs/latest/api/deprecations.html#dep0058-utilisundefined)

## Examples

| **Before** | **After** |
|-----------------------------------|---------------------------------------------|
| `util.isArray(value)` | `Array.isArray(value)` |
| `util.isBoolean(value)` | `typeof value === 'boolean'` |
| `util.isBuffer(value)` | `Buffer.isBuffer(value)` |
| `util.isDate(value)` | `value instanceof Date` |
| `util.isError(value)` | `Error.isError(value)` |
| `util.isFunction(value)` | `typeof value === 'function'` |
| `util.isNull(value)` | `value === null` |
| `util.isNullOrUndefined(value)` | `value === null || value === undefined` |
| `util.isNumber(value)` | `typeof value === 'number'` |
| `util.isObject(value)` | `value && typeof value === 'object'` |
| `util.isPrimitive(value)` | `Object(value) !== value` |
| `util.isRegExp(value)` | `value instanceof RegExp` |
| `util.isString(value)` | `typeof value === 'string'` |
| `util.isSymbol(value)` | `typeof value === 'symbol'` |
| `util.isUndefined(value)` | `typeof value === 'undefined'` |
21 changes: 21 additions & 0 deletions recipes/util-is/codemod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema_version: "1.0"
name: "@nodejs/util-is"
version: 1.0.0
description: "Replaces deprecated `util.is*()` methods with their modern equivalents."
author: Augustin Mauroy
license: MIT
workflow: workflow.yaml
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration

registry:
access: public
visibility: public
24 changes: 24 additions & 0 deletions recipes/util-is/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@nodejs/util-is",
"version": "1.0.0",
"description": "Replaces deprecated `util.is*()` methods with their modern equivalents.",
"type": "module",
"scripts": {
"test": "npx codemod@next jssg test -l typescript ./src/workflow.ts ./"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/util-is",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Augustin Mauroy",
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/util-is/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.0.3"
},
"dependencies": {
"@nodejs/codemod-utils": "0.0.0"
}
}
246 changes: 246 additions & 0 deletions recipes/util-is/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import {
getNodeImportStatements,
getDefaultImportIdentifier,
} from "@nodejs/codemod-utils/ast-grep/import-statement";
import {
getNodeRequireCalls,
getRequireNamespaceIdentifier,
} from "@nodejs/codemod-utils/ast-grep/require-call";
import { removeBinding } from "@nodejs/codemod-utils/ast-grep/remove-binding";
import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path";
import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines";
import type { SgRoot, Edit, Range } from "@codemod.com/jssg-types/main";
import type { SgNode } from "@ast-grep/napi";

// Clean up unused imports using removeBinding
const allIsMethods = [
'isArray',
'isBoolean',
'isBuffer',
'isDate',
'isError',
'isFunction',
'isNull',
'isNullOrUndefined',
'isNumber',
'isObject',
'isPrimitive',
'isRegExp',
'isString',
'isSymbol',
'isUndefined'
];

// helper to test named import specifiers (kept at module root so it's not re-created per run)
function hasAnyOtherNamedImports(spec: SgNode): boolean {
const firstIdent = spec.find({ rule: { kind: 'identifier' } });
const name = firstIdent?.text();
return Boolean(name && allIsMethods.includes(name));
}

// Map deprecated util.is*() calls to their modern equivalents
const replacements = new Map<string, (arg: string) => string>([
['isArray', (arg: string) => `Array.isArray(${arg})`],
['isBoolean', (arg: string) => `typeof ${arg} === 'boolean'`],
['isBuffer', (arg: string) => `Buffer.isBuffer(${arg})`],
['isDate', (arg: string) => `${arg} instanceof Date`],
['isError', (arg: string) => `Error.isError(${arg})`],
['isFunction', (arg: string) => `typeof ${arg} === 'function'`],
['isNull', (arg: string) => `${arg} === null`],
['isNullOrUndefined', (arg: string) => `${arg} === null || ${arg} === undefined`],
['isNumber', (arg: string) => `typeof ${arg} === 'number'`],
['isObject', (arg: string) => `${arg} && typeof ${arg} === 'object'`],
['isPrimitive', (arg: string) => `Object(${arg}) !== ${arg}`],
['isRegExp', (arg: string) => `${arg} instanceof RegExp`],
['isString', (arg: string) => `typeof ${arg} === 'string'`],
['isSymbol', (arg: string) => `typeof ${arg} === 'symbol'`],
['isUndefined', (arg: string) => `typeof ${arg} === 'undefined'`],
]);

/**
* Transform function that converts deprecated util.is*() calls
* to their modern equivalents.
*
* Handles:
* 1. util.isArray() → Array.isArray()
* 2. util.isBoolean() → typeof value === 'boolean'
* 3. util.isBuffer() → Buffer.isBuffer()
* 4. util.isDate() → value instanceof Date
* 5. util.isError() → value instanceof Error
* 6. util.isFunction() → typeof value === 'function'
* 7. util.isNull() → value === null
* 8. util.isNullOrUndefined() → value === null || value === undefined
* 9. util.isNumber() → typeof value === 'number'
* 10. util.isObject() → typeof value === 'object' && value !== null
* 11. util.isPrimitive() → value !== Object(value)
* 12. util.isRegExp() → value instanceof RegExp
* 13. util.isString() → typeof value === 'string'
* 14. util.isSymbol() → typeof value === 'symbol'
* 15. util.isUndefined() → typeof value === 'undefined'
*/
export default function transform(root: SgRoot): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
const linesToRemove: Range[] = [];

const usedMethods = new Set<string>();
const nonIsMethodsUsed = new Set<string>();

// Collect util import/require nodes once
const importOrRequireNodes = [
...getNodeImportStatements(root, "util"),
...getNodeRequireCalls(root, "util"),
];

// Detect namespace/default identifiers to check for non-is usages later
const namespaceBindings = new Set<string>();
for (const node of importOrRequireNodes) {
// namespace import: import * as ns from 'node:util'
const nsImport = node.find({
rule: { kind: 'namespace_import' },
});
if (nsImport) {
const id = nsImport.find({ rule: { kind: 'identifier' } });
if (id) namespaceBindings.add(id.text());
}

// default import: import util from 'node:util'
const importClause = (
node.kind() === 'import_statement'
|| node.kind() === 'import_clause'
)
&& (
node.find({ rule: { kind: 'import_clause' } })
?? node
);

if (importClause) {
const hasNamed = Boolean(
importClause.find({ rule: { kind: 'named_imports' } })
);
if (!hasNamed) {
const defaultId = importClause.find({
rule: { kind: 'identifier', not: { inside: { kind: 'namespace_import' } } },
});
if (defaultId) namespaceBindings.add(defaultId.text());
}
}

// require namespace: const util = require('node:util')
const reqNs = getRequireNamespaceIdentifier(node);
if (reqNs) namespaceBindings.add(reqNs.text());
}

// Mark non-is util usages for any namespace binding discovered
for (const ns of namespaceBindings) {
const usages = rootNode.findAll({ rule: { pattern: `${ns}.$METHOD($$$)` } });
for (const usage of usages) {
const methodMatch = usage.getMatch('METHOD');
if (methodMatch) {
const methodName = methodMatch.text();
if (!replacements.has(methodName)) nonIsMethodsUsed.add(methodName);
}
}
}

// Resolve local bindings for each util.is* and replace invocations
const localRefsByMethod = new Map<string, Set<string>>();
for (const method of replacements.keys()) {
localRefsByMethod.set(method, new Set());
for (const node of importOrRequireNodes) {
const resolved = resolveBindingPath(node, `$.${method}`);
if (resolved) localRefsByMethod.get(method)!.add(resolved);
}
}

for (const [method, replacement] of replacements) {
const refs = localRefsByMethod.get(method)!;
for (const ref of refs) {
const calls = rootNode.findAll({ rule: { pattern: `${ref}($ARG)` } });

if (!calls.length) continue;

for (const call of calls) {
const arg = call.getMatch('ARG');
if (!arg) continue;
const newCallText = replacement(arg.text());
edits.push(call.replace(newCallText));
usedMethods.add(method);
}
}
}

if (!edits.length) return null;

const importStatements = getNodeImportStatements(root, 'util');
for (const importNode of importStatements) {
const hasNamespace = Boolean(importNode.find({ rule: { kind: 'namespace_import' } }));
const namedImportSpecifiers = importNode.findAll({ rule: { kind: 'import_specifier' } });
const hasNamed = namedImportSpecifiers.length > 0;
const defaultIdentifier = getDefaultImportIdentifier(importNode);

// If all named specifiers are util.is* and there is no default or namespace, drop whole line
if (
hasNamed && !defaultIdentifier && !hasNamespace &&
namedImportSpecifiers.every(spec => hasAnyOtherNamedImports(spec as SgNode))
) {
linesToRemove.push(importNode.range());
continue;
}

// Otherwise, remove only named is* bindings; after replacement they are unused
for (const method of allIsMethods) {
const change = removeBinding(importNode, method);
if (change?.edit) edits.push(change.edit);
if (change?.lineToRemove) linesToRemove.push(change.lineToRemove);
}

// If no other util.is* methods are used, drop default/namespace imports entirely
if (nonIsMethodsUsed.size === 0) {
if ((hasNamespace && !hasNamed) || (defaultIdentifier && !hasNamed)) {
linesToRemove.push(importNode.range());
}
}
}

const requireStatements = getNodeRequireCalls(root, 'util');
for (const requireNode of requireStatements) {
const objectPattern = requireNode.find({ rule: { kind: 'object_pattern' } });
if (objectPattern) {
const shorthand = objectPattern.findAll({
rule: { kind: 'shorthand_property_identifier_pattern' }
});
const pairs = objectPattern.findAll({ rule: { kind: 'pair_pattern' } });
const importedNames: string[] = [];
for (const s of shorthand) importedNames.push(s.text());
for (const p of pairs) {
const key = p.find({ rule: { kind: 'property_identifier' } });
if (key) importedNames.push(key.text());
}
if (importedNames.length > 0 && importedNames.every((n) => allIsMethods.includes(n))) {
linesToRemove.push(requireNode.range());
continue;
}
}

// Otherwise, remove named util.is* bindings; after replacement they are unused
for (const method of allIsMethods) {
const change = removeBinding(requireNode, method);
if (change?.edit) edits.push(change.edit);
if (change?.lineToRemove) linesToRemove.push(change.lineToRemove);
}

// If no other util.* methods are used, drop namespace requires entirely
if (nonIsMethodsUsed.size === 0) {
const reqNs = getRequireNamespaceIdentifier(requireNode);
const hasObject = Boolean(objectPattern);
if (reqNs && !hasObject) linesToRemove.push(requireNode.range());
}
}

let sourceCode = rootNode.commitEdits(edits);
// Remove all lines marked for removal (including the whole util require/import if needed)
sourceCode = removeLines(sourceCode, linesToRemove);

return sourceCode;
}
46 changes: 46 additions & 0 deletions recipes/util-is/tests/expected/file-1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

if (Array.isArray(someValue)) {
console.log('someValue is an array');
}
if (typeof someValue === 'boolean') {
console.log('someValue is a boolean');
}
if (Buffer.isBuffer(someValue)) {
console.log('someValue is a buffer');
}
if (someValue instanceof Date) {
console.log('someValue is a date');
}
if (Error.isError(someValue)) {
console.log('someValue is an error');
}
if (typeof someValue === 'function') {
console.log('someValue is a function');
}
if (someValue === null) {
console.log('someValue is null');
}
if (someValue === null || someValue === undefined) {
console.log('someValue is null or undefined');
}
if (typeof someValue === 'number') {
console.log('someValue is a number');
}
if (someValue && typeof someValue === 'object') {
console.log('someValue is an object');
}
if (Object(someValue) !== someValue) {
console.log('someValue is a primitive');
}
if (someValue instanceof RegExp) {
console.log('someValue is a regular expression');
}
if (typeof someValue === 'string') {
console.log('someValue is a string');
}
if (typeof someValue === 'symbol') {
console.log('someValue is a symbol');
}
if (typeof someValue === 'undefined') {
console.log('someValue is undefined');
}
7 changes: 7 additions & 0 deletions recipes/util-is/tests/expected/file-10.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

if (Array.isArray(someValue)) {
console.log('someValue is an array');
}
if (typeof someValue === 'string') {
console.log('someValue is a string');
}
7 changes: 7 additions & 0 deletions recipes/util-is/tests/expected/file-11.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

if (someValue === null) {
console.log('someValue is null');
}
if (typeof someValue === 'undefined') {
console.log('someValue is undefined');
}
Loading
Loading