rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;
use ra_ap_syntax::{ast, ast::HasName};
use std::collections::HashSet;

use super::support::type_name;
use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "iter with IntoIterator for ref",
        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n}\nimpl<'a> IntoIterator for &'a Bag {\n    type Item = &'a u32;\n    type IntoIter = std::slice::Iter<'a, u32>;\n    fn into_iter(self) -> Self::IntoIter { self.0.iter() }\n}",
        pass: true,
    },
    Example {
        label: "iter without IntoIterator for ref",
        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n}",
        pass: false,
    },
    Example {
        label: "iter_mut without IntoIterator for mut ref",
        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n}",
        pass: false,
    },
    Example {
        label: "iter_mut with IntoIterator for mut ref",
        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n}\nimpl<'a> IntoIterator for &'a mut Bag {\n    type Item = &'a mut u32;\n    type IntoIter = std::slice::IterMut<'a, u32>;\n    fn into_iter(self) -> Self::IntoIter { self.0.iter_mut() }\n}",
        pass: true,
    },
    Example {
        label: "FromIterator without Extend",
        code: "struct Bag(Vec<u32>);\nimpl FromIterator<u32> for Bag {\n    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self { Bag(iter.into_iter().collect()) }\n}",
        pass: false,
    },
    Example {
        label: "Extend without FromIterator",
        code: "struct Bag(Vec<u32>);\nimpl Extend<u32> for Bag {\n    fn extend<I: IntoIterator<Item = u32>>(&mut self, iter: I) { self.0.extend(iter) }\n}",
        pass: false,
    },
    Example {
        label: "FromIterator with Extend",
        code: "struct Bag(Vec<u32>);\nimpl FromIterator<u32> for Bag {\n    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self { Bag(iter.into_iter().collect()) }\n}\nimpl Extend<u32> for Bag {\n    fn extend<I: IntoIterator<Item = u32>>(&mut self, iter: I) { self.0.extend(iter) }\n}",
        pass: true,
    },
    Example {
        label: "no collection surface",
        code: "struct Bag(Vec<u32>);\nimpl Bag {\n    fn len(&self) -> usize { self.0.len() }\n}",
        pass: true,
    },
    Example {
        label: "iter in test module",
        code: "#[cfg(test)]\nmod tests {\n    struct Bag(Vec<u32>);\n    impl Bag {\n        fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n    }\n}",
        pass: true,
    },
    Example {
        label: "associated fn iter without receiver",
        code: "struct Gen;\nimpl Gen {\n    fn iter() -> std::ops::Range<u32> { 0..4 }\n}",
        pass: true,
    },
    Example {
        label: "free fn iter",
        code: "fn iter() -> std::ops::Range<u32> { 0..4 }",
        pass: true,
    },
];

crate::ast_rule!(
    collection_trait_completeness,
    "Require collection trait counterparts: `iter()` needs `impl IntoIterator for &T`, `iter_mut()` needs `impl IntoIterator for &mut T`, and `FromIterator`/`Extend` come in pairs.",
    "Collections missing the matching std iterator traits break for-loops over references and generic code expecting the standard surface.",
    Low,
);

#[derive(Default)]
struct TraitImpls {
    into_iter_ref: HashSet<String>,
    into_iter_mut: HashSet<String>,
    from_iter: HashSet<String>,
    extend: HashSet<String>,
}

fn collect_trait_impls(ctx: &AstCtx<'_>) -> TraitImpls {
    let mut found = TraitImpls::default();

    for item in ctx.nodes::<ast::Impl>() {
        record_impl(&item, &mut found);
    }

    found
}

fn record_impl(item: &ast::Impl, found: &mut TraitImpls) {
    let Some(trait_name) = item.trait_().and_then(|ty| type_name(&ty)) else {
        return;
    };

    if trait_name == "IntoIterator" {
        let Some(ast::Type::RefType(reference)) = item.self_ty() else {
            return;
        };
        let Some(name) = reference.ty().and_then(|ty| type_name(&ty)) else {
            return;
        };

        if reference.mut_token().is_some() {
            found.into_iter_mut.insert(name);
        } else {
            found.into_iter_ref.insert(name);
        }
    } else if trait_name == "FromIterator" || trait_name == "Extend" {
        let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
            return;
        };

        if trait_name == "FromIterator" {
            found.from_iter.insert(name);
        } else {
            found.extend.insert(name);
        }
    }
}

