-
-
Notifications
You must be signed in to change notification settings - Fork 31
feat(tls-securepair-to-tlssocket): introduce #286
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
base: main
Are you sure you want to change the base?
Changes from 5 commits
6ef1d20
1c4fe70
c638006
485fb15
3e43dea
3e78f7b
2a069ed
52de208
b12b649
fe681b9
153faf9
40986bd
0cd5eee
af0f097
a2b7796
6f891fe
9d501a2
8fbb180
9cdb7cc
91216ec
4148701
700224b
28a3357
9bcc4d9
b6a95fa
62a72f7
0a1f785
8ffc05c
b18b065
9c54544
d22e788
be3ccee
43324bf
9b0dd42
df79099
2560ceb
8fd0b6c
9d3224c
23e8baa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # tls-securepair-to-tlssocket | ||
|
|
||
| Codemod to migrate from the deprecated `tls.SecurePair` class to `tls.TLSSocket` in Node.js applications. `SecurePair` was deprecated and subsequently removed in favor of `TLSSocket`. | ||
|
|
||
| ## What it does | ||
|
|
||
| This codemod transforms usages of `tls.SecurePair` into `tls.TLSSocket`. Since `TLSSocket` wraps an existing socket, the codemod injects a `socket` argument that you may need to define or bind in your context. | ||
|
|
||
| Key transformations: | ||
| - **Constructor:** Replaces `new SecurePair()` with `new TLSSocket(socket)`. | ||
| - **Imports:** Updates `require` and `import` statements from `SecurePair` to `TLSSocket`. | ||
| - **Renaming:** Intelligently renames variables (e.g., `pair` → `socket`, `securePairInstance` → `socketInstance`) while preserving CamelCase. | ||
| - **Cleanup:** Removes deprecated property accesses like `.cleartext` and `.encrypted`. | ||
| - **Annotations:** Adds comments to highlight where manual API verification is needed. | ||
|
|
||
| ## Supports | ||
|
|
||
| - **Module Systems:** | ||
| - CommonJS: `const tls = require('node:tls')` / `const { SecurePair } = ...` | ||
| - ESM: `import tls from 'node:tls'` / `import { SecurePair } ...` | ||
| - **Variable Renaming:** | ||
| - Updates variable declarations: `const pair = ...` → `const socket = ...` | ||
| - Updates references deep in the scope: `pair.on('error')` → `socket.on('error')` | ||
| - Handles naming variations: `myPair` → `mySocket`, `securePair` → `secureSocket`. | ||
| - **Cleanup:** | ||
| - Identifies and removes lines accessing `cleartext` or `encrypted` properties. | ||
| - **Namespace Handling:** | ||
| - Supports both `new tls.SecurePair()` and `new SecurePair()`. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Case 1: CommonJS & Variable Renaming | ||
|
|
||
| **Before** | ||
|
|
||
| ```js | ||
| const tls = require('node:tls'); | ||
|
|
||
| // Using tls.SecurePair constructor | ||
| const pair = new tls.SecurePair(); | ||
| const cleartext = pair.cleartext; | ||
| const encrypted = pair.encrypted; | ||
|
|
||
| pair.on('error', (err) => { | ||
| console.error(err); | ||
| }); | ||
| ``` | ||
|
|
||
| **After** | ||
|
|
||
| ``` | ||
| const tls = require('node:tls'); | ||
|
|
||
| // Using tls.TLSSocket instead | ||
| const socket = new tls.TLSSocket(socket); | ||
| // Note: Direct migration may require additional context-specific changes | ||
| // as SecurePair and TLSSocket have different APIs | ||
|
|
||
| socket.on('error', (err) => { | ||
| console.error(err); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Case 2: ESM & Destructuring | ||
|
|
||
| **Before** | ||
|
|
||
| ``` | ||
| import { SecurePair } from 'node:tls'; | ||
|
|
||
| const myPair = new SecurePair(); | ||
| myPair.cleartext.write('hello'); | ||
| ``` | ||
|
|
||
| **After** | ||
|
|
||
| ``` | ||
| import { TLSSocket } from 'node:tls'; | ||
|
|
||
| const mySocket = new TLSSocket(socket); | ||
| // Note: Direct migration may require additional context-specific changes | ||
| // as SecurePair and TLSSocket have different APIs | ||
| ``` | ||
|
|
||
| ## Warning | ||
|
|
||
| The tls.TLSSocket constructor requires an existing socket instance (net.Socket) as an argument. This codemod automatically inserts socket as the argument: | ||
| JavaScript | ||
|
|
||
| ``` | ||
menace31 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| new TLSSocket(socket) | ||
| ``` | ||
|
|
||
| You must ensure that a variable named socket exists in the scope or rename it to match your existing socket variable (e.g., clientSocket, stream, etc.). | ||
|
|
||
| ## Test | ||
|
|
||
| The test.sh script runs all the tests located in the tests folder. All input files are temporarily copied to a new folder and compared against their expected results found in the expected folder. This helps identify which tests failed and why. Feel free to add new tests if necessary. | ||
menace31 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| schema_version: "1.0" | ||
| name: "@nodejs/tls-securepair-to-tlssocket" | ||
| version: "1.0.0" | ||
| description: Migrate usages of `tls.SecurePair` to `tls.TLSSocket` where possible. | ||
| author: Maxime Devillet | ||
| license: MIT | ||
| workflow: workflow.yaml | ||
| category: migration | ||
|
|
||
| targets: | ||
| languages: | ||
| - javascript | ||
| - typescript | ||
|
|
||
| keywords: | ||
| - transformation | ||
| - migration | ||
| - tls | ||
| - securepair | ||
|
|
||
| registry: | ||
| access: public | ||
| visibility: public |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "@nodejs/tls-securepair-to-tlssocket", | ||
| "version": "1.0.0", | ||
| "description": "Migrate usages of tls.SecurePair to 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-securepair-to-tlssocket", | ||
| "bugs": "https://github.com/nodejs/userland-migrations/issues" | ||
| }, | ||
| "author": "Maxime Devillet", | ||
| "license": "MIT", | ||
| "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/tls-securepair-to-tlssocket/README.md", | ||
| "devDependencies": { | ||
| "@codemod.com/jssg-types": "^1.0.9" | ||
| }, | ||
| "dependencies": { | ||
| "@nodejs/codemod-utils": "*", | ||
| "@ast-grep/napi": "^0.40.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||
| import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines"; | ||||||||
| import type { Edit, SgRoot, Range } from "@codemod.com/jssg-types/main"; | ||||||||
| import type Js from "@codemod.com/jssg-types/langs/javascript"; | ||||||||
|
|
||||||||
| function getClosest(node: any, kinds: string[]): any | null { | ||||||||
| let current = node.parent(); | ||||||||
| while (current) { | ||||||||
| if (kinds.includes(current.kind())) { | ||||||||
| return current; | ||||||||
| } | ||||||||
| current = current.parent(); | ||||||||
| } | ||||||||
| return null; | ||||||||
menace31 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||
| } | ||||||||
|
|
||||||||
| export default function transform(root: SgRoot<Js>): string | null { | ||||||||
| const rootNode = root.root(); | ||||||||
| const edits: Edit[] = []; | ||||||||
| const linesToRemove: Range[] = []; | ||||||||
|
|
||||||||
| const importNodes = rootNode.findAll({ | ||||||||
|
||||||||
| rule: { | ||||||||
| any: [ | ||||||||
| { kind: "import_specifier", has: { kind: "identifier", regex: "^SecurePair$" } }, | ||||||||
| { kind: "shorthand_property_identifier_pattern", regex: "^SecurePair$" }, | ||||||||
| { kind: "property_identifier", regex: "^SecurePair$" } | ||||||||
| ] | ||||||||
| } | ||||||||
| }); | ||||||||
|
|
||||||||
| for (const node of importNodes) { | ||||||||
menace31 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||
| if (node.text() === "SecurePair") { | ||||||||
| edits.push(node.replace("TLSSocket")); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| const newExpressions = rootNode.findAll({ | ||||||||
| rule: { | ||||||||
| kind: "new_expression", | ||||||||
| has: { | ||||||||
| any: [ | ||||||||
| { kind: "member_expression", has: { field: "property", regex: "^SecurePair$" } }, | ||||||||
| { kind: "identifier", regex: "^SecurePair$" } | ||||||||
| ] | ||||||||
| } | ||||||||
| } | ||||||||
| }); | ||||||||
|
|
||||||||
| for (const node of newExpressions) { | ||||||||
| const callee = node.field("constructor"); | ||||||||
| if (!callee) continue; | ||||||||
|
|
||||||||
| let newConstructorName = "TLSSocket"; | ||||||||
| if (callee.kind() === "member_expression") { | ||||||||
| const object = callee.field("object"); | ||||||||
| if (object) { | ||||||||
| newConstructorName = `${object.text()}.TLSSocket`; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| edits.push(node.replace(`new ${newConstructorName}(socket)`)); | ||||||||
|
|
||||||||
| const declarator = getClosest(node, ["variable_declarator"]); | ||||||||
| if (declarator) { | ||||||||
| const idNode = declarator.field("name"); | ||||||||
| if (idNode) { | ||||||||
| const oldName = idNode.text(); | ||||||||
|
|
||||||||
| let newName = "socket"; | ||||||||
| if (oldName !== "pair" && oldName !== "SecurePair") { | ||||||||
| if (oldName.includes("Pair")) { | ||||||||
| newName = oldName.replace("Pair", "Socket"); | ||||||||
| } | ||||||||
| else if (oldName.includes("pair")) { | ||||||||
| newName = oldName.replace("pair", "socket"); | ||||||||
| } | ||||||||
| else { | ||||||||
| newName = "socket"; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| const obsoleteUsages = rootNode.findAll({ | ||||||||
| rule: { | ||||||||
| kind: "member_expression", | ||||||||
| all: [ | ||||||||
| { has: { field: "object", regex: `^${oldName}$` } }, | ||||||||
| { has: { field: "property", regex: "^(cleartext|encrypted)$" } } | ||||||||
| ] | ||||||||
| } | ||||||||
| }); | ||||||||
|
|
||||||||
| for (const usage of obsoleteUsages) { | ||||||||
| const statement = getClosest(usage, ["lexical_declaration", "expression_statement"]); | ||||||||
| if (statement) { | ||||||||
| linesToRemove.push(statement.range()); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| edits.push(idNode.replace(newName)); | ||||||||
|
|
||||||||
| const references = rootNode.findAll({ | ||||||||
| rule: { | ||||||||
| kind: "identifier", | ||||||||
| regex: `^${oldName}$` | ||||||||
| } | ||||||||
| }); | ||||||||
|
|
||||||||
| for (const ref of references) { | ||||||||
| const parent = ref.parent(); | ||||||||
| if (parent && parent.kind() === 'member_expression') { | ||||||||
| const property = parent.field('property'); | ||||||||
| if (property && property.id() === ref.id()) { | ||||||||
| continue; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (parent && (parent.kind() === 'import_specifier' || parent.kind() === 'shorthand_property_identifier_pattern')) { | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| if (ref.id() === idNode.id()) continue; | ||||||||
|
|
||||||||
| edits.push(ref.replace(newName)); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| let sourceCode = rootNode.commitEdits(edits); | ||||||||
| sourceCode = removeLines(sourceCode, linesToRemove); | ||||||||
| return sourceCode; | ||||||||
menace31 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||
| } | ||||||||
|
||||||||
| } | |
| } | |
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| #!/bin/bash | ||
|
|
||
| GREEN='\033[0;32m' | ||
| RED='\033[0;31m' | ||
| BLUE='\033[0;34m' | ||
| YELLOW='\033[1;33m' | ||
| BOLD='\033[1m' | ||
| NC='\033[0m' | ||
|
|
||
| INPUT_DIR="tests/input" | ||
| EXPECTED_DIR="tests/expected" | ||
| TEMP_DIR="tests/temp_workzone" | ||
|
|
||
| declare -a passed_files | ||
| declare -a failed_files | ||
|
|
||
| echo -e "${BLUE}${BOLD}=== Starting Codemod Test Suite ===${NC}" | ||
|
|
||
| rm -rf "$TEMP_DIR" | ||
| mkdir -p "$TEMP_DIR" | ||
| cp -r "$INPUT_DIR"/* "$TEMP_DIR" | ||
|
|
||
| echo -e "➜ Running codemod on temporary files..." | ||
| npx codemod workflow run -w workflow.yaml -t "$TEMP_DIR" > /dev/null 2>&1 | ||
|
|
||
| echo -e "➜ Verifying results...\n" | ||
|
|
||
| for expected_file in "$EXPECTED_DIR"/*; do | ||
| filename=$(basename "$expected_file") | ||
| generated_file="$TEMP_DIR/$filename" | ||
|
|
||
| if [ ! -f "$generated_file" ]; then | ||
| echo -e "${RED} [MISSING] $filename${NC}" | ||
| failed_files+=("$filename (Missing)") | ||
| continue | ||
| fi | ||
|
|
||
| diff_output=$(diff -u --color=always "$expected_file" "$generated_file") | ||
| exit_code=$? | ||
|
|
||
| if [ $exit_code -eq 0 ]; then | ||
| echo -e " ${GREEN}✔ $filename${NC}" | ||
| passed_files+=("$filename") | ||
| else | ||
| echo -e " ${RED}✘ $filename${NC}" | ||
| echo -e "${YELLOW}--- Differences for $filename ---${NC}" | ||
| echo "$diff_output" | ||
| echo -e "${YELLOW}----------------------------------${NC}\n" | ||
| failed_files+=("$filename") | ||
| fi | ||
| done | ||
|
|
||
| rm -rf "$TEMP_DIR" | ||
|
|
||
| echo -e "\n${BLUE}${BOLD}=== FINAL REPORT ===${NC}" | ||
|
|
||
| if [ ${#passed_files[@]} -gt 0 ]; then | ||
| echo -e "\n${GREEN}${BOLD}Passed Tests (${#passed_files[@]}) :${NC}" | ||
| for f in "${passed_files[@]}"; do | ||
| echo -e " ${GREEN}✔ $f${NC}" | ||
| done | ||
| fi | ||
|
|
||
| if [ ${#failed_files[@]} -gt 0 ]; then | ||
| echo -e "\n${RED}${BOLD}Failed Tests (${#failed_files[@]}) :${NC}" | ||
| for f in "${failed_files[@]}"; do | ||
| echo -e " ${RED}✘ $f${NC}" | ||
| done | ||
| echo -e "\n${RED}➔ Result: FAILURE${NC}" | ||
| exit 1 | ||
| else | ||
| echo -e "\n${GREEN}➔ Result: SUCCESS${NC}" | ||
| exit 0 | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| const tls = require('node:tls'); | ||
| const fs = require('fs'); // Code non lié | ||
menace31 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function createSecureConnection() { | ||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Ces lignes doivent disparaître | ||
|
|
||
| return socket; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| const tls = require('node:tls'); | ||
|
|
||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Direct import | ||
| const { TLSSocket } = require('node:tls'); | ||
| const socket2 = new TLSSocket(socket); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import tls from 'node:tls'; | ||
|
|
||
| // Using tls.SecurePair constructor | ||
| const socket = new tls.TLSSocket(socket); | ||
|
|
||
| // Direct import | ||
| import { TLSSocket } from 'node:tls'; | ||
| const socket2 = new TLSSocket(socket); |
Uh oh!
There was an error while loading. Please reload this page.