ci: add GitHub Actions workflows with semantic-release auto-versioning

chore: fix all 702 clippy warnings across codebase
- auto-fix 475 via cargo clippy --fix
- fix remaining 227 manually: uninlined_format_args, redundant_closure, match_same_arms,
  underscore_binding, format_push_string, items_after_statements, needless_pass_by_value,
  clone_on_copy, case_sensitive_extension, single_match/let-else, write_with_newline,
  and other clippy lints
This commit is contained in:
asepharyana
2026-07-13 08:12:12 +07:00
parent be921d6836
commit 29a9fae3f6
79 changed files with 826 additions and 904 deletions
+7 -7
View File
@@ -47,24 +47,24 @@ impl Tool for Delete {
}
let metadata = path.metadata()
.map_err(|e| anyhow!("failed to read metadata for '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read metadata for '{rel}': {e}"))?;
if metadata.is_dir() {
let is_empty = fs::read_dir(&path)
.map_err(|e| anyhow!("failed to read directory '{}': {}", rel, e))?
.map_err(|e| anyhow!("failed to read directory '{rel}': {e}"))?
.next()
.is_none();
if is_empty {
fs::remove_dir(&path)
.map_err(|e| anyhow!("failed to remove directory '{}': {}", rel, e))?;
Ok(format!("removed empty directory {}", rel))
.map_err(|e| anyhow!("failed to remove directory '{rel}': {e}"))?;
Ok(format!("removed empty directory {rel}"))
} else {
anyhow::bail!("directory '{}' is not empty (refusing to delete)", rel);
anyhow::bail!("directory '{rel}' is not empty (refusing to delete)");
}
} else {
fs::remove_file(&path)
.map_err(|e| anyhow!("failed to delete '{}': {}", rel, e))?;
Ok(format!("deleted {}", rel))
.map_err(|e| anyhow!("failed to delete '{rel}': {e}"))?;
Ok(format!("deleted {rel}"))
}
}
}
+9 -11
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Tool: `edit` — replace a substring in a file with a new string.
use std::fs;
@@ -70,25 +71,24 @@ impl Tool for Edit {
anyhow::bail!("'old' must be a non-empty string; use 'write' to replace entire file contents");
}
let check_matches = check_graduated_checks(&rel, &new_str, &ctx.graduated_checks);
let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false);
let replace_all = args.get("replace_all").and_then(serde_json::Value::as_bool).unwrap_or(false);
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
if !path.exists() {
anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display());
}
if path.is_dir() {
anyhow::bail!("'{}' is a directory, not a file", rel);
anyhow::bail!("'{rel}' is a directory, not a file");
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read '{rel}': {e}"))?;
if !content.contains(&old) {
anyhow::bail!("old string not found in '{}'", rel);
anyhow::bail!("old string not found in '{rel}'");
}
if !replace_all {
let count = content.matches(&old).count();
if count > 1 {
anyhow::bail!(
"old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.",
count, rel
"old string appears {count} times in '{rel}'. Set replace_all=true to replace all occurrences, or provide a more specific match."
);
}
}
@@ -98,7 +98,7 @@ impl Tool for Edit {
content.replacen(&old, &new_str, 1)
};
fs::write(&path, &new_content)
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to write '{rel}': {e}"))?;
let bytes_diff = if new_content.len() > content.len() {
new_content.len() - content.len()
} else {
@@ -108,10 +108,8 @@ impl Tool for Edit {
// Never fail the edit because of this — LSP errors are surfaced as a
// trailing annotation on the success message instead.
let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() {
match lsp.did_change_file(&path) {
Ok(()) => String::new(),
Err(e) => format!(" (LSP: {})", e),
}
lsp.did_change_file(&path);
String::new()
} else {
String::new()
};
+6 -6
View File
@@ -12,8 +12,8 @@ use anyhow::{Result, anyhow};
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
args.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| anyhow!("missing required argument: {}", name))
.map(std::string::ToString::to_string)
.ok_or_else(|| anyhow!("missing required argument: {name}"))
}
/// Produce a user-friendly diagnostic string when a path doesn't resolve or exist.
@@ -25,17 +25,17 @@ pub fn arg_str(args: &Value, name: &str) -> Result<String> {
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let in_ws = ctx.workspaces.iter().any(|w| {
let wc = w.canonicalize().unwrap_or_else(|_| w.to_path_buf());
let wc = w.canonicalize().unwrap_or_else(|_| w.clone());
canon.starts_with(&wc)
});
if !in_ws {
if in_ws {
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
} else {
format!(
"path '{}' is outside all workspace roots. Workspace roots: {}",
rel,
ctx.workspaces.iter().map(|w| w.display().to_string()).collect::<Vec<_>>().join(", ")
)
} else {
format!("path '{}' does not exist (resolved to {})", rel, canon.display())
}
}
+4 -3
View File
@@ -1,3 +1,4 @@
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
//! Tool: `read` — display file contents with line numbers.
use std::fs;
@@ -47,7 +48,7 @@ impl Tool for Read {
/// exist; a "is a directory" message if the path points at a directory.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
let limit = args.get("limit").and_then(serde_json::Value::as_u64).map(|v| v as usize);
let path: PathBuf = match resolve_path(&ctx.workspaces, &rel) {
Ok(p) => p,
Err(_e) => return Ok(not_found_help(ctx, &PathBuf::from(&rel), &rel)),
@@ -56,10 +57,10 @@ impl Tool for Read {
return Ok(not_found_help(ctx, &path, &rel));
}
if path.is_dir() {
return Ok(format!("'{}' is a directory, not a file. Use ls or glob to list directory contents.", rel));
return Ok(format!("'{rel}' is a directory, not a file. Use ls or glob to list directory contents."));
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to read '{rel}': {e}"))?;
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let take = limit.unwrap_or(total).min(total);
+4 -6
View File
@@ -60,18 +60,16 @@ impl Tool for Write {
let path = resolve_path(&ctx.workspaces, &rel)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow!("failed to create parent directories for '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to create parent directories for '{rel}': {e}"))?;
}
fs::write(&path, &content)
.map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?;
.map_err(|e| anyhow!("failed to write '{rel}': {e}"))?;
// Notify the LSP server of the on-disk change so diagnostics stay in
// sync. Never fails the write itself: a lock failure or LSP error is
// folded into the returned message instead of propagated as an Err.
let lsp_note = if let Ok(mut lsp) = ctx.lsp_manager.lock() {
match lsp.did_change_file(&path) {
Ok(()) => String::new(),
Err(e) => format!(" (LSP: {})", e),
}
lsp.did_change_file(&path);
String::new()
} else {
String::new()
};