necessist-backends 0.6.2

necessist-backends
Documentation
use super::{Call, GenericVisitor, MacroCall, Rust, Storage, Test};
use anyhow::Result;
use necessist_core::{framework::TestSpanMaps, warn, WarnFlags, Warning};

use std::cell::RefCell;
use syn::{
    visit::{
        visit_expr_call, visit_expr_macro, visit_expr_method_call, visit_item_fn, visit_item_mod,
        visit_stmt, visit_stmt_macro, Visit,
    },
    ExprCall, ExprMacro, ExprMethodCall, File, Ident, ItemFn, ItemMod, PathSegment, Stmt,
    StmtMacro,
};

#[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
pub(super) fn visit<'ast>(
    generic_visitor: GenericVisitor<'_, '_, '_, 'ast, Rust>,
    storage: &RefCell<Storage<'ast>>,
    file: &'ast File,
) -> Result<TestSpanMaps> {
    let mut visitor = Visitor::new(generic_visitor, storage);
    visitor.visit_file(file);
    for (test_name, error) in &storage.borrow().tests_needing_warnings {
        warn(
            visitor.generic_visitor.context,
            Warning::ModulePathUnknown,
            &format!("Failed to determine module path for test `{test_name}`: {error:?}"),
            WarnFlags::empty(),
        )?;
    }
    if let Some(error) = storage.borrow_mut().error.take() {
        return Err(error);
    }
    let _: &Vec<String> = visitor.generic_visitor.backend.cached_source_file_flags(
        &mut storage.borrow_mut().source_file_package_cache,
        &visitor.generic_visitor.source_file,
    )?;
    Ok(visitor.generic_visitor.test_span_maps())
}

struct Visitor<'context, 'config, 'backend, 'ast, 'storage> {
    generic_visitor: GenericVisitor<'context, 'config, 'backend, 'ast, Rust>,
    storage: &'storage RefCell<Storage<'ast>>,
    test_ident: Option<&'ast Ident>,
}

impl<'context, 'config, 'backend, 'ast, 'storage>
    Visitor<'context, 'config, 'backend, 'ast, 'storage>
{
    fn new(
        generic_visitor: GenericVisitor<'context, 'config, 'backend, 'ast, Rust>,
        storage: &'storage RefCell<Storage<'ast>>,
    ) -> Self {
        Self {
            generic_visitor,
            storage,
            test_ident: None,
        }
    }
}

impl<'context, 'config, 'backend, 'ast, 'storage> Visit<'ast>
    for Visitor<'context, 'config, 'backend, 'ast, 'storage>
{
    fn visit_item_mod(&mut self, item: &'ast ItemMod) {
        if self.test_ident.is_none() {
            self.storage.borrow_mut().module_path.push(&item.ident);
        }

        visit_item_mod(self, item);

        if self.test_ident.is_none() {
            assert_eq!(
                self.storage.borrow_mut().module_path.pop(),
                Some(&item.ident)
            );
        }
    }

    fn visit_item_fn(&mut self, item: &'ast ItemFn) {
        if let Some(ident) = is_test(item) {
            assert!(self.test_ident.is_none());
            self.test_ident = Some(ident);

            if let Some(test) = Test::new(self.storage, &self.generic_visitor.source_file, item) {
                let walk = self.generic_visitor.visit_test(self.storage, test);

                if walk {
                    visit_item_fn(self, item);
                }

                self.generic_visitor.visit_test_post(self.storage, test);
            }

            assert!(self.test_ident == Some(ident));
            self.test_ident = None;
        }
    }

    fn visit_stmt(&mut self, stmt: &'ast Stmt) {
        let walk = self.generic_visitor.visit_statement(self.storage, stmt);

        if walk {
            visit_stmt(self, stmt);
        }

        self.generic_visitor
            .visit_statement_post(self.storage, stmt);
    }

    fn visit_expr_call(&mut self, function_call: &'ast ExprCall) {
        let call = Call::FunctionCall(function_call);

        let walk = self.generic_visitor.visit_call(self.storage, call);

        if walk {
            visit_expr_call(self, function_call);
        } else {
            self.visit_expr(&function_call.func);
        }

        self.generic_visitor.visit_call_post(self.storage, call);
    }

    fn visit_stmt_macro(&mut self, mac: &'ast StmtMacro) {
        let macro_call = MacroCall::Stmt(mac);

        let walk = self
            .generic_visitor
            .visit_macro_call(self.storage, macro_call);

        if walk {
            visit_stmt_macro(self, mac);
        }

        self.generic_visitor
            .visit_macro_call_post(self.storage, macro_call);
    }

    fn visit_expr_macro(&mut self, mac: &'ast ExprMacro) {
        let macro_call = MacroCall::Expr(mac);

        let walk = self
            .generic_visitor
            .visit_macro_call(self.storage, macro_call);

        if walk {
            visit_expr_macro(self, mac);
        }

        self.generic_visitor
            .visit_macro_call_post(self.storage, macro_call);
    }

    fn visit_expr_method_call(&mut self, method_call: &'ast ExprMethodCall) {
        let call = Call::MethodCall(method_call);

        let walk = self.generic_visitor.visit_call(self.storage, call);

        if walk {
            visit_expr_method_call(self, method_call);
        } else {
            self.visit_expr(&method_call.receiver);
        }

        self.generic_visitor.visit_call_post(self.storage, call);
    }
}

