Files
zesdex/apps/infrastructure/src/best_practice/commit.rs
T
asepharyana 8ecc588a3e feat(best_practice): add code quality scanning and commit message validation
- Implemented a code quality scanner that checks for common clean-code violations in Rust source files, including missing documentation, usage of `.unwrap()` in production code, and commented-out code.
- Introduced a commit message validator that follows the Conventional Commits specification, ensuring proper formatting and providing suggestions for invalid messages.
- Created a unified BestPracticeEngine to encapsulate the functionalities of skills, architecture audits, code quality checks, and commit message validation.
- Added tests for both the code quality scanner and commit message validator to ensure reliability and correctness.
2026-07-21 07:21:22 +07:00

257 lines
7.7 KiB
Rust

//! Commit message validation following Conventional Commits (Bahasa Indonesia).
//!
//! # Flow
//!
//! `validate_commit_message(msg)` → parse subject → check type, scope,
//! description format → return list of errors (empty = valid).
//!
//! # Format
//!
//! ```text
//! feat(scope): description
//! fix(scope): description
//! chore: description
//! docs: description
//! refactor: description
//! test: description
//! style: description
//! perf: description
//! ci: description
//! ```
use regex::Regex;
/// Valid commit types.
const VALID_TYPES: &[&str] = &[
"feat", "fix", "chore", "docs", "refactor", "test", "style", "perf", "ci", "build", "revert",
];
/// Validate a commit message against the Conventional Commits spec.
///
/// Returns `Ok(())` if valid, or `Err(errors)` with human-readable messages.
pub fn validate_commit_message(msg: &str) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let msg = msg.trim();
if msg.is_empty() {
errors.push("Commit message must not be empty.".to_string());
return Err(errors);
}
// Split subject and body.
let subject = msg.lines().next().unwrap_or(msg);
// Check subject length.
if subject.len() > 72 {
errors.push(format!(
"Subject line is {} characters (max 72).",
subject.len()
));
}
// Parse: `type(scope): description` or `type!: description` or `type: description`
let re = Regex::new(
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
)
.expect("valid regex for commit parsing");
match re.captures(subject) {
None => {
errors.push(format!(
"Subject does not match Conventional Commits format.\n\
Expected: `type(scope): description`\n\
Got: {subject}\n\
Valid types: {}",
VALID_TYPES.join(", ")
));
}
Some(caps) => {
let type_ = caps.name("type").map(|m| m.as_str()).unwrap_or("");
let scope = caps.name("scope").map(|m| m.as_str());
let desc = caps.name("desc").map(|m| m.as_str()).unwrap_or("");
// Validate type.
if !VALID_TYPES.contains(&type_) {
errors.push(format!(
"Invalid commit type '{type_}'. Valid types: {}",
VALID_TYPES.join(", ")
));
}
// Scope is lowercase, no spaces.
if let Some(s) = scope {
if s.contains(' ') {
errors.push(format!("Scope must not contain spaces: '{s}'"));
}
if s.chars().any(|c| c.is_uppercase()) {
errors.push(format!("Scope must be lowercase: '{s}'"));
}
}
// Description rules.
if desc.is_empty() {
errors.push("Description must not be empty.".to_string());
} else {
let first_char = desc.chars().next().unwrap_or(' ');
if first_char.is_uppercase() {
errors.push(format!(
"Description must start with lowercase: '{desc}'"
));
}
if desc.ends_with('.') {
errors.push(format!(
"Description must not end with a period: '{desc}'"
));
}
}
// Type-specific rules.
match type_ {
"chore" | "docs" | "refactor" | "test" | "style" | "perf" | "ci" | "build"
| "revert" => {
if scope.is_some() {
errors.push(format!(
"'{type_}' commits should not use a scope. \
Only 'feat' and 'fix' require scopes."
));
}
}
_ => {}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Convert a commit message to a structured representation.
pub struct CommitInfo {
pub type_: String,
pub scope: Option<String>,
pub breaking: bool,
pub description: String,
pub body: Option<String>,
}
/// Parse a commit message into its structured components.
pub fn parse_commit_message(msg: &str) -> Option<CommitInfo> {
let msg = msg.trim();
let subject = msg.lines().next()?;
let re = Regex::new(
r"^(?P<type>[a-z]+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<desc>.+)$",
)
.expect("valid regex");
let caps = re.captures(subject)?;
let body_lines: Vec<&str> = msg.lines().skip(1).collect();
let body = if body_lines.is_empty() {
None
} else {
Some(body_lines.join("\n"))
};
Some(CommitInfo {
type_: caps.name("type").map(|m| m.as_str()).unwrap_or("").to_string(),
scope: caps.name("scope").map(|m| m.as_str().to_string()),
breaking: caps.name("breaking").is_some(),
description: caps.name("desc").map(|m| m.as_str()).unwrap_or("").to_string(),
body,
})
}
/// Suggest a commit message template for a given change type.
pub fn suggest_template(type_: &str, scope: Option<&str>) -> String {
match type_ {
"feat" | "fix" if scope.is_some() => {
format!("{}({}): <imperative description>", type_, scope.unwrap())
}
"feat" | "fix" => {
format!("{}(<scope>): <imperative description>", type_)
}
_ => {
format!("{}: <imperative description>", type_)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_feat_commit() {
assert!(validate_commit_message("feat(tool): add batch file delete").is_ok());
}
#[test]
fn valid_fix_commit() {
assert!(validate_commit_message("fix(ipc): reconnect loop on socket timeout").is_ok());
}
#[test]
fn valid_chore_commit() {
assert!(validate_commit_message("chore: bump reqwest to 0.13").is_ok());
}
#[test]
fn valid_docs_commit() {
assert!(validate_commit_message("docs: add architecture diagram to README").is_ok());
}
#[test]
fn rejects_empty_message() {
assert!(validate_commit_message("").is_err());
}
#[test]
fn rejects_missing_colon() {
assert!(validate_commit_message("feat:missing space").is_err());
}
#[test]
fn rejects_uppercase_description() {
let result = validate_commit_message("feat(tool): Add new feature");
assert!(result.is_err(), "should reject uppercase start");
}
#[test]
fn rejects_trailing_period() {
let result = validate_commit_message("feat(tool): add new feature.");
assert!(result.is_err(), "should reject trailing period");
}
#[test]
fn rejects_invalid_type() {
let result = validate_commit_message("wat(tool): something broke");
assert!(result.is_err(), "should reject invalid type");
}
#[test]
fn parses_valid_commit() {
let parsed = parse_commit_message("feat(agent): add parallel execution\n\nWith cycle orchestration.").unwrap();
assert_eq!(parsed.type_, "feat");
assert_eq!(parsed.scope, Some("agent".to_string()));
assert!(!parsed.breaking);
assert_eq!(parsed.description, "add parallel execution");
assert!(parsed.body.unwrap().contains("cycle orchestration"));
}
#[test]
fn parses_breaking_change() {
let parsed = parse_commit_message("feat(api)!: change response format").unwrap();
assert!(parsed.breaking);
}
#[test]
fn suggests_template() {
let tpl = suggest_template("feat", Some("tool"));
assert_eq!(tpl, "feat(tool): <imperative description>");
}
}