Skip to content

feat(DEP0164): handle non-integer codes passed to process.exit & process.exitCode#413

Open
KevinSailema wants to merge 3 commits intonodejs:mainfrom
KevinSailema:feat/process-exit-coercion-to-integer-405
Open

feat(DEP0164): handle non-integer codes passed to process.exit & process.exitCode#413
KevinSailema wants to merge 3 commits intonodejs:mainfrom
KevinSailema:feat/process-exit-coercion-to-integer-405

Conversation

@KevinSailema
Copy link
Copy Markdown

Summary

This PR adds a new codemod recipe to handle DEP0164 by coercing unsupported values passed to process.exit(code) and assigned to process.exitCode into integer-compatible values.

Closes #405.

What this adds

  • New recipe: @nodejs/process-exit-coercion-to-integer
  • New transform to migrate:
    • process.exit(value)
    • process.exitCode = value
  • Full recipe wiring:
    • codemod manifest
    • workflow definition
    • package configuration
    • input/expected fixture tests

Transformation behavior

The codemod applies explicit and safe coercions aligned with the issue requirements:

  • Preserves valid values (no change):
    • undefined
    • null
    • integer numbers
    • integer strings (example: "1")
  • Converts boolean values:
    • process.exit(true) -> process.exit(1)
    • process.exit(false) -> process.exit(0)
    • process.exitCode = success -> process.exitCode = success ? 0 : 1
  • Converts floating-point and numeric expressions explicitly:
    • process.exit(1.5) -> process.exit(Math.floor(1.5))
    • process.exit(0.5 + 0.7) -> process.exit(Math.floor(0.5 + 0.7))
  • Converts non-integer string literals in process.exit to 1
  • Handles object literal assignment for process.exitCode with code extraction when present:
    • process.exitCode = { code: 1 } -> process.exitCode = 1

Safety and scope

  • Conservative by design: ambiguous identifier values are left unchanged unless safely inferable from local literal initialization.
  • No dynamic evaluation and no runtime behavior injection.
  • No unrelated refactors or formatting-only changes.

Tests

Added fixture coverage for the 10 scenarios from the issue plus additional edge cases:

  • import/require aliases for process
  • unknown identifier no-op behavior
  • object code field with float coercion

Validation run:

  • Recipe tests passed
  • Repository pre-commit checks passed (lint, type-check, workspace tests)

@JakobJingleheimer JakobJingleheimer changed the title feat(process-exit-coercion-to-integer): add DEP0164 codemod for process.exit(code) and process.exitCode feat(DEP0164): handle non-integer codes passed to process.exit & process.exitCode Mar 29, 2026
@JakobJingleheimer
Copy link
Copy Markdown
Member

Given the mere moments between opening this PR and your #405 (comment), it's fairly clear this was fully AI-generated. Could you please confirm what you used to generate it, how much of it, and which specific pieces you reviewed of the generated code.

@KevinSailema
Copy link
Copy Markdown
Author

Thanks for calling this out, yes, I used AI assistance (GitHub Copilot Chat in VS Code) to speed up drafting.

What was AI-assisted:
initial recipe scaffolding (package/workflow/codemod manifests)
first draft of transform structure
initial test fixture generation

What I manually reviewed and adjusted:
all transform logic in src/workflow.ts, including binding resolution and replacement safety
each input/expected fixture pair in tests
edge-case behavior (no-op vs transform) to avoid unsafe rewrites
final command validation (workspace test + pre-commit)

You are right that I should have disclosed this in the PR description from the start.

Copy link
Copy Markdown
Member

@AugustinMauroy AugustinMauroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also missing readme.

@AugustinMauroy
Copy link
Copy Markdown
Member

aslo for you info you have to use this structure of testing

Codemod leverages a before ("input") + after ("expected") snapshot comparison. Codemod supports 2 options:

  • 👍 Single-file fixtures
    tests/
      some-test-case-description/
        input.ts
        expected.ts
      another-test-case-description/
        input.ts
        expected.ts
    
  • 👎 Directory snapshot fixtures
    tests/
      input/
        some-test-case-description.ts
        another-test-case-description.ts
      expected
        some-test-case-description.ts
        another-test-case-description.ts
    

Use the Single-file fixtures option.

@KevinSailema
Copy link
Copy Markdown
Author

  1. Added missing README for the recipe.
  2. Refactored workflow.ts to rely more on AST node kinds/fields and reduce string-based manipulation.
  3. Migrated tests to the single-file fixture structure (tests/<case>/input.js + expected.js) as requested.
  4. Updated the recipe test script to run against ./tests.

Copy link
Copy Markdown
Member

@AugustinMauroy AugustinMauroy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better but still something to improve

return /^-?\d+$/.test(value);
}

function getStringLiteralValue(node: SgNode<JS>): string | null {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind "string_fragment" exist so when you have node you can find() with rule that catch kind string_fragment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d61d4c9


if (kind === 'object') return coerceFromObjectLiteral(node, mode);

if (node.kind() === 'identifier') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here var naming is complex it's difficult to make diff between node.kind and kind from inferIdentifierKind

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d61d4c9

const processDependencies = [
...getNodeImportStatements(root, 'process'),
...getNodeRequireCalls(root, 'process'),
];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
];
];

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d61d4c9

const exitBindings = new Set<string>(['process.exit']);
const exitCodeBindings = new Set<string>(['process.exitCode']);

const processDependencies = [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have an utility for that read utils/readme to know which one you should use

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d61d4c9

}

if (!edits.length) return null;
return rootNode.commitEdits(edits);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return rootNode.commitEdits(edits);
return rootNode.commitEdits(edits);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d61d4c9

@AugustinMauroy AugustinMauroy added the awaiting author Reviewer has requested something from the author label Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author Reviewer has requested something from the author ⚠️ fully-AI-generated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

process.exit(code) and process.exitCode coercion to integer

3 participants