Use fs_err and forbid raw std::fs methods via clippy
fs_err includes file paths in error messages automatically, so manual
with_context wrappers restating the path are removed.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/Cargo.lock b/Cargo.lock
index 650c6d5..dafba70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -73,6 +73,12 @@ version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
[[package]]
name = "backtrace"
version = "0.3.76"
@@ -183,6 +189,15 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "fs-err"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "gimli"
version = "0.32.3"
@@ -320,6 +335,7 @@ version = "0.1.0"
dependencies = [
"clap",
"color-eyre",
+ "fs-err",
"serde",
"serde_json",
"serde_yml",
diff --git a/Cargo.toml b/Cargo.toml
index 7b94347..2378f7f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
clap = { version = "*", features = ["derive"] }
color-eyre = "*"
+fs-err = "*"
tracing = "*"
tracing-subscriber = "*"
serde = { version = "*", features = ["derive"] }
diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 0000000..e8f917e
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,10 @@
+disallowed-methods = [
+ { path = "std::fs::read_to_string", reason = "use fs_err::read_to_string" },
+ { path = "std::fs::write", reason = "use fs_err::write" },
+ { path = "std::fs::read_dir", reason = "use fs_err::read_dir" },
+ { path = "std::fs::remove_file", reason = "use fs_err::remove_file" },
+ { path = "std::fs::remove_dir_all", reason = "use fs_err::remove_dir_all" },
+ { path = "std::fs::create_dir_all", reason = "use fs_err::create_dir_all" },
+ { path = "std::fs::read_link", reason = "use fs_err::read_link" },
+ { path = "std::fs::symlink_metadata", reason = "use fs_err::symlink_metadata" },
+]
diff --git a/src/config.rs b/src/config.rs
index 34f09bc..4a2d891 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -24,8 +24,7 @@ pub fn load() -> Result<Vec<Plugin>> {
);
}
- let mut entries: Vec<_> = std::fs::read_dir(&config_dir)
- .with_context(|| format!("failed to read config dir: {}", config_dir.display()))?
+ let mut entries: Vec<_> = fs_err::read_dir(&config_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "yml"))
.collect();
@@ -41,8 +40,7 @@ pub fn load() -> Result<Vec<Plugin>> {
for entry in &entries {
let path = entry.path();
- let contents = std::fs::read_to_string(&path)
- .with_context(|| format!("failed to read {}", path.display()))?;
+ let contents = fs_err::read_to_string(&path)?;
let file: ConfigFile = serde_yml::from_str(&contents)
.with_context(|| format!("failed to parse {}", path.display()))?;
diff --git a/src/git.rs b/src/git.rs
index 44e80b6..5daf332 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -32,8 +32,7 @@ pub fn clone_or_fetch(repo_path: &Path, src: &str) -> Result<()> {
bail!("git fetch failed for {}", src);
}
} else {
- std::fs::create_dir_all(repo_path)
- .with_context(|| format!("failed to create {}", repo_path.display()))?;
+ fs_err::create_dir_all(repo_path)?;
let status = Command::new("git")
.args(["clone", "--quiet", "--bare", src, "."])
.current_dir(repo_path)
@@ -91,7 +90,7 @@ pub fn ensure_worktree(repo_path: &Path, rev: &str) -> Result<PathBuf> {
bail!("git checkout failed for {} in {}", rev, wt_path.display());
}
} else {
- std::fs::create_dir_all(wt_path.parent().unwrap())?;
+ fs_err::create_dir_all(wt_path.parent().unwrap())?;
let status = Command::new("git")
.args([
"worktree",
@@ -139,7 +138,7 @@ fn visit_worktree_dirs(
dir: &Path,
used: &std::collections::HashMap<PathBuf, std::collections::HashSet<String>>,
) -> Result<()> {
- let entries = match std::fs::read_dir(dir) {
+ let entries = match fs_err::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
@@ -157,7 +156,7 @@ fn visit_worktree_dirs(
let repo_path = path.parent().unwrap();
let used_revs = used.get(repo_path);
- for wt_entry in std::fs::read_dir(&path)? {
+ for wt_entry in fs_err::read_dir(&path)? {
let wt_entry = wt_entry?;
let wt_name = wt_entry.file_name();
let wt_name_str = wt_name.to_string_lossy();
@@ -173,7 +172,7 @@ fn visit_worktree_dirs(
.status();
// If that fails, just remove the directory
if wt_path.exists() {
- std::fs::remove_dir_all(&wt_path)?;
+ fs_err::remove_dir_all(&wt_path)?;
}
info!("pruned worktree: {}", wt_path.display());
}
diff --git a/src/lockfile.rs b/src/lockfile.rs
index cbe306f..dd47291 100644
--- a/src/lockfile.rs
+++ b/src/lockfile.rs
@@ -18,8 +18,7 @@ pub struct LockedPlugin {
pub fn load() -> Result<Lockfile> {
let path = crate::paths::lockfile_path()?;
- let contents = std::fs::read_to_string(&path)
- .with_context(|| format!("failed to read lockfile: {}", path.display()))?;
+ let contents = fs_err::read_to_string(&path)?;
let lockfile: Lockfile = serde_yml::from_str(&contents)
.with_context(|| format!("failed to parse lockfile: {}", path.display()))?;
Ok(lockfile)
@@ -28,7 +27,6 @@ pub fn load() -> Result<Lockfile> {
pub fn save(lockfile: &Lockfile) -> Result<()> {
let path = crate::paths::lockfile_path()?;
let contents = serde_yml::to_string(lockfile).context("failed to serialize lockfile")?;
- std::fs::write(&path, contents)
- .with_context(|| format!("failed to write lockfile: {}", path.display()))?;
+ fs_err::write(&path, contents)?;
Ok(())
}
diff --git a/src/main.rs b/src/main.rs
index f347ff1..7190421 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+#![deny(clippy::disallowed_methods)]
+
mod config;
mod git;
mod lockfile;
@@ -193,7 +195,7 @@ fn do_install() -> Result<()> {
);
}
- for entry in std::fs::read_dir(&skills_source)? {
+ for entry in fs_err::read_dir(&skills_source)? {
let entry = entry?;
if !entry.path().is_dir() {
continue;
@@ -204,12 +206,7 @@ fn do_install() -> Result<()> {
// Remove existing symlink if present
if symlink_path.symlink_metadata().is_ok() {
- std::fs::remove_file(&symlink_path).with_context(|| {
- format!(
- "failed to remove existing symlink: {}",
- symlink_path.display()
- )
- })?;
+ fs_err::remove_file(&symlink_path)?;
}
std::os::unix::fs::symlink(entry.path(), &symlink_path).with_context(|| {
@@ -229,7 +226,7 @@ fn do_install() -> Result<()> {
}
}
- for entry in std::fs::read_dir(&skills_dir)? {
+ for entry in fs_err::read_dir(&skills_dir)? {
let entry = entry?;
let path = entry.path();
@@ -242,11 +239,11 @@ fn do_install() -> Result<()> {
let name_str = name.to_string_lossy();
// Only touch symlinks pointing into our cache
- if let Ok(target) = std::fs::read_link(&path)
+ if let Ok(target) = fs_err::read_link(&path)
&& target.starts_with(&cache_dir)
&& !installed_skills.contains(&name)
{
- std::fs::remove_file(&path)?;
+ fs_err::remove_file(&path)?;
info!("removed stale symlink: {}", name_str);
}
}
diff --git a/src/marketplace.rs b/src/marketplace.rs
index f4034f8..4047a59 100644
--- a/src/marketplace.rs
+++ b/src/marketplace.rs
@@ -24,8 +24,7 @@ pub fn find_plugin_source(
) -> Result<String> {
let manifest_path = repo_path.join(marketplace_override.unwrap_or(DEFAULT_PATH));
- let contents = std::fs::read_to_string(&manifest_path)
- .with_context(|| format!("failed to read {}", manifest_path.display()))?;
+ let contents = fs_err::read_to_string(&manifest_path)?;
let marketplace: Marketplace = serde_json::from_str(&contents)
.with_context(|| format!("failed to parse {}", manifest_path.display()))?;
diff --git a/src/paths.rs b/src/paths.rs
index de884f0..be8d845 100644
--- a/src/paths.rs
+++ b/src/paths.rs
@@ -28,7 +28,7 @@ pub fn lockfile_path() -> Result<PathBuf> {
pub fn skills_dir() -> Result<PathBuf> {
let path = home_dir().join(".claude").join("skills");
- std::fs::create_dir_all(&path)
+ fs_err::create_dir_all(&path)
.with_context(|| format!("failed to create skills dir: {}", path.display()))?;
Ok(path)
}