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
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions recipes/dns-lookup-options-coercion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# dns.lookup options type coercion DEP0153

Handle DEP0153 by converting literal `dns.lookup()` and `dnsPromises.lookup()` option values to their proper types.

See [DEP0153](https://nodejs.org/api/deprecations.html#dep0153-dnslookup-and-dnspromiseslookup-options-type-coercion).

## Example

```diff
const dns = require("node:dns");

- dns.lookup("example.com", { family: "4", all: 1 }, callback);
+ dns.lookup("example.com", { family: 4, all: true }, callback);
```

```diff
import { lookup } from "node:dns/promises";

- await lookup("example.com", { family: "6", verbatim: 0 });
+ await lookup("example.com", { family: 6, verbatim: false });
```

## Limitations

This recipe only changes literal option values. Dynamic values such as `{ family: familyOption }` need manual review.
24 changes: 24 additions & 0 deletions recipes/dns-lookup-options-coercion/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
schema_version: "1.0"
name: "@nodejs/dns-lookup-options-coercion"
version: "1.0.0"
description: Handle DEP0153 by converting literal dns.lookup option values to their proper types.
author: Herrtian
license: MIT
workflow: workflow.yaml
category: migration
repository: https://github.com/nodejs/userland-migrations

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration
- nodejs
- dns

registry:
access: public
visibility: public
24 changes: 24 additions & 0 deletions recipes/dns-lookup-options-coercion/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@nodejs/dns-lookup-options-coercion",
"version": "1.0.0",
"description": "Handle DEP0153 by converting literal dns.lookup option values to their proper types",
"type": "module",
"scripts": {
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/dns-lookup-options-coercion",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Herrtian",
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/dns-lookup-options-coercion/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.6.0"
},
"dependencies": {
"@nodejs/codemod-utils": "*"
}
}
186 changes: 186 additions & 0 deletions recipes/dns-lookup-options-coercion/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
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 NUMERIC_OPTIONS = new Set(['family', 'hints']);
const BOOLEAN_OPTIONS = new Set(['all', 'verbatim']);

export default function transform(root: SgRoot<Js>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
const lookupCallees = collectLookupCallees(root);

if (!lookupCallees.size) return null;

for (const callee of lookupCallees) {
const calls = rootNode.findAll<'call_expression'>({
rule: {
kind: 'call_expression',
has: {
field: 'function',
kind: callee.includes('.') ? 'member_expression' : 'identifier',
pattern: callee,
},
},
});

for (const call of calls) {
processLookupCall(call, edits);
}
}

if (!edits.length) return null;

return rootNode.commitEdits(edits);
}

function collectLookupCallees(root: SgRoot<Js>): Set<string> {
const callees = new Set<string>();

for (const statement of getModuleDependencies(root, 'dns')) {
addResolvedBinding(callees, statement, '$.lookup');
addResolvedBinding(callees, statement, '$.promises.lookup');
}

for (const statement of getModuleDependencies(root, 'dns/promises')) {
addResolvedBinding(callees, statement, '$.lookup');
}

return callees;
}

function addResolvedBinding(
callees: Set<string>,
statement: SgNode<Js>,
path: string,
): void {
const binding = resolveBindingPath(statement, path);

if (binding) {
callees.add(binding);
}
}

/**
* Rewrites the second argument only when it is an inline options object.
*
* DEP0153 affects option values, so calls without options or with a shared
* options variable are intentionally left unchanged for manual review.
*/
function processLookupCall(call: SgNode<Js>, edits: Edit[]): void {
const args = call.field('arguments');
if (!args) return;

const optionArg = args.children().filter((child) => child.isNamed())[1];

if (!optionArg || optionArg.kind() !== 'object') return;

edits.push(...transformOptionsObject(optionArg));
}

