diff --git a/Cargo.toml b/Cargo.toml index c7dcfe86bbb9..a350ccf82cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,11 +201,11 @@ overflow-checks = false # back to "warn" (their priority 0 beats the group's -1) where a warning level # is intentional. warnings = { level = "deny", priority = -1 } -# `bun_asan` / `bun_debug` / `socket_fault_injection` are set via RUSTFLAGS -# (`--cfg=...` + `--check-cfg=cfg(...)`) by scripts/build/rust.ts; register -# them here so a plain `cargo build` / `cargo check` (without those flags) -# doesn't warn. -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_debug)', 'cfg(socket_fault_injection)'] } +# `bun_asan` / `bun_debug` / `bun_logs` / `socket_fault_injection` are set via +# RUSTFLAGS (`--cfg=...` + `--check-cfg=cfg(...)`) by scripts/build/rust.ts; +# register them here so a plain `cargo build` / `cargo check` (without those +# flags) doesn't warn. +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_debug)', 'cfg(bun_logs)', 'cfg(socket_fault_injection)'] } # link.exe unconditionally prints "Creating library X.dll.lib and object # X.dll.exp" to stdout when linking each proc-macro DLL on Windows hosts; # there is no linker flag to suppress it. The lint already exempts itself diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts index 0ea2c440a5be..393a3667d8e8 100644 --- a/scripts/build/buildOptionsRs.ts +++ b/scripts/build/buildOptionsRs.ts @@ -12,10 +12,12 @@ * keeps the mtime stable so a reconfigure with the same sha doesn't * recompile `bun_core` and its dependents. * - * Target-dependent constants (`ENABLE_TINYCC`, `ENABLE_ASAN`, `ENABLE_LOGS`) + * Target-dependent constants (`ENABLE_TINYCC`) and the ones mirroring a + * `--cfg` that `rust.ts` passes in RUSTFLAGS (`ENABLE_ASAN`, `ENABLE_LOGS`) * stay as `cfg!()` expressions inside the generated file rather than literals - * so a `cargo check --target ` against the same generated file - * still evaluates them per-target. + * so a `cargo check --target `, or a bare `cargo check` / `cargo + * miri test` with no RUSTFLAGS at all, against the same generated file still + * evaluates them per invocation. * * Written at configure time alongside `depVersionsHeader.ts` / * `cargo-config.ts` — it's a constant manifest, not a build edge. @@ -61,10 +63,10 @@ export function generateBuildOptionsRs(cfg: Config): string { "", "// Target/profile-derived — kept as `cfg!()` so cross-target", "// `cargo check` evaluates per-triple. Values agree with `Config`:", - "// rust.ts sets `--cfg=bun_debug` ⇔ `cfg.debug`, `--cfg=bun_asan` ⇔", + "// rust.ts sets `--cfg=bun_logs` ⇔ `cfg.logs`, `--cfg=bun_asan` ⇔", "// `cfg.asan`, and `cfg.tinycc`'s default (config.ts) is the negation", "// of this predicate.", - "pub const ENABLE_LOGS: bool = cfg!(bun_debug);", + "pub const ENABLE_LOGS: bool = cfg!(bun_logs);", "pub const ENABLE_ASAN: bool = cfg!(bun_asan);", "pub const ENABLE_TINYCC: bool = !cfg!(any(", ` target_os = "android",`, diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index ce1d728d6d67..8812996e8daf 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -454,8 +454,8 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { rustflags.push("--cfg=bun_asan"); } // `bun_debug`: the cargo profile is `dev` (a Debug-buildtype build). - // `bun_core::env::IS_DEBUG` and `build_options::ENABLE_LOGS` key on this - // instead of `cfg!(debug_assertions)` so that release-asan / + // `bun_core::env::IS_DEBUG` keys on this instead of + // `cfg!(debug_assertions)` so that release-asan / // release-assertions (which enable `debug-assertions` below for // `debug_assert!()` coverage) don't also flip on Debug-only conveniences: // `DUMP_SOURCE` (per-module writes to /tmp/bun-debug-src/), `debug_warn!` @@ -467,6 +467,17 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { if (cfg.debug) { rustflags.push("--cfg=bun_debug"); } + // `bun_logs`: `build_options::ENABLE_LOGS`, the compile-time gate on + // `scoped_log!`. Follows `cfg.logs`, which `release-assertions` / `--logs` + // set independently of `cfg.debug`. A cfg rather than a literal in + // build_options.rs so that bare `cargo check` / `cargo miri test` (no + // RUSTFLAGS, reading build/debug's file) keep the log bodies dead like + // `bun_debug` does; live, `ScopedLogger::is_visible()` would scan the + // environment through the Highway FFI, which Miri can't call. + rustflags.push("--check-cfg=cfg(bun_logs)"); + if (cfg.logs) { + rustflags.push("--cfg=bun_logs"); + } // `bun_codegen_embed`: embed codegen-output `.js` (`include_bytes!`) instead // of reading them from `BUN_CODEGEN_DIR` at runtime. Mirrors Zig // `BunBuildOptions.shouldEmbedCode() = optimize != .Debug or codegen_embed`. @@ -497,9 +508,10 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { // file:line server-side, so the panic call site is recoverable from the trace // without embedding the location in the binary — same as the Zig build, which // had ~0 embedded source paths. Kept off for debug and `release-assertions` - // where panic messages are read locally. Nightly-only; the pinned toolchain - // is nightly. - if (cfg.release && !cfg.assertions) { + // where panic messages are read locally, and for logs builds, whose + // `mark_binding()`-style loggers print `Location::caller()` (`:0` + // under this flag). Nightly-only; the pinned toolchain is nightly. + if (cfg.release && !cfg.assertions && !cfg.logs) { rustflags.push("-Zlocation-detail=none"); } // Path remapping (CI reproducibility) — rustc equivalent of the C/C++ diff --git a/src/bun_core/Global.rs b/src/bun_core/Global.rs index 56a912aec7f0..0bfc33f65c4c 100644 --- a/src/bun_core/Global.rs +++ b/src/bun_core/Global.rs @@ -435,11 +435,9 @@ macro_rules! mark_binding { $crate::mark_binding!(::core::panic::Location::caller().file()) }; ($fn_name:expr) => { - // Opt-in via BUN_DEBUG_JSC=1. The `JSC` scope is owned by bun_core. Gate on - // `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) — never on a Cargo - // feature, since `cfg!(feature = ..)` is resolved against the *calling* - // crate and would warn (or silently no-op) in crates without it. - if $crate::env::IS_DEBUG && $crate::Global::JSC_SCOPE.is_visible() { + // Opt-in via BUN_DEBUG_JSC=1. Same gate as `scoped_log!`; not a Cargo + // feature, which `cfg!` would resolve against the *calling* crate. + if $crate::env::ENABLE_LOGS && $crate::Global::JSC_SCOPE.is_visible() { $crate::Global::JSC_SCOPE.log(::core::format_args!( "[JSC] {} ({}:{})\n", $fn_name, diff --git a/src/bun_core/env.rs b/src/bun_core/env.rs index 9f4c52ef8c2f..112191304229 100644 --- a/src/bun_core/env.rs +++ b/src/bun_core/env.rs @@ -65,6 +65,8 @@ pub(crate) const CANARY_REVISION: &str = if IS_CANARY { }; pub const DUMP_SOURCE: bool = IS_DEBUG && !IS_TEST; pub const BASE_PATH: &[u8] = build_options::BASE_PATH; +/// The build's `logs` option (`--cfg=bun_logs` from scripts/build/rust.ts; defaults +/// to `IS_DEBUG`, `release-assertions` and `--logs=on|off` override). Gates `scoped_log!`. pub const ENABLE_LOGS: bool = build_options::ENABLE_LOGS; pub const ENABLE_ASAN: bool = build_options::ENABLE_ASAN; pub const ENABLE_FUZZILLI: bool = build_options::ENABLE_FUZZILLI; diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index e0f1aa363edc..438aa57a4059 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1540,14 +1540,15 @@ macro_rules! declare_scope { /// `bun.Output.scoped(.X, vis)("fmt", .{args})` → `scoped_log!(X, "fmt", args...)` /// -/// MUST gate arg evaluation: expands to a dead branch in release builds. +/// MUST gate arg evaluation: expands to a dead branch in builds without logs. #[macro_export] macro_rules! scoped_log { ($scope:path, $fmt:expr $(, $arg:expr)* $(,)?) => { - // Gate on `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) so release - // builds dead-strip the body. Do NOT gate on a Cargo feature — there - // is no `debug_logs` feature and §Forbidden bans silent no-ops. - if $crate::env::IS_DEBUG && $scope.is_visible() { + // Gate on `env::ENABLE_LOGS` (the build's `logs` option, which the + // `--logs` / `release-assertions` configs set independently of + // `IS_DEBUG`) so builds without logs dead-strip the body. Do NOT gate + // on a Cargo feature: there is none and §Forbidden bans silent no-ops. + if $crate::env::ENABLE_LOGS && $scope.is_visible() { const __NL: &str = $crate::output::_needs_nl($crate::pretty_fmt!($fmt, false)); // Branch on ANSI *before* `format_args!` so each `$arg` evaluates // exactly once. @@ -2591,13 +2592,11 @@ fn init_scoped_debug_writer_at_startup() { } fn scoped_writer() -> QuietWriter { - // All callers are already gated on `Environment::ENABLE_LOGS`; this is a - // Debug-build self-check (release-asan/release-assertions enable - // `debug_assertions` with `ENABLE_LOGS == false`, so keying on - // `debug_assertions` would turn it into a guaranteed abort there). + // Callers are gated on `ENABLE_LOGS`; this self-check is `bun_debug`, not + // `debug_assertions`, which release-asan enables with logs off. #[cfg(bun_debug)] if !Environment::ENABLE_LOGS { - unreachable!("scopedWriter() should only be called in debug mode"); + unreachable!("scopedWriter() should only be called when logs are enabled"); } // SAFETY: initialized in init_scoped_debug_writer_at_startup; QuietWriter is Copy POD. unsafe { scoped_debug_writer::SCOPED_FILE_WRITER.read() } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 24999fd82b68..01383906a2b7 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1978,7 +1978,7 @@ pub mod bv2_impl { } } - if bun_core::env::IS_DEBUG && ReachableFiles.is_visible() { + if bun_core::env::ENABLE_LOGS && ReachableFiles.is_visible() { bun_core::scoped_log!( ReachableFiles, "Reachable count: {} / {}", diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 605f0c9e9e16..4f1a4d92547e 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1381,7 +1381,7 @@ pub use self::Node as node; #[track_caller] #[inline] pub fn mark_binding() { - if bun_core::env::IS_DEBUG && bun_core::Global::JSC_SCOPE.is_visible() { + if bun_core::env::ENABLE_LOGS && bun_core::Global::JSC_SCOPE.is_visible() { let loc = core::panic::Location::caller(); bun_core::Global::JSC_SCOPE.log(format_args!("[jsc] ({}:{})\n", loc.file(), loc.line())); } @@ -1390,7 +1390,7 @@ pub fn mark_binding() { /// Like [`mark_binding`], with a class-name prefix. #[inline] pub(crate) fn mark_member_binding(class: &'static str, src: &core::panic::Location<'static>) { - if bun_core::env::IS_DEBUG && bun_core::Global::JSC_SCOPE.is_visible() { + if bun_core::env::ENABLE_LOGS && bun_core::Global::JSC_SCOPE.is_visible() { bun_core::Global::JSC_SCOPE.log(format_args!( "[jsc] {} ({}:{})\n", class, diff --git a/src/output/lib.rs b/src/output/lib.rs index cded6c31c9e5..71cea98cf7de 100644 --- a/src/output/lib.rs +++ b/src/output/lib.rs @@ -17,8 +17,8 @@ // bun_output::scoped_log!(X, "fmt {} {}", a, b); // // `declare_scope!` expands to a `pub static X: ScopedLogger`; `scoped_log!` -// gates arg evaluation behind `env::IS_DEBUG` so release builds pay zero -// cost (see PORTING.md — args MUST sit inside the dead branch). +// gates arg evaluation behind `env::ENABLE_LOGS` so builds without logs pay +// zero cost (see PORTING.md: args MUST sit inside the dead branch). pub use bun_core::declare_scope; pub use bun_core::define_scoped_log; pub use bun_core::scoped_log; diff --git a/src/sys/lib.rs b/src/sys/lib.rs index e563f9bb54b8..116d97d17576 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -4925,9 +4925,9 @@ pub type EnvMap = std::collections::HashMap; #[macro_export] macro_rules! syslog { ($fmt:literal $(, $arg:expr)* $(,)?) => { - // Gate on `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) — matches - // bun_core::scoped_log!; there is no `debug_logs` Cargo feature. - if ::bun_core::env::IS_DEBUG && $crate::fd::SYS.is_visible() { + // Gate on `env::ENABLE_LOGS`, matching bun_core::scoped_log!; there is + // no `debug_logs` Cargo feature. + if ::bun_core::env::ENABLE_LOGS && $crate::fd::SYS.is_visible() { const __NL: &str = ::bun_core::output::_needs_nl(::bun_core::pretty_fmt!($fmt, false)); // Branch on ANSI *before* `format_args!` so each `$arg` evaluates diff --git a/src/sys/windows/mod.rs b/src/sys/windows/mod.rs index 13427b1c5445..94fb008be6cb 100644 --- a/src/sys/windows/mod.rs +++ b/src/sys/windows/mod.rs @@ -397,7 +397,7 @@ unsafe extern "system" { pub fn GetFileType(hFile: HANDLE) -> DWORD { let rc = GetFileType_raw(hFile); - // `syslog!` self-gates on `env::IS_DEBUG` (see lib.rs); no extra feature + // `syslog!` self-gates on `env::ENABLE_LOGS` (see lib.rs); no extra feature // flag needed (there is no `debug_logs` feature in bun_sys). bun_sys::syslog!("GetFileType({}) = {}", Fd::from_system(hFile), rc); rc diff --git a/test/internal/source-lints/build-logs-option.test.ts b/test/internal/source-lints/build-logs-option.test.ts new file mode 100644 index 000000000000..c0f6fd75cd60 --- /dev/null +++ b/test/internal/source-lints/build-logs-option.test.ts @@ -0,0 +1,223 @@ +/** + * The build's `logs` option (scripts/build/config.ts: on by default in debug + * builds, set by the `release-assertions` profile and `bun run build:logs`, + * overridable with `--logs=on|off`) decides whether `scoped_log!` / + * `BUN_DEBUG_=1` logging is compiled into the Rust side. It reaches + * Rust as `bun_core::Environment::ENABLE_LOGS`, through pieces that have to + * agree with each other, pinned here: + * + * - scripts/build/buildOptionsRs.ts emits `ENABLE_LOGS` into build_options.rs + * as a `cfg!(...)`, and scripts/build/rust.ts passes that `--cfg` exactly + * when `cfg.logs` is set (it used to be `cfg!(bun_debug)`, which made the + * option dead: a release build configured with logs had none, a debug + * build configured without still logged); + * - Cargo.toml registers the cfg so a bare `cargo check` doesn't warn; + * - a release build with logs keeps `#[track_caller]` locations, which the + * `mark_binding()` style of logger prints (`-Zlocation-detail=none` would + * turn them into `:0`); + * - the loggers in src gate on `ENABLE_LOGS`, not on `IS_DEBUG`, otherwise + * a non-debug build configured with logs still compiles them out. + * + * Pure config evaluation plus a source scan: no compiler is involved. + */ +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { generateBuildOptionsRs } from "../../../scripts/build/buildOptionsRs.ts"; +import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../../scripts/build/config.ts"; +import { getProfile } from "../../../scripts/build/profiles.ts"; +import { cargoBuildInvocation } from "../../../scripts/build/rust.ts"; + +const repoRoot = resolve(import.meta.dir, "..", "..", ".."); + +/** A fully-populated fake toolchain; nothing here is ever executed. */ +function mockToolchain(): Toolchain { + return { + cc: "/fake/llvm/bin/clang", + cxx: "/fake/llvm/bin/clang++", + hostCc: undefined, + hostCxx: undefined, + clangVersion: "21.1.8", + clangResourceDir: "/fake/llvm/lib/clang/21", + ar: "/fake/llvm/bin/llvm-ar", + ranlib: "/fake/llvm/bin/llvm-ranlib", + ld: "/fake/llvm/bin/ld.lld", + ld64Lld: undefined, + rustLld: undefined, + rustLlvmVersion: "22.1.4", + rustSysroot: undefined, + rustHostTriple: undefined, + strip: "/fake/llvm/bin/llvm-strip", + llvmStrip: "/fake/llvm/bin/llvm-strip", + dsymutil: undefined, + bun: "/fake/bin/bun", + jsRuntime: "/fake/bin/bun", + esbuild: "/fake/bin/esbuild", + ccache: undefined, + cmake: "/fake/bin/cmake", + cargo: undefined, + cargoHome: undefined, + rustupHome: undefined, + msvcLinker: undefined, + rc: undefined, + mt: undefined, + nasm: undefined, + }; +} + +/** + * A linux-x64 target resolves on every host once it is told where its sysroot + * is (the path is only recorded, never opened). + */ +function linuxConfig(partial: PartialConfig, buildDir: string): Config { + return resolveConfig( + { os: "linux", arch: "x64", abi: "gnu", buildDir, linuxSysroot: buildDir, ...partial }, + mockToolchain(), + ); +} + +function rustflags(cfg: Config): string[] { + return cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS?.split("\x1f") ?? []; +} + +/** The right-hand side of `pub const ENABLE_LOGS: bool = ...;` in the build_options.rs generated for `cfg`. */ +function generatedEnableLogs(cfg: Config): string { + const source = readFileSync(generateBuildOptionsRs(cfg), "utf8"); + const match = /^pub const ENABLE_LOGS: bool = (.+);$/m.exec(source); + if (!match) throw new Error(`build_options.rs has no ENABLE_LOGS constant:\n${source}`); + return match[1]; +} + +/** The name inside `cfg!(...)`, or undefined when the constant is a plain literal. */ +function cfgName(expr: string): string | undefined { + return /^cfg!\((\w+)\)$/.exec(expr)?.[1]; +} + +/** + * What `bun_core::Environment::ENABLE_LOGS` evaluates to in the cargo build + * rust.ts emits for `cfg`: the generated constant, resolved against the + * rustflags of that same build. + */ +function rustEnableLogs(cfg: Config): boolean { + const expr = generatedEnableLogs(cfg); + if (expr === "true" || expr === "false") return expr === "true"; + const name = cfgName(expr); + if (name === undefined) throw new Error(`unexpected ENABLE_LOGS initializer: ${expr}`); + const flags = rustflags(cfg); + // rustc's unexpected_cfgs lint needs the cfg declared in the same build + // that may set it, whether or not this configuration sets it. + expect(flags).toContain(`--check-cfg=cfg(${name})`); + return flags.includes(`--cfg=${name}`); +} + +describe("ENABLE_LOGS follows the logs option", () => { + const cases: { name: string; partial: PartialConfig; logs: boolean }[] = [ + // The profile's doc comment promises "Release + assertions + logs". + { name: "release-assertions profile", partial: getProfile("release-assertions"), logs: true }, + // `bun run build:logs`. + { name: "release --logs=on", partial: { buildType: "Release", logs: true }, logs: true }, + { name: "debug --logs=off", partial: { buildType: "Debug", logs: false }, logs: false }, + { name: "debug (default on)", partial: { buildType: "Debug" }, logs: true }, + { name: "release (default off)", partial: { buildType: "Release" }, logs: false }, + // release-asan / the CI asan lane: assertions on, logs stay off. + { + name: "release-asan (default off)", + partial: { buildType: "Release", asan: true, assertions: true }, + logs: false, + }, + ]; + + for (const { name, partial, logs } of cases) { + test(`${name}: rust ENABLE_LOGS is ${logs}`, () => { + using dir = tempDir("build-logs-option", {}); + const cfg = linuxConfig(partial, String(dir)); + // The option itself resolves as documented; what is under test is + // whether the Rust build sees that value. + expect(cfg.logs).toBe(logs); + expect(rustEnableLogs(cfg)).toBe(logs); + }); + } + + test("logs is independent of the Debug-build cfg", () => { + using dir = tempDir("build-logs-option", {}); + const releaseWithLogs = rustflags(linuxConfig({ buildType: "Release", logs: true }, String(dir))); + expect(releaseWithLogs).not.toContain("--cfg=bun_debug"); + const debugWithoutLogs = rustflags(linuxConfig({ buildType: "Debug", logs: false }, String(dir))); + expect(debugWithoutLogs).toContain("--cfg=bun_debug"); + }); + + test("a release build with logs keeps the call-site locations its loggers print", () => { + using dir = tempDir("build-logs-option", {}); + const locationDetail = (partial: PartialConfig) => + rustflags(linuxConfig(partial, String(dir))).includes("-Zlocation-detail=none"); + expect({ + release: locationDetail({ buildType: "Release" }), + "release --logs=on": locationDetail({ buildType: "Release", logs: true }), + "release-assertions": locationDetail(getProfile("release-assertions")), + }).toEqual({ + release: true, + "release --logs=on": false, + "release-assertions": false, + }); + }); + + test("the cfg build_options.rs reads is registered for bare cargo in Cargo.toml", () => { + using dir = tempDir("build-logs-option", {}); + const name = cfgName(generatedEnableLogs(linuxConfig({ buildType: "Debug" }, String(dir)))); + if (name === undefined) return; // a literal needs no registration + const cargoToml = readFileSync(resolve(repoRoot, "Cargo.toml"), "utf8"); + const unexpectedCfgs = /^unexpected_cfgs\s*=.*$/m.exec(cargoToml)?.[0]; + expect(unexpectedCfgs).toBeDefined(); + expect(unexpectedCfgs).toContain(`'cfg(${name})'`); + }); +}); + +describe("the loggers in src gate on ENABLE_LOGS", () => { + /** `//` comments blanked out (newlines kept, so line numbers survive). */ + const stripComments = (source: string) => source.replace(/\/\/[^\n]*/g, ""); + + /** Body of `macro_rules! { ... }` in `source`, up to the first line that is just `}`. */ + function macroBody(source: string, name: string): string { + const match = new RegExp(String.raw`^macro_rules! ${name} \{\n([\s\S]*?)^\}`, "m").exec(source); + if (!match) throw new Error(`macro_rules! ${name} not found`); + return match[1]; + } + + // ` && scope.is_visible()`: one `if` condition, however rustfmt + // wraps it, since `;` and `{` cannot occur inside it. + const guardOn = (constant: string, flags = "") => + new RegExp(String.raw`\b${constant}\b[^;{]*\.is_visible\(\)`, flags); + + test("scoped_log!, syslog! and mark_binding! check ENABLE_LOGS before the scope", () => { + const macros = [ + { file: "src/bun_core/output.rs", name: "scoped_log" }, + { file: "src/sys/lib.rs", name: "syslog" }, + { file: "src/bun_core/Global.rs", name: "mark_binding" }, + ]; + for (const { file, name } of macros) { + const body = macroBody(stripComments(readFileSync(resolve(repoRoot, file), "utf8")), name); + expect(body, `${name}! in ${file}`).toMatch(guardOn("ENABLE_LOGS")); + expect(body, `${name}! in ${file}`).not.toMatch(/\bIS_DEBUG\b/); + } + }); + + test("no logger guard keys on IS_DEBUG", () => { + // `IS_DEBUG && scope.is_visible()` compiles the log out of every non-debug + // build, including the ones configured with logs. Gate on ENABLE_LOGS + // instead, or just call scoped_log!, which does. + const offenders: string[] = []; + const guard = guardOn("IS_DEBUG", "g"); + for (const rel of new Bun.Glob("src/**/*.rs").scanSync({ cwd: repoRoot })) { + const raw = readFileSync(resolve(repoRoot, rel), "utf8"); + if (!raw.includes(".is_visible()")) continue; + const source = stripComments(raw); + for (const match of source.matchAll(guard)) { + const line = source.slice(0, match.index).split("\n").length; + offenders.push(`${rel.replaceAll("\\", "/")}:${line}`); + } + } + expect(offenders.sort()).toEqual([]); + }); +});