use crate::config::StructuralConfig;
use crate::findings::Dimension;
use super::{StructuralWarning, StructuralWarningKind};
pub(crate) fn detect_btc(
warnings: &mut Vec<StructuralWarning>,
parsed: &[(String, String, syn::File)],
config: &StructuralConfig,
) {
if !config.check_btc {
return;
}
parsed.iter().for_each(|(path, _, syntax)| {
syntax.items.iter().for_each(|item| {
check_item(item, path, warnings);
});
});
}
fn check_item(item: &syn::Item, path: &str, warnings: &mut Vec<StructuralWarning>) {
let impl_check = |imp: &syn::ItemImpl, p: &str, w: &mut Vec<StructuralWarning>| {
check_impl(imp, p, w);
};
match item {
syn::Item::Impl(imp) => impl_check(imp, path, warnings),
syn::Item::Mod(m) if !super::has_cfg_test_attr(&m.attrs) => {
m.content.iter().for_each(|(_, items)| {
items.iter().for_each(|i| check_item(i, path, warnings));
});
}
_ => {}
}
}
fn check_impl(imp: &syn::ItemImpl, path: &str, warnings: &mut Vec<StructuralWarning>) {
let (_, trait_path, _) = match &imp.trait_ {
Some(t) => t,
None => return,
};
let trait_name = trait_path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
let methods: Vec<&syn::ImplItemFn> = imp
.items
.iter()
.filter_map(|i| match i {
syn::ImplItem::Fn(f) => Some(f),
_ => None,
})
.collect();
if methods.is_empty() {
return;
}
methods
.iter()
.filter(|m| is_stub_body(&m.block))
.for_each(|m| {
let line = m.sig.ident.span().start().line;
warnings.push(StructuralWarning {
file: path.to_string(),
line,
name: m.sig.ident.to_string(),
kind: StructuralWarningKind::BrokenTraitContract {
trait_name: trait_name.clone(),
},
dimension: Dimension::Srp,
suppressed: false,
});
});
}
fn is_stub_body(block: &syn::Block) -> bool {
if block.stmts.len() != 1 {
return false;
}
let expr = match &block.stmts[0] {
syn::Stmt::Expr(expr, _) => expr,
_ => return false,
};
let mac = match expr {
syn::Expr::Macro(m) => &m.mac,
_ => return false,
};
let name = mac
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
match name.as_str() {
"todo" | "unimplemented" => true,
"panic" => {
let tokens = mac.tokens.to_string();
tokens.contains("not implemented") || tokens.contains("not yet implemented")
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn detect_in(source: &str) -> Vec<StructuralWarning> {
let syntax = syn::parse_file(source).expect("test source");
let parsed = vec![("test.rs".to_string(), source.to_string(), syntax)];
let config = StructuralConfig::default();
let mut warnings = Vec::new();
detect_btc(&mut warnings, &parsed, &config);
warnings
}
#[test]
fn test_all_stub_methods_flagged() {
let w = detect_in("trait Foo { fn bar(&self); } impl Foo for MyType { fn bar(&self) { todo!() } } struct MyType;");
assert_eq!(w.len(), 1);
assert!(matches!(
w[0].kind,
StructuralWarningKind::BrokenTraitContract { .. }
));
}
#[test]
fn test_unimplemented_flagged() {
let w = detect_in("trait Foo { fn bar(&self); } impl Foo for MyType { fn bar(&self) { unimplemented!() } } struct MyType;");
assert_eq!(w.len(), 1);
}
#[test]
fn test_panic_not_implemented_flagged() {
let w = detect_in("trait Foo { fn bar(&self); } impl Foo for MyType { fn bar(&self) { panic!(\"not implemented\") } } struct MyType;");
assert_eq!(w.len(), 1);
}
#[test]
fn test_real_impl_not_flagged() {
let w = detect_in("trait Foo { fn bar(&self) -> i32; } impl Foo for MyType { fn bar(&self) -> i32 { 42 } } struct MyType;");
assert!(w.is_empty());
}
#[test]
fn test_inherent_impl_not_flagged() {
let w = detect_in("struct MyType; impl MyType { fn bar(&self) { todo!() } }");
assert!(w.is_empty());
}
#[test]
fn test_empty_impl_not_flagged() {
let w = detect_in("trait Foo {} impl Foo for MyType {} struct MyType;");
assert!(w.is_empty());
}
#[test]
fn test_partial_stub_flags_only_stubs() {
let w = detect_in("trait Foo { fn a(&self); fn b(&self) -> i32; } impl Foo for M { fn a(&self) { todo!() } fn b(&self) -> i32 { 42 } } struct M;");
assert_eq!(w.len(), 1);
assert_eq!(w[0].name, "a");
}
#[test]
fn test_disabled_check() {
let syntax = syn::parse_file(
"trait Foo { fn bar(&self); } impl Foo for M { fn bar(&self) { todo!() } } struct M;",
)
.expect("test source");
let parsed = vec![("test.rs".to_string(), String::new(), syntax)];
let config = StructuralConfig {
check_btc: false,
..StructuralConfig::default()
};
let mut warnings = Vec::new();
detect_btc(&mut warnings, &parsed, &config);
assert!(warnings.is_empty());
}
}