Repository navigation
process: print Node-style warning report and hide internal frames in emitWarning #34608
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
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c9c1099
process: print Node-style warning report, hide internal frames in emi…
robobun c55fd8f
[autofix.ci] apply automated fixes
autofix-ci[bot] b3521de
process: wire --trace-deprecation/--trace-warnings to print warning.s…
robobun 77a8fe4
lint: single-read warning.stack in onWarning trace branch
robobun 9aa0faf
process: only re-capture stack for synthesized warnings, not user Errors
robobun fe65d4c
process: address onWarning review nits
robobun e6a0c1d
test: pin NODE_NO_WARNINGS off in run() helper; clarify re-capture co…
robobun 5f701eb
process: only write code/detail onto synthesized warnings
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Default process warning printer, mirroring Node's lib/internal/process/warning.js | ||
| // onWarning. Invoked from BunProcess.cpp's doEmitWarning nextTick handler after | ||
| // the 'warning' event fires; it is not installed as a listener (so user | ||
| // listeners do not suppress the stderr report, matching Node where onWarning is | ||
| // just another listener alongside any user-installed ones). | ||
|
|
||
| const ErrorPrototypeToString = Error.prototype.toString; | ||
|
|
||
| let traceWarningHelperShown = false; | ||
|
|
||
| function hasExecFlag(flag: string): boolean { | ||
| const argv = process.execArgv; | ||
| for (let i = 0; i < argv.length; i++) if (argv[i] === flag) return true; | ||
| return false; | ||
| } | ||
|
|
||
| function lazyBasename(p: string): string { | ||
| let i = p.length; | ||
| while (i > 0) { | ||
| const c = p.charCodeAt(i - 1); | ||
| if (c === 47 /* / */ || c === 92 /* \ */) break; | ||
| i--; | ||
| } | ||
| let base = p.slice(i); | ||
| if (process.platform === "win32" && base.length > 4 && base.slice(-4).toLowerCase() === ".exe") { | ||
| base = base.slice(0, -4); | ||
| } | ||
| return base; | ||
| } | ||
|
|
||
| function writeOut(msg: string): void { | ||
| const stderr = process.stderr; | ||
| if (stderr && typeof stderr.write === "function") { | ||
| stderr.write(msg + "\n"); | ||
| } | ||
| } | ||
|
Check warning on line 36 in src/js/internal/process/warning.ts
|
||
|
robobun marked this conversation as resolved.
Resolved
|
||
|
|
||
| function onWarning(warning: Error): void { | ||
| if (!(warning instanceof Error)) return; | ||
| const isDeprecation = warning.name === "DeprecationWarning"; | ||
| if (isDeprecation && process.noDeprecation) return; | ||
| const trace = | ||
| (process as any).traceProcessWarnings || | ||
| hasExecFlag("--trace-warnings") || | ||
| (isDeprecation && ((process as any).traceDeprecation || hasExecFlag("--trace-deprecation"))); | ||
| let msg = `(${process.release?.name || "node"}:${process.pid}) `; | ||
| const code = (warning as any).code; | ||
| if (code) msg += `[${code}] `; | ||
| let stack: unknown; | ||
| if (trace && (stack = warning.stack)) { | ||
| msg += `${stack}`; | ||
| } else { | ||
| let body: string; | ||
| try { | ||
| body = typeof warning.toString === "function" ? `${warning.toString()}` : ErrorPrototypeToString.$call(warning); | ||
| } catch { | ||
| body = ErrorPrototypeToString.$call(warning); | ||
| } | ||
| msg += body; | ||
| } | ||
| const detail = (warning as any).detail; | ||
| if (typeof detail === "string") { | ||
| msg += `\n${detail}`; | ||
| } | ||
| if (!trace && !traceWarningHelperShown) { | ||
| const flag = isDeprecation ? "--trace-deprecation" : "--trace-warnings"; | ||
| const argv0 = lazyBasename(process.argv0 || "node"); | ||
| msg += `\n(Use \`${argv0} ${flag} ...\` to show where the warning was created)`; | ||
| traceWarningHelperShown = true; | ||
| } | ||
| writeOut(msg); | ||
| } | ||
|
|
||
| export default { | ||
| onWarning, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe } from "harness"; | ||
|
|
||
| async function run(execArgv: string[], src: string) { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), ...execArgv, "-e", src], | ||
| env: { ...bunEnv, NODE_NO_WARNINGS: undefined }, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| return { stdout, stderr, exitCode }; | ||
| } | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
|
|
||
| describe("process.emitWarning default stderr report", () => { | ||
| test.concurrent("a user 'warning' listener does not suppress the stderr report", async () => { | ||
| const src = ` | ||
| const util = require("node:util"); | ||
| process.on("warning", () => {}); | ||
| util.deprecate(() => 1, "dep msg A", "DEP0T1")(); | ||
| setTimeout(() => {}, 30); | ||
| `; | ||
| const { stderr, exitCode } = await run([], src); | ||
| const first = stderr.split("\n")[0]; | ||
| expect(first).toMatch(/^\(\w+:\d+\) \[DEP0T1\] DeprecationWarning: dep msg A$/); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("default report uses Node's one-line (name:pid) [CODE] form", async () => { | ||
| const src = ` | ||
| const util = require("node:util"); | ||
| util.deprecate(() => 1, "dep msg B", "DEP0T2")(); | ||
| `; | ||
| const { stderr, exitCode } = await run([], src); | ||
| const lines = stderr.split("\n"); | ||
| expect(lines[0]).toMatch(/^\(\w+:\d+\) \[DEP0T2\] DeprecationWarning: dep msg B$/); | ||
| expect(lines[1]).toMatch(/--trace-deprecation/); | ||
| // No stack frames printed by default. | ||
| expect(stderr).not.toMatch(/^\s+at /m); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("process.emitWarning with code and detail formats Node-style", async () => { | ||
| const src = `process.emitWarning("plain msg", { type: "CustomWarning", code: "CODE9", detail: "some detail" });`; | ||
| const { stderr, exitCode } = await run([], src); | ||
| const lines = stderr.split("\n"); | ||
| expect(lines[0]).toMatch(/^\(\w+:\d+\) \[CODE9\] CustomWarning: plain msg$/); | ||
| expect(lines[1]).toBe("some detail"); | ||
| expect(lines[2]).toMatch(/--trace-warnings/); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("--throw-deprecation does not leak internal builtin source", async () => { | ||
| const src = ` | ||
| const util = require("node:util"); | ||
| util.deprecate(() => 1, "dep msg C", "DEP0T3")(); | ||
| `; | ||
| const { stderr, exitCode } = await run(["--throw-deprecation"], src); | ||
| // The thrown DeprecationWarning's stack must start at the user's call | ||
| // site, not inside the internal util/deprecate emitter, so no builtin | ||
| // source snippet (with raw @-intrinsic syntax) should appear. | ||
| expect(stderr).not.toMatch(/getDeprecationWarningEmitter/); | ||
| expect(stderr).not.toMatch(/@undefined/); | ||
| expect(stderr).not.toMatch(/internal:util\/deprecate/); | ||
| expect(stderr).toContain("DeprecationWarning: dep msg C"); | ||
| expect(exitCode).toBe(1); | ||
| }); | ||
|
|
||
| test.concurrent("NODE_NO_WARNINGS=1 still delivers the event but suppresses stderr", async () => { | ||
| const src = ` | ||
| process.on("warning", w => console.log("event:" + w.name + ":" + w.code)); | ||
| process.emitWarning("hush", "DeprecationWarning", "DEP0T4"); | ||
| `; | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "-e", src], | ||
| env: { ...bunEnv, NODE_NO_WARNINGS: "1" }, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| expect(stderr).not.toMatch(/DEP0T4|DeprecationWarning/); | ||
| expect(stdout).toBe("event:DeprecationWarning:DEP0T4\n"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("--trace-deprecation prints the warning stack instead of the hint", async () => { | ||
| const src = ` | ||
| const util = require("node:util"); | ||
| util.deprecate(() => 1, "dep msg E", "DEP0T5")(); | ||
| `; | ||
| const { stderr, exitCode } = await run(["--trace-deprecation"], src); | ||
| expect(stderr.split("\n")[0]).toMatch(/^\(\w+:\d+\) \[DEP0T5\] DeprecationWarning: dep msg E$/); | ||
| expect(stderr).toMatch(/^\s+at /m); | ||
| expect(stderr).not.toMatch(/internal:util\/deprecate/); | ||
| expect(stderr).not.toContain("--trace-deprecation ..."); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("passing an Error instance leaves it unmutated even when ctor/code/detail are given", async () => { | ||
| const src = ` | ||
| function outer() { | ||
| const e = new Error("boom"); | ||
| const orig = e.stack; | ||
| process.emitWarning(e, { ctor: outer, code: "X", detail: "Y" }); | ||
| process.emitWarning(e, "Type", "CODE", outer); | ||
| console.log(JSON.stringify({ stack: e.stack === orig, code: e.code, detail: e.detail })); | ||
| } | ||
| outer(); | ||
| `; | ||
| const { stdout, exitCode } = await run([], src); | ||
| expect(JSON.parse(stdout)).toEqual({ stack: true, code: undefined, detail: undefined }); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("--trace-warnings prints the warning stack for non-deprecation warnings", async () => { | ||
| const src = `process.emitWarning("trace me", "CustomWarning", "CODE10");`; | ||
| const { stderr, exitCode } = await run(["--trace-warnings"], src); | ||
| expect(stderr.split("\n")[0]).toMatch(/^\(\w+:\d+\) \[CODE10\] CustomWarning: trace me$/); | ||
| expect(stderr).toMatch(/^\s+at /m); | ||
| expect(stderr).not.toMatch(/at emitWarning/); | ||
| expect(stderr).not.toContain("--trace-warnings ..."); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test.concurrent("process.traceDeprecation = true enables stack printing at runtime", async () => { | ||
| const src = ` | ||
| process.traceDeprecation = true; | ||
| require("node:util").deprecate(() => 1, "dep msg F", "DEP0T6")(); | ||
| `; | ||
| const { stderr, exitCode } = await run([], src); | ||
| expect(stderr.split("\n")[0]).toMatch(/^\(\w+:\d+\) \[DEP0T6\] DeprecationWarning: dep msg F$/); | ||
| expect(stderr).toMatch(/^\s+at /m); | ||
| expect(stderr).not.toContain("--trace-deprecation ..."); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.