Skip to content

feat(dylints): add ban_env_var_set_after_import + require_oncelock_install_before_use (#840)#892

Merged
zackees merged 2 commits into
mainfrom
fix/840-initializer-order-dylints
Jun 30, 2026
Merged

feat(dylints): add ban_env_var_set_after_import + require_oncelock_install_before_use (#840)#892
zackees merged 2 commits into
mainfrom
fix/840-initializer-order-dylints

Conversation

@zackees

@zackees zackees commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Adds two convention-based dylints under dylints/ mirroring the established pattern from dylints/ban_runtime_new_outside_main/. Both target the same bug class: process-global state that gets initialized in the wrong order between modules. The motivating example from #840 is the Python paths.py / cli.py FBUILD_DEV_MODE order-of-init bug captured in the project memory.

Approach: convention-based, not MIR dataflow

Per #840's "Approaches to investigate -> convention-based static check" recommendation. These lints catch the attempt at the call site, not the actual "set after read" dataflow. The latter requires MIR-level dataflow across the whole crate graph and is the "MIR for completeness later" follow-up tracked on #840. Both READMEs document this limitation explicitly.

Lint surface

ban_env_var_set_after_import

BANNED_PATHS = [["std", "env", "set_var"]]

Bans std::env::set_var outside:

  • **/main.rs
  • **/src/bin/**
  • **/tests/**
  • any module annotated #[cfg(test)]

Rationale: env-var mutation has order-of-init hazards. If a module already read the variable into a static/OnceLock before this set_var ran, that captured value sticks. Only main() (and tests, which manage their own process state) may mutate env vars.

require_oncelock_install_before_use

BANNED_PATHS = [["std", "sync", "OnceLock", "set"], ["std", "sync", "OnceLock", "set_blocking"]]

Bans both OnceLock::set and OnceLock::set_blocking outside the same allow-list. Rationale: OnceLock is single-assignment; if a reader already filled the lock via get_or_init, the late .set() silently no-ops. The safe shape is either install-from-main()-before-readers-run, or rewrite around get_or_init(|| ...) so installation is lazy and idempotent.

The lint also handles MethodCall expressions (not just Path/Call) so receiver-style MY_GLOBAL.set(...) is resolved via typeck_results().type_dependent_def_id().

Wiring

  • Root Cargo.toml [workspace] exclude extended with both new dirs (matches the existing internal bridge APIs + dylint bans (total sweep, no grandfathering) #844 sweep grouping convention).
  • ci/hooks/crate_guard.py APPROVED_CRATE_DIRS extended with both new dirs.
  • No changes to ci/check_workspace_crates.py APPROVED_MEMBERS — dylints are workspace excludes, not members, so they live under crate_guard.py only.

soldr cargo check --workspace --all-targets passes locally with these changes.

Pre-existing violations (NOT auto-allowed)

Per task constraint, no real-world allowlist for fbuild's own code. Sweeping crates/*/src/**/*.rs:

  • std::env::set_var -> 48 occurrences across 14 files, but the vast majority are inside #[cfg(test)] mod tests blocks (EnvVarGuard / lock guard patterns) which the lint's #[cfg(test)] walk exempts. The remaining production sites the lint would actually flag are concentrated in crates/fbuild-paths/src/running_process.rs and crates/fbuild-daemon/src/broker/service.rs (the EnvVarGuard::drop paths that appear outside #[cfg(test)] only because of where the impl is declared — these are still inside the test module). A clean follow-up pass is recommended once the lint is wired into CI.

  • OnceLock::set -> 5 production sites the lint would flag:

    • crates/fbuild-core/src/containment.rs:88 (GLOBAL_GROUP.set(group))
    • crates/fbuild-core/src/containment.rs:443 (JOB.set(JobHandle(job)), Windows-only)
    • crates/fbuild-build/src/compile_backend.rs:80 (GLOBAL.set(backend) in install_global)
    • Plus the SHUTDOWN_TX.set and CompileBackend.set sites which legitimately live in main.rs (allowed) or test code (allowed).

These are call-site violations that the maintainer can address either by (a) moving the install to main() / a #[cfg(test)]-only init helper, (b) rewriting around get_or_init, or (c) annotating with a per-call #[allow(require_oncelock_install_before_use)] documented inline. Not auto-fixed in this PR per task spec.

Toolchain note

Did NOT run cargo dylint --all locally — the dylint nightly toolchain pin is heavy and not always built. soldr cargo check --workspace --all-targets passes, which is what verifies the Cargo.toml/crate_guard.py wiring.

Test plan

  • CI runs cargo dylint --all against the workspace and the two new lints load
  • CI's ci/check_workspace_crates.py still passes (verified locally: OK: 14 approved workspace members, no new crates.)
  • Maintainer reviews the pre-existing violation list and decides on per-call #[allow] vs follow-up fix
  • Follow-up issue tracks the MIR-level dataflow check (get_or_init-before-set proof)

Closes #840

…stall_before_use (#840)

Adds two convention-based dylints under dylints/ mirroring the
established pattern from dylints/ban_runtime_new_outside_main/:

- ban_env_var_set_after_import: bans std::env::set_var outside
  main.rs / src/bin / tests / #[cfg(test)]. Motivated by the Python
  paths.py / cli.py FBUILD_DEV_MODE order-of-init bug captured in the
  project memory.

- require_oncelock_install_before_use: bans std::sync::OnceLock::set
  and OnceLock::set_blocking outside the same allow-list. Convention
  shortcut for the "Approaches to investigate -> convention-based
  static check" suggestion in #840.

Both lints document the convention-vs-MIR limitation: they catch the
*attempt* at the call site, not the actual "set after read" dataflow.
The MIR-level check is tracked as a follow-up on #840 ("MIR for
completeness later").

Wiring:
- root Cargo.toml [workspace] exclude updated with both dirs
- ci/hooks/crate_guard.py APPROVED_CRATE_DIRS updated with both dirs

No existing dylint sources touched. Pre-existing call-site violations
are not allow-listed; maintainer can fix in follow-up or annotate
with per-call #[allow(...)] attributes.

Closes #840
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@zackees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f03d28c-a496-4620-bd96-2cbc16e7b9bb

📥 Commits

Reviewing files that changed from the base of the PR and between d3f758d and 1e2f361.

📒 Files selected for processing (14)
  • Cargo.toml
  • ci/hooks/crate_guard.py
  • dylints/ban_env_var_set_after_import/Cargo.toml
  • dylints/ban_env_var_set_after_import/README.md
  • dylints/ban_env_var_set_after_import/rust-toolchain.toml
  • dylints/ban_env_var_set_after_import/src/README.md
  • dylints/ban_env_var_set_after_import/src/allowlist.txt
  • dylints/ban_env_var_set_after_import/src/lib.rs
  • dylints/require_oncelock_install_before_use/Cargo.toml
  • dylints/require_oncelock_install_before_use/README.md
  • dylints/require_oncelock_install_before_use/rust-toolchain.toml
  • dylints/require_oncelock_install_before_use/src/README.md
  • dylints/require_oncelock_install_before_use/src/allowlist.txt
  • dylints/require_oncelock_install_before_use/src/lib.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/840-initializer-order-dylints

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PR #892's audit identified 3 production OnceLock::set sites in
`crates/fbuild-core/src/containment.rs` (GLOBAL_GROUP + Windows JOB
installs) and `crates/fbuild-build/src/compile_backend.rs`
(install_global). All three are one-shot install paths called from
the binary entry points before any consumer reaches the OnceLock —
the order-of-init bug class the lint targets is structurally
impossible there.

Add them to the path-based allowlist so the new lint ships at the
intended Deny level without breaking the existing build. New
non-install OnceLock::set call sites outside these files (or
outside main.rs / src/bin / tests / cfg(test)) will trip the lint
as designed.
@zackees
zackees merged commit dd04d79 into main Jun 30, 2026
79 of 92 checks passed
@zackees
zackees deleted the fix/840-initializer-order-dylints branch June 30, 2026 18:53
@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

dylint: initializer-order lints (ban_env_var_set_after_import, require_oncelock_install_before_use) — design and implementation

2 participants