-
-
Notifications
You must be signed in to change notification settings - Fork 53
feat(DEP0153): add dns lookup options codemod #473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Herrtian
wants to merge
5
commits into
nodejs:main
Choose a base branch
from
Herrtian:dns-lookup-options-coercion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7ce877d
feat(DEP0153): add dns lookup options codemod
Herrtian e7259af
docs: explain dns lookup codemod scope
Herrtian 6b9e5ce
Address dns lookup review
Herrtian 138c038
Merge branch 'main' into dns-lookup-options-coercion
AugustinMauroy b6b47d6
Address dns lookup review comments
Herrtian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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": "*" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/commonjs-destructured/expected.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/commonjs-destructured/input.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/commonjs-namespace/expected.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/commonjs-namespace/input.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/dns-promises-import/expected.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/dns-promises-import/input.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" }); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/expected.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/input.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/promises-alias/expected.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); |
3 changes: 3 additions & 0 deletions
3
recipes/dns-lookup-options-coercion/tests/promises-alias/input.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.