lintje 0.11.1

Lintje is an opinionated linter for Git.
use core::ops::Range;
use regex::{Regex, RegexBuilder};

use crate::commit::Commit;
use crate::issue::{Context, Issue, Position};
use crate::rule::Rule;
use crate::rule::RuleValidator;

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

pub struct SubjectCliche {}

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

impl RuleValidator<Commit> for SubjectCliche {
    fn validate(&self, commit: &Commit) -> Option<Vec<Issue>> {
        let subject = &commit.subject.to_lowercase();
        let wip_commit = subject.starts_with("wip ") || subject == &"wip".to_string();
        if wip_commit || SUBJECT_WITH_CLICHE.is_match(subject) {
            let context = vec![Context::subject_error(
                commit.subject.to_string(),
                Range {
                    start: 0,
                    end: commit.subject.len(),
                },
                "Describe the change in more detail".to_string(),
            )];
            Some(vec![Issue::error(
                Rule::SubjectCliche,
                "The subject does not explain the change in much detail".to_string(),
                Position::Subject { line: 1, column: 1 },
                context,
            )])
        } else {
            None
        }
    }
}

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

    fn validate(commit: &Commit) -> Option<Vec<Issue>> {
        SubjectCliche::new().validate(commit)
    }

    fn assert_subject_as_valid(subject: &str) {
        assert_eq!(validate(&commit(subject, "")), None);
    }

    fn assert_subject_as_invalid(subject: &str) {
        assert!(validate(&commit(subject, "")).is_some());
    }

    #[test]
    fn with_valid_subjects() {
        let subjects = vec![
            "I am not a cliche",
            "Fix user bug",
            "Fix test for some feature",
            "Fix bug for some feature",
            "Fixes bug for some feature",
            "Fixed bug for some feature",
            "Fixing bug for some feature",
        ];
        for subject in subjects {
            assert_subject_as_valid(subject);
        }
    }

    #[test]
    fn wip_commit() {
        let issue = first_issue(validate(&commit("WIP", "")));
        assert_eq!(
            issue.message,
            "The subject does not explain the change in much detail"
        );
        assert_eq!(issue.position, subject_position(1));
        assert_contains_issue_output(
            &issue,
            "1 | WIP\n\
               | ^^^ Describe the change in more detail",
        );
    }

    #[test]
    fn cliche_subjects() {
        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 invalid_subjects = 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(),
            };

            invalid_subjects.push(uppercase_word.to_string());
            invalid_subjects.push(capitalized_word.to_string());
            invalid_subjects.push(word.to_string());
            invalid_subjects.push(format!("{} test", uppercase_word));
            invalid_subjects.push(format!("{} issue", capitalized_word));
            invalid_subjects.push(format!("{} bug", word));
            invalid_subjects.push(format!("{} readme", word));
            invalid_subjects.push(format!("{} something", word));
        }
        for subject in invalid_subjects {
            assert_subject_as_invalid(subject.as_str());
        }
    }

    #[test]
    fn cliche_subject_detail() {
        let issue = first_issue(validate(&commit("Fixed bug", "")));
        assert_eq!(issue.position, subject_position(1));
        assert_contains_issue_output(
            &issue,
            "1 | Fixed bug\n\
               | ^^^^^^^^^ Describe the change in more detail",
        );
    }
}