pub(crate) mod btc;
pub(crate) mod deh;
pub(crate) mod iet;
pub(crate) mod nms;
pub(crate) mod oi;
pub(crate) mod sit;
pub(crate) mod slm;
use std::collections::HashMap;
use crate::config::StructuralConfig;
use crate::findings::Dimension;
#[derive(Debug, Clone)]
pub struct StructuralWarning {
pub file: String,
pub line: usize,
pub name: String,
pub kind: StructuralWarningKind,
pub dimension: Dimension,
pub suppressed: bool,
}
#[derive(Debug, Clone)]
pub enum StructuralWarningKind {
BrokenTraitContract { trait_name: String },
SelflessMethod,
NeedlessMutSelf,
OrphanedImpl { defining_file: String },
SingleImplTrait { impl_type: String },
DowncastEscapeHatch,
InconsistentErrorTypes { error_types: Vec<String> },
}
impl StructuralWarningKind {
pub fn code(&self) -> &'static str {
match self {
Self::BrokenTraitContract { .. } => "BTC",
Self::SelflessMethod => "SLM",
Self::NeedlessMutSelf => "NMS",
Self::OrphanedImpl { .. } => "OI",
Self::SingleImplTrait { .. } => "SIT",
Self::DowncastEscapeHatch => "DEH",
Self::InconsistentErrorTypes { .. } => "IET",
}
}
pub fn detail(&self) -> String {
match self {
Self::BrokenTraitContract { trait_name } => format!("stub impl of trait {trait_name}"),
Self::SelflessMethod => "self never referenced".to_string(),
Self::NeedlessMutSelf => "&mut self but no mutation".to_string(),
Self::OrphanedImpl { defining_file } => format!("type defined in {defining_file}"),
Self::SingleImplTrait { impl_type } => format!("only impl: {impl_type}"),
Self::DowncastEscapeHatch => "downcast usage".to_string(),
Self::InconsistentErrorTypes { error_types } => {
format!("error types: {}", error_types.join(", "))
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StructuralAnalysis {
pub warnings: Vec<StructuralWarning>,
}
pub(crate) struct StructuralMetadata {
pub enum_defs: HashMap<String, (String, Vec<String>)>,
pub type_defs: HashMap<String, String>,
pub trait_defs: HashMap<String, TraitInfo>,
pub trait_impls: HashMap<String, Vec<(String, String)>>,
pub inherent_impls: Vec<(String, String)>,
}
pub(crate) struct TraitInfo {
pub file: String,
pub is_pub: bool,
pub method_count: usize,
}
pub(crate) fn collect_metadata(parsed: &[(String, String, syn::File)]) -> StructuralMetadata {
let mut meta = StructuralMetadata {
enum_defs: HashMap::new(),
type_defs: HashMap::new(),
trait_defs: HashMap::new(),
trait_impls: HashMap::new(),
inherent_impls: Vec::new(),
};
parsed.iter().for_each(|(path, _, syntax)| {
syntax.items.iter().for_each(|item| {
collect_item_metadata(item, path, &mut meta);
});
});
meta
}
fn collect_item_metadata(item: &syn::Item, path: &str, meta: &mut StructuralMetadata) {
let impl_type_name = |imp: &syn::ItemImpl| -> Option<String> { extract_impl_type_name(imp) };
let cfg_test = |attrs: &[syn::Attribute]| -> bool { has_cfg_test_attr(attrs) };
match item {
syn::Item::Enum(e) => {
let name = e.ident.to_string();
let variants: Vec<String> = e.variants.iter().map(|v| v.ident.to_string()).collect();
meta.type_defs.insert(name.clone(), path.to_string());
meta.enum_defs.insert(name, (path.to_string(), variants));
}
syn::Item::Struct(s) => {
meta.type_defs.insert(s.ident.to_string(), path.to_string());
}
syn::Item::Trait(t) => {
let is_pub = matches!(t.vis, syn::Visibility::Public(_));
let method_count = t
.items
.iter()
.filter(|i| matches!(i, syn::TraitItem::Fn(_)))
.count();
meta.trait_defs.insert(
t.ident.to_string(),
TraitInfo {
file: path.to_string(),
is_pub,
method_count,
},
);
}
syn::Item::Impl(imp) => {
if let Some(ref type_name) = impl_type_name(imp) {
if let Some((_, ref tp, _)) = imp.trait_ {
let tn = tp
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
meta.trait_impls
.entry(tn)
.or_default()
.push((type_name.clone(), path.to_string()));
} else {
meta.inherent_impls
.push((type_name.clone(), path.to_string()));
}
}
}
syn::Item::Mod(m) if !cfg_test(&m.attrs) => {
m.content.iter().for_each(|(_, items)| {
items
.iter()
.for_each(|i| collect_item_metadata(i, path, meta));
});
}
_ => {}
}
}
fn extract_impl_type_name(imp: &syn::ItemImpl) -> Option<String> {
match &*imp.self_ty {
syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
use crate::dry::has_cfg_test as has_cfg_test_attr;
pub(crate) fn visit_inherent_methods(
parsed: &[(String, String, syn::File)],
mut callback: impl FnMut(&syn::ImplItemFn, &str),
) {
let visit_item = |item: &syn::Item, path: &str, cb: &mut dyn FnMut(&syn::ImplItemFn, &str)| {
visit_item_methods(item, path, cb);
};
parsed.iter().for_each(|(path, _, syntax)| {
syntax
.items
.iter()
.for_each(|item| visit_item(item, path, &mut callback));
});
}
fn visit_item_methods(
item: &syn::Item,
path: &str,
callback: &mut dyn FnMut(&syn::ImplItemFn, &str),
) {
match item {
syn::Item::Impl(imp) => {
if imp.trait_.is_some() {
return;
}
imp.items.iter().for_each(|i| {
if let syn::ImplItem::Fn(method) = i {
callback(method, path);
}
});
}
syn::Item::Mod(m) if !has_cfg_test_attr(&m.attrs) => {
m.content.iter().for_each(|(_, items)| {
items
.iter()
.for_each(|i| visit_item_methods(i, path, callback));
});
}
_ => {}
}
}
pub(crate) fn analyze_structural(
parsed: &[(String, String, syn::File)],
config: &StructuralConfig,
) -> StructuralAnalysis {
let meta = collect_metadata(parsed);
let mut warnings = Vec::new();
btc::detect_btc(&mut warnings, parsed, config);
slm::detect_slm(&mut warnings, parsed, config);
nms::detect_nms(&mut warnings, parsed, config);
deh::detect_deh(&mut warnings, parsed, config);
oi::detect_oi(&mut warnings, &meta, config);
sit::detect_sit(&mut warnings, &meta, config);
iet::detect_iet(&mut warnings, parsed, config);
StructuralAnalysis { warnings }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_structural_analysis_default_empty() {
let analysis = StructuralAnalysis::default();
assert!(analysis.warnings.is_empty());
}
#[test]
fn test_collect_metadata_empty() {
let parsed: Vec<(String, String, syn::File)> = vec![];
let meta = collect_metadata(&parsed);
assert!(meta.enum_defs.is_empty());
assert!(meta.type_defs.is_empty());
assert!(meta.trait_defs.is_empty());
}
#[test]
fn test_collect_metadata_enum() {
let source = "pub enum Color { Red, Green, Blue }";
let syntax = syn::parse_file(source).expect("test source");
let parsed = vec![("lib.rs".to_string(), source.to_string(), syntax)];
let meta = collect_metadata(&parsed);
assert!(meta.enum_defs.contains_key("Color"));
let (file, variants) = &meta.enum_defs["Color"];
assert_eq!(file, "lib.rs");
assert_eq!(variants, &["Red", "Green", "Blue"]);
}
#[test]
fn test_collect_metadata_struct_and_impl() {
let source = "struct Foo {} impl Foo { fn bar() {} }";
let syntax = syn::parse_file(source).expect("test source");
let parsed = vec![("lib.rs".to_string(), source.to_string(), syntax)];
let meta = collect_metadata(&parsed);
assert_eq!(meta.type_defs.get("Foo"), Some(&"lib.rs".to_string()));
assert_eq!(meta.inherent_impls.len(), 1);
assert_eq!(meta.inherent_impls[0].0, "Foo");
}
#[test]
fn test_collect_metadata_trait_and_impl() {
let source = "trait Drawable { fn draw(&self); } struct Circle; impl Drawable for Circle { fn draw(&self) {} }";
let syntax = syn::parse_file(source).expect("test source");
let parsed = vec![("lib.rs".to_string(), source.to_string(), syntax)];
let meta = collect_metadata(&parsed);
assert!(meta.trait_defs.contains_key("Drawable"));
assert!(!meta.trait_defs["Drawable"].is_pub);
assert_eq!(meta.trait_defs["Drawable"].method_count, 1);
assert_eq!(meta.trait_impls["Drawable"].len(), 1);
}
#[test]
fn test_cfg_test_module_excluded() {
let source = "#[cfg(test)] mod tests { struct TestOnly; }";
let syntax = syn::parse_file(source).expect("test source");
let parsed = vec![("lib.rs".to_string(), source.to_string(), syntax)];
let meta = collect_metadata(&parsed);
assert!(!meta.type_defs.contains_key("TestOnly"));
}
}