#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_cb400_git_hooks_quality(project_path: &Path) -> Vec<CbPatternViolation> {
let hooks_dir = project_path.join(".git/hooks");
if !hooks_dir.exists() {
return Vec::new();
}
["pre-commit", "pre-push", "commit-msg", "post-commit"]
.iter()
.flat_map(|name| lint_single_hook(&hooks_dir, name))
.collect()
}
fn lint_single_hook(hooks_dir: &Path, hook_name: &str) -> Vec<CbPatternViolation> {
let hook_path = hooks_dir.join(hook_name);
if !hook_path.exists() || hook_path.to_string_lossy().ends_with(".sample") {
return Vec::new();
}
let file = format!(".git/hooks/{hook_name}");
match run_bashrs_lint(&hook_path) {
Ok(issues) => issues
.into_iter()
.map(|issue| CbPatternViolation {
pattern_id: format!("CB-400-{}", issue.code),
file: file.clone(),
line: issue.line,
description: format!("{}: {}", issue.code, issue.message),
severity: match issue.severity.as_str() {
"error" => Severity::Error,
"warning" => Severity::Warning,
_ => Severity::Info,
},
})
.collect(),
Err(e) if !e.contains("not found") => vec![CbPatternViolation {
pattern_id: "CB-400".to_string(),
file,
line: 0,
description: format!("bashrs lint error: {e}"),
severity: Severity::Warning,
}],
Err(_) => Vec::new(),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_cb401_makefile_quality(project_path: &Path) -> Vec<CbPatternViolation> {
let mut violations = Vec::new();
let makefile_path = project_path.join("Makefile");
if !makefile_path.exists() {
return violations;
}
match run_bashrs_make_lint(&makefile_path) {
Ok(issues) if !issues.is_empty() => {
for issue in issues {
violations.push(CbPatternViolation {
pattern_id: format!("CB-401-{}", issue.code),
file: "Makefile".to_string(),
line: issue.line,
description: format!("{}: {}", issue.code, issue.message),
severity: match issue.severity.as_str() {
"error" => Severity::Error,
"warning" => Severity::Warning,
_ => Severity::Info,
},
});
}
}
Ok(_) => {} Err(e) => {
if !e.contains("not found") {
violations.push(CbPatternViolation {
pattern_id: "CB-401".to_string(),
file: "Makefile".to_string(),
line: 0,
description: format!("bashrs make lint error: {}", e),
severity: Severity::Warning,
});
}
}
}
violations
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect_cb402_shell_script_quality(project_path: &Path) -> Vec<CbPatternViolation> {
let mut violations = Vec::new();
let sh_files: Vec<_> = walkdir::WalkDir::new(project_path)
.max_depth(4)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
let path = e.path();
path.extension().is_some_and(|ext| ext == "sh")
&& !path.to_string_lossy().contains("target/")
&& !path.to_string_lossy().contains("node_modules/")
})
.take(20) .collect();
for entry in sh_files {
match run_bashrs_lint(entry.path()) {
Ok(issues) if !issues.is_empty() => {
for issue in issues {
violations.push(CbPatternViolation {
pattern_id: format!("CB-402-{}", issue.code),
file: entry
.path()
.strip_prefix(project_path)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| entry.path().display().to_string()),
line: issue.line,
description: format!("{}: {}", issue.code, issue.message),
severity: match issue.severity.as_str() {
"error" => Severity::Error,
"warning" => Severity::Warning,
_ => Severity::Info,
},
});
}
}
Ok(_) => {} Err(_) => {} }
}
violations
}
pub(super) fn run_bashrs_lint(path: &Path) -> Result<Vec<BashrsIssue>, String> {
use std::process::Command;
let output = Command::new("bashrs")
.args(["lint", "--format", "json", "--level", "warning"])
.arg(path)
.output()
.map_err(|e| format!("bashrs not found: {}", e))?;
if output.status.success() {
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_bashrs_json_output(&stdout)
}
pub(super) fn run_bashrs_make_lint(path: &Path) -> Result<Vec<BashrsIssue>, String> {
use std::process::Command;
let output = Command::new("bashrs")
.args(["make", "lint", "--format", "json"])
.arg(path)
.output()
.map_err(|e| format!("bashrs not found: {}", e))?;
if output.status.success() {
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_bashrs_json_output(&stdout)
}
pub(super) fn parse_bashrs_json_output(json_str: &str) -> Result<Vec<BashrsIssue>, String> {
#[derive(serde::Deserialize)]
struct BashrsOutput {
#[serde(default)]
diagnostics: Vec<BashrsDiagnostic>,
}
#[derive(serde::Deserialize)]
struct BashrsDiagnostic {
code: String,
message: String,
#[serde(default)]
line: usize,
#[serde(default)]
severity: String,
}
if let Ok(diagnostics) = serde_json::from_str::<Vec<BashrsDiagnostic>>(json_str) {
return Ok(diagnostics
.into_iter()
.map(|d| BashrsIssue {
code: d.code,
message: d.message,
line: d.line,
severity: d.severity,
})
.collect());
}
if let Ok(output) = serde_json::from_str::<BashrsOutput>(json_str) {
return Ok(output
.diagnostics
.into_iter()
.map(|d| BashrsIssue {
code: d.code,
message: d.message,
line: d.line,
severity: d.severity,
})
.collect());
}
Ok(Vec::new())
}