Use cfg!() runtime checks over #[cfg] conditionals
ADR-0007
Use cfg!() runtime checks over #[cfg] compile-time conditionals
Context
Rust provides two ways to handle conditional compilation based on build configuration:
- Compile-time conditionals (
#[cfg]): Code is included/excluded at compile time - Runtime checks (
cfg!()): All code is compiled, conditions evaluated at runtime
We need to decide which approach to use for debug vs. release behavior differences (GraphQL complexity limits, introspection, playground access, etc.).
Compile-time issues we encountered:
- Debug and release builds can have different compilation errors
cargo checkvscargo check --releasemay succeed/fail differently- Conditional compilation makes code harder to maintain and reason about
Decision
Use cfg!() runtime checks instead of #[cfg] compile-time conditionals for configuration-dependent behavior.
Examples:
// Preferred: Runtime check
.limit_complexity(if cfg!(debug_assertions) { 1000 } else { 100 })
// Avoid: Compile-time conditional
#[cfg(debug_assertions)]
.limit_complexity(1000)
#[cfg(not(debug_assertions))]
.limit_complexity(100)
Exception: Only use #[cfg] for test-only code that shouldn't exist in production builds:
#[cfg(test)]
mod tests { ... }
Consequences
What Becomes Easier
- Single code path - both debug and release builds compile the same code
- Consistent compilation -
cargo checkandcargo check --releasebehave identically - Easier maintenance - no duplicate code paths for different configurations
- Better IDE support - code analysis tools see all code paths
What Becomes More Difficult
- Slight runtime overhead - configuration checked at runtime vs. compile time
- Dead code in binaries - unused branches included in final executable
- Less aggressive optimization - compiler can't eliminate dead code paths completely