feat(validation): replace rustfmt toggle with cargo check

This commit is contained in:
James Peter
2025-09-22 08:10:50 +10:00
parent 5deb7347c4
commit cb9a2134aa
7 changed files with 412 additions and 84 deletions
+1 -1
View File
@@ -919,7 +919,7 @@ impl Session {
"markdownlint" => tools.markdownlint = Some(enable),
"hadolint" => tools.hadolint = Some(enable),
"yamllint" => tools.yamllint = Some(enable),
"rustfmt" => tools.rustfmt = Some(enable),
"cargo-check" => tools.cargo_check = Some(enable),
"shfmt" => tools.shfmt = Some(enable),
"prettier" => tools.prettier = Some(enable),
_ => {}
+2 -1
View File
@@ -270,7 +270,8 @@ pub struct ValidationTools {
pub markdownlint: Option<bool>,
pub hadolint: Option<bool>,
pub yamllint: Option<bool>,
pub rustfmt: Option<bool>,
#[serde(rename = "cargo-check")]
pub cargo_check: Option<bool>,
pub shfmt: Option<bool>,
pub prettier: Option<bool>,
}
+303 -55
View File
@@ -2,9 +2,11 @@ use crate::config_types::{GithubConfig, ValidationConfig};
use crate::workflow_validation::maybe_run_actionlint;
use codex_apply_patch::{ApplyPatchAction, ApplyPatchFileChange};
use serde_json as json;
use std::collections::{BTreeSet, HashSet};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use tempfile::TempDir;
#[derive(Debug, Clone)]
@@ -36,20 +38,23 @@ pub fn run_patch_harness(
// 1) Built-in structural parses (JSON/TOML/YAML).
for (path, change) in action.changes() {
let (ext, contents_opt) = match change {
ApplyPatchFileChange::Add { content } => (path.extension().and_then(|e| e.to_str()), Some(content)),
ApplyPatchFileChange::Update { new_content, .. } => (path.extension().and_then(|e| e.to_str()), Some(new_content)),
ApplyPatchFileChange::Delete { .. } => (path.extension().and_then(|e| e.to_str()), None),
let (analysis_path, contents_opt) = match change {
ApplyPatchFileChange::Add { content } => (path.as_path(), Some(content)),
ApplyPatchFileChange::Update { new_content, move_path, .. } => (
move_path.as_ref().map_or(path.as_path(), |dest| dest.as_path()),
Some(new_content),
),
ApplyPatchFileChange::Delete { .. } => (path.as_path(), None),
};
let Some(contents) = contents_opt else { continue };
match ext.unwrap_or("") {
match analysis_path.extension().and_then(|e| e.to_str()).unwrap_or("") {
"json" => {
record_ran("json-parse");
if let Err(err) = json::from_str::<json::Value>(contents) {
findings.push(HarnessFinding {
tool: "json-parse".to_string(),
file: Some(path.clone()),
file: Some(analysis_path.to_path_buf()),
message: format!("invalid JSON: {err}"),
});
}
@@ -59,7 +64,7 @@ pub fn run_patch_harness(
if let Err(err) = toml::from_str::<toml::Value>(contents) {
findings.push(HarnessFinding {
tool: "toml-parse".to_string(),
file: Some(path.clone()),
file: Some(analysis_path.to_path_buf()),
message: format!("invalid TOML: {err}"),
});
}
@@ -69,7 +74,7 @@ pub fn run_patch_harness(
if let Err(err) = serde_yaml::from_str::<serde_yaml::Value>(contents) {
findings.push(HarnessFinding {
tool: "yaml-parse".to_string(),
file: Some(path.clone()),
file: Some(analysis_path.to_path_buf()),
message: format!("invalid YAML: {err}"),
});
}
@@ -96,22 +101,32 @@ pub fn run_patch_harness(
let temp = TempDir::new().ok()?;
let staged_root = temp.path();
let mut changed_paths: Vec<PathBuf> = Vec::new();
for (path, change) in action.changes() {
let rel = path.strip_prefix(cwd).unwrap_or(path);
let dest = staged_root.join(rel);
let _ = fs::create_dir_all(dest.parent().unwrap_or(staged_root));
match change {
ApplyPatchFileChange::Add { content } | ApplyPatchFileChange::Update { new_content: content, .. } => {
if let Ok(mut file) = fs::File::create(&dest) {
let _ = file.write_all(content.as_bytes());
ApplyPatchFileChange::Add { content } => {
if let Some(rel) = stage_file(staged_root, cwd, path, content) {
changed_paths.push(rel);
}
}
ApplyPatchFileChange::Update { new_content, move_path, .. } => {
let dest_path = move_path.as_ref().unwrap_or(path);
if let Some(rel) = stage_file(staged_root, cwd, dest_path, new_content) {
changed_paths.push(rel);
}
if move_path.is_some() && move_path.as_ref().map(|p| p.as_path()) != Some(path.as_path()) {
remove_staged_file(staged_root, cwd, path);
}
}
ApplyPatchFileChange::Delete { .. } => {
let _ = fs::remove_file(&dest);
remove_staged_file(staged_root, cwd, path);
}
}
}
changed_paths.sort();
changed_paths.dedup();
let is_allowed = |tool: &str| allow.is_empty() || allow.iter().any(|entry| entry == tool);
let run_tool = |tool: &str, args: &[&str], files: &[PathBuf]| -> Vec<HarnessFinding> {
if files.is_empty() || !is_allowed(tool) {
@@ -122,33 +137,23 @@ pub fn run_patch_harness(
cmd.current_dir(staged_root);
cmd.args(args);
cmd.args(files);
let output = run_with_timeout(cmd, timeout);
output
.into_iter()
.flat_map(|(stdout, stderr)| {
let mut lines: Vec<String> = Vec::new();
if !stdout.is_empty() {
lines.extend(String::from_utf8_lossy(&stdout).lines().map(|s| s.to_string()));
}
if !stderr.is_empty() {
lines.extend(String::from_utf8_lossy(&stderr).lines().map(|s| s.to_string()));
}
lines
})
.map(|message| HarnessFinding { tool: tool.to_string(), file: None, message })
.collect()
match run_with_timeout(cmd, timeout) {
Some(output) => collect_output_lines(&output.stdout, &output.stderr)
.into_iter()
.map(|message| HarnessFinding { tool: tool.to_string(), file: None, message })
.collect(),
None => vec![HarnessFinding {
tool: tool.to_string(),
file: None,
message: format!("{tool} timed out after {timeout} second(s)"),
}],
}
};
let changed_paths: Vec<&Path> = action
.changes()
.iter()
.filter_map(|(path, change)| matches!(change, ApplyPatchFileChange::Add { .. } | ApplyPatchFileChange::Update { .. }).then_some(path.as_path()))
.collect();
let shell_scripts: Vec<PathBuf> = changed_paths
.iter()
.filter(|path| is_shell_script(path))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.filter(|path| is_shell_script(staged_root, path))
.cloned()
.collect();
if cfg.tools.shellcheck.unwrap_or(true) && !shell_scripts.is_empty() {
if which(Path::new("shellcheck")).is_some() {
@@ -160,7 +165,7 @@ pub fn run_patch_harness(
let markdown_files: Vec<PathBuf> = changed_paths
.iter()
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("md"))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.cloned()
.collect();
if cfg.tools.markdownlint.unwrap_or(true) && !markdown_files.is_empty() {
if which(Path::new("markdownlint")).is_some() || which(Path::new("markdownlint-cli2")).is_some() {
@@ -176,7 +181,7 @@ pub fn run_patch_harness(
let docker_files: Vec<PathBuf> = changed_paths
.iter()
.filter(|path| is_dockerfile(path))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.cloned()
.collect();
if cfg.tools.hadolint.unwrap_or(true) && !docker_files.is_empty() {
if which(Path::new("hadolint")).is_some() {
@@ -188,7 +193,7 @@ pub fn run_patch_harness(
let yaml_files: Vec<PathBuf> = changed_paths
.iter()
.filter(|path| matches!(path.extension().and_then(|ext| ext.to_str()), Some("yml" | "yaml")))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.cloned()
.collect();
if cfg.tools.yamllint.unwrap_or(true) && !yaml_files.is_empty() {
if which(Path::new("yamllint")).is_some() {
@@ -200,14 +205,8 @@ pub fn run_patch_harness(
let rust_files: Vec<PathBuf> = changed_paths
.iter()
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.cloned()
.collect();
if cfg.tools.rustfmt.unwrap_or(true) && !rust_files.is_empty() {
if which(Path::new("rustfmt")).is_some() {
record_ran("rustfmt");
}
findings.extend(run_tool("rustfmt", &["--check"], &rust_files));
}
if cfg.tools.shfmt.unwrap_or(true) && !shell_scripts.is_empty() {
if which(Path::new("shfmt")).is_some() {
@@ -226,7 +225,7 @@ pub fn run_patch_harness(
.and_then(|ext| ext.to_str())
.map(|ext| prettier_exts.contains(&ext))
.unwrap_or(false))
.map(|path| path.strip_prefix(cwd).unwrap_or(path).to_path_buf())
.cloned()
.collect();
if cfg.tools.prettier.unwrap_or(true) && !prettier_files.is_empty() {
if which(Path::new("prettier")).is_some() {
@@ -235,6 +234,74 @@ pub fn run_patch_harness(
findings.extend(run_tool("prettier", &["--check"], &prettier_files));
}
if cfg.tools.cargo_check.unwrap_or(true) && !rust_files.is_empty() {
if which(Path::new("cargo")).is_none() {
findings.push(HarnessFinding {
tool: "cargo-check".to_string(),
file: None,
message: "cargo executable not found; install the Rust toolchain".to_string(),
});
} else {
match WorkspaceOverlay::apply(action) {
Ok(overlay) => {
let manifests = collect_rust_manifests(cwd, &rust_files);
let rust_timeout = timeout.max(30);
for manifest in manifests {
let label = manifest
.strip_prefix(cwd)
.unwrap_or(&manifest)
.display()
.to_string();
let mut cmd = std::process::Command::new("cargo");
cmd.current_dir(cwd);
cmd.arg("check");
cmd.arg("--quiet");
cmd.arg("--all-targets");
cmd.arg("--manifest-path");
cmd.arg(manifest.to_string_lossy().to_string());
cmd.env("RUSTFLAGS", "-Dwarnings");
match run_with_timeout(cmd, rust_timeout) {
Some(output) => {
record_ran(&format!("cargo-check({label})"));
if output.status.map_or(true, |status| !status.success()) {
let mut lines = collect_output_lines(&output.stdout, &output.stderr);
if lines.is_empty() {
lines.push("cargo check failed (no output)".to_string());
}
for line in lines.into_iter().take(24) {
findings.push(HarnessFinding {
tool: format!("cargo-check({label})"),
file: None,
message: line,
});
}
}
}
None => {
findings.push(HarnessFinding {
tool: format!("cargo-check({label})"),
file: None,
message: format!(
"cargo check timed out after {rust_timeout} second(s)"
),
});
}
}
}
drop(overlay);
}
Err(err) => {
findings.push(HarnessFinding {
tool: "cargo-check".to_string(),
file: None,
message: format!("failed to stage workspace for cargo check: {err}"),
});
}
}
}
}
if findings.is_empty() && ran.is_empty() {
None
} else {
@@ -242,11 +309,12 @@ pub fn run_patch_harness(
}
}
fn is_shell_script(path: &Path) -> bool {
match path.extension().and_then(|ext| ext.to_str()) {
fn is_shell_script(staged_root: &Path, relative: &Path) -> bool {
match relative.extension().and_then(|ext| ext.to_str()) {
Some("sh") => true,
_ => {
std::fs::read(path)
let staged = staged_root.join(relative);
std::fs::read(staged)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.map(|contents| contents.starts_with("#!/"))
@@ -277,7 +345,7 @@ fn which(exe: &Path) -> Option<PathBuf> {
None
}
fn run_with_timeout(mut cmd: std::process::Command, timeout_secs: u64) -> Option<(Vec<u8>, Vec<u8>)> {
fn run_with_timeout(mut cmd: std::process::Command, timeout_secs: u64) -> Option<CommandCapture> {
use std::process::Stdio;
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let Ok(mut child) = cmd.spawn() else { return None };
@@ -285,7 +353,6 @@ fn run_with_timeout(mut cmd: std::process::Command, timeout_secs: u64) -> Option
let start = std::time::Instant::now();
loop {
if let Some(status) = child.try_wait().ok().flatten() {
let _ = status; // success/failure surfaces through captured output
let mut stdout = Vec::new();
let mut stderr = Vec::new();
if let Some(mut out) = child.stdout.take() {
@@ -294,7 +361,7 @@ fn run_with_timeout(mut cmd: std::process::Command, timeout_secs: u64) -> Option
if let Some(mut err) = child.stderr.take() {
let _ = std::io::Read::read_to_end(&mut err, &mut stderr);
}
return Some((stdout, stderr));
return Some(CommandCapture { status: Some(status), stdout, stderr });
}
if start.elapsed().as_secs() >= timeout_secs {
@@ -305,3 +372,184 @@ fn run_with_timeout(mut cmd: std::process::Command, timeout_secs: u64) -> Option
std::thread::sleep(std::time::Duration::from_millis(40));
}
}
fn stage_file(staged_root: &Path, cwd: &Path, path: &Path, contents: &str) -> Option<PathBuf> {
let relative = path.strip_prefix(cwd).ok()?;
let dest = staged_root.join(relative);
if let Some(parent) = dest.parent() {
let _ = fs::create_dir_all(parent);
}
if let Ok(mut file) = fs::File::create(&dest) {
if file.write_all(contents.as_bytes()).is_ok() {
return Some(relative.to_path_buf());
}
}
None
}
fn remove_staged_file(staged_root: &Path, cwd: &Path, path: &Path) {
if let Ok(relative) = path.strip_prefix(cwd) {
let dest = staged_root.join(relative);
let _ = fs::remove_file(dest);
}
}
fn collect_output_lines(stdout: &[u8], stderr: &[u8]) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
if !stdout.is_empty() {
lines.extend(String::from_utf8_lossy(stdout).lines().map(|s| s.to_string()));
}
if !stderr.is_empty() {
lines.extend(String::from_utf8_lossy(stderr).lines().map(|s| s.to_string()));
}
lines.retain(|line| !line.trim().is_empty());
lines
}
fn collect_rust_manifests(cwd: &Path, rust_files: &[PathBuf]) -> Vec<PathBuf> {
let mut manifests = BTreeSet::new();
for relative in rust_files {
if let Some(manifest) = find_manifest(cwd, relative) {
manifests.insert(manifest);
}
}
manifests.into_iter().collect()
}
fn find_manifest(cwd: &Path, relative: &Path) -> Option<PathBuf> {
let absolute = cwd.join(relative);
let mut current = absolute.parent()?;
loop {
let candidate = current.join("Cargo.toml");
if candidate.exists() {
return Some(candidate);
}
if current == cwd {
break;
}
current = current.parent()?;
}
None
}
struct WorkspaceOverlay {
backups: Vec<(PathBuf, Option<Vec<u8>>)>,
created_dirs: Vec<PathBuf>,
}
impl WorkspaceOverlay {
fn apply(action: &ApplyPatchAction) -> std::io::Result<Self> {
let mut overlay = WorkspaceOverlay { backups: Vec::new(), created_dirs: Vec::new() };
let mut seen: HashSet<PathBuf> = HashSet::new();
for (path, change) in action.changes() {
match change {
ApplyPatchFileChange::Add { content } => {
overlay.write_file(path, content, &mut seen)?;
}
ApplyPatchFileChange::Update { new_content, move_path, .. } => {
if let Some(dest) = move_path {
overlay.write_file(dest, new_content, &mut seen)?;
if dest != path {
overlay.remove_file(path, &mut seen)?;
}
} else {
overlay.write_file(path, new_content, &mut seen)?;
}
}
ApplyPatchFileChange::Delete { .. } => {
overlay.remove_file(path, &mut seen)?;
}
}
}
Ok(overlay)
}
fn write_file(
&mut self,
path: &Path,
contents: &str,
seen: &mut HashSet<PathBuf>,
) -> std::io::Result<()> {
self.backup_if_needed(path, seen)?;
if let Some(parent) = path.parent() {
self.ensure_dir(parent)?;
}
let mut file = fs::File::create(path)?;
file.write_all(contents.as_bytes())?;
Ok(())
}
fn remove_file(&mut self, path: &Path, seen: &mut HashSet<PathBuf>) -> std::io::Result<()> {
self.backup_if_needed(path, seen)?;
if let Err(err) = fs::remove_file(path) {
if err.kind() != std::io::ErrorKind::NotFound {
return Err(err);
}
}
Ok(())
}
fn backup_if_needed(&mut self, path: &Path, seen: &mut HashSet<PathBuf>) -> std::io::Result<()> {
if !seen.insert(path.to_path_buf()) {
return Ok(());
}
let original = match fs::read(path) {
Ok(bytes) => Some(bytes),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => return Err(err),
};
self.backups.push((path.to_path_buf(), original));
Ok(())
}
fn ensure_dir(&mut self, dir: &Path) -> std::io::Result<()> {
if dir.exists() {
return Ok(());
}
let mut to_create: Vec<PathBuf> = Vec::new();
let mut current = dir.to_path_buf();
while !current.exists() {
to_create.push(current.clone());
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
for path in to_create.iter().rev() {
fs::create_dir(path)?;
self.created_dirs.push(path.clone());
}
Ok(())
}
}
impl Drop for WorkspaceOverlay {
fn drop(&mut self) {
for (path, original) in self.backups.iter().rev() {
match original {
Some(bytes) => {
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::File::create(path).and_then(|mut file| file.write_all(bytes));
}
None => {
let _ = fs::remove_file(path);
}
}
}
for dir in self.created_dirs.iter().rev() {
let _ = fs::remove_dir(dir);
}
}
}
struct CommandCapture {
status: Option<ExitStatus>,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
+93 -18
View File
@@ -29,6 +29,14 @@ fn is_workflow_path(path: &Path, cwd: &Path) -> bool {
false
}
fn is_in_github_dir(path: &Path, cwd: &Path) -> bool {
let absolute = if path.is_absolute() { path.to_path_buf() } else { cwd.join(path) };
if let Ok(relative) = absolute.strip_prefix(cwd) {
return relative.starts_with(".github");
}
false
}
/// Run `actionlint` against a temporary workspace containing the proposed
/// workflow changes. Returns diagnostic lines when the tool is available and
/// produced output.
@@ -64,29 +72,46 @@ pub fn maybe_run_actionlint(
let temp = TempDir::new().ok()?;
let temp_root = temp.path();
let temp_workflows = temp_root.join(".github/workflows");
let temp_github = temp_root.join(".github");
let temp_workflows = temp_github.join("workflows");
let _ = fs::create_dir_all(&temp_workflows);
let source_workflows = cwd.join(".github/workflows");
if source_workflows.exists() {
let _ = copy_dir_flat(&source_workflows, &temp_workflows);
let source_github = cwd.join(".github");
if source_github.exists() {
let _ = copy_dir_recursive(&source_github, &temp_github);
}
for (path, change) in action.changes() {
if !is_workflow_path(path, cwd) {
continue;
}
let relative = path.strip_prefix(cwd).unwrap_or(path);
let destination = temp_root.join(relative);
let _ = fs::create_dir_all(destination.parent().unwrap_or(&temp_workflows));
let path_in_github = is_in_github_dir(path, cwd);
match change {
ApplyPatchFileChange::Add { content } | ApplyPatchFileChange::Update { new_content: content, .. } => {
if let Ok(mut file) = fs::File::create(&destination) {
let _ = file.write_all(content.as_bytes());
ApplyPatchFileChange::Add { content } => {
if path_in_github {
let _ = stage_file_with_contents(temp_root, cwd, path, content);
}
}
ApplyPatchFileChange::Update { new_content, move_path, .. } => {
let dest_path = move_path.as_ref().unwrap_or(path);
let dest_in_github = is_in_github_dir(dest_path, cwd);
if !dest_in_github && !path_in_github {
continue;
}
if dest_in_github {
let _ = stage_file_with_contents(temp_root, cwd, dest_path, new_content);
}
if move_path.is_none() && path_in_github && !dest_in_github {
let _ = stage_file_with_contents(temp_root, cwd, path, new_content);
}
if path_in_github && move_path.is_some() {
remove_from_staged(temp_root, cwd, path);
}
}
ApplyPatchFileChange::Delete { .. } => {
let _ = fs::remove_file(&destination);
if path_in_github {
remove_from_staged(temp_root, cwd, path);
}
}
}
}
@@ -112,15 +137,23 @@ pub fn maybe_run_actionlint(
}
}
fn copy_dir_flat(src: &Path, dst: &Path) -> std::io::Result<()> {
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
if !dst.exists() {
fs::create_dir_all(dst)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
if entry.file_type()?.is_file() {
let target = dst.join(entry.file_name());
let _ = fs::copy(entry.path(), target);
let file_type = entry.file_type()?;
let target = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&entry.path(), &target)?;
} else if file_type.is_file() {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let _ = fs::copy(entry.path(), &target);
} else if file_type.is_symlink() {
copy_symlink(&entry.path(), &target)?;
}
}
Ok(())
@@ -152,3 +185,45 @@ fn is_executable(path: &Path) -> bool {
fn is_executable(path: &Path) -> bool {
path.is_file()
}
fn stage_file_with_contents(temp_root: &Path, cwd: &Path, target: &Path, contents: &str) -> bool {
let relative = match target.strip_prefix(cwd) {
Ok(rel) => rel,
Err(_) => return false,
};
let destination = temp_root.join(relative);
if let Some(parent) = destination.parent() {
if fs::create_dir_all(parent).is_err() {
return false;
}
}
match fs::File::create(&destination) {
Ok(mut file) => file.write_all(contents.as_bytes()).is_ok(),
Err(_) => false,
}
}
fn remove_from_staged(temp_root: &Path, cwd: &Path, target: &Path) {
if let Ok(relative) = target.strip_prefix(cwd) {
let destination = temp_root.join(relative);
let _ = fs::remove_file(&destination);
}
}
fn copy_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
let target = std::fs::read_link(src)?;
let _ = fs::remove_file(dst);
std::os::unix::fs::symlink(target, dst)?;
Ok(())
}
#[cfg(not(unix))]
{
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
let _ = fs::copy(src, dst)?;
Ok(())
}
}
@@ -221,12 +221,16 @@ pub(crate) fn detect_tools() -> Vec<ToolStatus> {
("markdownlint", markdownlint_hint()),
("hadolint", hadolint_hint()),
("yamllint", yamllint_hint()),
("rustfmt", rustfmt_hint()),
("cargo-check", cargo_check_hint()),
("shfmt", shfmt_hint()),
("prettier", prettier_hint()),
];
for (name, hint) in tools.into_iter() {
let installed = which(name).is_some();
let installed = if name == "cargo-check" {
has("cargo")
} else {
which(name).is_some()
};
result.push(ToolStatus { name, installed, install_hint: hint });
}
result
@@ -331,11 +335,11 @@ pub fn yamllint_hint() -> String {
"https://yamllint.readthedocs.io/".to_string()
}
pub fn rustfmt_hint() -> String {
if has("rustup") {
return "rustup component add rustfmt".to_string();
pub fn cargo_check_hint() -> String {
if has("cargo") {
return "cargo check --all-targets".to_string();
}
"Install Rust (rustup) then run: rustup component add rustfmt".to_string()
"Install Rust (https://rustup.rs) to enable cargo check".to_string()
}
pub fn shfmt_hint() -> String {
+2 -2
View File
@@ -9566,7 +9566,7 @@ impl ChatWidget<'_> {
"markdownlint" => Some(&mut tools.markdownlint),
"hadolint" => Some(&mut tools.hadolint),
"yamllint" => Some(&mut tools.yamllint),
"rustfmt" => Some(&mut tools.rustfmt),
"cargo-check" => Some(&mut tools.cargo_check),
"shfmt" => Some(&mut tools.shfmt),
"prettier" => Some(&mut tools.prettier),
_ => None,
@@ -9581,7 +9581,7 @@ impl ChatWidget<'_> {
"markdownlint" => tools.markdownlint.unwrap_or(true),
"hadolint" => tools.hadolint.unwrap_or(true),
"yamllint" => tools.yamllint.unwrap_or(true),
"rustfmt" => tools.rustfmt.unwrap_or(true),
"cargo-check" => tools.cargo_check.unwrap_or(true),
"shfmt" => tools.shfmt.unwrap_or(true),
"prettier" => tools.prettier.unwrap_or(true),
_ => true,
+1 -1
View File
@@ -392,7 +392,7 @@ shellcheck = true
markdownlint = true
hadolint = true
yamllint = true
rustfmt = true
cargo-check = true
shfmt = true
prettier = true
```