lintje 0.12.0

Lintje is an opinionated linter for Git.
use core::ops::Range;

use crate::branch::Branch;
use crate::issue::{Context, Issue, Position};
use crate::rule::Rule;
use crate::rule::RuleValidator;
use regex::{Regex, RegexBuilder};

lazy_static! {
    static ref BRANCH_WITH_CLICHE: Regex = {
        let mut tempregex = RegexBuilder::new(
            r"^(wip|fix(es|ed|ing)?|add(s|ed|ing)?|(updat|chang|remov|delet)(e|es|ed|ing))([-_/]+\w+)?$",
        );
        tempregex.case_insensitive(true);
        tempregex.multi_line(false);
        tempregex.build().unwrap()
    };
}

pub struct BranchNameCliche {}

impl BranchNameCliche {
    pub fn new() -> Self {
        Self {}
    }
}

impl RuleValidator<Branch> for BranchNameCliche {
    fn validate(&self, branch: &Branch) -> Option<Vec<Issue>> {
        let name = &branch.name.to_lowercase();
        if BRANCH_WITH_CLICHE.is_match(name) {
            let context = vec![Context::branch_error(
                name.to_string(),
                Range {
                    start: 0,
                    end: name.len(),
                },
                "Describe the change in more detail".to_string(),
            )];
            Some(vec![Issue::error(
                Rule::BranchNameCliche,
                "The branch name does not explain the change in much detail".to_string(),
                Position::Branch { column: 1 },
                context,
            )])
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test::*;

    fn validate(branch: &Branch) -> Option<Vec<Issue>> {
        BranchNameCliche::new().validate(branch)
    }

    fn assert_valid(name: &str) {
        assert_eq!(validate(&branch(name)), None);
    }

    fn assert_invalid(name: &str) {
        assert!(validate(&branch(name)).is_some());
    }

    #[test]
    fn valid_names() {
        let names = vec!["add-email-validation", "fix-brittle-test"];
        for name in names {
            assert_valid(name);
        }
    }

    #[test]
    fn invalid_names() {
        let prefixes = vec![
            "wip", "fix", "fixes", "fixed", "fixing", "add", "adds", "added", "adding", "update",
            "updates", "updated", "updating", "change", "changes", "changed", "changing", "remove",
            "removes", "removed", "removing", "delete", "deletes", "deleted", "deleting",
        ];
        let mut names = vec![];
        for word in prefixes.iter() {
            let uppercase_word = word.to_uppercase();
            let mut chars = word.chars();
            let capitalized_word = match chars.next() {
                None => panic!("Could not capitalize word: {}", word),
                Some(letter) => letter.to_uppercase().collect::<String>() + chars.as_str(),
            };

            names.push(uppercase_word.to_string());
            names.push(capitalized_word.to_string());
            names.push(word.to_string());
            names.push(format!("{}-test", uppercase_word));
            names.push(format!("{}-issue", capitalized_word));
            names.push(format!("{}-bug", word));
            names.push(format!("{}-readme", word));
            names.push(format!("{}-something", word));
            names.push(format!("{}_something", word));
            names.push(format!("{}/something", word));
        }
        for name in names {
            assert_invalid(&name);
        }
    }

    #[test]
    fn with_ticket_number() {
        let issue = first_issue(validate(&branch("fix-bug")));
        assert_eq!(
            issue.message,
            "The branch name does not explain the change in much detail"
        );
        assert_eq!(issue.position, Position::Branch { column: 1 });
        assert_contains_issue_output(
            &issue,
            "| fix-bug\n\
             | ^^^^^^^ Describe the change in more detail",
        );
    }
}