diff --git a/package-lock.json b/package-lock.json index 660fe7bd..5cd88548 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1563,6 +1563,10 @@ "resolved": "recipes/timers-deprecations", "link": true }, + "node_modules/@nodejs/tls-create-secure-pair-to-tls-socket": { + "resolved": "recipes/tls-create-secure-pair-to-tls-socket", + "link": true + }, "node_modules/@nodejs/tmpdir-to-tmpdir": { "resolved": "recipes/tmpdir-to-tmpdir", "link": true @@ -4447,7 +4451,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@nodejs/codemod-utils": "0.0.0" + "@nodejs/codemod-utils": "*" }, "devDependencies": { "@codemod.com/jssg-types": "^1.3.1" @@ -4538,6 +4542,17 @@ "@codemod.com/jssg-types": "^1.5.0" } }, + "recipes/tls-create-secure-pair-to-tls-socket": { + "name": "@nodejs/tls-create-secure-pair-to-tls-socket", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", "version": "1.0.1", diff --git a/recipes/tls-create-secure-pair-to-tls-socket/README.md b/recipes/tls-create-secure-pair-to-tls-socket/README.md new file mode 100644 index 00000000..d66f541a --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/README.md @@ -0,0 +1,104 @@ +# `tls.createSecurePair` deprecation DEP0064 + +This recipe transforms the usage from the deprecated `createSecurePair()` to `TLSSocket()`. + +See [DEP0064](https://nodejs.org/api/deprecations.html#dep0064-tlscreatesecurepair). + +## Examples + +### 1) Basic `createSecurePair` usage +```diff +-const { createSecurePair } = require('node:tls'); +-const pair = createSecurePair(credentials); ++const { TLSSocket } = require('node:tls'); ++const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 2) Namespace import (CJS) +```diff +-const tls = require('node:tls'); +-const pair = tls.createSecurePair(credentials); ++const tls = require('node:tls'); ++const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 3) With server context +```diff +-const { createSecurePair } = require('node:tls'); +-const pair = createSecurePair(credentials, true, true, false); ++const { TLSSocket } = require('node:tls'); ++const socket = new TLSSocket(underlyingSocket, { ++ secureContext: credentials, ++ isServer: true, ++ requestCert: true, ++ rejectUnauthorized: false ++}); +``` + +--- + +### 4) ESM named import +```diff +-import { createSecurePair } from 'node:tls'; +-const pair = createSecurePair(credentials); ++import { TLSSocket } from 'node:tls'; ++const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 5) ESM namespace import +```diff +-import * as tls from 'node:tls'; +-const pair = tls.createSecurePair(credentials); ++import * as tls from 'node:tls'; ++const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 6) Mixed usage with other TLS functions +```diff +-const { createSecurePair, createServer } = require('node:tls'); +-const pair = createSecurePair(credentials); +-const server = createServer(options); ++const { TLSSocket, createServer } = require('node:tls'); ++const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); ++const server = createServer(options); +``` + +--- + +### 7) ESM default import +```diff +-import tls from 'node:tls'; +-const pair = tls.createSecurePair(credentials); ++import tls, { TLSSocket } from 'node:tls'; ++const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 8) ESM dynamic import (assignment) +```diff +-const tls = await import('node:tls'); +-const pair = tls.createSecurePair(credentials); ++const tls = await import('node:tls'); ++const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +``` + +--- + +### 9) ESM dynamic import (thenable) +```diff +-import('node:tls').then(tls => { +- const pair = tls.createSecurePair(credentials); +-}); ++import('node:tls').then(tls => { ++ const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); ++}); +``` diff --git a/recipes/tls-create-secure-pair-to-tls-socket/codemod.yaml b/recipes/tls-create-secure-pair-to-tls-socket/codemod.yaml new file mode 100644 index 00000000..978eb8c9 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: "@nodejs/tls-create-secure-pair-to-tls-socket" +version: 1.0.0 +description: Handle DEP0064 by transforming `createSecurePair` to `TLSSocket` +author: Leonardo Trevizo +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - tls + - createSecurePair + - TLSSocket + +registry: + access: public + visibility: public diff --git a/recipes/tls-create-secure-pair-to-tls-socket/package.json b/recipes/tls-create-secure-pair-to-tls-socket/package.json new file mode 100644 index 00000000..88d0efbf --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/tls-create-secure-pair-to-tls-socket", + "version": "1.0.0", + "description": "Handle DEP0064 replacing `tls.createSecurePair()` with `tls.TLSSocket()`", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/tls-create-secure-pair-to-tls-socket", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Leo Trevizo", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/tls-create-secure-pair-to-tls-socket/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/tls-create-secure-pair-to-tls-socket/src/workflow.ts b/recipes/tls-create-secure-pair-to-tls-socket/src/workflow.ts new file mode 100644 index 00000000..08bf96ac --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/src/workflow.ts @@ -0,0 +1,136 @@ +import type { SgRoot, SgNode, Edit } from '@codemod.com/jssg-types/main'; +import type JS from '@codemod.com/jssg-types/langs/javascript'; +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'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const tlsStmts = getModuleDependencies(root, 'tls'); + + if (!tlsStmts.length) return null; + + const cspBindings = []; + + for (const stmt of tlsStmts) { + const binding = resolveBindingPath(stmt, '$.createSecurePair'); + if (binding) cspBindings.push(binding); + } + + if (!cspBindings.length) return null; + + const edits: Edit[] = []; + + // Transform all createSecurePair calls + const calls = rootNode.findAll({ rule: { kind: 'call_expression' } }); + + for (const call of calls) { + const callee = call.field('function'); + if (!callee) continue; + + const binding = getCallBinding(callee); + if (!binding || !cspBindings.includes(binding)) continue; + + // Extract arguments + const args = call.field('arguments'); + if (!args) continue; + + const argNodes = args.children().filter((n) => n.isNamed()); + + const options = buildOptions( + argNodes[0]?.text() || null, + argNodes[1]?.text() || null, + argNodes[2]?.text() || null, + argNodes[3]?.text() || null, + ); + + const replacement = binding.includes('.') + ? `new ${binding.replace(/\.createSecurePair$/, '.TLSSocket')}(underlyingSocket, ${options})` + : `new TLSSocket(underlyingSocket, ${options})`; + + edits.push(call.replace(replacement)); + } + + // Rename variables named 'pair' to 'socket' + edits.push(...renamePairVariables(rootNode, cspBindings)); + + // Update imports + const importStmts = tlsStmts.filter( + (s) => s.is('import_statement') || s.is('variable_declarator'), + ); + + for (const importStmt of importStmts) { + const result = updateBinding(importStmt, { + old: 'createSecurePair', + new: 'TLSSocket', + removeAlias: true, + }); + + if (result?.edit) { + edits.push(result.edit); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} + +function getCallBinding(callee: SgNode): string | null { + if (callee.is('member_expression')) { + const obj = callee.field('object'); + const prop = callee.field('property'); + if (!obj || !prop) return null; + return `${obj.text()}.${prop.text()}`; + } + if (callee.is('identifier')) { + return callee.text(); + } + return null; +} + +function buildOptions( + secureContext?: string | null, + isServer?: string | null, + requestCert?: string | null, + rejectUnauthorized?: string | null, +) { + const kv: string[] = []; + if (secureContext) kv.push(`secureContext: ${secureContext}`); + if (isServer) kv.push(`isServer: ${isServer}`); + if (requestCert) kv.push(`requestCert: ${requestCert}`); + if (rejectUnauthorized) kv.push(`rejectUnauthorized: ${rejectUnauthorized}`); + return kv.length > 0 ? `{ ${kv.join(', ')} }` : '{}'; +} + +function renamePairVariables(rootNode: SgNode, bindings: string[]): Edit[] { + const edits: Edit[] = []; + + const decls = rootNode.findAll({ + rule: { + kind: 'variable_declarator', + all: [ + { has: { field: 'name', pattern: 'pair' } }, + { has: { field: 'value', kind: 'call_expression' } }, + ], + }, + }); + + for (const decl of decls) { + const callExpr = decl.field('value'); + if (!callExpr) continue; + + const callee = callExpr.field('function'); + if (!callee) continue; + + const binding = getCallBinding(callee); + if (!binding || !bindings.includes(binding)) continue; + + const name = decl.field('name'); + if (name.is('identifier')) { + edits.push(name.replace('socket')); + } + } + + return edits; +} diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-alias.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-alias.js new file mode 100644 index 00000000..fb233f4f --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-alias.js @@ -0,0 +1,4 @@ +const { TLSSocket } = require('node:tls'); + +// Using an alias in CJS +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-basic.js new file mode 100644 index 00000000..aade280a --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-basic.js @@ -0,0 +1,2 @@ +const { TLSSocket } = require('node:tls'); +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-flags.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-flags.js new file mode 100644 index 00000000..7f6653dd --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-destructured-flags.js @@ -0,0 +1,2 @@ +const { TLSSocket } = require('node:tls'); +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true, requestCert: true, rejectUnauthorized: false }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-named.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-named.js new file mode 100644 index 00000000..aade280a --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-named.js @@ -0,0 +1,2 @@ +const { TLSSocket } = require('node:tls'); +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-namespace-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-namespace-basic.js new file mode 100644 index 00000000..a4845d28 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/cjs-namespace-basic.js @@ -0,0 +1,2 @@ +const tls = require('node:tls'); +const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-assign.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-assign.js new file mode 100644 index 00000000..c6fd3cea --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-assign.js @@ -0,0 +1,2 @@ +const tls = await import('node:tls'); +const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-function.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-function.js new file mode 100644 index 00000000..f44d4897 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-function.js @@ -0,0 +1,3 @@ +import('node:tls').then(function (tls) { + const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-then.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-then.js new file mode 100644 index 00000000..ef1bd895 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/dynamic-import-then.js @@ -0,0 +1,3 @@ +import('node:tls').then(tls => { + const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); +}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-already-has-tlssocket.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-already-has-tlssocket.js new file mode 100644 index 00000000..51e36fef --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-already-has-tlssocket.js @@ -0,0 +1,5 @@ +import { TLSSocket } from 'node:tls'; + +// Already has TLSSocket in imports +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); +const existingSocket = new TLSSocket(socket, {}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-default-import.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-default-import.js new file mode 100644 index 00000000..941426a9 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-default-import.js @@ -0,0 +1,2 @@ +import tls from 'node:tls'; +const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-mixed-imports.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-mixed-imports.js new file mode 100644 index 00000000..0d18ed04 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-mixed-imports.js @@ -0,0 +1,4 @@ +import tls, { createServer } from 'node:tls'; + +const server = createServer(options); +const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-alias.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-alias.js new file mode 100644 index 00000000..1306c64d --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-alias.js @@ -0,0 +1,4 @@ +import { TLSSocket } from 'node:tls'; + +// Using an alias +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-basic.js new file mode 100644 index 00000000..946f330a --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-named-basic.js @@ -0,0 +1,2 @@ +import { TLSSocket } from 'node:tls'; +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-namespace-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-namespace-basic.js new file mode 100644 index 00000000..9255483a --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/esm-namespace-basic.js @@ -0,0 +1,2 @@ +import * as tls from 'node:tls'; +const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/mixed-with-other-symbols.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/mixed-with-other-symbols.js new file mode 100644 index 00000000..ae0e16c1 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/mixed-with-other-symbols.js @@ -0,0 +1,3 @@ +const { createServer, TLSSocket } = require('node:tls'); +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); +const server = createServer(options); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/multiple-calls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/multiple-calls.js new file mode 100644 index 00000000..986cccdf --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/multiple-calls.js @@ -0,0 +1,8 @@ +const { TLSSocket } = require('node:tls'); + +// Multiple calls with different arguments +const pair1 = new TLSSocket(underlyingSocket, {}); +const pair2 = new TLSSocket(underlyingSocket, { secureContext: credentials }); +const pair3 = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true }); +const pair4 = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true, requestCert: false }); +const pair5 = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true, requestCert: false, rejectUnauthorized: true }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/nested-calls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/nested-calls.js new file mode 100644 index 00000000..463f0081 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/nested-calls.js @@ -0,0 +1,12 @@ +const tls = require('node:tls'); + +function setupTLS() { + const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); + return pair; +} + +class TLSManager { + init() { + this.pair = new tls.TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true }); + } +} diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/no-changes-needed.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/no-changes-needed.js new file mode 100644 index 00000000..28cc6216 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/no-changes-needed.js @@ -0,0 +1,4 @@ +const { TLSSocket } = require('node:tls'); + +// Code that already uses TLSSocket - should not change +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/not-from-tls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/not-from-tls.js new file mode 100644 index 00000000..8e3f80cf --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/expected/not-from-tls.js @@ -0,0 +1,3 @@ +// Without tls module - should not transform +const createSecurePair = someOtherModule.createSecurePair; +const pair = createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-alias.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-alias.js new file mode 100644 index 00000000..b6236075 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-alias.js @@ -0,0 +1,4 @@ +const { createSecurePair: csp } = require('node:tls'); + +// Using an alias in CJS +const pair = csp(credentials, true); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-basic.js new file mode 100644 index 00000000..85389cf3 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-basic.js @@ -0,0 +1,2 @@ +const { createSecurePair } = require('node:tls'); +const pair = createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-flags.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-flags.js new file mode 100644 index 00000000..33c62eb0 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-destructured-flags.js @@ -0,0 +1,2 @@ +const { createSecurePair } = require('node:tls'); +const pair = createSecurePair(credentials, true, true, false); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-named.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-named.js new file mode 100644 index 00000000..85389cf3 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-named.js @@ -0,0 +1,2 @@ +const { createSecurePair } = require('node:tls'); +const pair = createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-namespace-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-namespace-basic.js new file mode 100644 index 00000000..4ad73152 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/cjs-namespace-basic.js @@ -0,0 +1,2 @@ +const tls = require('node:tls'); +const pair = tls.createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-assign.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-assign.js new file mode 100644 index 00000000..fe969fef --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-assign.js @@ -0,0 +1,2 @@ +const tls = await import('node:tls'); +const pair = tls.createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-function.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-function.js new file mode 100644 index 00000000..ce198886 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-function.js @@ -0,0 +1,3 @@ +import('node:tls').then(function (tls) { + const pair = tls.createSecurePair(credentials); +}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-then.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-then.js new file mode 100644 index 00000000..6ee9842f --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/dynamic-import-then.js @@ -0,0 +1,3 @@ +import('node:tls').then(tls => { + const pair = tls.createSecurePair(credentials); +}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-already-has-tlssocket.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-already-has-tlssocket.js new file mode 100644 index 00000000..34889666 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-already-has-tlssocket.js @@ -0,0 +1,5 @@ +import { createSecurePair, TLSSocket } from 'node:tls'; + +// Already has TLSSocket in imports +const pair = createSecurePair(credentials); +const existingSocket = new TLSSocket(socket, {}); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-default-import.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-default-import.js new file mode 100644 index 00000000..3b456881 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-default-import.js @@ -0,0 +1,2 @@ +import tls from 'node:tls'; +const pair = tls.createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-mixed-imports.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-mixed-imports.js new file mode 100644 index 00000000..b115e395 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-mixed-imports.js @@ -0,0 +1,4 @@ +import tls, { createServer } from 'node:tls'; + +const server = createServer(options); +const pair = tls.createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-alias.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-alias.js new file mode 100644 index 00000000..cfc1085c --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-alias.js @@ -0,0 +1,4 @@ +import { createSecurePair as csp } from 'node:tls'; + +// Using an alias +const pair = csp(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-basic.js new file mode 100644 index 00000000..9dccd7ed --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-named-basic.js @@ -0,0 +1,2 @@ +import { createSecurePair } from 'node:tls'; +const pair = createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-namespace-basic.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-namespace-basic.js new file mode 100644 index 00000000..28119184 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/esm-namespace-basic.js @@ -0,0 +1,2 @@ +import * as tls from 'node:tls'; +const pair = tls.createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/mixed-with-other-symbols.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/mixed-with-other-symbols.js new file mode 100644 index 00000000..e6a341db --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/mixed-with-other-symbols.js @@ -0,0 +1,3 @@ +const { createSecurePair, createServer } = require('node:tls'); +const pair = createSecurePair(credentials); +const server = createServer(options); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/multiple-calls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/multiple-calls.js new file mode 100644 index 00000000..4e93d447 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/multiple-calls.js @@ -0,0 +1,8 @@ +const { createSecurePair } = require('node:tls'); + +// Multiple calls with different arguments +const pair1 = createSecurePair(); +const pair2 = createSecurePair(credentials); +const pair3 = createSecurePair(credentials, true); +const pair4 = createSecurePair(credentials, true, false); +const pair5 = createSecurePair(credentials, true, false, true); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/nested-calls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/nested-calls.js new file mode 100644 index 00000000..ec7f1b0c --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/nested-calls.js @@ -0,0 +1,12 @@ +const tls = require('node:tls'); + +function setupTLS() { + const pair = tls.createSecurePair(credentials); + return pair; +} + +class TLSManager { + init() { + this.pair = tls.createSecurePair(credentials, true); + } +} diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/no-changes-needed.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/no-changes-needed.js new file mode 100644 index 00000000..28cc6216 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/no-changes-needed.js @@ -0,0 +1,4 @@ +const { TLSSocket } = require('node:tls'); + +// Code that already uses TLSSocket - should not change +const socket = new TLSSocket(underlyingSocket, { secureContext: credentials }); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/tests/input/not-from-tls.js b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/not-from-tls.js new file mode 100644 index 00000000..8e3f80cf --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/tests/input/not-from-tls.js @@ -0,0 +1,3 @@ +// Without tls module - should not transform +const createSecurePair = someOtherModule.createSecurePair; +const pair = createSecurePair(credentials); diff --git a/recipes/tls-create-secure-pair-to-tls-socket/workflow.yaml b/recipes/tls-create-secure-pair-to-tls-socket/workflow.yaml new file mode 100644 index 00000000..1b82d8a3 --- /dev/null +++ b/recipes/tls-create-secure-pair-to-tls-socket/workflow.yaml @@ -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 DEP0064 via replacing deprecated `tls.createSecurePair()` to `tls.Socket`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript diff --git a/utils/src/ast-grep/resolve-binding-path.test.ts b/utils/src/ast-grep/resolve-binding-path.test.ts index 686d1816..50e48c1d 100644 --- a/utils/src/ast-grep/resolve-binding-path.test.ts +++ b/utils/src/ast-grep/resolve-binding-path.test.ts @@ -1,14 +1,13 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import astGrep from "@ast-grep/napi"; -import dedent from "dedent"; -import type Js from "@codemod.com/jssg-types/langs/javascript"; -import type { SgNode } from "@codemod.com/jssg-types/main"; - -import { resolveBindingPath } from "./resolve-binding-path.ts"; - -describe("resolve-binding-path", () => { - it("should be able to solve binding path to simple requires", () => { +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import astGrep from '@ast-grep/napi'; +import dedent from 'dedent'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; +import type { SgNode } from '@codemod.com/jssg-types/main'; +import { resolveBindingPath } from './resolve-binding-path.ts'; + +describe('resolve-binding-path', () => { + it('should be able to solve binding path to simple requires', () => { const code = dedent` const util = require('node:util'); `; @@ -16,16 +15,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "util.types.isNativeError"); + assert.strictEqual(bindingPath, 'util.types.isNativeError'); }); - it("should be able to resolve binding path from namespace ESM import", () => { + it('should be able to resolve binding path from namespace ESM import', () => { const code = dedent` import foo from "node:foo" `; @@ -33,16 +35,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.bar"); + const bindingPath = resolveBindingPath(importStatement!, '$.bar'); - assert.strictEqual(bindingPath, "foo.bar"); + assert.strictEqual(bindingPath, 'foo.bar'); }); - it("should be able to solve binding path when destructuring happen", () => { + it('should be able to solve binding path when destructuring happen', () => { const code = dedent` const { types } = require('node:util'); `; @@ -50,16 +52,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "types.isNativeError"); + assert.strictEqual(bindingPath, 'types.isNativeError'); }); - it("should be able to solve binding when have multiple destructurings", () => { + it('should be able to solve binding when have multiple destructurings', () => { const code = dedent` const { types: { isNativeError } } = require('node:util'); `; @@ -67,16 +72,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + requireStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "isNativeError"); + assert.strictEqual(bindingPath, 'isNativeError'); }); - it("should be able to solve binding when a rename happen", () => { + it('should be able to solve binding when a rename happen', () => { const code = dedent` const { types: typesRenamed } = require('node:util'); `; @@ -84,16 +92,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "typesRenamed.isNativeError"); + assert.strictEqual(bindingPath, 'typesRenamed.isNativeError'); }); - it("should throw an error if unsupported node kind is passed", () => { + it('should throw an error if unsupported node kind is passed', () => { const code = dedent` function test() { } `; @@ -101,14 +112,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const functionDeclaration = (rootNode.root() as SgNode).find({ rule: { - kind: "function_declaration", + kind: 'function_declaration', }, }); - assert.throws(() => resolveBindingPath(functionDeclaration!, "$.types.isNativeError")); + assert.throws(() => + resolveBindingPath(functionDeclaration!, '$.types.isNativeError'), + ); }); - it("should be able to solve binding using esmodule with default import", () => { + it('should be able to solve binding using esmodule with default import', () => { const code = dedent` import util from 'node:util'; `; @@ -116,16 +129,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "util.types.isNativeError"); + assert.strictEqual(bindingPath, 'util.types.isNativeError'); }); - it("should be able to solve binding using esmodule with named imports", () => { + it('should be able to solve binding using esmodule with named imports', () => { const code = dedent` import { types } from 'node:util'; `; @@ -133,16 +149,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "types.isNativeError"); + assert.strictEqual(bindingPath, 'types.isNativeError'); }); - it("should be able to solve binding using esmodule with named imports using alias", () => { + it('should be able to solve binding using esmodule with named imports using alias', () => { const code = dedent` import { types as renamedTypes } from 'node:util'; `; @@ -150,16 +169,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "renamedTypes.isNativeError"); + assert.strictEqual(bindingPath, 'renamedTypes.isNativeError'); }); - it("should be able to solve binding using esmodule with namespace import", () => { + it('should be able to solve binding using esmodule with namespace import', () => { const code = dedent` import * as example from 'node:util'; `; @@ -167,16 +189,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "example.types.isNativeError"); + assert.strictEqual(bindingPath, 'example.types.isNativeError'); }); - it("should handle deep nested destructuring with multiple levels", () => { + it('should handle deep nested destructuring with multiple levels', () => { const code = dedent` const { types: { isNativeError: nativeErrorCheck } } = require('node:util'); `; @@ -184,16 +209,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + requireStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "nativeErrorCheck"); + assert.strictEqual(bindingPath, 'nativeErrorCheck'); }); - it("should handle complex path resolution with longer dotted paths", () => { + it('should handle complex path resolution with longer dotted paths', () => { const code = dedent` const util = require('node:util'); `; @@ -201,16 +229,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "lexical_declaration", + kind: 'lexical_declaration', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.types.format.inspect.custom"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.types.format.inspect.custom', + ); - assert.strictEqual(bindingPath, "util.types.format.inspect.custom"); + assert.strictEqual(bindingPath, 'util.types.format.inspect.custom'); }); - it("should handle multiple named imports with different aliases", () => { + it('should handle multiple named imports with different aliases', () => { const code = dedent` import { types as utilTypes, format as utilFormat } from 'node:util'; `; @@ -218,16 +249,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const importStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "import_statement", + kind: 'import_statement', }, }); - const bindingPath = resolveBindingPath(importStatement!, "$.format.inspect"); + const bindingPath = resolveBindingPath( + importStatement!, + '$.format.inspect', + ); - assert.strictEqual(bindingPath, "utilFormat.inspect"); + assert.strictEqual(bindingPath, 'utilFormat.inspect'); }); - it("should handle require with complex destructuring and renaming", () => { + it('should handle require with complex destructuring and renaming', () => { const code = dedent` const { types: renamed, format: { inspect } } = require('node:util'); `; @@ -235,16 +269,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + requireStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "renamed.isNativeError"); + assert.strictEqual(bindingPath, 'renamed.isNativeError'); }); - it("should handle deep destructuring and return remaining path after resolved binding", () => { + it('should handle deep destructuring and return remaining path after resolved binding', () => { const code = dedent` const { a: {b: { c: { d }} } } = require('node:util'); `; @@ -252,16 +289,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + const bindingPath = resolveBindingPath(requireStatement!, '$.a.b.c.d.e'); - assert.strictEqual(bindingPath, "d.e"); + assert.strictEqual(bindingPath, 'd.e'); }); - it("should handle single character variable names", () => { + it('should handle single character variable names', () => { const code = dedent` const { types: t } = require('node:util'); `; @@ -269,16 +306,19 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + requireStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "t.isNativeError"); + assert.strictEqual(bindingPath, 't.isNativeError'); }); - it("should handle mixed require patterns with array destructuring context", () => { + it('should handle mixed require patterns with array destructuring context', () => { const code = dedent` const [, { types }] = [null, require('node:util')]; `; @@ -287,16 +327,19 @@ describe("resolve-binding-path", () => { const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.types.isNativeError"); + const bindingPath = resolveBindingPath( + requireStatement!, + '$.types.isNativeError', + ); - assert.strictEqual(bindingPath, "types.isNativeError"); + assert.strictEqual(bindingPath, 'types.isNativeError'); }); - it("should resolve correctly when have member-expression", () => { + it('should resolve correctly when have member-expression', () => { const code = dedent` const SlowBuffer = require('buffer').SlowBuffer; `; @@ -304,16 +347,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.SlowBuffer"); + const bindingPath = resolveBindingPath(requireStatement!, '$.SlowBuffer'); - assert.strictEqual(bindingPath, "SlowBuffer"); + assert.strictEqual(bindingPath, 'SlowBuffer'); }); - it("should resolve correctly when there are multiple property accesses", () => { + it('should resolve correctly when there are multiple property accesses', () => { const code = dedent` const variable = require('buffer').a.b; `; @@ -321,16 +364,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.a.b"); + const bindingPath = resolveBindingPath(requireStatement!, '$.a.b'); - assert.strictEqual(bindingPath, "variable"); + assert.strictEqual(bindingPath, 'variable'); }); - it("should resolve correctly when there are multiple property accesses but not the entire path", () => { + it('should resolve correctly when there are multiple property accesses but not the entire path', () => { const code = dedent` const variable = require('buffer').a.b; `; @@ -338,16 +381,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + const bindingPath = resolveBindingPath(requireStatement!, '$.a.b.c.d.e'); - assert.strictEqual(bindingPath, "variable.c.d.e"); + assert.strictEqual(bindingPath, 'variable.c.d.e'); }); - it("should resolve correctly when there are multiple property accesses and destructuring", () => { + it('should resolve correctly when there are multiple property accesses and destructuring', () => { const code = dedent` const { c: { d } } = require('buffer').a.b; `; @@ -355,16 +398,16 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + const bindingPath = resolveBindingPath(requireStatement!, '$.a.b.c.d.e'); - assert.strictEqual(bindingPath, "d.e"); + assert.strictEqual(bindingPath, 'd.e'); }); - it("should resolve as undefined when property accesses is different than path to solve", () => { + it('should resolve as undefined when property accesses is different than path to solve', () => { const code = dedent` const c = require('buffer').a.g.c; `; @@ -372,12 +415,76 @@ describe("resolve-binding-path", () => { const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); const requireStatement = (rootNode.root() as SgNode).find({ rule: { - kind: "variable_declarator", + kind: 'variable_declarator', }, }); - const bindingPath = resolveBindingPath(requireStatement!, "$.a.b.c.d.e"); + const bindingPath = resolveBindingPath(requireStatement!, '$.a.b.c.d.e'); assert.strictEqual(bindingPath, undefined); }); + + it('should resolve binding when used with await import()', () => { + const code = dedent` + const tls = await import('tls'); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const requireStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: 'variable_declarator', + }, + }); + + const bindingPath = resolveBindingPath( + requireStatement!, + '$.createSecurePair', + ); + + assert.strictEqual(bindingPath, 'tls.createSecurePair'); + }); + + it('should resolve binding when used with import() with arrow function callback', () => { + const code = dedent` + import('node:tls').then(tls => { + const pair = tls.createSecurePair(credentials); + }); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const importStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: 'expression_statement', + }, + }); + + const bindingPath = resolveBindingPath( + importStatement!, + '$.createSecurePair', + ); + + assert.strictEqual(bindingPath, 'tls.createSecurePair'); + }); + + it('should resolve binding when used with import() with function callback', () => { + const code = dedent` + import('node:tls').then(function (tls) { + const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials }); + }); + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const importStatement = (rootNode.root() as SgNode).find({ + rule: { + kind: 'expression_statement', + }, + }); + + const bindingPath = resolveBindingPath( + importStatement!, + '$.createSecurePair', + ); + + assert.strictEqual(bindingPath, 'tls.createSecurePair'); + }); }); diff --git a/utils/src/ast-grep/resolve-binding-path.ts b/utils/src/ast-grep/resolve-binding-path.ts index 25d6b944..3cd3bfbb 100644 --- a/utils/src/ast-grep/resolve-binding-path.ts +++ b/utils/src/ast-grep/resolve-binding-path.ts @@ -3,7 +3,8 @@ import type Js from '@codemod.com/jssg-types/langs/javascript'; const requireKinds = ['lexical_declaration', 'variable_declarator']; const importKinds = ['import_statement', 'import_clause']; -const supportedKinds = [...requireKinds, ...importKinds]; +const importCallback = ['expression_statement']; +const supportedKinds = [...requireKinds, ...importKinds, ...importCallback]; /** * Resolves a global function path to its local binding path based on the import structure in the AST. @@ -53,6 +54,10 @@ export function resolveBindingPath(node: SgNode, path: string) { if (requireKinds.includes(rootKind)) { return resolveBindingPathRequire(activeNode, path); } + + if (importCallback.includes(rootKind)) { + return resolveBindingPathImportCallback(activeNode, path); + } } function resolveBindingPathRequire(node: SgNode, path: string) { @@ -245,3 +250,46 @@ function resolveBindingPathImport(node: SgNode, path: string) { } } } + +function resolveBindingPathImportCallback(node: SgNode, path: string) { + const imp = node.find({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'import', + }, + }, + }); + + if (!imp) return; + + const fn = imp.find<'call_expression'>({ + rule: { + inside: { + kind: 'call_expression', + stopBy: 'end', + }, + }, + }); + + const callbackArgs = fn?.field('arguments')?.child(1) as SgNode< + Js, + 'arrow_function' + >; + + if (!callbackArgs) return; + + let args = + callbackArgs.field('parameter') || callbackArgs.field('parameters'); + + if (args.is('formal_parameters')) { + args = args.child(1) as SgNode; + } + + if (args.is('identifier') || args.is('required_parameter')) { + return path.replace('$', args.text()); + } + + return undefined; +} diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index bb1e1b07..6002a88b 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -249,6 +249,32 @@ describe('update-binding', () => { ); }); + it('should remove alias when renaming with removeAlias options is true', () => { + const code = dedent` + import { types as utilTypes } from 'node:util'; + `; + + const rootNode = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = rootNode.root() as SgNode; + + const importStatement = node.find({ + rule: { + kind: 'import_statement', + }, + }); + + const change = updateBinding(importStatement!, { + old: 'utilTypes', + new: 'newTypes', + removeAlias: true, + }); + const sourceCode = node.commitEdits([change.edit!]); + + assert.notEqual(change, null); + assert.strictEqual(change?.lineToRemove, undefined); + assert.strictEqual(sourceCode, "import { newTypes } from 'node:util';"); + }); + it('should remove only the aliased import binding when it matches the provided alias', () => { const code = dedent` import { types as utilTypes, diff } from 'node:util'; diff --git a/utils/src/ast-grep/update-binding.ts b/utils/src/ast-grep/update-binding.ts index 37b85617..fcde97b2 100644 --- a/utils/src/ast-grep/update-binding.ts +++ b/utils/src/ast-grep/update-binding.ts @@ -16,6 +16,7 @@ type UpdateBindingOptions = { ignoredRanges?: Range[]; }; root?: SgNode; + removeAlias?: boolean; }; function isRangeWithin(inner: Range, outer: Range): boolean { @@ -292,6 +293,11 @@ function handleNamedImportBindings( } if ((matchesName || matchesAlias) && nameNode) { + if (options.removeAlias) { + return { + edit: renamedImport.replace(options.new), + }; + } return { edit: nameNode.replace(options.new), };