/**
* Converts known dns.lookup option keys when the value can be replaced without
* changing surrounding code. Other keys are ignored so this recipe stays scoped
* to the DEP0153 runtime coercions.
*/
function transformOptionsObject(options: SgNode<Js>): Edit[] {
const edits: Edit[] = [];
const pairs = options.findAll<'pair'>({ rule: { kind: 'pair' } });

for (const pair of pairs) {
const key = getOptionKey(pair);
const value = pair.field('value');

if (!key || !value) continue;

const replacement = getValueReplacement(key, value);

if (replacement !== null) {
edits.push(value.replace(replacement));
}
}

return edits;
}

function getOptionKey(pair: SgNode<Js, 'pair'>): string | null {
const key = pair.field('key');
if (!key) return null;

const keyKind = key.kind();
if (keyKind === 'property_identifier') {
return key.text();
}

if (keyKind === 'string') {
return getStringLiteralValue(key);
}

return null;
}

/**
* Returns a replacement for deprecated literal coercions only.
*
* Semantic propagation for identifiers is deliberately avoided here: options
* objects can be reused, mutated, or passed through helpers, and an unsafe
* rewrite would be harder to review than leaving those cases for the user.
*/
function getValueReplacement(key: string, value: SgNode<Js>): string | null {
if (NUMERIC_OPTIONS.has(key)) {
return getNumericReplacement(value);
}

if (BOOLEAN_OPTIONS.has(key)) {
return getBooleanReplacement(value);
}

return null;
}

function getNumericReplacement(value: SgNode<Js>): string | null {
const valueKind = value.kind();

switch (valueKind) {
case 'string': {
const stringValue = getStringLiteralValue(value);
if (!stringValue || !/^\d+$/.test(stringValue)) return null;

return String(Number.parseInt(stringValue, 10));
}
default:
return null;
}
}
Comment thread
AugustinMauroy marked this conversation as resolved.

function getBooleanReplacement(value: SgNode<Js>): string | null {
const valueKind = value.kind();

switch (valueKind) {
case 'number':
if (value.text() === '0') return 'false';
if (value.text() === '1') return 'true';
return null;
case 'string': {
const stringValue = getStringLiteralValue(value);
return stringValue === 'true' || stringValue === 'false'
? stringValue
: null;
}
default:
return null;
}
}

function getStringLiteralValue(node: SgNode<Js>): string | null {
const stringFragment = node.find({ rule: { kind: 'string_fragment' } });

if (stringFragment) {
return stringFragment.text();
}

const text = node.text();

return text.length >= 2 ? text.slice(1, -1) : null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { lookup } = require("node:dns");

lookup("example.com", { family: 4, all: false }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { lookup } = require("node:dns");

lookup("example.com", { family: "4", all: "false" }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const dns = require("node:dns");

dns.lookup("example.com", { family: 4, hints: 0, all: true, verbatim: false }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const dns = require("node:dns");

dns.lookup("example.com", { family: "4", hints: "0", all: 1, verbatim: 0 }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { lookup } from "node:dns/promises";

await lookup("example.com", { family: 6, all: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { lookup } from "node:dns/promises";

await lookup("example.com", { family: "6", all: "true" });
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const dns = require("node:dns");

dns.lookup("example.com", { family: familyOption, hints: dns.ADDRCONFIG, all: shouldReturnAll }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const dns = require("node:dns");

dns.lookup("example.com", { family: familyOption, hints: dns.ADDRCONFIG, all: shouldReturnAll }, callback);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { promises as dnsPromises } from "node:dns";

await dnsPromises.lookup("example.com", { family: 4, verbatim: false });
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { promises as dnsPromises } from "node:dns";

await dnsPromises.lookup("example.com", { family: "4", verbatim: "false" });
25 changes: 25 additions & 0 deletions recipes/dns-lookup-options-coercion/workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json

version: "1"

nodes:
- id: apply-transforms
name: Apply AST Transformations
type: automatic
steps:
- name: Handle DEP0153 by converting literal dns.lookup option values to their proper types.
js-ast-grep:
js_file: src/workflow.ts
base_path: .
include:
- "**/*.js"
- "**/*.jsx"
- "**/*.mjs"
- "**/*.cjs"
- "**/*.cts"
- "**/*.mts"
- "**/*.ts"
- "**/*.tsx"
exclude:
- "**/node_modules/**"
language: typescript
Loading