fn check_collection_trait_completeness(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let impls = collect_trait_impls(ctx);

    ctx.nodes::<ast::Impl>()
        .filter(|item| !ctx.is_in_test(item))
        .flat_map(|item| match item.trait_() {
            None => check_inherent_impl(ctx, &item, &impls),
            Some(_) => check_pair_impl(ctx, &item, &impls),
        })
        .collect()
}

fn check_inherent_impl(ctx: &AstCtx<'_>, item: &ast::Impl, impls: &TraitImpls) -> Vec<Violation> {
    let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
        return Vec::new();
    };

    let associated_items = item
        .assoc_item_list()
        .into_iter()
        .flat_map(|list| list.assoc_items());

    associated_items
        .filter_map(|assoc| match assoc {
            ast::AssocItem::Fn(method) => Some(method),
            _ => None,
        })
        .filter_map(|method| {
            let method_name = method.name()?;
            let mutable = ref_receiver_mutability(&method)?;

            if method_name.text() == "iter" && !mutable && !impls.into_iter_ref.contains(&name) {
                Some(missing_counterpart(
                    ctx,
                    &method_name,
                    &name,
                    "iter()",
                    "impl IntoIterator for &",
                ))
            } else if method_name.text() == "iter_mut"
                && mutable
                && !impls.into_iter_mut.contains(&name)
            {
                Some(missing_counterpart(
                    ctx,
                    &method_name,
                    &name,
                    "iter_mut()",
                    "impl IntoIterator for &mut ",
                ))
            } else {
                None
            }
        })
        .collect()
}

fn ref_receiver_mutability(function: &ast::Fn) -> Option<bool> {
    let receiver = function.param_list()?.self_param()?;

    receiver.amp_token().map(|_| receiver.mut_token().is_some())
}

fn check_pair_impl(ctx: &AstCtx<'_>, item: &ast::Impl, impls: &TraitImpls) -> Vec<Violation> {
    let Some(trait_ty) = item.trait_() else {
        return Vec::new();
    };
    let Some(trait_name) = type_name(&trait_ty) else {
        return Vec::new();
    };
    let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
        return Vec::new();
    };

    if trait_name == "FromIterator" && !impls.extend.contains(&name) {
        vec![missing_counterpart(
            ctx,
            &trait_ty,
            &name,
            "impl FromIterator",
            "impl Extend<_> for ",
        )]
    } else if trait_name == "Extend" && !impls.from_iter.contains(&name) {
        vec![missing_counterpart(
            ctx,
            &trait_ty,
            &name,
            "impl Extend",
            "impl FromIterator<_> for ",
        )]
    } else {
        Vec::new()
    }
}

fn missing_counterpart<N>(
    ctx: &AstCtx<'_>,
    node: &N,
    type_name: &str,
    has: &str,
    needs: &str,
) -> Violation
where
    N: ra_ap_syntax::AstNode,
{
    ctx.violation(
        node,
        format!("type `{type_name}` has `{has}` but no `{needs}{type_name}` in this file"),
    )
}

crate::rulewright_ast_test!(check_collection_trait_completeness, {
    crate::example_tests!(EXAMPLES, check_collection_trait_completeness);

    #[gtest]
    fn one_violation_per_missing_counterpart() -> Result<()> {
        let v = run("struct Bag(Vec<u32>);\n\
             impl Bag {\n\
                 fn iter(&self) -> std::slice::Iter<'_, u32> { self.0.iter() }\n\
                 fn iter_mut(&mut self) -> std::slice::IterMut<'_, u32> { self.0.iter_mut() }\n\
             }");
        verify_eq!(v.len(), 2)?;

        Ok(())
    }
});