fn is_test(item: &ItemFn) -> Option<&Ident> {
    if item.attrs.iter().any(|attr| {
        let path = attr
            .path()
            .segments
            .iter()
            .map(|PathSegment { ident, arguments }| {
                if arguments.is_empty() {
                    Some(ident.to_string())
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();
        matches!(
            path.iter()
                .map(Option::as_deref)
                .collect::<Vec<_>>()
                .as_slice(),
            &[Some("test")] | &[Some("tokio"), Some("test")]
        )
    }) {
        Some(&item.sig.ident)
    } else {
        None
    }
}

#[cfg(test)]
mod test {
    use super::Rust;
    use crate::ParseLow;
    use if_chain::if_chain;
    use std::fs::read_to_string;
    use syn::{parse_file, Expr, ExprArray, ExprLit, ExprReference, Item, ItemConst, Lit};

    const UNNECESSARY_CONVERSION_FOR_TRAIT_URL: &str = "https://raw.githubusercontent.com/trailofbits/dylint/master/examples/supplementary/unnecessary_conversion_for_trait/src/lib.rs";

    const REMOVED_METHODS: &[&str] = &["path", "new"];

    const ADDED_METHODS: &[&str] = &[
        "clone",
        "cloned",
        "copied",
        "expect",
        "expect_err",
        "into_owned",
        "success",
        "unwrap",
        "unwrap_err",
    ];

    #[test]
    fn readme_contains_ignored_macros() {
        assert!(readme_contains_code_bulleted_list(
            Rust::IGNORED_MACROS.unwrap()
        ));
    }

    #[test]
    fn readme_contains_ignored_methods() {
        assert!(readme_contains_code_bulleted_list(
            Rust::IGNORED_METHODS.unwrap()
        ));
    }

    #[test]
    fn readme_contains_ignored_method_additions() {
        assert!(readme_contains_code_bulleted_list(ADDED_METHODS));
    }

    #[cfg_attr(
        dylint_lib = "assert_eq_arg_misordering",
        allow(assert_eq_arg_misordering)
    )]
    #[test]
    fn ignored_macros_are_sorted() {
        assert_eq!(
            sort(Rust::IGNORED_MACROS.unwrap()),
            Rust::IGNORED_MACROS.unwrap()
        );
    }

    #[cfg_attr(
        dylint_lib = "assert_eq_arg_misordering",
        allow(assert_eq_arg_misordering)
    )]
    #[test]
    fn ignored_methods_are_sorted() {
        assert_eq!(
            sort(Rust::IGNORED_METHODS.unwrap()),
            Rust::IGNORED_METHODS.unwrap()
        );
    }

    #[cfg_attr(
        dylint_lib = "assert_eq_arg_misordering",
        allow(assert_eq_arg_misordering)
    )]
    #[test]
    fn added_methods_are_sorted() {
        assert_eq!(sort(ADDED_METHODS), ADDED_METHODS);
    }

    #[cfg_attr(
        dylint_lib = "assert_eq_arg_misordering",
        allow(assert_eq_arg_misordering)
    )]
    #[test]
    fn ignored_methods_match_unnecessary_conversion_for_trait_watched_methods() {
        let data = get(UNNECESSARY_CONVERSION_FOR_TRAIT_URL).unwrap();
        let contents = std::str::from_utf8(&data).unwrap();
        #[allow(clippy::panic)]
        let file = parse_file(contents).unwrap_or_else(|_| panic!("Failed to parse: {contents:?}"));
        let mut watched_methods = file
            .items
            .into_iter()
            .flat_map(|item| {
                let elems = if_chain! {
                    if let Item::Const(ItemConst { ident, expr, .. }) = item;
                    if ["WATCHED_TRAITS", "WATCHED_INHERENTS"].contains(&ident.to_string().as_str());
                    if let Expr::Reference(ExprReference { expr, .. }) = *expr;
                    if let Expr::Array(ExprArray { elems, .. }) = *expr;
                    then {
                        elems.iter().cloned().collect::<Vec<_>>()
                    } else {
                        Vec::new()
                    }
                };
                elems
                    .into_iter()
                    .filter_map(|expr| {
                        if_chain! {
                            if let Expr::Reference(ExprReference { expr, .. }) = expr;
                            if let Expr::Array(ExprArray { elems, .. }) = *expr;
                            if let Some(Expr::Lit(ExprLit { lit, .. })) = elems.last();
                            if let Lit::Str(lit_str) = lit;
                            let s = lit_str.value();
                            if !REMOVED_METHODS.contains(&s.as_str());
                            then {
                                Some(s)
                            } else {
                                None
                            }
                        }
                    })
            })
            .chain(ADDED_METHODS.iter().map(ToString::to_string))
            .collect::<Vec<_>>();
        watched_methods.sort_unstable();
        watched_methods.dedup();
        assert_eq!(watched_methods, Rust::IGNORED_METHODS.unwrap());
    }

    fn readme_contains_code_bulleted_list(items: &[&str]) -> bool {
        let n = items.len();
        #[allow(clippy::unwrap_used)]
        let readme = read_to_string("../README.md").unwrap();
        readme.lines().collect::<Vec<_>>().windows(n).any(|window| {
            window
                .iter()
                .zip(items)
                .all(|(line, item)| line.starts_with(&format!("- `{item}`")))
        })
    }

    fn sort<'a>(items: &'a [&str]) -> Vec<&'a str> {
        let mut items = items.to_vec();
        items.sort_unstable();
        items
    }

    fn get(url: &str) -> Result<Vec<u8>, curl::Error> {
        let mut data = Vec::new();
        let mut handle = curl::easy::Easy::new();
        handle.url(url)?;
        {
            let mut transfer = handle.transfer();
            transfer.write_function(|new_data| {
                data.extend_from_slice(new_data);
                Ok(new_data.len())
            })?;
            transfer.perform()?;
        }
        Ok(data)
    }
}