use syn::visit::Visit;
use super::{TqWarning, TqWarningKind};
#[cfg(doc)]
const _ASSERTION_MACROS: &[&str] = &[
"assert",
"assert_eq",
"assert_ne",
"debug_assert",
"debug_assert_eq",
"debug_assert_ne",
];
pub(crate) fn detect_assertion_free_tests(
parsed: &[(String, String, syn::File)],
extra_assertion_macros: &[String],
) -> Vec<TqWarning> {
let mut warnings = Vec::new();
for (path, _, syntax) in parsed {
let mut collector = TestFunctionCollector::default();
collector.visit_file(syntax);
for test_fn in &collector.test_fns {
let mut visitor = TestAssertionVisitor {
extra_macros: extra_assertion_macros,
..Default::default()
};
visitor.visit_block(&test_fn.body);
if !(visitor.has_assertion
|| (test_fn.should_panic && visitor.has_panic)
|| visitor.has_call)
{
warnings.push(TqWarning {
file: path.clone(),
line: test_fn.line,
function_name: test_fn.name.clone(),
kind: TqWarningKind::NoAssertion,
suppressed: false,
});
}
}
}
warnings
}
struct TestFnInfo {
name: String,
line: usize,
body: syn::Block,
should_panic: bool,
}
#[derive(Default)]
struct TestFunctionCollector {
test_fns: Vec<TestFnInfo>,
in_cfg_test: bool,
}
impl<'ast> Visit<'ast> for TestFunctionCollector {
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
let was_in_cfg_test = self.in_cfg_test;
if has_cfg_test(&node.attrs) {
self.in_cfg_test = true;
}
syn::visit::visit_item_mod(self, node);
self.in_cfg_test = was_in_cfg_test;
}
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if has_test_attr(&node.attrs) {
let line = node.sig.ident.span().start().line;
self.test_fns.push(TestFnInfo {
name: node.sig.ident.to_string(),
line,
body: (*node.block).clone(),
should_panic: has_should_panic_attr(&node.attrs),
});
}
syn::visit::visit_item_fn(self, node);
}
}
#[derive(Default)]
struct TestAssertionVisitor<'cfg> {
has_assertion: bool,
has_panic: bool,
has_call: bool,
extra_macros: &'cfg [String],
}
fn is_assertion_macro(name: &str, extra_macros: &[String]) -> bool {
name.starts_with("assert")
|| name.starts_with("debug_assert")
|| extra_macros.iter().any(|m| m == name)
}
impl<'ast> Visit<'ast> for TestAssertionVisitor<'_> {
fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
let macro_name = macro_ident_name(&node.mac);
if is_assertion_macro(¯o_name, self.extra_macros) {
self.has_assertion = true;
}
if macro_name == "panic" {
self.has_panic = true;
}
syn::visit::visit_expr_macro(self, node);
}
fn visit_macro(&mut self, node: &'ast syn::Macro) {
let macro_name = macro_ident_name(node);
if is_assertion_macro(¯o_name, self.extra_macros) {
self.has_assertion = true;
}
if macro_name == "panic" {
self.has_panic = true;
}
syn::visit::visit_macro(self, node);
}
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
self.has_call = true;
syn::visit::visit_expr_call(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
self.has_call = true;
syn::visit::visit_expr_method_call(self, node);
}
}
fn macro_ident_name(mac: &syn::Macro) -> String {
mac.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default()
}
use crate::adapters::shared::cfg_test::{has_cfg_test, has_test_attr};
fn has_should_panic_attr(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| a.path().is_ident("should_panic"))
}