Assign process.emitWarning

pi's CLI setup replaces process.emitWarning to silence warnings. The
assignment now stores the replacement: later process.emitWarning(...)
calls apply it to their arguments, and runtime-built warnings (Node's own
modules call process.emitWarning) reach it as (message, type[, code])
instead of the report and the 'warning' listeners. process.title writes
already lowered; the corpus program pins both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 15:15:03 -07:00
co-authored by Claude Opus 5.5
parent 1c1400272f
commit 5e964965f6
8 changed files with 134 additions and 0 deletions
@@ -1306,6 +1306,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"process.emitWarningText": "scr_process_emit_warning_text",
"process.deprecationFlag": "scr_process_deprecation_flag",
"process.setDeprecationFlag": "scr_process_set_deprecation_flag",
"process.setEmitWarning": "scr_process_set_emit_warning",
"process.onUncaughtException": "scr_process_on_uncaught_exception",
"process.offUncaughtException": "scr_process_off_uncaught_exception",
"process.onUnhandledRejection": "scr_process_on_unhandled_rejection",
@@ -6758,6 +6758,32 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
loc,
};
}
// process.emitWarning = fn: an ordinary writable property. Later
// process.emitWarning(...) calls, and the warnings Node's own
// modules emit through process.emitWarning, call the replacement
// instead (silencing them when it does nothing).
if (flag === "emitWarning") {
const loc = locOf(expr);
let rhs: ts.Expression = expr.right;
while (
ts.isParenthesizedExpression(rhs) ||
ts.isAsExpression(rhs) ||
ts.isSatisfiesExpression(rhs)
)
rhs = rhs.expression;
const value = lowerer.lowerExprExpecting(rhs, DYN);
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "process.setEmitWarning",
args: [value],
type: VOID,
loc,
},
loc,
};
}
// process.title = name: libuv's uv_set_process_title (the
// runtime truncates to the original argument area on POSIX).
if (flag === "title") {
@@ -4112,6 +4112,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* warnings and DeprecationWarning emissions read their truthiness. */
"process.deprecationFlag": { argTypes: [F64], result: DYN },
"process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID },
"process.setEmitWarning": { argTypes: [DYN], result: VOID },
/** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64
* handles; contexts are immutable fiber-carried snapshots (spawned
* fibers inherit the spawner's — Node's init-time capture). run/exit
@@ -67,6 +67,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.emitWarningText",
"process.deprecationFlag",
"process.setDeprecationFlag",
"process.setEmitWarning",
// The performance timeline (scr_perf.c, linked with this unit for its
// MaxPerformanceEntryBufferExceededWarning).
"perf.mark",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6231-process-emit-warning-assign.ts"
],
"diags": []
}
+6
View File
@@ -212,6 +212,12 @@ void scr_process_on_warning(ScrDyn *fn);
void scr_process_off_warning(ScrDyn *fn);
void scr_process_emit_warning(ScrDyn *args);
void scr_emit_warning(const char *name, const char *code, ScrStr *message);
/* process.emitWarning = fn (borrowed; the runtime keeps its own
* reference). Later process.emitWarning(...) calls apply the replacement
* to their arguments, and runtime-built warnings (Node's own modules call
* process.emitWarning) reach it as (message, type[, code]) instead of the
* report and the 'warning' listeners. */
void scr_process_set_emit_warning(ScrDyn *fn);
/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the
* listener registration): units that cannot link the dyn-async unit route
* their warnings through it, and print the default report otherwise. */
+58
View File
@@ -860,10 +860,53 @@ ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code) {
return w;
}
/* The program's process.emitWarning replacement, or NULL while the
* property holds Node's own function. */
static SCR_TL ScrDyn *scr_emit_warning_override = NULL;
static void scr_emit_warning_override_teardown(void) {
scr_dyn_release(scr_emit_warning_override);
scr_emit_warning_override = NULL;
}
void scr_process_set_emit_warning(ScrDyn *fn) {
if (scr_emit_warning_override == NULL) scr_atexit(scr_emit_warning_override_teardown);
ScrDyn *previous = scr_emit_warning_override;
scr_emit_warning_override = scr_dyn_retain(fn);
scr_dyn_release(previous);
/* Units that report their own warnings without a listener route them
* here once a replacement exists. */
scr_warning_emit_hook = scr_emit_warning;
}
/* A runtime-built warning while a replacement is installed: Node's modules
* call process.emitWarning(message, type, code), so the replacement sees
* those arguments; nothing is reported. A throw stays pending. */
static void scr_emit_warning_replaced(ScrStr *message, ScrStr *type, ScrStr *code) {
ScrDyn *args[3];
size_t argc = 0;
args[argc++] = scr_dyn_new_str(message);
ScrStr *fallback = type ? NULL : scr_str_new("Warning", 7);
args[argc++] = scr_dyn_new_str(type ? type : fallback);
scr_str_release(fallback);
if (code) args[argc++] = scr_dyn_new_str(code);
ScrDyn *r = scr_dyn_call(scr_emit_warning_override, args, argc, "process.emitWarning");
scr_dyn_release(r);
for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]);
}
/* A warning built from C parts (the runtime deprecation sites): name
* defaults to "Warning"; code/detail optional. Reported synchronously.
* Borrows message. */
void scr_emit_warning(const char *name, const char *code, ScrStr *message) {
if (scr_emit_warning_override != NULL) {
ScrStr *type_text = scr_str_new(name ? name : "Warning", strlen(name ? name : "Warning"));
ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL;
scr_emit_warning_replaced(message, type_text, code_text);
scr_str_release(type_text);
scr_str_release(code_text);
return;
}
ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL;
ScrDyn *w = scr_warning_new(message, name ? name : "Warning", code_text);
scr_str_release(code_text);
@@ -894,6 +937,16 @@ static void scr_warning_defer(ScrDyn *w, void (*fire)(ScrClosure *)) {
}
void scr_warning_emit_deferred(ScrDyn *w) {
if (scr_emit_warning_override != NULL) {
const ScrDyn *em = scr_dyn_obj_get(w, "message", 7);
const ScrDyn *ec = scr_dyn_obj_get(w, "code", 4);
ScrStr *name = scr_warning_name(w);
ScrStr *message = em && em->kind == SCR_DYN_STR ? scr_str_retain(em->v.str) : scr_str_new("", 0);
scr_emit_warning_replaced(message, name, ec && ec->kind == SCR_DYN_STR ? ec->v.str : NULL);
scr_str_release(message);
scr_str_release(name);
return;
}
if (scr_warning_is_deprecation(w)) {
if (scr_process_deprecation_flag_on(0)) return;
if (scr_process_deprecation_flag_on(1)) {
@@ -959,6 +1012,11 @@ static void scr_warn_bad_arg(const char *arg, const char *want) {
* ERR_INVALID_ARG_TYPE TypeErrors at the call; non-string details are
* ignored. The warning itself is emitted on the next tick. */
void scr_process_emit_warning(ScrDyn *args) {
if (scr_emit_warning_override != NULL) {
ScrDyn *r = scr_dyn_apply(scr_emit_warning_override, args, "process.emitWarning");
scr_dyn_release(r);
return;
}
size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0;
ScrDyn *const *items = args->kind == SCR_DYN_ARR ? args->v.arr.items : NULL;
ScrDyn *warning = argc >= 1 ? items[0] : NULL;
@@ -0,0 +1,35 @@
// process.emitWarning and process.title are ordinary writable properties
// (pi's CLI setup assigns both). After the replacement, process.emitWarning
// calls reach it with their own arguments, no report reaches stderr, and
// the warnings Node's own modules emit through process.emitWarning (a
// util.deprecate wrapper's first call) reach it too.
import { deprecate } from "node:util";
process.title = "pi";
console.log("title", process.title);
process.env.PI_CODING_AGENT = "true";
console.log("env", process.env.PI_CODING_AGENT);
const seen: string[] = [];
process.on("warning", (w) => seen.push(`listener ${w.name}`));
function show(a: unknown): string {
if (a instanceof Error) return `Error(${a.message})`;
return typeof a === "object" ? JSON.stringify(a) : String(a);
}
process.emitWarning = ((warning: unknown, type: unknown, code: unknown) => {
seen.push(`${show(warning)}|${show(type)}|${show(code)}`);
}) as typeof process.emitWarning;
process.emitWarning("plain");
process.emitWarning("typed", "CustomWarning", "CW_1");
process.emitWarning("options", { type: "OptWarning", code: "OW_1", detail: "d" });
process.emitWarning(new Error("as error"));
const old = deprecate(() => "result", "old() is deprecated", "DEP_SCRIPTC_1");
console.log("deprecated call", old());
console.log("second call", old());
console.log("sync", seen.join(" ; "));
// A replacement that does nothing silences warnings entirely.
process.emitWarning = (() => {}) as typeof process.emitWarning;
process.emitWarning("silenced");
setTimeout(() => console.log("after tick", seen.length), 0);