rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::{AstNode, ast};

use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "unwrap in map closure",
        code: r"fn f(v: Vec<Option<i32>>) -> Vec<i32> { v.into_iter().map(|x| x.unwrap()).collect() }",
        pass: false,
    },
    Example {
        label: "expect in filter_map",
        code: r#"fn f(v: Vec<&str>) -> Vec<i32> { v.iter().filter_map(|x| Some(x.parse::<i32>().expect("bad"))).collect() }"#,
        pass: false,
    },
    Example {
        label: "unwrap in for_each",
        code: r"fn f(v: Vec<Option<i32>>) { v.iter().for_each(|x| { x.unwrap(); }); }",
        pass: false,
    },
    Example {
        label: "no unwrap in map",
        code: "fn f(v: Vec<i32>) -> Vec<i32> { v.into_iter().map(|x| x + 1).collect() }",
        pass: true,
    },
    Example {
        label: "unwrap outside iterator",
        code: "fn f() { Some(1).unwrap(); }",
        pass: true,
    },
    Example {
        label: "unwrap in iterator in test",
        code: "#[cfg(test)]\nmod tests {\n    fn t(v: Vec<Option<i32>>) { v.into_iter().map(|x| x.unwrap()); }\n}",
        pass: true,
    },
];

crate::ast_rule!(
    fallible_in_iterator,
    "Flag `.unwrap()`/`.expect()` inside iterator adapter closures.",
    "unwrap/expect inside iterator adapters panics mid-iteration with no recovery. Use filter_map or collect::<Result<_>>.",
    Medium,
);

fn check_fallible_in_iterator(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::MethodCallExpr>()
        .filter(|call| !ctx.is_in_test(call))
        .filter_map(|call| {
            let method = call.name_ref()?;

            if !matches!(method.text().as_str(), "unwrap" | "expect")
                || !inside_iterator_closure(&call)
            {
                return None;
            }

            Some(ctx.violation(
                &method,
                format!(
                    ".{}() inside iterator adapter — use ? or filter_map instead",
                    method.text()
                ),
            ))
        })
        .collect()
}

const ITERATOR_METHODS: &[&str] = &[
    "map",
    "filter",
    "filter_map",
    "flat_map",
    "for_each",
    "find",
    "find_map",
    "any",
    "all",
    "inspect",
    "scan",
    "fold",
    "reduce",
    "try_fold",
    "try_for_each",
    "partition",
];

fn inside_iterator_closure(call: &ast::MethodCallExpr) -> bool {
    let closure = call
        .syntax()
        .ancestors()
        .skip(1)
        .find_map(ast::ClosureExpr::cast);
    let adapter = closure.and_then(|closure| {
        closure
            .syntax()
            .ancestors()
            .skip(1)
            .find_map(ast::MethodCallExpr::cast)
    });

    adapter
        .and_then(|adapter| adapter.name_ref())
        .is_some_and(|name| ITERATOR_METHODS.contains(&name.text().as_str()))
}

crate::rulewright_ast_test!(check_fallible_in_iterator, {
    crate::example_tests!(EXAMPLES, check_fallible_in_iterator);
});