Skip to content

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

Open
KevinSailema wants to merge 4 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 4 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.

Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts
@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

Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts
Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts
Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts Outdated
Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts Outdated
Comment thread recipes/process-exit-coercion-to-integer/src/workflow.ts
@AugustinMauroy AugustinMauroy added the awaiting author Reviewer has requested something from the author label Mar 31, 2026
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.

LGMT !

@AugustinMauroy AugustinMauroy added awaiting reviewer Author has responded and needs action from the reviewer and removed awaiting author Reviewer has requested something from the author labels Apr 2, 2026
function coerceBoolean(expressionText: string, mode: ExitMode): string {
if (mode === 'exit') return `${expressionText} ? 1 : 0`;
return `${expressionText} ? 0 : 1`;
}
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.

I think we can just coerce booleans with Numberto be more simple and consistent.

- process.exit(true)
+ process.exit(Number(true))

): string {
if (mode !== 'exitCode') return '1';

const codeValue = getObjectCodeValue(objectNode);
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.

inline it

Suggested change
const codeValue = getObjectCodeValue(objectNode);
const pair = objectNode.find({
rule: {
kind: 'pair',
has: {
kind: 'property_identifier',
field: 'key',
regex: 'code',
},
},
});
const valueValue = pair?.field('value') || null;

Comment on lines +82 to +97
function getObjectCodeValue(objectNode: SgNode<JS>): SgNode<JS> | null {
const pairs = objectNode.findAll({
rule: {
kind: 'pair',
},
});

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;
}

return 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.

it can be simplified, and doing that I think just make it inline is better, so replace the functions/implementation by the suggestion on the next comment

Suggested change
function getObjectCodeValue(objectNode: SgNode<JS>): SgNode<JS> | null {
const pairs = objectNode.findAll({
rule: {
kind: 'pair',
},
});
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;
}
return null;
}

Comment on lines +150 to +158
if (inferredKind === 'boolean_true' || inferredKind === 'boolean_false') {
return mode === 'exit'
? inferredKind === 'boolean_true'
? '1'
: '0'
: inferredKind === 'boolean_true'
? '0'
: '1';
}
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.

why inferredKind need to be compared with same value so many times? this code is quite confusing to me, could you explain why it is necessary or maybe refactor it a bit?

Comment on lines +192 to +196
const declarators = rootNode.findAll({
rule: {
kind: 'variable_declarator',
},
});
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.

this will look for all variable_declarators in the source code I think it not really needed.

https://docs.codemod.com/jssg/semantic-analysis#node-definition-options

LSP.goToDefinition is supported by jssg try use it instead


proc.exit(0);
exit(Math.floor(2.3));
proc.exitCode = 0;
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.

Why process.exitCode = true becomes process.exitCode = 0? It not need to be 1 instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting reviewer Author has responded and needs action from the reviewer ⚠️ 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

4 participants