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.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
//! 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>");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user