lintje 0.12.0

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

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

static SKIP_CHANGESET_TAG: &str = "[skip changeset]";

lazy_static! {
    static ref NON_WORD_CHARACTERS: Regex = Regex::new(r"([^\w]+)").unwrap();
}

pub struct DiffChangeset {}

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

impl RuleValidator<Commit> for DiffChangeset {
    fn validate(&self, commit: &Commit) -> Option<Vec<Issue>> {
        if !commit.has_changes() {
            return None;
        }

        if commit.message.contains(SKIP_CHANGESET_TAG) {
            return None;
        }

        let has_changesets = commit
            .file_changes
            .iter()
            .any(|filename| filename.contains(".changeset/") || filename.contains(".changesets/"));
        if has_changesets {
            return None;
        }

        let diff_line = format!(".changesets/{}.md", parameterize(&commit.subject));
        let diff_line_len = diff_line.len();
        let line_count = commit.message.lines().count();
        let new_line_count = if line_count == 0 { 3 } else { line_count + 2 };
        let tag = "[skip changeset]".to_string();
        let tag_len = tag.len();
        let context = vec![
            Context::diff_addition(
                diff_line,
                Range {
                    start: 0,
                    end: diff_line_len,
                },
                "Add a changeset file for changelog generation".to_string(),
            ),
            Context::gap(),
            Context::message_line_addition(
                new_line_count,
                tag,
                Range {
                    start: 0,
                    end: tag_len,
                },
                "Or add the skip changeset tag to the commit message".to_string(),
            ),
        ];
        Some(vec![Issue::hint(
            Rule::DiffChangeset,
            "No changeset file found in commit".to_string(),
            Position::Diff,
            context,
        )])
    }
}

fn parameterize(filename: &str) -> String {
    NON_WORD_CHARACTERS
        .replace_all(&filename.to_lowercase(), "-")
        .trim_start_matches('-')
        .trim_end_matches('-')
        .to_string()
}

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

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

    pub fn commit_with_files(files: Vec<String>) -> Commit {
        Commit::new(
            Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
            Some("test@example.com".to_string()),
            "Subject: of commit with /&_|[](){}\\'\"`@!=+*.,#$%^;: chars -- foo.test",
            "Some message".to_string(),
            "".to_string(),
            files,
        )
    }

    #[test]
    fn without_changeset() {
        let issue = first_issue(validate(&commit_with_files(vec![
            "README.md".to_string(),
            "src/main.rs".to_string(),
        ])));
        assert_eq!(issue.message, "No changeset file found in commit");
        assert_eq!(issue.position, Position::Diff);
        assert_contains_issue_output(
            &issue,
            "  | .changesets/subject-of-commit-with-_-chars-foo-test.md\n\
               | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Add a changeset file for changelog generation\n\
              ~~~\n\
             3 | [skip changeset]\n\
               | ++++++++++++++++ Or add the skip changeset tag to the commit message",
        );
    }

    #[test]
    fn without_changeset_with_skip_tag() {
        let commit = Commit::new(
            Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
            Some("test@example.com".to_string()),
            "Subject",
            "Some message\n[skip changeset]".to_string(),
            "".to_string(),
            vec!["README.md".to_string(), "src/main.rs".to_string()],
        );
        let issues = validate(&commit);
        assert_eq!(issues, None);
    }

    #[test]
    fn with_changesets_in_root_directory() {
        let issues = validate(&commit_with_files(vec![
            "src/main.rs".to_string(),
            ".changesets/changeset-name.md".to_string(),
        ]));
        assert_eq!(issues, None);
    }

    #[test]
    fn with_changeset_in_root_directory() {
        let issues = validate(&commit_with_files(vec![
            "src/main.rs".to_string(),
            ".changeset/changeset-name.md".to_string(),
        ]));
        assert_eq!(issues, None);
    }

    #[test]
    fn with_changesets_in_sub_directory() {
        let issues = validate(&commit_with_files(vec![
            "package/src/main.rs".to_string(),
            "package/.changesets/changeset-name.md".to_string(),
        ]));
        assert_eq!(issues, None);
    }

    #[test]
    fn with_changeset_in_sub_directory() {
        let issues = validate(&commit_with_files(vec![
            "package/src/main.rs".to_string(),
            "package/.changeset/changeset-name.md".to_string(),
        ]));
        assert_eq!(issues, None);
    }
}