From c9d071a1029233474d50790afac02bb530a9e27b Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 29 Mar 2026 09:11:29 -0500 Subject: [PATCH 1/3] feat(process-exit-coercion-to-integer): add DEP0164 exit code coercion codemod --- package-lock.json | 15 + .../codemod.yaml | 23 ++ .../package.json | 24 ++ .../src/workflow.ts | 272 ++++++++++++++++++ .../tests/expected/01-boolean-true-exit.js | 1 + .../tests/expected/02-boolean-false-exit.js | 1 + .../tests/expected/03-floating-exit.js | 1 + .../tests/expected/04-object-exitcode.js | 1 + .../expected/05-string-non-integer-exit.js | 1 + .../expected/06-conditional-boolean-exit.js | 2 + .../tests/expected/07-exitcode-boolean.js | 2 + .../tests/expected/08-valid-integer-string.js | 1 + .../tests/expected/09-valid-values.js | 6 + .../expected/10-arithmetic-expression.js | 1 + .../expected/11-import-require-aliases.js | 6 + .../12-unknown-identifier-no-change.js | 3 + .../expected/13-exitcode-object-float-code.js | 1 + .../tests/input/01-boolean-true-exit.js | 1 + .../tests/input/02-boolean-false-exit.js | 1 + .../tests/input/03-floating-exit.js | 1 + .../tests/input/04-object-exitcode.js | 1 + .../tests/input/05-string-non-integer-exit.js | 1 + .../input/06-conditional-boolean-exit.js | 2 + .../tests/input/07-exitcode-boolean.js | 2 + .../tests/input/08-valid-integer-string.js | 1 + .../tests/input/09-valid-values.js | 6 + .../tests/input/10-arithmetic-expression.js | 1 + .../tests/input/11-import-require-aliases.js | 6 + .../input/12-unknown-identifier-no-change.js | 3 + .../input/13-exitcode-object-float-code.js | 1 + .../workflow.yaml | 25 ++ 31 files changed, 413 insertions(+) create mode 100644 recipes/process-exit-coercion-to-integer/codemod.yaml create mode 100644 recipes/process-exit-coercion-to-integer/package.json create mode 100644 recipes/process-exit-coercion-to-integer/src/workflow.ts create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js create mode 100644 recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js create mode 100644 recipes/process-exit-coercion-to-integer/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 45c99671..84e112e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1539,6 +1539,10 @@ "resolved": "recipes/process-assert-to-node-assert", "link": true }, + "node_modules/@nodejs/process-exit-coercion-to-integer": { + "resolved": "recipes/process-exit-coercion-to-integer", + "link": true + }, "node_modules/@nodejs/process-main-module": { "resolved": "recipes/process-main-module", "link": true @@ -4476,6 +4480,17 @@ "@codemod.com/jssg-types": "^1.5.0" } }, + "recipes/process-exit-coercion-to-integer": { + "name": "@nodejs/process-exit-coercion-to-integer", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + } + }, "recipes/process-main-module": { "name": "@nodejs/process-main-module", "version": "1.0.2", diff --git a/recipes/process-exit-coercion-to-integer/codemod.yaml b/recipes/process-exit-coercion-to-integer/codemod.yaml new file mode 100644 index 00000000..5b4921e2 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/process-exit-coercion-to-integer" +version: "1.0.0" +description: Handle DEP0164 by coercing process.exit(code) and process.exitCode assignments to integer-compatible values. +author: Kevin Sailema +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - dep0164 + - process.exit + - process.exitCode + - migration + +registry: + access: public + visibility: public diff --git a/recipes/process-exit-coercion-to-integer/package.json b/recipes/process-exit-coercion-to-integer/package.json new file mode 100644 index 00000000..687dbe21 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/process-exit-coercion-to-integer", + "version": "1.0.0", + "description": "Handle DEP0164 by coercing process.exit(code) and process.exitCode assignments to integer-compatible values.", + "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/process-exit-coercion-to-integer", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Kevin Sailema", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations", + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} \ No newline at end of file diff --git a/recipes/process-exit-coercion-to-integer/src/workflow.ts b/recipes/process-exit-coercion-to-integer/src/workflow.ts new file mode 100644 index 00000000..77e461de --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/src/workflow.ts @@ -0,0 +1,272 @@ +import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +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'; + +type ExitMode = 'exit' | 'exitCode'; + +type InferredIdentifierKind = + | 'boolean_true' + | 'boolean_false' + | 'integer_number' + | 'float_number' + | 'integer_string' + | 'non_integer_string' + | 'null' + | 'undefined' + | 'object' + | 'unknown'; + +type InferredIdentifier = { + kind: InferredIdentifierKind; + initializerText: string; +}; + +const TRUE_LITERAL = 'true'; +const FALSE_LITERAL = 'false'; + +function isIntegerNumberLiteral(text: string): boolean { + return /^-?\d+$/.test(text); +} + +function isFloatNumberLiteral(text: string): boolean { + return /^-?(?:\d+\.\d+|\d+\.\d*|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(text); +} + +function isQuotedStringLiteral(text: string): boolean { + return /^(['"])(?:[^\\]|\\.)*\1$/.test(text); +} + +function isIntegerStringLiteral(text: string): boolean { + const match = text.match(/^(['"])((?:[^\\]|\\.)*)\1$/); + if (!match) return false; + return /^-?\d+$/.test(match[2]); +} + +function stripOuterParens(text: string): string { + let trimmed = text.trim(); + while (trimmed.startsWith('(') && trimmed.endsWith(')')) { + trimmed = trimmed.slice(1, -1).trim(); + } + return trimmed; +} + +function isBooleanExpression(text: string): boolean { + return /(===|!==|==|!=|>=|<=|>|<|&&|\|\||!)/.test(text); +} + +function isNumericExpressionKind(kind: string): boolean { + return ( + kind === 'binary_expression' || + kind === 'unary_expression' || + kind === 'update_expression' + ); +} + +function extractCodePropertyFromObjectLiteral(text: string): string | null { + const match = text.match(/\bcode\s*:\s*([^,}\n]+)/); + if (!match) return null; + return match[1].trim(); +} + +function inferIdentifierKind(valueNode: SgNode): InferredIdentifierKind { + const kind = valueNode.kind(); + const valueText = stripOuterParens(valueNode.text()); + + if (valueText === TRUE_LITERAL) return 'boolean_true'; + if (valueText === FALSE_LITERAL) return 'boolean_false'; + if (valueText === 'undefined') return 'undefined'; + if (valueText === 'null') return 'null'; + if (isIntegerNumberLiteral(valueText)) return 'integer_number'; + if (isFloatNumberLiteral(valueText)) return 'float_number'; + if (isIntegerStringLiteral(valueText)) return 'integer_string'; + if (isQuotedStringLiteral(valueText)) return 'non_integer_string'; + if (kind === 'object') return 'object'; + + return 'unknown'; +} + +function floorWrap(expressionText: string): string { + return `Math.floor(${expressionText})`; +} + +function coerceBoolean(identifierOrLiteral: string, mode: ExitMode): string { + if (mode === 'exit') return `${identifierOrLiteral} ? 1 : 0`; + return `${identifierOrLiteral} ? 0 : 1`; +} + +function coerceFromObjectLiteral(valueText: string, mode: ExitMode): string { + if (mode !== 'exitCode') return '1'; + + const extracted = extractCodePropertyFromObjectLiteral(valueText); + if (!extracted) return '1'; + + const normalized = stripOuterParens(extracted); + + if ( + normalized === 'undefined' || + normalized === 'null' || + isIntegerNumberLiteral(normalized) || + isIntegerStringLiteral(normalized) + ) { + return extracted; + } + + if (normalized === TRUE_LITERAL) return '0'; + if (normalized === FALSE_LITERAL) return '1'; + if (isFloatNumberLiteral(normalized)) return floorWrap(extracted); + if (isBooleanExpression(normalized)) return coerceBoolean(extracted, mode); + + return '1'; +} + +function coerceValueText( + rawValueText: string, + valueKind: string, + mode: ExitMode, + inferredIdentifiers: Map, +): string | null { + const normalized = stripOuterParens(rawValueText); + + if (normalized === 'undefined' || normalized === 'null') return null; + if (isIntegerNumberLiteral(normalized)) return null; + if (isIntegerStringLiteral(normalized)) return null; + + if (normalized === TRUE_LITERAL || normalized === FALSE_LITERAL) { + if (mode === 'exit') return normalized === TRUE_LITERAL ? '1' : '0'; + return normalized === TRUE_LITERAL ? '0' : '1'; + } + + if (isQuotedStringLiteral(normalized)) return '1'; + + if (valueKind === 'object' || normalized.startsWith('{')) { + return coerceFromObjectLiteral(rawValueText, mode); + } + + if (valueKind === 'identifier') { + const inferred = inferredIdentifiers.get(normalized); + if (!inferred) return null; + + switch (inferred.kind) { + case 'boolean_true': + case 'boolean_false': + return coerceBoolean(normalized, mode); + case 'float_number': + return floorWrap(normalized); + case 'non_integer_string': + return '1'; + case 'object': + return coerceFromObjectLiteral(inferred.initializerText, mode); + default: + return null; + } + } + + if (isFloatNumberLiteral(normalized)) return floorWrap(rawValueText); + + if (isNumericExpressionKind(valueKind) && !isBooleanExpression(normalized)) { + return floorWrap(rawValueText); + } + + if (isBooleanExpression(normalized)) { + return coerceBoolean(rawValueText, mode); + } + + return null; +} + +function collectInferredIdentifiers( + rootNode: SgNode, +): Map { + const inferred = new Map(); + const declarators = rootNode.findAll({ + rule: { + kind: 'variable_declarator', + }, + }); + + for (const declarator of declarators) { + const name = declarator.field('name'); + const value = declarator.field('value'); + if (!name || !value || name.kind() !== 'identifier') continue; + + inferred.set(name.text(), { + kind: inferIdentifierKind(value), + initializerText: value.text(), + }); + } + + return inferred; +} + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + const inferredIdentifiers = collectInferredIdentifiers(rootNode); + + const exitBindings = new Set(['process.exit']); + const exitCodeBindings = new Set(['process.exitCode']); + + const processDependencies = [ + ...getNodeImportStatements(root, 'process'), + ...getNodeRequireCalls(root, 'process'), + ]; + for (const dependency of processDependencies) { + const exitBinding = resolveBindingPath(dependency, '$.exit'); + if (exitBinding) exitBindings.add(exitBinding); + + const exitCodeBinding = resolveBindingPath(dependency, '$.exitCode'); + if (exitCodeBinding) exitCodeBindings.add(exitCodeBinding); + } + + for (const binding of exitBindings) { + const callNodes = rootNode.findAll({ + rule: { + pattern: `${binding}($ARG)`, + }, + }); + + for (const callNode of callNodes) { + const argNode = callNode.getMatch('ARG'); + if (!argNode) continue; + + const replacement = coerceValueText( + argNode.text(), + argNode.kind(), + 'exit', + inferredIdentifiers, + ); + + if (!replacement || replacement === argNode.text()) continue; + edits.push(argNode.replace(replacement)); + } + } + + for (const binding of exitCodeBindings) { + const assignmentNodes = rootNode.findAll({ + rule: { + pattern: `${binding} = $VALUE`, + }, + }); + + for (const assignmentNode of assignmentNodes) { + const valueNode = assignmentNode.getMatch('VALUE'); + if (!valueNode) continue; + + const replacement = coerceValueText( + valueNode.text(), + valueNode.kind(), + 'exitCode', + inferredIdentifiers, + ); + + if (!replacement || replacement === valueNode.text()) continue; + edits.push(valueNode.replace(replacement)); + } + } + + if (!edits.length) return null; + return rootNode.commitEdits(edits); +} diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js b/recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js new file mode 100644 index 00000000..6cee2e1e --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js @@ -0,0 +1 @@ +process.exit(1); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js b/recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js new file mode 100644 index 00000000..dcbbff6c --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js @@ -0,0 +1 @@ +process.exit(0); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js b/recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js new file mode 100644 index 00000000..9936dc7f --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js @@ -0,0 +1 @@ +process.exit(Math.floor(1.5)); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js b/recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js new file mode 100644 index 00000000..8dc808b7 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js @@ -0,0 +1 @@ +process.exitCode = 1; diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js b/recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js new file mode 100644 index 00000000..6cee2e1e --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js @@ -0,0 +1 @@ +process.exit(1); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js b/recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js new file mode 100644 index 00000000..91ae0bd3 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js @@ -0,0 +1,2 @@ +const hasError = true; +process.exit(hasError ? 1 : 0); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js b/recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js new file mode 100644 index 00000000..6c2d3ac4 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js @@ -0,0 +1,2 @@ +const success = false; +process.exitCode = success ? 0 : 1; diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js b/recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js new file mode 100644 index 00000000..21f30660 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js @@ -0,0 +1 @@ +process.exit('1'); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js b/recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js new file mode 100644 index 00000000..84bb8d78 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js @@ -0,0 +1,6 @@ +process.exit(0); +process.exit(1); +process.exit(undefined); +process.exit(null); +process.exitCode = 0; +process.exitCode = undefined; diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js b/recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js new file mode 100644 index 00000000..65333084 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js @@ -0,0 +1 @@ +process.exit(Math.floor(0.5 + 0.7)); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js b/recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js new file mode 100644 index 00000000..849c2b33 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js @@ -0,0 +1,6 @@ +import proc from 'node:process'; +const { exit } = require('node:process'); + +proc.exit(0); +exit(Math.floor(2.3)); +proc.exitCode = 0; diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js b/recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js new file mode 100644 index 00000000..9ddd8c85 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js @@ -0,0 +1,3 @@ +const code = getCode(); +process.exit(code); +process.exitCode = code; diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js b/recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js new file mode 100644 index 00000000..627d191e --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js @@ -0,0 +1 @@ +process.exitCode = Math.floor(1.7); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js b/recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js new file mode 100644 index 00000000..ac911f4e --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js @@ -0,0 +1 @@ +process.exit(true); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js b/recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js new file mode 100644 index 00000000..3f92ecfa --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js @@ -0,0 +1 @@ +process.exit(false); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js b/recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js new file mode 100644 index 00000000..39924638 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js @@ -0,0 +1 @@ +process.exit(1.5); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js b/recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js new file mode 100644 index 00000000..1f4b7864 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js @@ -0,0 +1 @@ +process.exitCode = { code: 1 }; diff --git a/recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js b/recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js new file mode 100644 index 00000000..3b4b6ef8 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js @@ -0,0 +1 @@ +process.exit('error'); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js b/recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js new file mode 100644 index 00000000..a77fa998 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js @@ -0,0 +1,2 @@ +const hasError = true; +process.exit(hasError); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js b/recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js new file mode 100644 index 00000000..0f4aba7a --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js @@ -0,0 +1,2 @@ +const success = false; +process.exitCode = success; diff --git a/recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js b/recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js new file mode 100644 index 00000000..21f30660 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js @@ -0,0 +1 @@ +process.exit('1'); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js b/recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js new file mode 100644 index 00000000..84bb8d78 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js @@ -0,0 +1,6 @@ +process.exit(0); +process.exit(1); +process.exit(undefined); +process.exit(null); +process.exitCode = 0; +process.exitCode = undefined; diff --git a/recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js b/recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js new file mode 100644 index 00000000..b301cfd6 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js @@ -0,0 +1 @@ +process.exit(0.5 + 0.7); diff --git a/recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js b/recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js new file mode 100644 index 00000000..6cbe6398 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js @@ -0,0 +1,6 @@ +import proc from 'node:process'; +const { exit } = require('node:process'); + +proc.exit(false); +exit(2.3); +proc.exitCode = true; diff --git a/recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js b/recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js new file mode 100644 index 00000000..9ddd8c85 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js @@ -0,0 +1,3 @@ +const code = getCode(); +process.exit(code); +process.exitCode = code; diff --git a/recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js b/recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js new file mode 100644 index 00000000..4928596b --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js @@ -0,0 +1 @@ +process.exitCode = { code: 1.7 }; diff --git a/recipes/process-exit-coercion-to-integer/workflow.yaml b/recipes/process-exit-coercion-to-integer/workflow.yaml new file mode 100644 index 00000000..14af4a0c --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/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: Coerce process.exit(code) and process.exitCode to integer-compatible values. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From 11e43bdb00dc547806cb2e8e1375b9e00e71ce55 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 30 Mar 2026 08:29:48 -0500 Subject: [PATCH 2/3] fix(process-exit-coercion-to-integer): address review feedback --- .../README.md | 31 +++ .../package.json | 2 +- .../src/workflow.ts | 235 +++++++++--------- .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 .../expected.js} | 0 .../input.js} | 0 29 files changed, 152 insertions(+), 116 deletions(-) create mode 100644 recipes/process-exit-coercion-to-integer/README.md rename recipes/process-exit-coercion-to-integer/tests/{expected/01-boolean-true-exit.js => 01-boolean-true-exit/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/01-boolean-true-exit.js => 01-boolean-true-exit/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/02-boolean-false-exit.js => 02-boolean-false-exit/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/02-boolean-false-exit.js => 02-boolean-false-exit/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/03-floating-exit.js => 03-floating-exit/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/03-floating-exit.js => 03-floating-exit/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/04-object-exitcode.js => 04-object-exitcode/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/04-object-exitcode.js => 04-object-exitcode/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/05-string-non-integer-exit.js => 05-string-non-integer-exit/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/05-string-non-integer-exit.js => 05-string-non-integer-exit/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/06-conditional-boolean-exit.js => 06-conditional-boolean-exit/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/06-conditional-boolean-exit.js => 06-conditional-boolean-exit/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/07-exitcode-boolean.js => 07-exitcode-boolean/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/07-exitcode-boolean.js => 07-exitcode-boolean/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/08-valid-integer-string.js => 08-valid-integer-string/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/08-valid-integer-string.js => 08-valid-integer-string/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/09-valid-values.js => 09-valid-values/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/09-valid-values.js => 09-valid-values/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/10-arithmetic-expression.js => 10-arithmetic-expression/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/10-arithmetic-expression.js => 10-arithmetic-expression/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/11-import-require-aliases.js => 11-import-require-aliases/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/11-import-require-aliases.js => 11-import-require-aliases/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/12-unknown-identifier-no-change.js => 12-unknown-identifier-no-change/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/12-unknown-identifier-no-change.js => 12-unknown-identifier-no-change/input.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{expected/13-exitcode-object-float-code.js => 13-exitcode-object-float-code/expected.js} (100%) rename recipes/process-exit-coercion-to-integer/tests/{input/13-exitcode-object-float-code.js => 13-exitcode-object-float-code/input.js} (100%) diff --git a/recipes/process-exit-coercion-to-integer/README.md b/recipes/process-exit-coercion-to-integer/README.md new file mode 100644 index 00000000..cfe34388 --- /dev/null +++ b/recipes/process-exit-coercion-to-integer/README.md @@ -0,0 +1,31 @@ +# `process.exit(code)` / `process.exitCode` DEP0164 + +This recipe migrates non-integer values passed to `process.exit(code)` and assigned to `process.exitCode`. + +See [DEP0164](https://nodejs.org/api/deprecations.html#DEP0164). + +## What it changes + +- Preserves valid values: + - integer numbers + - integer strings + - `undefined` + - `null` +- Converts boolean values to explicit numeric exit codes. +- Wraps floating-point numeric expressions with `Math.floor(...)`. +- Converts non-integer string literals to `1`. +- For `process.exitCode = { code: ... }`, extracts `code` when possible and coerces when needed. + +## Example + +```diff +- process.exit(0.5 + 0.7) ++ process.exit(Math.floor(0.5 + 0.7)) +``` + +```diff +- const success = false; +- process.exitCode = success; ++ const success = false; ++ process.exitCode = success ? 0 : 1; +``` diff --git a/recipes/process-exit-coercion-to-integer/package.json b/recipes/process-exit-coercion-to-integer/package.json index 687dbe21..8c67882e 100644 --- a/recipes/process-exit-coercion-to-integer/package.json +++ b/recipes/process-exit-coercion-to-integer/package.json @@ -4,7 +4,7 @@ "description": "Handle DEP0164 by coercing process.exit(code) and process.exitCode assignments to integer-compatible values.", "type": "module", "scripts": { - "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests" }, "repository": { "type": "git", diff --git a/recipes/process-exit-coercion-to-integer/src/workflow.ts b/recipes/process-exit-coercion-to-integer/src/workflow.ts index 77e461de..4c733251 100644 --- a/recipes/process-exit-coercion-to-integer/src/workflow.ts +++ b/recipes/process-exit-coercion-to-integer/src/workflow.ts @@ -20,68 +20,53 @@ type InferredIdentifierKind = type InferredIdentifier = { kind: InferredIdentifierKind; - initializerText: string; + initializerNode: SgNode; }; -const TRUE_LITERAL = 'true'; -const FALSE_LITERAL = 'false'; - -function isIntegerNumberLiteral(text: string): boolean { - return /^-?\d+$/.test(text); +function isIntegerNumber(text: string): boolean { + const numeric = Number(text); + return Number.isFinite(numeric) && Number.isInteger(numeric); } -function isFloatNumberLiteral(text: string): boolean { - return /^-?(?:\d+\.\d+|\d+\.\d*|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(text); +function isIntegerStringValue(value: string): boolean { + return /^-?\d+$/.test(value); } -function isQuotedStringLiteral(text: string): boolean { - return /^(['"])(?:[^\\]|\\.)*\1$/.test(text); +function getStringLiteralValue(node: SgNode): string | null { + if (node.kind() !== 'string') return null; + const text = node.text(); + if (text.length < 2) return null; + const quote = text[0]; + if ((quote !== '"' && quote !== "'") || text[text.length - 1] !== quote) { + return null; + } + return text.slice(1, -1); } -function isIntegerStringLiteral(text: string): boolean { - const match = text.match(/^(['"])((?:[^\\]|\\.)*)\1$/); - if (!match) return false; - return /^-?\d+$/.test(match[2]); -} +function inferIdentifierKind(valueNode: SgNode): InferredIdentifierKind { + const kind = valueNode.kind(); + if (kind === 'true') return 'boolean_true'; + if (kind === 'false') return 'boolean_false'; + if (kind === 'null') return 'null'; -function stripOuterParens(text: string): string { - let trimmed = text.trim(); - while (trimmed.startsWith('(') && trimmed.endsWith(')')) { - trimmed = trimmed.slice(1, -1).trim(); + if (kind === 'identifier' && valueNode.text() === 'undefined') { + return 'undefined'; } - return trimmed; -} - -function isBooleanExpression(text: string): boolean { - return /(===|!==|==|!=|>=|<=|>|<|&&|\|\||!)/.test(text); -} -function isNumericExpressionKind(kind: string): boolean { - return ( - kind === 'binary_expression' || - kind === 'unary_expression' || - kind === 'update_expression' - ); -} + if (kind === 'number') { + return isIntegerNumber(valueNode.text()) + ? 'integer_number' + : 'float_number'; + } -function extractCodePropertyFromObjectLiteral(text: string): string | null { - const match = text.match(/\bcode\s*:\s*([^,}\n]+)/); - if (!match) return null; - return match[1].trim(); -} + if (kind === 'string') { + const value = getStringLiteralValue(valueNode); + if (value === null) return 'unknown'; + return isIntegerStringValue(value) + ? 'integer_string' + : 'non_integer_string'; + } -function inferIdentifierKind(valueNode: SgNode): InferredIdentifierKind { - const kind = valueNode.kind(); - const valueText = stripOuterParens(valueNode.text()); - - if (valueText === TRUE_LITERAL) return 'boolean_true'; - if (valueText === FALSE_LITERAL) return 'boolean_false'; - if (valueText === 'undefined') return 'undefined'; - if (valueText === 'null') return 'null'; - if (isIntegerNumberLiteral(valueText)) return 'integer_number'; - if (isFloatNumberLiteral(valueText)) return 'float_number'; - if (isIntegerStringLiteral(valueText)) return 'integer_string'; - if (isQuotedStringLiteral(valueText)) return 'non_integer_string'; if (kind === 'object') return 'object'; return 'unknown'; @@ -91,86 +76,112 @@ function floorWrap(expressionText: string): string { return `Math.floor(${expressionText})`; } -function coerceBoolean(identifierOrLiteral: string, mode: ExitMode): string { - if (mode === 'exit') return `${identifierOrLiteral} ? 1 : 0`; - return `${identifierOrLiteral} ? 0 : 1`; +function coerceBoolean(expressionText: string, mode: ExitMode): string { + if (mode === 'exit') return `${expressionText} ? 1 : 0`; + return `${expressionText} ? 0 : 1`; } -function coerceFromObjectLiteral(valueText: string, mode: ExitMode): string { - if (mode !== 'exitCode') return '1'; +function getObjectCodeValue(objectNode: SgNode): SgNode | null { + const pairs = objectNode.findAll({ + rule: { + kind: 'pair', + }, + }); - const extracted = extractCodePropertyFromObjectLiteral(valueText); - if (!extracted) return '1'; + for (const pair of pairs) { + const key = pair.field('key'); + const value = pair.field('value'); + if (!key || !value) continue; + if (key.text() === 'code') return value; + } - const normalized = stripOuterParens(extracted); + return null; +} - if ( - normalized === 'undefined' || - normalized === 'null' || - isIntegerNumberLiteral(normalized) || - isIntegerStringLiteral(normalized) - ) { - return extracted; - } +function coerceFromObjectLiteral( + objectNode: SgNode, + mode: ExitMode, +): string { + if (mode !== 'exitCode') return '1'; - if (normalized === TRUE_LITERAL) return '0'; - if (normalized === FALSE_LITERAL) return '1'; - if (isFloatNumberLiteral(normalized)) return floorWrap(extracted); - if (isBooleanExpression(normalized)) return coerceBoolean(extracted, mode); + const codeValue = getObjectCodeValue(objectNode); + if (!codeValue) return '1'; + + const kind = inferIdentifierKind(codeValue); + if (kind === 'integer_number' || kind === 'integer_string') { + return codeValue.text(); + } + if (kind === 'null' || kind === 'undefined') { + return codeValue.text(); + } + if (kind === 'boolean_true' || kind === 'boolean_false') { + return coerceBoolean(codeValue.text(), mode); + } + if (kind === 'float_number') { + return floorWrap(codeValue.text()); + } return '1'; } -function coerceValueText( - rawValueText: string, - valueKind: string, +function shouldFloorExpression(node: SgNode): boolean { + const kind = node.kind(); + return ( + kind === 'binary_expression' || + kind === 'unary_expression' || + kind === 'update_expression' + ); +} + +function coerceValueNode( + node: SgNode, mode: ExitMode, inferredIdentifiers: Map, ): string | null { - const normalized = stripOuterParens(rawValueText); + const kind = inferIdentifierKind(node); - if (normalized === 'undefined' || normalized === 'null') return null; - if (isIntegerNumberLiteral(normalized)) return null; - if (isIntegerStringLiteral(normalized)) return null; + if ( + kind === 'undefined' || + kind === 'null' || + kind === 'integer_number' || + kind === 'integer_string' + ) { + return null; + } - if (normalized === TRUE_LITERAL || normalized === FALSE_LITERAL) { - if (mode === 'exit') return normalized === TRUE_LITERAL ? '1' : '0'; - return normalized === TRUE_LITERAL ? '0' : '1'; + if (kind === 'boolean_true' || kind === 'boolean_false') { + return mode === 'exit' + ? kind === 'boolean_true' + ? '1' + : '0' + : kind === 'boolean_true' + ? '0' + : '1'; } - if (isQuotedStringLiteral(normalized)) return '1'; + if (kind === 'non_integer_string') return '1'; - if (valueKind === 'object' || normalized.startsWith('{')) { - return coerceFromObjectLiteral(rawValueText, mode); - } + if (kind === 'float_number') return floorWrap(node.text()); + + if (kind === 'object') return coerceFromObjectLiteral(node, mode); - if (valueKind === 'identifier') { - const inferred = inferredIdentifiers.get(normalized); + if (node.kind() === 'identifier') { + const inferred = inferredIdentifiers.get(node.text()); if (!inferred) return null; - switch (inferred.kind) { - case 'boolean_true': - case 'boolean_false': - return coerceBoolean(normalized, mode); - case 'float_number': - return floorWrap(normalized); - case 'non_integer_string': - return '1'; - case 'object': - return coerceFromObjectLiteral(inferred.initializerText, mode); - default: - return null; + if (inferred.kind === 'boolean_true' || inferred.kind === 'boolean_false') { + return coerceBoolean(node.text(), mode); } + if (inferred.kind === 'float_number') return floorWrap(node.text()); + if (inferred.kind === 'non_integer_string') return '1'; + if (inferred.kind === 'object') { + return coerceFromObjectLiteral(inferred.initializerNode, mode); + } + return null; } - if (isFloatNumberLiteral(normalized)) return floorWrap(rawValueText); - - if (isNumericExpressionKind(valueKind) && !isBooleanExpression(normalized)) { - return floorWrap(rawValueText); - } - - if (isBooleanExpression(normalized)) { - return coerceBoolean(rawValueText, mode); + if (shouldFloorExpression(node)) { + return floorWrap(node.text()); } return null; @@ -193,7 +204,7 @@ function collectInferredIdentifiers( inferred.set(name.text(), { kind: inferIdentifierKind(value), - initializerText: value.text(), + initializerNode: value, }); } @@ -232,12 +243,7 @@ export default function transform(root: SgRoot): string | null { const argNode = callNode.getMatch('ARG'); if (!argNode) continue; - const replacement = coerceValueText( - argNode.text(), - argNode.kind(), - 'exit', - inferredIdentifiers, - ); + const replacement = coerceValueNode(argNode, 'exit', inferredIdentifiers); if (!replacement || replacement === argNode.text()) continue; edits.push(argNode.replace(replacement)); @@ -255,9 +261,8 @@ export default function transform(root: SgRoot): string | null { const valueNode = assignmentNode.getMatch('VALUE'); if (!valueNode) continue; - const replacement = coerceValueText( - valueNode.text(), - valueNode.kind(), + const replacement = coerceValueNode( + valueNode, 'exitCode', inferredIdentifiers, ); diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js b/recipes/process-exit-coercion-to-integer/tests/01-boolean-true-exit/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/01-boolean-true-exit.js rename to recipes/process-exit-coercion-to-integer/tests/01-boolean-true-exit/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js b/recipes/process-exit-coercion-to-integer/tests/01-boolean-true-exit/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/01-boolean-true-exit.js rename to recipes/process-exit-coercion-to-integer/tests/01-boolean-true-exit/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js b/recipes/process-exit-coercion-to-integer/tests/02-boolean-false-exit/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/02-boolean-false-exit.js rename to recipes/process-exit-coercion-to-integer/tests/02-boolean-false-exit/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js b/recipes/process-exit-coercion-to-integer/tests/02-boolean-false-exit/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/02-boolean-false-exit.js rename to recipes/process-exit-coercion-to-integer/tests/02-boolean-false-exit/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js b/recipes/process-exit-coercion-to-integer/tests/03-floating-exit/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/03-floating-exit.js rename to recipes/process-exit-coercion-to-integer/tests/03-floating-exit/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js b/recipes/process-exit-coercion-to-integer/tests/03-floating-exit/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/03-floating-exit.js rename to recipes/process-exit-coercion-to-integer/tests/03-floating-exit/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js b/recipes/process-exit-coercion-to-integer/tests/04-object-exitcode/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/04-object-exitcode.js rename to recipes/process-exit-coercion-to-integer/tests/04-object-exitcode/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js b/recipes/process-exit-coercion-to-integer/tests/04-object-exitcode/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/04-object-exitcode.js rename to recipes/process-exit-coercion-to-integer/tests/04-object-exitcode/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js b/recipes/process-exit-coercion-to-integer/tests/05-string-non-integer-exit/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/05-string-non-integer-exit.js rename to recipes/process-exit-coercion-to-integer/tests/05-string-non-integer-exit/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js b/recipes/process-exit-coercion-to-integer/tests/05-string-non-integer-exit/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/05-string-non-integer-exit.js rename to recipes/process-exit-coercion-to-integer/tests/05-string-non-integer-exit/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js b/recipes/process-exit-coercion-to-integer/tests/06-conditional-boolean-exit/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/06-conditional-boolean-exit.js rename to recipes/process-exit-coercion-to-integer/tests/06-conditional-boolean-exit/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js b/recipes/process-exit-coercion-to-integer/tests/06-conditional-boolean-exit/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/06-conditional-boolean-exit.js rename to recipes/process-exit-coercion-to-integer/tests/06-conditional-boolean-exit/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js b/recipes/process-exit-coercion-to-integer/tests/07-exitcode-boolean/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/07-exitcode-boolean.js rename to recipes/process-exit-coercion-to-integer/tests/07-exitcode-boolean/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js b/recipes/process-exit-coercion-to-integer/tests/07-exitcode-boolean/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/07-exitcode-boolean.js rename to recipes/process-exit-coercion-to-integer/tests/07-exitcode-boolean/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js b/recipes/process-exit-coercion-to-integer/tests/08-valid-integer-string/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/08-valid-integer-string.js rename to recipes/process-exit-coercion-to-integer/tests/08-valid-integer-string/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js b/recipes/process-exit-coercion-to-integer/tests/08-valid-integer-string/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/08-valid-integer-string.js rename to recipes/process-exit-coercion-to-integer/tests/08-valid-integer-string/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js b/recipes/process-exit-coercion-to-integer/tests/09-valid-values/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/09-valid-values.js rename to recipes/process-exit-coercion-to-integer/tests/09-valid-values/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js b/recipes/process-exit-coercion-to-integer/tests/09-valid-values/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/09-valid-values.js rename to recipes/process-exit-coercion-to-integer/tests/09-valid-values/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js b/recipes/process-exit-coercion-to-integer/tests/10-arithmetic-expression/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/10-arithmetic-expression.js rename to recipes/process-exit-coercion-to-integer/tests/10-arithmetic-expression/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js b/recipes/process-exit-coercion-to-integer/tests/10-arithmetic-expression/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/10-arithmetic-expression.js rename to recipes/process-exit-coercion-to-integer/tests/10-arithmetic-expression/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js b/recipes/process-exit-coercion-to-integer/tests/11-import-require-aliases/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/11-import-require-aliases.js rename to recipes/process-exit-coercion-to-integer/tests/11-import-require-aliases/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js b/recipes/process-exit-coercion-to-integer/tests/11-import-require-aliases/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/11-import-require-aliases.js rename to recipes/process-exit-coercion-to-integer/tests/11-import-require-aliases/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js b/recipes/process-exit-coercion-to-integer/tests/12-unknown-identifier-no-change/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/12-unknown-identifier-no-change.js rename to recipes/process-exit-coercion-to-integer/tests/12-unknown-identifier-no-change/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js b/recipes/process-exit-coercion-to-integer/tests/12-unknown-identifier-no-change/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/12-unknown-identifier-no-change.js rename to recipes/process-exit-coercion-to-integer/tests/12-unknown-identifier-no-change/input.js diff --git a/recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js b/recipes/process-exit-coercion-to-integer/tests/13-exitcode-object-float-code/expected.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/expected/13-exitcode-object-float-code.js rename to recipes/process-exit-coercion-to-integer/tests/13-exitcode-object-float-code/expected.js diff --git a/recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js b/recipes/process-exit-coercion-to-integer/tests/13-exitcode-object-float-code/input.js similarity index 100% rename from recipes/process-exit-coercion-to-integer/tests/input/13-exitcode-object-float-code.js rename to recipes/process-exit-coercion-to-integer/tests/13-exitcode-object-float-code/input.js From d61d4c9c05129cd736145b4b41e9ad2bd4434de8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 Apr 2026 13:55:26 -0500 Subject: [PATCH 3/3] style(process-exit-coercion-to-integer): apply final review suggestions --- .../src/workflow.ts | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/recipes/process-exit-coercion-to-integer/src/workflow.ts b/recipes/process-exit-coercion-to-integer/src/workflow.ts index 4c733251..b7b5c0f7 100644 --- a/recipes/process-exit-coercion-to-integer/src/workflow.ts +++ b/recipes/process-exit-coercion-to-integer/src/workflow.ts @@ -1,5 +1,4 @@ -import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement'; -import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +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'; @@ -34,32 +33,31 @@ function isIntegerStringValue(value: string): boolean { function getStringLiteralValue(node: SgNode): string | null { if (node.kind() !== 'string') return null; - const text = node.text(); - if (text.length < 2) return null; - const quote = text[0]; - if ((quote !== '"' && quote !== "'") || text[text.length - 1] !== quote) { - return null; - } - return text.slice(1, -1); + const stringFragment = node.find({ + rule: { + kind: 'string_fragment', + }, + }); + return stringFragment?.text() ?? ''; } function inferIdentifierKind(valueNode: SgNode): InferredIdentifierKind { - const kind = valueNode.kind(); - if (kind === 'true') return 'boolean_true'; - if (kind === 'false') return 'boolean_false'; - if (kind === 'null') return 'null'; + const nodeKind = valueNode.kind(); + if (nodeKind === 'true') return 'boolean_true'; + if (nodeKind === 'false') return 'boolean_false'; + if (nodeKind === 'null') return 'null'; - if (kind === 'identifier' && valueNode.text() === 'undefined') { + if (nodeKind === 'identifier' && valueNode.text() === 'undefined') { return 'undefined'; } - if (kind === 'number') { + if (nodeKind === 'number') { return isIntegerNumber(valueNode.text()) ? 'integer_number' : 'float_number'; } - if (kind === 'string') { + if (nodeKind === 'string') { const value = getStringLiteralValue(valueNode); if (value === null) return 'unknown'; return isIntegerStringValue(value) @@ -67,7 +65,7 @@ function inferIdentifierKind(valueNode: SgNode): InferredIdentifierKind { : 'non_integer_string'; } - if (kind === 'object') return 'object'; + if (nodeKind === 'object') return 'object'; return 'unknown'; } @@ -125,11 +123,11 @@ function coerceFromObjectLiteral( } function shouldFloorExpression(node: SgNode): boolean { - const kind = node.kind(); + const nodeKind = node.kind(); return ( - kind === 'binary_expression' || - kind === 'unary_expression' || - kind === 'update_expression' + nodeKind === 'binary_expression' || + nodeKind === 'unary_expression' || + nodeKind === 'update_expression' ); } @@ -138,32 +136,32 @@ function coerceValueNode( mode: ExitMode, inferredIdentifiers: Map, ): string | null { - const kind = inferIdentifierKind(node); + const inferredKind = inferIdentifierKind(node); if ( - kind === 'undefined' || - kind === 'null' || - kind === 'integer_number' || - kind === 'integer_string' + inferredKind === 'undefined' || + inferredKind === 'null' || + inferredKind === 'integer_number' || + inferredKind === 'integer_string' ) { return null; } - if (kind === 'boolean_true' || kind === 'boolean_false') { + if (inferredKind === 'boolean_true' || inferredKind === 'boolean_false') { return mode === 'exit' - ? kind === 'boolean_true' + ? inferredKind === 'boolean_true' ? '1' : '0' - : kind === 'boolean_true' + : inferredKind === 'boolean_true' ? '0' : '1'; } - if (kind === 'non_integer_string') return '1'; + if (inferredKind === 'non_integer_string') return '1'; - if (kind === 'float_number') return floorWrap(node.text()); + if (inferredKind === 'float_number') return floorWrap(node.text()); - if (kind === 'object') return coerceFromObjectLiteral(node, mode); + if (inferredKind === 'object') return coerceFromObjectLiteral(node, mode); if (node.kind() === 'identifier') { const inferred = inferredIdentifiers.get(node.text()); @@ -220,10 +218,8 @@ export default function transform(root: SgRoot): string | null { const exitBindings = new Set(['process.exit']); const exitCodeBindings = new Set(['process.exitCode']); - const processDependencies = [ - ...getNodeImportStatements(root, 'process'), - ...getNodeRequireCalls(root, 'process'), - ]; + const processDependencies = getModuleDependencies(root, 'process'); + for (const dependency of processDependencies) { const exitBinding = resolveBindingPath(dependency, '$.exit'); if (exitBinding) exitBindings.add(exitBinding); @@ -273,5 +269,6 @@ export default function transform(root: SgRoot): string | null { } if (!edits.length) return null; + return rootNode.commitEdits(edits); }