From 95b101a26fac4ac9151df6dabc23c86495116373 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 19 Apr 2026 16:06:02 -0400 Subject: [PATCH] lint(clippy): disallow blocking std::io and std::net on Tokio paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the workspace clippy.toml — which already bans std::thread::sleep, std::thread::spawn, and std::process::Command::new on Tokio paths — with: - disallowed-types: std::io::{Read, Write, BufRead, BufReader, BufWriter} and std::net::{TcpStream, TcpListener, UdpSocket} - disallowed-methods: std::io::{stdin, stdout, stderr} Non-blocking std::io items (Error, ErrorKind, Result, IsTerminal, Cursor) remain allowed. std::fs is intentionally deferred. Annotates ~24 pre-existing sync call sites with #[expect(..., reason = "...")] matching the established pattern. All annotations describe why blocking I/O is intentional in that context (sync CLI command, test helper, pre-fork flush, etc.), so a future conversion to async will surface as an unfulfilled lint expectation instead of silently drifting. Fixes one real Tokio-path issue surfaced by the new lint: fabro-cli's server-start daemon-health poller (try_connect) was a sync fn called from async execute_daemon; std::net::TcpStream::connect_timeout blocked a Tokio worker for up to 100ms per poll iteration. Converted to tokio::net::{TcpStream, UnixStream} with tokio::time::timeout. One follow-up flagged in-code: fabro-agent/src/cli.rs's JSON event writer uses std::io::stdout() inside tokio::spawn. Annotated with a FOLLOW-UP reason pointing at tokio::io::stdout; left unchanged since volume is low and scope exceeded this pass. Verified: clippy clean, cargo +nightly fmt --check clean, full nextest workspace run (4131 passed, 182 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) --- clippy.toml | 13 +++++++++++ lib/crates/fabro-agent/src/cli.rs | 22 +++++++++++++++++++ .../fabro-cli/src/commands/config/mod.rs | 9 ++++++++ lib/crates/fabro-cli/src/commands/graph.rs | 9 ++++++++ lib/crates/fabro-cli/src/commands/parse.rs | 9 ++++++++ .../fabro-cli/src/commands/render_graph.rs | 10 +++++++++ .../fabro-cli/src/commands/repo/init.rs | 5 +++++ .../fabro-cli/src/commands/run/attach.rs | 9 ++++++++ lib/crates/fabro-cli/src/commands/run/diff.rs | 9 ++++++++ lib/crates/fabro-cli/src/commands/run/logs.rs | 9 ++++++++ .../src/commands/run/run_progress/mod.rs | 13 +++++++++++ .../src/commands/run/run_progress/renderer.rs | 6 +++++ .../fabro-cli/src/commands/run/runner.rs | 6 +++++ lib/crates/fabro-cli/src/commands/run/wait.rs | 9 ++++++++ .../fabro-cli/src/commands/secret/set.rs | 9 ++++++++ .../fabro-cli/src/commands/server/start.rs | 16 +++++++++----- .../fabro-cli/src/commands/uninstall.rs | 5 +++++ lib/crates/fabro-cli/src/commands/upgrade.rs | 10 +++++++++ lib/crates/fabro-cli/src/commands/version.rs | 5 +++++ lib/crates/fabro-cli/src/main.rs | 11 ++++++++-- .../fabro-cli/src/shared/provider_auth.rs | 9 ++++++++ lib/crates/fabro-cli/src/shared/utilities.rs | 9 ++++++++ lib/crates/fabro-cli/tests/it/cmd/attach.rs | 5 +++++ .../fabro-cli/tests/it/cmd/render_graph.rs | 4 ++++ lib/crates/fabro-cli/tests/it/cmd/runner.rs | 4 ++++ .../fabro-cli/tests/it/cmd/server_start.rs | 6 +++++ lib/crates/fabro-devcontainer/src/compose.rs | 4 ++++ lib/crates/fabro-interview/src/console.rs | 6 +++++ lib/crates/fabro-server/src/serve.rs | 5 +++++ lib/crates/fabro-telemetry/src/spawn.rs | 6 +++++ lib/crates/fabro-util/src/dev_token.rs | 4 ++++ lib/crates/fabro-util/src/run_log.rs | 6 +++++ lib/crates/fabro-workflow/src/run_dump.rs | 4 ++++ test/twin/github/src/state.rs | 4 ++++ 34 files changed, 262 insertions(+), 8 deletions(-) diff --git a/clippy.toml b/clippy.toml index 93173b2be..83d5ab8de 100644 --- a/clippy.toml +++ b/clippy.toml @@ -5,9 +5,22 @@ disallowed-methods = [ { path = "std::thread::spawn", reason = "Prefer Tokio task APIs on async paths; document intentional dedicated OS threads with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, { path = "std::thread::Builder::spawn", reason = "Prefer Tokio task APIs on async paths; document intentional dedicated OS threads with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, { path = "std::process::Command::new", reason = "Prefer tokio::process::Command on Tokio paths; document intentional synchronous subprocesses with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, + { path = "std::io::stdin", reason = "Returns a blocking handle; prefer tokio::io::stdin on Tokio paths. Document intentional sync stdin with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, + { path = "std::io::stdout", reason = "Returns a blocking handle; prefer tokio::io::stdout on Tokio paths. Document intentional sync stdout with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, + { path = "std::io::stderr", reason = "Returns a blocking handle; prefer tokio::io::stderr on Tokio paths. Document intentional sync stderr with #[expect(clippy::disallowed_methods, reason = \"...\")]" }, { path = "reqwest::Client::new", reason = "Use fabro_http::http_client() or fabro_http::test_http_client()", allow-invalid = true }, { path = "reqwest::Client::builder", reason = "Use fabro_http::HttpClientBuilder::new()", allow-invalid = true }, { path = "reqwest::blocking::Client::new", reason = "Use fabro_http::blocking_http_client() or fabro_http::blocking_test_http_client()", allow-invalid = true }, { path = "reqwest::blocking::Client::builder", reason = "Use fabro_http::BlockingHttpClientBuilder::new()", allow-invalid = true }, { path = "reqwest::get", reason = "Build a fabro_http client and send the request explicitly", allow-invalid = true }, ] +disallowed-types = [ + { path = "std::io::Read", reason = "Blocking trait; prefer tokio::io::AsyncReadExt on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::io::Write", reason = "Blocking trait; prefer tokio::io::AsyncWriteExt on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::io::BufRead", reason = "Blocking trait; prefer tokio::io::AsyncBufReadExt on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::io::BufReader", reason = "Blocking buffered reader; prefer tokio::io::BufReader on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::io::BufWriter", reason = "Blocking buffered writer; prefer tokio::io::BufWriter on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::net::TcpStream", reason = "Blocking socket; prefer tokio::net::TcpStream on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::net::TcpListener", reason = "Blocking accept; prefer tokio::net::TcpListener on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, + { path = "std::net::UdpSocket", reason = "Blocking recv/send; prefer tokio::net::UdpSocket on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_types, reason = \"...\")]" }, +] diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index 30fb303ae..5515986a3 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -1,3 +1,8 @@ +#[expect( + clippy::disallowed_types, + reason = "CLI entry point writes to stdout/stderr; blocking std::io::Write is intentional and \ + scoped to the CLI binary, not to any library code used by Tokio services" +)] use std::io::{IsTerminal, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -137,6 +142,12 @@ fn is_auto_approved(level: PermissionLevel, category: &str) -> bool { } #[allow(clippy::print_stderr)] +#[expect( + clippy::disallowed_methods, + reason = "Interactive tool-approval prompt: blocking on stderr flush and stdin read_line is \ + the entire point. Invoked from an async tool hook, but the callback is a \ + user-gated pause point; concurrent tasks on the same worker are acceptable." +)] fn build_tool_approval( permissions: PermissionLevel, is_interactive: bool, @@ -442,6 +453,10 @@ pub async fn run_with_args_and_client( // Build tool approval callback let permissions = args.permissions.unwrap_or(PermissionLevel::ReadWrite); + #[expect( + clippy::disallowed_methods, + reason = "is_terminal() on stdin is a non-blocking fstat; no actual I/O performed" + )] let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve; let tool_approval = build_tool_approval(permissions, is_interactive, styles); let tool_hooks: Arc = Arc::new(ToolApprovalAdapter(tool_approval)); @@ -539,6 +554,13 @@ pub async fn run_with_args_and_client( OutputFormat::Json => { while let Ok(event) = rx.recv().await { if let Ok(json) = serde_json::to_string(&event) { + #[expect( + clippy::disallowed_methods, + reason = "FOLLOW-UP: blocking stdout inside tokio::spawn. Acceptable \ + today (low-volume event stream, CLI output), but should \ + migrate to tokio::io::stdout to avoid worker stalls under \ + pipe backpressure." + )] let mut stdout = std::io::stdout().lock(); let _ = writeln!(stdout, "{json}"); let _ = stdout.flush(); diff --git a/lib/crates/fabro-cli/src/commands/config/mod.rs b/lib/crates/fabro-cli/src/commands/config/mod.rs index 90b0a8586..a2db35feb 100644 --- a/lib/crates/fabro-cli/src/commands/config/mod.rs +++ b/lib/crates/fabro-cli/src/commands/config/mod.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `config` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `config` command: blocking std::io::stdout is the intended output mechanism" +)] + use std::io::Write; use std::path::Path; diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index a16001cc5..7f7b21e93 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `graph` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `graph` command: blocking std::io::stdout is the intended output mechanism" +)] + use std::io::Write; use anyhow::{Context, bail}; diff --git a/lib/crates/fabro-cli/src/commands/parse.rs b/lib/crates/fabro-cli/src/commands/parse.rs index 32fad0040..bc0a21306 100644 --- a/lib/crates/fabro-cli/src/commands/parse.rs +++ b/lib/crates/fabro-cli/src/commands/parse.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `parse` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `parse` command: blocking std::io::stdout is the intended output mechanism" +)] + use std::io::Write; use fabro_config::project::resolve_workflow; diff --git a/lib/crates/fabro-cli/src/commands/render_graph.rs b/lib/crates/fabro-cli/src/commands/render_graph.rs index 6e8ef81c3..3c4718742 100644 --- a/lib/crates/fabro-cli/src/commands/render_graph.rs +++ b/lib/crates/fabro-cli/src/commands/render_graph.rs @@ -1,3 +1,13 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `render-graph` command: reads DOT from stdin and writes rendered output \ + to stdout via blocking std::io" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `render-graph` command: uses blocking std::io::stdin/stdout" +)] + use std::io::{Read, Write}; const RENDER_ERROR_PREFIX: &str = "RENDER_ERROR:"; diff --git a/lib/crates/fabro-cli/src/commands/repo/init.rs b/lib/crates/fabro-cli/src/commands/repo/init.rs index 4d452ec40..04ae9be34 100644 --- a/lib/crates/fabro-cli/src/commands/repo/init.rs +++ b/lib/crates/fabro-cli/src/commands/repo/init.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `repo init` command: interactive prompts read from std::io::stdin" +)] + use std::path::PathBuf; use anyhow::{Context, Result, bail}; diff --git a/lib/crates/fabro-cli/src/commands/run/attach.rs b/lib/crates/fabro-cli/src/commands/run/attach.rs index d03bdd5f8..ac6b5d6e1 100644 --- a/lib/crates/fabro-cli/src/commands/run/attach.rs +++ b/lib/crates/fabro-cli/src/commands/run/attach.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run attach` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `run attach` command: writes to std::io::stdout/stderr directly" +)] + use std::io::{IsTerminal, Write}; #[cfg(test)] use std::path::Path; diff --git a/lib/crates/fabro-cli/src/commands/run/diff.rs b/lib/crates/fabro-cli/src/commands/run/diff.rs index 73a0b4214..2588d1adf 100644 --- a/lib/crates/fabro-cli/src/commands/run/diff.rs +++ b/lib/crates/fabro-cli/src/commands/run/diff.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run diff` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `run diff` command: writes diff output to std::io::stdout directly" +)] + use std::io::{self, IsTerminal, Write}; use anyhow::{Context, Result, bail}; diff --git a/lib/crates/fabro-cli/src/commands/run/logs.rs b/lib/crates/fabro-cli/src/commands/run/logs.rs index 121761d96..ea2bc10a5 100644 --- a/lib/crates/fabro-cli/src/commands/run/logs.rs +++ b/lib/crates/fabro-cli/src/commands/run/logs.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run logs` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `run logs` command: streams log lines to std::io::stdout directly" +)] + use std::fmt::Write as _; use std::io::{self, IsTerminal, Write}; use std::time::Duration; diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs index 4640061f5..2d7689213 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/mod.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_methods, + reason = "sync CLI run-progress renderer: writes to std::io::stderr directly" +)] + use fabro_types::RunEvent; mod event; @@ -43,6 +48,10 @@ impl ProgressUI { } #[cfg(test)] + #[expect( + clippy::disallowed_types, + reason = "test helper accepts a sync blocking writer to capture rendered output" + )] fn new_plain_test(out: Box, verbose: bool, colors: bool) -> Self { Self::with_renderer(ProgressRenderer::new_plain(out, colors), verbose) } @@ -412,6 +421,10 @@ impl ProgressUI { #[cfg(test)] mod tests { #![allow(clippy::absolute_paths, clippy::needless_pass_by_value)] + #![expect( + clippy::disallowed_types, + reason = "tests capture rendered output into a Vec via the std::io::Write trait" + )] use std::io::{self, Write}; use std::sync::{Arc, Mutex}; diff --git a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs index c522efac2..516e3a854 100644 --- a/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs +++ b/lib/crates/fabro-cli/src/commands/run/run_progress/renderer.rs @@ -1,3 +1,9 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI run-progress renderer: generic over blocking std::io::Write to render \ + terminal progress lines" +)] + use std::io::Write; use std::sync::Mutex; diff --git a/lib/crates/fabro-cli/src/commands/run/runner.rs b/lib/crates/fabro-cli/src/commands/run/runner.rs index 88a7a1f9d..9358ebe14 100644 --- a/lib/crates/fabro-cli/src/commands/run/runner.rs +++ b/lib/crates/fabro-cli/src/commands/run/runner.rs @@ -1,3 +1,9 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run` subprocess wrapper: reads server subprocess stdout line-by-line via \ + std::io::BufReader; not on a Tokio path" +)] + use std::io::{BufRead as StdBufRead, BufReader as StdBufReader}; use std::path::{Path, PathBuf}; use std::sync::Arc; diff --git a/lib/crates/fabro-cli/src/commands/run/wait.rs b/lib/crates/fabro-cli/src/commands/run/wait.rs index afdd89c77..7bd1848c0 100644 --- a/lib/crates/fabro-cli/src/commands/run/wait.rs +++ b/lib/crates/fabro-cli/src/commands/run/wait.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run wait` command: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `run wait` command: prints status to std::io::stdout directly" +)] + use std::io::Write; use anyhow::{Result, bail}; diff --git a/lib/crates/fabro-cli/src/commands/secret/set.rs b/lib/crates/fabro-cli/src/commands/secret/set.rs index e73924548..9812b3ec8 100644 --- a/lib/crates/fabro-cli/src/commands/secret/set.rs +++ b/lib/crates/fabro-cli/src/commands/secret/set.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `secret set` command: reads secret from stdin via blocking std::io::Read" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `secret set` command: reads secret from std::io::stdin" +)] + use std::io::{IsTerminal, Read as _}; use anyhow::{Context as _, Result, bail}; diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index e7235e94d..e7ef7c1bd 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -12,6 +12,7 @@ use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::{Home, dev_token, session_secret}; +use tokio::net::{TcpStream, UnixStream}; use tokio::process::Command as TokioCommand; use tokio::time; @@ -438,7 +439,7 @@ async fn execute_daemon( while elapsed < timeout { if let Some(record) = record::read_server_record(&record_path) { - if try_connect(&record.bind) { + if try_connect(&record.bind).await { if announce { let pid = child.id().unwrap_or_default(); maybe_warn_host_port_fallback(bind, &record.bind, printer); @@ -534,12 +535,15 @@ async fn acquire_lock(storage_dir: &Path) -> Result { Ok(lock_file) } -fn try_connect(bind: &Bind) -> bool { +async fn try_connect(bind: &Bind) -> bool { + let connect_timeout = Duration::from_millis(100); match bind { - Bind::Tcp(addr) => { - std::net::TcpStream::connect_timeout(addr, Duration::from_millis(100)).is_ok() - } - Bind::Unix(path) => std::os::unix::net::UnixStream::connect(path).is_ok(), + Bind::Tcp(addr) => time::timeout(connect_timeout, TcpStream::connect(addr)) + .await + .is_ok_and(|r| r.is_ok()), + Bind::Unix(path) => time::timeout(connect_timeout, UnixStream::connect(path)) + .await + .is_ok_and(|r| r.is_ok()), } } diff --git a/lib/crates/fabro-cli/src/commands/uninstall.rs b/lib/crates/fabro-cli/src/commands/uninstall.rs index 58ada15d6..8e2c0904e 100644 --- a/lib/crates/fabro-cli/src/commands/uninstall.rs +++ b/lib/crates/fabro-cli/src/commands/uninstall.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `uninstall` command: blocking std::io::Write is the intended output mechanism" +)] + use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index af09a1a37..ddfaaf659 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -1,3 +1,13 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI `upgrade` command: blocking std::io::Write and std::io::BufReader for \ + release download + prompt output" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `upgrade` command: interactive confirm prompt reads from std::io::stdin" +)] + use std::fs; use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; diff --git a/lib/crates/fabro-cli/src/commands/version.rs b/lib/crates/fabro-cli/src/commands/version.rs index 0ac3f999e..e924a5bb9 100644 --- a/lib/crates/fabro-cli/src/commands/version.rs +++ b/lib/crates/fabro-cli/src/commands/version.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `version` command: writes version info to std::io::stderr" +)] + use std::io::IsTerminal; use anyhow::Result; diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 9c2903ce7..e9158a3ec 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -367,8 +367,15 @@ async fn main_inner() -> (String, Result<()>) { })); match result { Ok(buf) => { - use std::io::Write; - std::io::stdout().write_all(&buf)?; + #[expect( + clippy::disallowed_types, + clippy::disallowed_methods, + reason = "sync CLI: stream shell completions to stdout" + )] + { + use std::io::Write; + std::io::stdout().write_all(&buf)?; + } } Err(_) => { anyhow::bail!( diff --git a/lib/crates/fabro-cli/src/shared/provider_auth.rs b/lib/crates/fabro-cli/src/shared/provider_auth.rs index 2c356a1f8..296a36234 100644 --- a/lib/crates/fabro-cli/src/shared/provider_auth.rs +++ b/lib/crates/fabro-cli/src/shared/provider_auth.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI: provider auth reads an API key from stdin via std::io::Read" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI: provider auth reads an API key from std::io::stdin" +)] + use std::io::Read; use std::sync::Arc; diff --git a/lib/crates/fabro-cli/src/shared/utilities.rs b/lib/crates/fabro-cli/src/shared/utilities.rs index e2e2e77d6..a58bb3dff 100644 --- a/lib/crates/fabro-cli/src/shared/utilities.rs +++ b/lib/crates/fabro-cli/src/shared/utilities.rs @@ -1,3 +1,12 @@ +#![expect( + clippy::disallowed_types, + reason = "sync CLI utilities: blocking std::io::Write is the intended output mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI utilities: blocking std::io::stdout is the intended output mechanism" +)] + use std::io::Write; use std::path::{Path, PathBuf}; use std::time::Duration; diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index d3c692ee2..0a9bccb98 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_types, + reason = "integration tests: read child-process stdout line-by-line via std::io::BufReader" +)] + use std::io::{BufRead, BufReader, Read}; use std::process::{Output, Stdio}; use std::sync::mpsc; diff --git a/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs b/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs index 3313dee01..22714fdb4 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs @@ -2,6 +2,10 @@ clippy::disallowed_methods, reason = "These CLI integration tests intentionally spawn the real fabro binary and stream DOT over stdio to verify the internal render subprocess contract." )] +#![expect( + clippy::disallowed_types, + reason = "integration tests write DOT to the spawned child's stdin via std::io::Write" +)] use std::io::Write; use std::process::{Command, Stdio}; diff --git a/lib/crates/fabro-cli/tests/it/cmd/runner.rs b/lib/crates/fabro-cli/tests/it/cmd/runner.rs index f92161c37..0a082b533 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/runner.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/runner.rs @@ -2,6 +2,10 @@ clippy::disallowed_methods, reason = "These CLI integration tests spawn real fabro worker subprocesses and observe their lifecycle." )] +#![expect( + clippy::disallowed_types, + reason = "integration tests read the spawned child's stdout via std::io::Read" +)] use std::io::Read; use std::process::{Child, ExitStatus, Output, Stdio}; diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs index 40e0196ca..83673a2e4 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs @@ -1,3 +1,9 @@ +#![expect( + clippy::disallowed_types, + reason = "integration test: occupies a fixed TCP port via sync std::net::TcpListener to \ + verify the server-start fallback path when the default port is unavailable" +)] + use std::process::Stdio; use std::sync::{Arc, Barrier}; use std::time::{Duration, Instant}; diff --git a/lib/crates/fabro-devcontainer/src/compose.rs b/lib/crates/fabro-devcontainer/src/compose.rs index 997d65efb..80450e848 100644 --- a/lib/crates/fabro-devcontainer/src/compose.rs +++ b/lib/crates/fabro-devcontainer/src/compose.rs @@ -211,6 +211,10 @@ pub(crate) fn parse_compose_multi( } #[cfg(test)] +#[expect( + clippy::disallowed_types, + reason = "test helpers write compose fixtures to temp files via sync std::io" +)] mod tests { use std::io::Write; diff --git a/lib/crates/fabro-interview/src/console.rs b/lib/crates/fabro-interview/src/console.rs index 311cc2f85..b7203d801 100644 --- a/lib/crates/fabro-interview/src/console.rs +++ b/lib/crates/fabro-interview/src/console.rs @@ -218,6 +218,12 @@ impl Interviewer for ConsoleInterviewer { async fn ask(&self, question: Question) -> Answer { // If stdin is a TTY, use dialoguer for interactive arrow-key navigation. // Otherwise, fall back to the line-based reader for piped input. + #[expect( + clippy::disallowed_methods, + reason = "is_terminal() on the std stdin handle is a non-blocking fstat check; no \ + actual I/O performed. The real blocking read runs inside spawn_blocking \ + below." + )] if std::io::stdin().is_terminal() { if let Some(ref context_text) = question.context_display { let rendered = self.styles.render_markdown(context_text); diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 9c1c2aab2..9711dcd93 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -725,6 +725,11 @@ fn server_bind_title(bind: &Bind) -> String { } #[cfg(test)] +#[expect( + clippy::disallowed_types, + reason = "tests reserve/probe ports via sync std::net::TcpListener; the async server under \ + test uses tokio::net::TcpListener separately" +)] mod tests { use std::path::PathBuf; use std::time::Duration; diff --git a/lib/crates/fabro-telemetry/src/spawn.rs b/lib/crates/fabro-telemetry/src/spawn.rs index cfedb0fd1..508e834b6 100644 --- a/lib/crates/fabro-telemetry/src/spawn.rs +++ b/lib/crates/fabro-telemetry/src/spawn.rs @@ -26,6 +26,12 @@ pub fn spawn_detached(args: &[&str], env: &[(&str, &str)], env_remove: &[&str]) #[cfg(unix)] #[allow(clippy::exit)] +#[expect( + clippy::disallowed_types, + clippy::disallowed_methods, + reason = "pre-fork cleanup: flush the blocking stdout/stderr buffers before double-forking \ + so the child does not inherit buffered data; sync std::io is required here" +)] fn spawn_detached_unix(args: &[&str], env: &[(&str, &str)], env_remove: &[&str]) { // Flush stdout/stderr before forking so the child process doesn't inherit // buffered data that would be flushed again on child exit, causing diff --git a/lib/crates/fabro-util/src/dev_token.rs b/lib/crates/fabro-util/src/dev_token.rs index 238a86f59..d192cd261 100644 --- a/lib/crates/fabro-util/src/dev_token.rs +++ b/lib/crates/fabro-util/src/dev_token.rs @@ -1,4 +1,8 @@ use std::fs; +#[expect( + clippy::disallowed_types, + reason = "sync atomic write of the local dev token file; not on an async path" +)] use std::io::Write as _; use std::path::Path; diff --git a/lib/crates/fabro-util/src/run_log.rs b/lib/crates/fabro-util/src/run_log.rs index 75ec04a73..51b8a5edd 100644 --- a/lib/crates/fabro-util/src/run_log.rs +++ b/lib/crates/fabro-util/src/run_log.rs @@ -1,3 +1,9 @@ +#![expect( + clippy::disallowed_types, + reason = "file-backed tracing sink: sync BufWriter is intentional; writes happen on a \ + dedicated per-event guard and are not in an async hot path" +)] + use std::io::{self, BufWriter, Write}; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/lib/crates/fabro-workflow/src/run_dump.rs b/lib/crates/fabro-workflow/src/run_dump.rs index 509c3b09f..96c9b3c0e 100644 --- a/lib/crates/fabro-workflow/src/run_dump.rs +++ b/lib/crates/fabro-workflow/src/run_dump.rs @@ -1,4 +1,8 @@ use std::collections::HashMap; +#[expect( + clippy::disallowed_types, + reason = "in-memory Vec::write_all for jsonl serialization; no filesystem or network I/O" +)] use std::io::Write; use std::path::{Component, Path, PathBuf}; diff --git a/test/twin/github/src/state.rs b/test/twin/github/src/state.rs index 21764dd50..2ebb3a156 100644 --- a/test/twin/github/src/state.rs +++ b/test/twin/github/src/state.rs @@ -250,6 +250,10 @@ pub struct AppState { /// Panics if openssl is not available or the key is invalid. This is acceptable /// because the fake server is test infrastructure and openssl is already /// required by the test helpers that generate key pairs. +#[expect( + clippy::disallowed_types, + reason = "test harness: piping private key PEM into openssl via sync std::io is intentional" +)] pub fn derive_public_key_pem(private_key_pem: &str) -> String { use std::io::Write; use std::process::{Command, Stdio};