elint 0.1.1

Erlang code linter
Documentation
//! `strict_generator` lint: require strict comprehension generators.

use super::{Rule, token_span_in_node};
use crate::{BranchContext, Context, Finding};

/// Lint rule that flags relaxed comprehension generators.
pub const RULE: Rule = Rule::new(
    "strict_generator",
    include_str!("../../rules/strict_generator/rule.md"),
    check,
);

fn check(_ctx: &Context, branch: &BranchContext) -> Vec<Finding> {
    let mut errors = Vec::new();
    for root in branch.tree.roots() {
        walk(branch, root, &mut errors);
    }
    errors
}

fn walk(branch: &BranchContext, node: erl_parse::NodeView<'_>, errors: &mut Vec<Finding>) {
    if node.kind() == erl_parse::SyntaxKind::ZipQualifier {
        return;
    }
    check_node(branch, node, errors);
    for child in node.children() {
        walk(branch, child, errors);
    }
}

fn check_node(branch: &BranchContext, node: erl_parse::NodeView<'_>, errors: &mut Vec<Finding>) {
    let expected = match node.kind() {
        erl_parse::SyntaxKind::Generator | erl_parse::SyntaxKind::MapGenerator => {
            erl_tokenize::Symbol::LeftArrow
        }
        erl_parse::SyntaxKind::BitstringGenerator => erl_tokenize::Symbol::DoubleLeftArrow,
        _ => return,
    };
    let Some(span) = token_span_in_node(branch, node, |token| {
        token.kind() == erl_tokenize::TokenKind::Symbol(expected)
    }) else {
        return;
    };
    errors.push(Finding {
        span,
        node: node.node_id(),
    });
}

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

    #[test]
    fn flags_list_generator() {
        let src = "-module(t).\nfoo() -> [X || X <- [1, 2, 3]].\n";
        assert_eq!(findings(check, src), ["<-"]);
    }

    #[test]
    fn flags_bitstring_generator() {
        let src = "-module(t).\nfoo() -> [X || <<X:8>> <= <<1, 2, 3>>].\n";
        assert_eq!(findings(check, src), ["<="]);
    }

    #[test]
    fn flags_map_generator() {
        let src = "-module(t).\nfoo(M) -> #{K => V || K := V <- M}.\n";
        assert_eq!(findings(check, src), ["<-"]);
    }

    #[test]
    fn ignores_strict_list_generator() {
        let src = "-module(t).\nfoo() -> [X || X <:- [1, 2, 3]].\n";
        assert!(findings(check, src).is_empty());
    }

    #[test]
    fn ignores_strict_bitstring_generator() {
        let src = "-module(t).\nfoo() -> [X || <<X:8>> <:= <<1, 2, 3>>].\n";
        assert!(findings(check, src).is_empty());
    }

    #[test]
    fn ignores_strict_map_generator() {
        let src = "-module(t).\nfoo(M) -> #{K => V || K := V <:- M}.\n";
        assert!(findings(check, src).is_empty());
    }

    #[test]
    fn ignores_zip_generator() {
        let src = "-module(t).\nfoo() -> [{X, Y} || X <- [1, 2] && Y <- [3, 4]].\n";
        assert!(findings(check, src).is_empty());
    }

    #[test]
    fn flags_each_generator_in_a_comprehension_once() {
        let src = "-module(t).\nfoo() -> [[X || X <- L] || L <- [[1], [2]]].\n";
        assert_eq!(findings(check, src), ["<-", "<-"]);
    }

    #[test]
    fn ng_fixture_has_three_findings() {
        let src = include_str!("../../rules/strict_generator/ng.erl");
        assert_eq!(findings(check, src).len(), 3);
    }

    #[test]
    fn ok_fixture_has_no_findings() {
        let src = include_str!("../../rules/strict_generator/ok.erl");
        assert!(findings(check, src).is_empty());
    }
}