use std::path::{Path, PathBuf};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::visit::{self, Visit};
use syn::{
Arm, Attribute, Block, Expr, ExprLit, ExprMethodCall, GenericArgument, ImplItemFn, ItemFn,
ItemImpl, ItemMod, ItemTrait, Lit, Local, Macro, Meta, Pat, Path as SynPath, PathArguments,
ReturnType, Stmt, Token, TraitItemFn, Type, TypeParamBound, Visibility,
};
use crate::finding::{Finding, Severity};
use crate::functions::{has_test_attr, qualified_item_path, read_and_parse_source, type_name};
use crate::ingest::SourceFile;
use crate::rules::api_surface::{build_finding, has_doc_comment};
pub const SWALLOWED_RESULT_RULE: &str = "swallowed-result";
pub const SWALLOWED_RESULT_RULE_REVISION: u32 = 1;
pub const EMPTY_ERROR_ARM_RULE: &str = "empty-error-arm";
pub const EMPTY_ERROR_ARM_RULE_REVISION: u32 = 1;
pub const CATCH_ALL_ERROR_RULE: &str = "catch-all-error";
pub const CATCH_ALL_ERROR_RULE_REVISION: u32 = 2;
pub const SUPPRESSION_DEBT_RULE: &str = "suppression-debt";
pub const SUPPRESSION_DEBT_RULE_REVISION: u32 = 1;
pub const MERGED_STUB_RULE: &str = "merged-stub";
pub const MERGED_STUB_RULE_REVISION: u32 = 1;
pub const EMPTY_IMPL_RULE: &str = "empty-impl";
pub const EMPTY_IMPL_RULE_REVISION: u32 = 1;
pub const ASSERTION_FREE_TEST_RULE: &str = "assertion-free-test";
pub const ASSERTION_FREE_TEST_RULE_REVISION: u32 = 1;
pub const TAUTOLOGICAL_TEST_RULE: &str = "tautological-test";
pub const TAUTOLOGICAL_TEST_RULE_REVISION: u32 = 1;
pub const IGNORED_TEST_ACCUMULATION_RULE: &str = "ignored-test-accumulation";
pub const IGNORED_TEST_ACCUMULATION_RULE_REVISION: u32 = 1;
pub const CONVERSATIONAL_ARTIFACT_RULE: &str = "conversational-artifact";
pub const CONVERSATIONAL_ARTIFACT_RULE_REVISION: u32 = 1;
pub const RESTATING_COMMENT_RULE: &str = "restating-comment";
pub const RESTATING_COMMENT_RULE_REVISION: u32 = 1;
pub const STEP_COMMENT_INFLATION_RULE: &str = "step-comment-inflation";
pub const STEP_COMMENT_INFLATION_RULE_REVISION: u32 = 1;
pub const GENERIC_NAMING_RULE: &str = "generic-naming";
pub const GENERIC_NAMING_RULE_REVISION: u32 = 1;
pub const DOC_RESTATES_SIGNATURE_RULE: &str = "doc-restates-signature";
pub const DOC_RESTATES_SIGNATURE_RULE_REVISION: u32 = 1;
pub const SILENT_DEFAULT_RULE: &str = "silent-default";
pub const SILENT_DEFAULT_RULE_REVISION: u32 = 1;
pub const CONTEXT_FREE_PROPAGATION_RULE: &str = "context-free-propagation";
pub const CONTEXT_FREE_PROPAGATION_RULE_REVISION: u32 = 1;
pub const DEBUG_FORMAT_LEAK_RULE: &str = "debug-format-leak";
pub const DEBUG_FORMAT_LEAK_RULE_REVISION: u32 = 1;
#[derive(Debug)]
pub enum SlopError {
Io(PathBuf, std::io::Error),
Parse(PathBuf, syn::Error),
}
impl std::fmt::Display for SlopError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(path, err) => write!(f, "{}: failed to read file: {err}", path.display()),
Self::Parse(path, err) => write!(f, "{}: failed to parse: {err}", path.display()),
}
}
}
impl std::error::Error for SlopError {}
#[derive(Debug, Default)]
pub struct WorkspaceSlop {
pub findings: Vec<Finding>,
pub errors: Vec<SlopError>,
pub excluded_generated: usize,
}
pub fn analyze_file(
path: &Path,
allow_anyhow_at_boundary: bool,
) -> Result<Vec<Finding>, SlopError> {
let (source, ast) = read_and_parse_source(
path,
|err| SlopError::Io(path.to_path_buf(), err),
|err| SlopError::Parse(path.to_path_buf(), err),
)?;
let mut visitor = SlopVisitor {
file: path,
path: Vec::new(),
findings: Vec::new(),
feature_gated_depth: 0,
item_spans: Vec::new(),
allow_anyhow_at_boundary,
display_impl_type_stack: Vec::new(),
visitor_trait_impl_stack: Vec::new(),
drop_impl_stack: Vec::new(),
in_drop_drop_body: false,
};
visitor.visit_file(&ast);
let mut findings = visitor.findings;
findings.extend(crate::rules::slop_text::scan_comments(
&source,
&visitor.item_spans,
path,
));
Ok(findings)
}
pub fn analyze_workspace<'a>(
source_files: impl IntoIterator<Item = &'a SourceFile>,
include_generated: bool,
allow_anyhow_at_boundary: bool,
) -> WorkspaceSlop {
let mut report = WorkspaceSlop::default();
for file in source_files {
if !include_generated && !file.kind.is_locally_reportable() {
report.excluded_generated += 1;
continue;
}
match analyze_file(&file.path, allow_anyhow_at_boundary) {
Ok(mut findings) => report.findings.append(&mut findings),
Err(err) => report.errors.push(err),
}
}
report
}
pub(crate) struct ItemSpan {
pub start_line: usize,
pub end_line: usize,
pub item_path: String,
}
struct SlopVisitor<'a> {
file: &'a Path,
path: Vec<String>,
findings: Vec<Finding>,
feature_gated_depth: usize,
pub(crate) item_spans: Vec<ItemSpan>,
allow_anyhow_at_boundary: bool,
display_impl_type_stack: Vec<String>,
visitor_trait_impl_stack: Vec<bool>,
drop_impl_stack: Vec<bool>,
in_drop_drop_body: bool,
}
impl SlopVisitor<'_> {
fn current_item_path(&self) -> String {
qualified_item_path(self.file, &self.path)
}
fn record(
&mut self,
rule: &'static str,
span: proc_macro2::Span,
severity: Severity,
item_path: String,
) {
self.record_with_evidence(rule, span, severity, item_path, None);
}
fn record_with_evidence(
&mut self,
rule: &'static str,
span: proc_macro2::Span,
severity: Severity,
item_path: String,
evidence: Option<serde_json::Value>,
) {
self.findings.push(build_finding(
self.file, rule, span, severity, item_path, evidence,
));
}
fn enter_feature_gated(&mut self, attrs: &[Attribute]) -> bool {
let gated = has_feature_cfg(attrs);
if gated {
self.feature_gated_depth += 1;
}
gated
}
fn exit_feature_gated(&mut self, gated: bool) {
if gated {
self.feature_gated_depth -= 1;
}
}
fn enter_fn(
&mut self,
vis: &Visibility,
attrs: &[Attribute],
sig: &syn::Signature,
block: &Block,
span: proc_macro2::Span,
) -> bool {
self.item_spans.push(ItemSpan {
start_line: span.start().line,
end_line: span.end().line,
item_path: self.current_item_path(),
});
self.check_catch_all_error(vis, sig, block, span);
self.check_doc_restates_signature(attrs, sig, span);
self.check_silent_default(block);
self.check_context_free_propagation(sig, block, span);
self.enter_feature_gated(attrs)
}
fn check_catch_all_error(
&mut self,
vis: &Visibility,
sig: &syn::Signature,
block: &Block,
span: proc_macro2::Span,
) {
if !matches!(vis, Visibility::Public(_)) {
return;
}
let Some(ty) = return_type(&sig.output) else {
return;
};
if !contains_catch_all_error(ty, self.allow_anyhow_at_boundary) {
return;
}
if !discards_error_via_map_err(block) {
return;
}
let item_path = self.current_item_path();
self.record(CATCH_ALL_ERROR_RULE, span, Severity::Warn, item_path);
}
fn check_empty_impl(&mut self, attrs: &[Attribute], block: &Block, span: proc_macro2::Span) {
if has_doc_comment(attrs) && block.stmts.is_empty() {
let item_path = self.current_item_path();
self.record(EMPTY_IMPL_RULE, span, Severity::Warn, item_path);
}
}
fn check_assertion_free_test(&mut self, node: &ItemFn) {
if !has_test_attr(&node.attrs)
|| node
.attrs
.iter()
.any(|attr| attr.path().is_ident("should_panic"))
{
return;
}
let returns_result = returns_result_type(&node.sig.output);
let mut scanner = AssertionScanner {
found: false,
returns_result,
};
scanner.visit_block(&node.block);
if !scanner.found {
let item_path = self.current_item_path();
self.record(
ASSERTION_FREE_TEST_RULE,
node.span(),
Severity::Warn,
item_path,
);
}
}
fn check_generic_naming_local(&mut self, pat: &Pat, span: proc_macro2::Span) {
let Pat::Ident(pat_ident) = pat else {
return;
};
let name = pat_ident.ident.to_string();
let stripped = strip_trailing_digits(&name);
if stripped.len() == name.len() {
return;
}
if is_generic_word(stripped) {
let item_path = self.current_item_path();
self.record(GENERIC_NAMING_RULE, span, Severity::Info, item_path);
}
}
fn check_generic_naming_named_item(
&mut self,
vis: &Visibility,
ident: &syn::Ident,
span: proc_macro2::Span,
) {
if matches!(vis, Visibility::Public(_)) && is_generic_word(&ident.to_string()) {
let item_path = self.current_item_path();
self.record(GENERIC_NAMING_RULE, span, Severity::Info, item_path);
}
}
fn check_generic_naming_item_fn(&mut self, node: &ItemFn) {
self.check_generic_naming_named_item(&node.vis, &node.sig.ident, node.span());
}
fn check_generic_naming_item_struct(&mut self, node: &syn::ItemStruct) {
self.check_generic_naming_named_item(&node.vis, &node.ident, node.span());
for field in &node.fields {
if let Some(ident) = &field.ident {
self.check_generic_naming_named_item(&field.vis, ident, field.span());
}
}
}
fn check_generic_naming_item_enum(&mut self, node: &syn::ItemEnum) {
self.check_generic_naming_named_item(&node.vis, &node.ident, node.span());
}
fn check_doc_restates_signature(
&mut self,
attrs: &[Attribute],
sig: &syn::Signature,
span: proc_macro2::Span,
) {
let doc_text = doc_comment_text(attrs);
let Some(doc_text) = doc_text else {
return;
};
let doc_tokens = tokenize(&doc_text);
if doc_tokens.is_empty() || doc_tokens.len() > 6 {
return;
}
const STOPWORDS: &[&str] = &[
"returns", "return", "get", "gets", "the", "a", "an", "of", "for",
];
let content_tokens: Vec<&String> = doc_tokens
.iter()
.filter(|token| !STOPWORDS.contains(&token.as_str()))
.collect();
if content_tokens.is_empty() {
return;
}
let sig_tokens = signature_tokens(sig);
if content_tokens
.iter()
.all(|token| sig_tokens.contains(token.as_str()))
{
let item_path = self.current_item_path();
self.record(DOC_RESTATES_SIGNATURE_RULE, span, Severity::Info, item_path);
}
}
fn check_silent_default(&mut self, block: &Block) {
let mut collector = SilentDefaultVisitor { sites: Vec::new() };
collector.visit_block(block);
if collector.sites.is_empty() || error_observation_present(block) {
return;
}
let item_path = self.current_item_path();
for (span, method) in collector.sites {
self.record_with_evidence(
SILENT_DEFAULT_RULE,
span,
Severity::Warn,
item_path.clone(),
Some(serde_json::json!({
"file": self.file.display().to_string(),
"line": span.start().line,
"method": method,
})),
);
}
}
fn check_context_free_propagation(
&mut self,
sig: &syn::Signature,
block: &Block,
span: proc_macro2::Span,
) {
let Some(ty) = return_type(&sig.output) else {
return;
};
if !is_opaque_error_return_type(ty) {
return;
}
let mut scanner = TrySiteScanner {
try_site_keys: std::collections::HashSet::new(),
has_context_call: false,
};
scanner.visit_block(block);
if scanner.try_site_keys.len() < 2 || scanner.has_context_call {
return;
}
let item_path = self.current_item_path();
self.record_with_evidence(
CONTEXT_FREE_PROPAGATION_RULE,
span,
Severity::Warn,
item_path,
Some(serde_json::json!({
"file": self.file.display().to_string(),
"function": sig.ident.to_string(),
"line": span.start().line,
"try_site_count": scanner.try_site_keys.len(),
})),
);
}
}
impl<'ast> Visit<'ast> for SlopVisitor<'_> {
fn visit_item_mod(&mut self, node: &'ast ItemMod) {
let gated = self.enter_feature_gated(&node.attrs);
if node.content.is_some() {
self.path.push(node.ident.to_string());
visit::visit_item_mod(self, node);
self.path.pop();
} else {
visit::visit_item_mod(self, node);
}
self.exit_feature_gated(gated);
}
fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
let gated = self.enter_feature_gated(&node.attrs);
let type_ident = type_name(&node.self_ty);
self.path.push(type_ident.clone());
let is_display_impl = is_display_trait_impl(node);
if is_display_impl {
self.display_impl_type_stack.push(type_ident);
}
self.visitor_trait_impl_stack
.push(is_ast_visitor_trait_impl(node));
self.drop_impl_stack.push(is_drop_trait_impl(node));
visit::visit_item_impl(self, node);
self.drop_impl_stack.pop();
self.visitor_trait_impl_stack.pop();
if is_display_impl {
self.display_impl_type_stack.pop();
}
self.path.pop();
self.exit_feature_gated(gated);
}
fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
self.path.push(node.ident.to_string());
visit::visit_item_trait(self, node);
self.path.pop();
}
fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
self.path.push(node.ident.to_string());
self.check_generic_naming_item_struct(node);
visit::visit_item_struct(self, node);
self.path.pop();
}
fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
self.path.push(node.ident.to_string());
self.check_generic_naming_item_enum(node);
visit::visit_item_enum(self, node);
self.path.pop();
}
fn visit_item_fn(&mut self, node: &'ast ItemFn) {
self.path.push(node.sig.ident.to_string());
self.check_empty_impl(&node.attrs, &node.block, node.span());
self.check_assertion_free_test(node);
self.check_generic_naming_item_fn(node);
let gated = self.enter_fn(&node.vis, &node.attrs, &node.sig, &node.block, node.span());
visit::visit_item_fn(self, node);
self.exit_feature_gated(gated);
self.path.pop();
}
fn visit_impl_item_fn(&mut self, node: &'ast ImplItemFn) {
self.path.push(node.sig.ident.to_string());
let is_visitor_trait_override = self
.visitor_trait_impl_stack
.last()
.copied()
.unwrap_or(false);
if !is_visitor_trait_override {
self.check_empty_impl(&node.attrs, &node.block, node.span());
}
let gated = self.enter_fn(&node.vis, &node.attrs, &node.sig, &node.block, node.span());
let is_drop_drop =
self.drop_impl_stack.last().copied().unwrap_or(false) && node.sig.ident == "drop";
let prev_in_drop_drop_body = self.in_drop_drop_body;
self.in_drop_drop_body = is_drop_drop;
visit::visit_impl_item_fn(self, node);
self.in_drop_drop_body = prev_in_drop_drop_body;
self.exit_feature_gated(gated);
self.path.pop();
}
fn visit_trait_item_fn(&mut self, node: &'ast TraitItemFn) {
self.path.push(node.sig.ident.to_string());
if let Some(default) = &node.default {
self.check_empty_impl(&node.attrs, default, node.span());
}
visit::visit_trait_item_fn(self, node);
self.path.pop();
}
fn visit_local(&mut self, node: &'ast Local) {
if !self.in_drop_drop_body
&& matches!(node.pat, Pat::Wild(_))
&& let Some(init) = &node.init
&& matches!(init.expr.as_ref(), Expr::Call(_) | Expr::MethodCall(_))
{
let item_path = self.current_item_path();
self.record(
SWALLOWED_RESULT_RULE,
node.span(),
Severity::Warn,
item_path,
);
}
self.check_generic_naming_local(&node.pat, node.span());
visit::visit_local(self, node);
}
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
if !self.in_drop_drop_body
&& let Stmt::Expr(Expr::MethodCall(call), Some(_)) = stmt
&& call.method == "ok"
{
let item_path = self.current_item_path();
self.record(
SWALLOWED_RESULT_RULE,
call.span(),
Severity::Warn,
item_path,
);
}
visit::visit_stmt(self, stmt);
}
fn visit_arm(&mut self, arm: &'ast Arm) {
if is_err_wildcard_pat(&arm.pat) && is_empty_block_expr(&arm.body) {
let item_path = self.current_item_path();
self.record(EMPTY_ERROR_ARM_RULE, arm.span(), Severity::Warn, item_path);
}
visit::visit_arm(self, arm);
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Some((if_expr, pat)) = if_let_cond_pat(expr)
&& if_expr.else_branch.is_none()
&& if_expr.then_branch.stmts.is_empty()
&& is_err_wildcard_pat(pat)
{
let item_path = self.current_item_path();
self.record(
EMPTY_ERROR_ARM_RULE,
if_expr.span(),
Severity::Warn,
item_path,
);
}
visit::visit_expr(self, expr);
}
fn visit_attribute(&mut self, attr: &'ast Attribute) {
if attr.path().is_ident("allow") || attr.path().is_ident("expect") {
let item_path = attr
.parse_args_with(Punctuated::<SynPath, Token![,]>::parse_terminated)
.ok()
.map(|paths| {
paths
.iter()
.map(path_to_string)
.collect::<Vec<_>>()
.join(", ")
})
.filter(|names| !names.is_empty())
.unwrap_or_else(|| self.file.display().to_string());
self.record(
SUPPRESSION_DEBT_RULE,
attr.span(),
Severity::Info,
item_path,
);
} else if attr.path().is_ident("ignore") {
let reason = meta_name_value_str(&attr.meta);
let item_path = self.current_item_path();
self.record_with_evidence(
IGNORED_TEST_ACCUMULATION_RULE,
attr.span(),
Severity::Info,
item_path,
reason.map(|reason| serde_json::json!({ "reason": reason })),
);
}
visit::visit_attribute(self, attr);
}
fn visit_macro(&mut self, mac: &'ast Macro) {
if (mac.path.is_ident("todo") || mac.path.is_ident("unimplemented"))
&& self.feature_gated_depth == 0
{
let item_path = self.current_item_path();
self.record(MERGED_STUB_RULE, mac.span(), Severity::Warn, item_path);
} else if mac.path.is_ident("assert") {
if let Some(args) = parse_macro_expr_args(mac)
&& let Some(Expr::Lit(ExprLit {
lit: Lit::Bool(value),
..
})) = args.first()
&& value.value
{
let item_path = self.current_item_path();
self.record(
TAUTOLOGICAL_TEST_RULE,
mac.span(),
Severity::Warn,
item_path,
);
}
} else if mac.path.is_ident("assert_eq")
&& let Some(args) = parse_macro_expr_args(mac)
&& let (Some(lhs), Some(rhs)) = (args.first(), args.get(1))
&& quote!(#lhs).to_string() == quote!(#rhs).to_string()
{
let item_path = self.current_item_path();
self.record(
TAUTOLOGICAL_TEST_RULE,
mac.span(),
Severity::Warn,
item_path,
);
} else if (mac.path.is_ident("write") || mac.path.is_ident("format"))
&& let Some(type_name) = self.display_impl_type_stack.last()
&& macro_format_string_has_debug_placeholder(mac)
{
let item_path = self.current_item_path();
self.record_with_evidence(
DEBUG_FORMAT_LEAK_RULE,
mac.span(),
Severity::Warn,
item_path,
Some(serde_json::json!({
"file": self.file.display().to_string(),
"type_name": type_name,
"line": mac.span().start().line,
})),
);
}
visit::visit_macro(self, mac);
}
}
fn path_to_string(path: &SynPath) -> String {
path.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
fn as_err_tuple_struct(pat: &Pat) -> Option<&syn::PatTupleStruct> {
match pat {
Pat::TupleStruct(tuple_struct)
if tuple_struct
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Err") =>
{
Some(tuple_struct)
}
_ => None,
}
}
fn is_err_wildcard_pat(pat: &Pat) -> bool {
as_err_tuple_struct(pat).is_some_and(|tuple_struct| {
tuple_struct.elems.len() == 1
&& matches!(tuple_struct.elems[0], Pat::Wild(_) | Pat::Rest(_))
})
}
fn is_empty_block_expr(expr: &Expr) -> bool {
matches!(expr, Expr::Block(block) if block.block.stmts.is_empty())
}
fn if_let_cond_pat(expr: &Expr) -> Option<(&syn::ExprIf, &Pat)> {
let Expr::If(if_expr) = expr else {
return None;
};
let Expr::Let(let_expr) = if_expr.cond.as_ref() else {
return None;
};
Some((if_expr, &let_expr.pat))
}
fn contains_catch_all_error(ty: &Type, allow_anyhow_at_boundary: bool) -> bool {
match ty {
Type::TraitObject(trait_object) => is_error_trait_object(trait_object),
Type::Path(type_path) => {
let segments = &type_path.path.segments;
if !allow_anyhow_at_boundary && segments.len() >= 2 {
let last = segments.last().unwrap();
let prev = &segments[segments.len() - 2];
if prev.ident == "anyhow" && (last.ident == "Error" || last.ident == "Result") {
return true;
}
}
segments.iter().any(|segment| {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return false;
};
args.args.iter().any(|arg| match arg {
GenericArgument::Type(inner) => {
contains_catch_all_error(inner, allow_anyhow_at_boundary)
}
_ => false,
})
})
}
_ => false,
}
}
fn is_error_trait_object(trait_object: &syn::TypeTraitObject) -> bool {
trait_object.bounds.iter().any(|bound| {
if let TypeParamBound::Trait(trait_bound) = bound {
trait_bound
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Error")
} else {
false
}
})
}
fn discards_error_via_map_err(block: &Block) -> bool {
struct MapErrDiscardVisitor {
found: bool,
}
impl<'ast> Visit<'ast> for MapErrDiscardVisitor {
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
if node.method == "map_err" && first_arg_is_wildcard_closure(&node.args).is_some() {
self.found = true;
}
visit::visit_expr_method_call(self, node);
}
fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
}
let mut visitor = MapErrDiscardVisitor { found: false };
visitor.visit_block(block);
visitor.found
}
fn pat_is_wildcard(pat: &Pat) -> bool {
match pat {
Pat::Wild(_) => true,
Pat::Type(pat_type) => pat_is_wildcard(&pat_type.pat),
_ => false,
}
}
fn first_arg_is_wildcard_closure(args: &Punctuated<Expr, Token![,]>) -> Option<&syn::ExprClosure> {
let Some(Expr::Closure(closure)) = args.first() else {
return None;
};
if closure.inputs.len() == 1 && pat_is_wildcard(&closure.inputs[0]) {
Some(closure)
} else {
None
}
}
fn is_default_call(expr: &Expr) -> bool {
let expr = match expr {
Expr::Block(block) if block.block.stmts.len() == 1 => match &block.block.stmts[0] {
Stmt::Expr(inner, None) => inner,
_ => return false,
},
other => other,
};
let Expr::Call(call) = expr else {
return false;
};
if !call.args.is_empty() {
return false;
}
let Expr::Path(path) = call.func.as_ref() else {
return false;
};
path.path
.segments
.last()
.is_some_and(|segment| segment.ident == "default")
}
struct SilentDefaultVisitor {
sites: Vec<(proc_macro2::Span, &'static str)>,
}
impl<'ast> Visit<'ast> for SilentDefaultVisitor {
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
if node.method == "unwrap_or_default" && node.args.is_empty() {
self.sites.push((node.span(), "unwrap_or_default"));
} else if node.method == "unwrap_or_else"
&& let Some(closure) = first_arg_is_wildcard_closure(&node.args)
&& is_default_call(&closure.body)
{
self.sites.push((node.span(), "unwrap_or_else"));
}
visit::visit_expr_method_call(self, node);
}
fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
}
fn error_observation_present(block: &Block) -> bool {
struct ErrorObservationScanner {
found: bool,
}
impl<'ast> Visit<'ast> for ErrorObservationScanner {
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
if node.method == "inspect_err" {
self.found = true;
}
visit::visit_expr_method_call(self, node);
}
fn visit_macro(&mut self, mac: &'ast Macro) {
let first_segment = mac.path.segments.first().map(|s| s.ident.to_string());
let last_segment = mac.path.segments.last().map(|s| s.ident.to_string());
let is_log_or_tracing_qualified =
matches!(first_segment.as_deref(), Some("log") | Some("tracing"))
&& mac.path.segments.len() >= 2;
let is_direct_observation_macro = matches!(
last_segment.as_deref(),
Some("eprintln") | Some("warn") | Some("error")
);
if is_log_or_tracing_qualified || is_direct_observation_macro {
self.found = true;
}
visit::visit_macro(self, mac);
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Some((_, pat)) = if_let_cond_pat(expr)
&& is_err_pat(pat)
{
self.found = true;
}
visit::visit_expr(self, expr);
}
fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
}
let mut scanner = ErrorObservationScanner { found: false };
scanner.visit_block(block);
scanner.found
}
fn is_err_pat(pat: &Pat) -> bool {
as_err_tuple_struct(pat).is_some()
}
fn is_opaque_error_return_type(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
let segments = &type_path.path.segments;
let Some(last) = segments.last() else {
return false;
};
if last.ident != "Result" {
return false;
}
if segments.len() >= 2 {
let prev = &segments[segments.len() - 2];
if prev.ident == "anyhow" || prev.ident == "eyre" {
return true;
}
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return false;
};
args.args
.iter()
.filter_map(|arg| match arg {
GenericArgument::Type(inner) => Some(inner),
_ => None,
})
.nth(1)
.is_some_and(is_opaque_error_type)
}
fn is_opaque_error_type(ty: &Type) -> bool {
match ty {
Type::TraitObject(trait_object) => is_error_trait_object(trait_object),
Type::Path(type_path) => {
let segments = &type_path.path.segments;
let Some(last) = segments.last() else {
return false;
};
if last.ident == "Box" {
let PathArguments::AngleBracketed(args) = &last.arguments else {
return false;
};
return args.args.iter().any(|arg| match arg {
GenericArgument::Type(Type::TraitObject(trait_object)) => {
is_error_trait_object(trait_object)
}
_ => false,
});
}
if segments.len() >= 2 {
let prev = &segments[segments.len() - 2];
return (prev.ident == "anyhow" && last.ident == "Error")
|| (prev.ident == "eyre" && last.ident == "Report");
}
false
}
_ => false,
}
}
struct TrySiteScanner {
try_site_keys: std::collections::HashSet<String>,
has_context_call: bool,
}
impl<'ast> Visit<'ast> for TrySiteScanner {
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Expr::Try(try_expr) = expr {
self.try_site_keys.insert(quote!(#try_expr).to_string());
}
visit::visit_expr(self, expr);
}
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
if node.method == "context" || node.method == "with_context" {
self.has_context_call = true;
}
visit::visit_expr_method_call(self, node);
}
fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
}
fn impl_trait_ident_matches(node: &ItemImpl, predicate: impl Fn(&str) -> bool) -> bool {
node.trait_.as_ref().is_some_and(|(bang, path, _)| {
bang.is_none()
&& path
.segments
.last()
.is_some_and(|segment| predicate(&segment.ident.to_string()))
})
}
fn is_display_trait_impl(node: &ItemImpl) -> bool {
impl_trait_ident_matches(node, |ident| ident == "Display")
}
fn is_ast_visitor_trait_impl(node: &ItemImpl) -> bool {
impl_trait_ident_matches(node, |ident| matches!(ident, "Visit" | "VisitMut" | "Fold"))
}
fn is_drop_trait_impl(node: &ItemImpl) -> bool {
impl_trait_ident_matches(node, |ident| ident == "Drop")
}
fn parse_macro_expr_args(mac: &Macro) -> Option<Punctuated<Expr, Token![,]>> {
mac.parse_body_with(Punctuated::<Expr, Token![,]>::parse_terminated)
.ok()
}
fn expr_str_lit(expr: &Expr) -> Option<String> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(text),
..
}) => Some(text.value()),
_ => None,
}
}
fn meta_name_value_str(meta: &Meta) -> Option<String> {
match meta {
Meta::NameValue(name_value) => expr_str_lit(&name_value.value),
_ => None,
}
}
fn macro_format_string_has_debug_placeholder(mac: &Macro) -> bool {
let Some(args) = parse_macro_expr_args(mac) else {
return false;
};
args.iter()
.find_map(expr_str_lit)
.is_some_and(|text| text.contains("{:?}") || text.contains("{:#?}"))
}
fn has_feature_cfg(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|attr| attr.path().is_ident("cfg") && quote!(#attr).to_string().contains("feature"))
}
fn return_type(output: &ReturnType) -> Option<&Type> {
match output {
ReturnType::Type(_, ty) => Some(ty.as_ref()),
ReturnType::Default => None,
}
}
fn returns_result_type(output: &ReturnType) -> bool {
let Some(ty) = return_type(output) else {
return false;
};
let Type::Path(type_path) = ty else {
return false;
};
type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Result")
}
const GENERIC_WORDS: &[&str] = &[
"data",
"result",
"temp",
"handler",
"manager",
"processor",
"helper",
"utils",
];
fn is_generic_word(word: &str) -> bool {
let lower = word.to_lowercase();
GENERIC_WORDS.contains(&lower.as_str())
}
fn strip_trailing_digits(name: &str) -> &str {
name.trim_end_matches(|c: char| c.is_ascii_digit())
}
fn doc_comment_text(attrs: &[Attribute]) -> Option<String> {
let parts: Vec<String> = attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.filter_map(|attr| meta_name_value_str(&attr.meta))
.collect();
if parts.is_empty() {
return None;
}
let joined = parts.join(" ").trim().to_string();
if joined.is_empty() {
None
} else {
Some(joined)
}
}
fn tokenize(text: &str) -> Vec<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric())
.filter(|token| !token.is_empty())
.map(str::to_string)
.collect()
}
fn signature_tokens(sig: &syn::Signature) -> std::collections::HashSet<String> {
let mut tokens = std::collections::HashSet::new();
tokens.insert(sig.ident.to_string().to_lowercase());
if let ReturnType::Type(_, ty) = &sig.output {
tokens.insert(type_name(ty).to_lowercase());
}
let non_self_params: Vec<&syn::PatType> = sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(pat_type) => Some(pat_type),
syn::FnArg::Receiver(_) => None,
})
.collect();
if let [pat_type] = non_self_params.as_slice() {
if let Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
tokens.insert(pat_ident.ident.to_string().to_lowercase());
}
tokens.insert(type_name(&pat_type.ty).to_lowercase());
}
tokens
}
struct AssertionScanner {
found: bool,
returns_result: bool,
}
impl<'ast> Visit<'ast> for AssertionScanner {
fn visit_macro(&mut self, mac: &'ast Macro) {
const ASSERT_MACROS: [&str; 16] = [
"assert",
"assert_eq",
"assert_ne",
"debug_assert",
"debug_assert_eq",
"debug_assert_ne",
"panic",
"unreachable",
"assert_snapshot",
"assert_json_snapshot",
"assert_debug_snapshot",
"assert_display_snapshot",
"assert_yaml_snapshot",
"assert_ron_snapshot",
"assert_csv_snapshot",
"assert_toml_snapshot",
];
if mac
.path
.segments
.last()
.is_some_and(|segment| ASSERT_MACROS.contains(&segment.ident.to_string().as_str()))
{
self.found = true;
}
visit::visit_macro(self, mac);
}
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
if matches!(
node.method.to_string().as_str(),
"unwrap" | "expect" | "unwrap_err" | "expect_err"
) {
self.found = true;
}
visit::visit_expr_method_call(self, node);
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Expr::Try(_) = expr
&& self.returns_result
{
self.found = true;
}
visit::visit_expr(self, expr);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::finding::EvidenceClass;
use crate::ingest::SourceKind;
use crate::test_util::TempDir;
fn authored(path: PathBuf) -> SourceFile {
SourceFile {
path,
kind: SourceKind::Authored,
}
}
fn findings_for(source: &str, name: &str) -> Vec<Finding> {
findings_for_with_config(source, name, false)
}
fn findings_for_with_config(
source: &str,
name: &str,
allow_anyhow_at_boundary: bool,
) -> Vec<Finding> {
let dir = TempDir::new(name);
let file = dir.join("lib.rs");
std::fs::write(&file, source).unwrap();
analyze_file(&file, allow_anyhow_at_boundary).unwrap()
}
fn rule_findings<'a>(findings: &'a [Finding], rule: &str) -> Vec<&'a Finding> {
findings.iter().filter(|f| f.rule == rule).collect()
}
#[test]
fn let_underscore_call_is_flagged() {
let findings = findings_for(
"fn f() { let _ = some_call(); }\nfn some_call() -> i32 { 1 }\n",
"slop-let-underscore",
);
let hits = rule_findings(&findings, SWALLOWED_RESULT_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn swallowed_result_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(SWALLOWED_RESULT_RULE)
.expect("swallowed-result has a registry entry")
.example
.expect("swallowed-result has a curated example")
.before;
let findings = findings_for(example, "slop-swallowed-result-registry-example");
assert_eq!(rule_findings(&findings, SWALLOWED_RESULT_RULE).len(), 1);
}
#[test]
fn let_bound_to_a_real_name_is_not_flagged() {
let findings = findings_for(
"fn f() { let x = some_call(); let _ = x; }\nfn some_call() -> i32 { 1 }\n",
"slop-let-real-name",
);
assert!(rule_findings(&findings, SWALLOWED_RESULT_RULE).is_empty());
}
#[test]
fn let_underscore_inside_drop_drop_is_not_flagged() {
let findings = findings_for(
"struct S(String);\nimpl Drop for S {\n fn drop(&mut self) {\n let _ = std::fs::remove_dir_all(&self.0);\n }\n}\n",
"slop-swallowed-result-drop-drop",
);
assert!(rule_findings(&findings, SWALLOWED_RESULT_RULE).is_empty());
}
#[test]
fn let_underscore_inside_normal_fn_still_flagged() {
let findings = findings_for(
"fn cleanup() {\n let _ = std::fs::remove_dir_all(\"/tmp/x\");\n}\n",
"slop-swallowed-result-normal-fn",
);
let hits = rule_findings(&findings, SWALLOWED_RESULT_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn bare_dot_ok_statement_is_flagged() {
let findings = findings_for(
"fn f() -> Result<i32, ()> { Ok(1) }\nfn g() { f().ok(); }\n",
"slop-dot-ok",
);
let hits = rule_findings(&findings, SWALLOWED_RESULT_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn dot_ok_bound_to_a_name_is_not_flagged() {
let findings = findings_for(
"fn f() -> Result<i32, ()> { Ok(1) }\nfn g() { let x = f().ok(); let _ = x; }\n",
"slop-dot-ok-bound",
);
assert!(rule_findings(&findings, SWALLOWED_RESULT_RULE).is_empty());
}
#[test]
fn swallowed_result_inside_cfg_gated_fn_still_flagged() {
let findings = findings_for(
"#[cfg(feature = \"not-enabled-by-default\")]\nfn f() { let _ = some_call(); }\nfn some_call() -> i32 { 1 }\n",
"slop-swallowed-result-cfg-gated",
);
let hits = rule_findings(&findings, SWALLOWED_RESULT_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn empty_err_arm_is_flagged() {
let findings = findings_for(
r#"
fn f(r: Result<i32, ()>) {
match r {
Err(_) => {}
Ok(_) => {}
}
}
"#,
"slop-empty-err-arm",
);
let hits = rule_findings(&findings, EMPTY_ERROR_ARM_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn non_empty_err_arm_is_not_flagged() {
let findings = findings_for(
r#"
fn f(r: Result<i32, ()>) {
match r {
Err(e) => log::warn!("{:?}", e),
Ok(_) => {}
}
}
"#,
"slop-non-empty-err-arm",
);
assert!(rule_findings(&findings, EMPTY_ERROR_ARM_RULE).is_empty());
}
#[test]
fn empty_if_let_err_is_flagged() {
let findings = findings_for(
"fn f() -> Result<(), ()> { Ok(()) }\nfn g() { if let Err(_) = f() { } }\n",
"slop-if-let-empty",
);
let hits = rule_findings(&findings, EMPTY_ERROR_ARM_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn non_empty_if_let_err_is_not_flagged() {
let findings = findings_for(
"fn f() -> Result<(), ()> { Ok(()) }\nfn g() { if let Err(_) = f() { return; } }\n",
"slop-if-let-non-empty",
);
assert!(rule_findings(&findings, EMPTY_ERROR_ARM_RULE).is_empty());
}
#[test]
fn empty_error_arm_inside_macro_body_produces_no_finding() {
let findings = findings_for(
r#"
macro_rules! handle_it {
($r:expr) => {
match $r {
Err(_) => {}
Ok(_) => {}
}
};
}
fn f(r: Result<i32, ()>) {
handle_it!(r);
}
"#,
"slop-empty-error-arm-macro-body",
);
assert!(rule_findings(&findings, EMPTY_ERROR_ARM_RULE).is_empty());
}
#[test]
fn pub_fn_with_boxed_dyn_error_is_flagged() {
let findings = findings_for(
"pub fn f() -> Result<(), Box<dyn std::error::Error>> {\n std::fs::read_to_string(\"x\").map_err(|_| \"failed\".into())?;\n Ok(())\n}\n",
"slop-catch-all-boxed",
);
let hits = rule_findings(&findings, CATCH_ALL_ERROR_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn pub_fn_with_boxed_dyn_error_and_plain_propagation_is_not_flagged() {
let findings = findings_for(
"pub fn f() -> Result<(), Box<dyn std::error::Error>> {\n std::fs::read_to_string(\"x\")?;\n Ok(())\n}\n",
"slop-catch-all-boxed-plain-propagation",
);
assert!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).is_empty());
}
#[test]
fn map_err_with_a_bound_parameter_is_not_flagged() {
let findings = findings_for(
"pub fn f() -> Result<(), Box<dyn std::error::Error>> {\n std::fs::read_to_string(\"x\").map_err(|e| format!(\"failed: {e}\").into())?;\n Ok(())\n}\n",
"slop-catch-all-boxed-bound-map-err",
);
assert!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).is_empty());
}
#[test]
fn pub_fn_with_concrete_error_type_is_not_flagged() {
let findings = findings_for(
"struct MyError;\npub fn f() -> Result<(), MyError> { Ok(()) }\n",
"slop-catch-all-concrete",
);
assert!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).is_empty());
}
#[test]
fn private_fn_with_boxed_dyn_error_is_not_flagged() {
let findings = findings_for(
"fn f() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }\n",
"slop-catch-all-private",
);
assert!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).is_empty());
}
#[test]
fn anyhow_result_is_flagged() {
let findings = findings_for(
"pub fn f() -> anyhow::Result<()> {\n std::fs::read_to_string(\"x\").map_err(|_| anyhow::anyhow!(\"failed\"))?;\n Ok(())\n}\n",
"slop-catch-all-anyhow",
);
let hits = rule_findings(&findings, CATCH_ALL_ERROR_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn anyhow_result_is_not_flagged_when_allowed_at_boundary() {
let findings = findings_for_with_config(
"pub fn f() -> anyhow::Result<()> {\n std::fs::read_to_string(\"x\").map_err(|_| anyhow::anyhow!(\"failed\"))?;\n Ok(())\n}\n",
"slop-catch-all-anyhow-allowed",
true,
);
assert!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).is_empty());
}
#[test]
fn boxed_dyn_error_is_still_flagged_when_anyhow_allowed_at_boundary() {
let findings = findings_for_with_config(
"pub fn f() -> Result<(), Box<dyn std::error::Error>> {\n std::fs::read_to_string(\"x\").map_err(|_| \"failed\".into())?;\n Ok(())\n}\n",
"slop-catch-all-boxed-allowed",
true,
);
let hits = rule_findings(&findings, CATCH_ALL_ERROR_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn catch_all_error_inside_cfg_gated_pub_fn_still_flagged() {
let findings = findings_for(
"#[cfg(feature = \"not-enabled-by-default\")]\npub fn f() -> Result<(), Box<dyn std::error::Error>> {\n std::fs::read_to_string(\"x\").map_err(|_| \"failed\".into())?;\n Ok(())\n}\n",
"slop-catch-all-error-cfg-gated",
);
let hits = rule_findings(&findings, CATCH_ALL_ERROR_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn allow_and_expect_each_produce_one_finding() {
let findings = findings_for(
"#[allow(dead_code)]\nfn f() {}\n\n#[expect(clippy::foo)]\nfn g() {}\n",
"slop-suppression-debt",
);
let hits = rule_findings(&findings, SUPPRESSION_DEBT_RULE);
assert_eq!(hits.len(), 2);
for hit in &hits {
assert_eq!(hit.severity, Severity::Info);
assert_eq!(hit.evidence_class, EvidenceClass::DerivedFact);
}
let item_paths: Vec<_> = hits.iter().map(|f| f.location.item_path.as_str()).collect();
assert!(item_paths.contains(&"dead_code"));
assert!(item_paths.contains(&"clippy::foo"));
}
#[test]
fn no_suppressions_produces_zero_findings() {
let findings = findings_for("fn f() {}\n", "slop-no-suppression-debt");
assert!(rule_findings(&findings, SUPPRESSION_DEBT_RULE).is_empty());
}
#[test]
fn suppression_debt_inside_macro_body_is_not_counted() {
let findings = findings_for(
"macro_rules! define_it {\n () => {\n #[allow(dead_code)]\n fn generated() {}\n };\n}\ndefine_it!();\n",
"slop-suppression-debt-macro-body",
);
assert!(rule_findings(&findings, SUPPRESSION_DEBT_RULE).is_empty());
}
#[test]
fn generated_files_are_excluded_unless_included() {
let dir = TempDir::new("slop-generated");
let file = dir.join("schema.rs");
std::fs::write(
&file,
"fn f() { let _ = some_call(); }\nfn some_call() -> i32 { 1 }\n",
)
.unwrap();
let files = [SourceFile {
path: file,
kind: SourceKind::Generated,
}];
let excluded = analyze_workspace(files.iter(), false, false);
assert!(excluded.findings.is_empty());
assert_eq!(excluded.excluded_generated, 1);
let included = analyze_workspace(files.iter(), true, false);
assert_eq!(included.findings.len(), 1);
assert_eq!(included.excluded_generated, 0);
}
#[test]
fn analyze_workspace_reports_parse_errors() {
let dir = TempDir::new("slop-parse-error");
let file = dir.join("broken.rs");
std::fs::write(&file, "fn broken( {").unwrap();
let files = [authored(file)];
let report = analyze_workspace(files.iter(), false, false);
assert_eq!(report.errors.len(), 1);
assert!(report.findings.is_empty());
}
#[test]
fn ignored_test_is_flagged() {
let findings = findings_for(
"#[test]\n#[ignore]\nfn f() { assert_eq!(1 + 1, 2); }\n",
"slop-ignored-test",
);
let hits = rule_findings(&findings, IGNORED_TEST_ACCUMULATION_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Info);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
assert_eq!(hits[0].evidence, None);
}
#[test]
fn ignored_test_with_reason_captures_evidence() {
let findings = findings_for(
"#[test]\n#[ignore = \"slow\"]\nfn f() { assert_eq!(1 + 1, 2); }\n",
"slop-ignored-test-reason",
);
let hits = rule_findings(&findings, IGNORED_TEST_ACCUMULATION_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(
hits[0].evidence,
Some(serde_json::json!({ "reason": "slow" }))
);
}
#[test]
fn test_without_ignore_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { assert_eq!(1 + 1, 2); }\n",
"slop-not-ignored-test",
);
assert!(rule_findings(&findings, IGNORED_TEST_ACCUMULATION_RULE).is_empty());
}
#[test]
fn assert_true_is_flagged() {
let findings = findings_for("fn f() { assert!(true); }\n", "slop-assert-true");
let hits = rule_findings(&findings, TAUTOLOGICAL_TEST_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn assert_condition_is_not_flagged() {
let findings = findings_for(
"fn f() { assert!(condition()); }\nfn condition() -> bool { true }\n",
"slop-assert-condition",
);
assert!(rule_findings(&findings, TAUTOLOGICAL_TEST_RULE).is_empty());
}
#[test]
fn assert_eq_same_expr_is_flagged() {
let findings = findings_for(
"fn f(x: i32) { assert_eq!(x, x); }\n",
"slop-assert-eq-same",
);
let hits = rule_findings(&findings, TAUTOLOGICAL_TEST_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn assert_eq_different_exprs_is_not_flagged() {
let findings = findings_for(
"fn f(a: i32, b: i32) { assert_eq!(a, b); }\n",
"slop-assert-eq-different",
);
assert!(rule_findings(&findings, TAUTOLOGICAL_TEST_RULE).is_empty());
}
#[test]
fn doc_commented_empty_fn_is_flagged() {
let findings = findings_for("/// Does nothing yet.\nfn f() {}\n", "slop-empty-impl-fn");
let hits = rule_findings(&findings, EMPTY_IMPL_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn doc_commented_nonempty_fn_is_not_flagged() {
let findings = findings_for(
"/// Returns a default.\nfn f() -> i32 { some_default() }\nfn some_default() -> i32 { 1 }\n",
"slop-empty-impl-nonempty",
);
assert!(rule_findings(&findings, EMPTY_IMPL_RULE).is_empty());
}
#[test]
fn empty_fn_without_doc_comment_is_not_flagged() {
let findings = findings_for("fn f() {}\n", "slop-empty-impl-no-doc");
assert!(rule_findings(&findings, EMPTY_IMPL_RULE).is_empty());
}
#[test]
fn doc_commented_empty_impl_method_is_flagged() {
let findings = findings_for(
"struct S;\nimpl S {\n /// Does nothing yet.\n fn f(&self) {}\n}\n",
"slop-empty-impl-method",
);
let hits = rule_findings(&findings, EMPTY_IMPL_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn doc_commented_empty_trait_default_is_flagged() {
let findings = findings_for(
"trait T {\n /// Does nothing yet.\n fn f(&self) {}\n}\n",
"slop-empty-impl-trait-default",
);
let hits = rule_findings(&findings, EMPTY_IMPL_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn doc_commented_empty_visit_trait_override_is_not_flagged() {
let findings = findings_for(
"struct V;\nimpl Visit for V {\n /// Never descends into an attribute.\n fn visit_attribute(&mut self, _node: &Attribute) {}\n}\n",
"slop-empty-impl-visit-override",
);
assert!(rule_findings(&findings, EMPTY_IMPL_RULE).is_empty());
}
#[test]
fn doc_commented_empty_non_visitor_trait_override_is_flagged() {
let findings = findings_for(
"trait Handler {\n fn handle(&self);\n}\nstruct S;\nimpl Handler for S {\n /// Does nothing yet.\n fn handle(&self) {}\n}\n",
"slop-empty-impl-non-visitor-override",
);
let hits = rule_findings(&findings, EMPTY_IMPL_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn todo_macro_is_flagged() {
let findings = findings_for("fn f() { todo!() }\n", "slop-merged-stub-todo");
let hits = rule_findings(&findings, MERGED_STUB_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn unimplemented_macro_is_flagged() {
let findings = findings_for(
"fn f() { unimplemented!() }\n",
"slop-merged-stub-unimplemented",
);
assert_eq!(rule_findings(&findings, MERGED_STUB_RULE).len(), 1);
}
#[test]
fn feature_gated_fn_todo_is_not_flagged() {
let findings = findings_for(
"#[cfg(feature = \"wip\")]\nfn f() { todo!() }\n",
"slop-merged-stub-gated-fn",
);
assert!(rule_findings(&findings, MERGED_STUB_RULE).is_empty());
}
#[test]
fn feature_gated_mod_todo_is_not_flagged() {
let findings = findings_for(
"#[cfg(feature = \"wip\")]\nmod m {\n fn f() { todo!() }\n}\n",
"slop-merged-stub-gated-mod",
);
assert!(rule_findings(&findings, MERGED_STUB_RULE).is_empty());
}
#[test]
fn test_without_assertion_is_flagged() {
let findings = findings_for(
"#[test]\nfn f() { let x = 1 + 1; }\n",
"slop-assertion-free-test",
);
let hits = rule_findings(&findings, ASSERTION_FREE_TEST_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn test_with_assert_eq_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { assert_eq!(1 + 1, 2); }\n",
"slop-assertion-free-with-assert",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn test_with_unwrap_only_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { let x: Option<i32> = Some(1); x.unwrap(); }\n",
"slop-assertion-free-unwrap",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn test_returning_result_with_try_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() -> Result<(), String> { might_fail()?; Ok(()) }\nfn might_fail() -> Result<(), String> { Ok(()) }\n",
"slop-assertion-free-try",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn should_panic_test_without_assertion_is_not_flagged() {
let findings = findings_for(
"#[test]\n#[should_panic]\nfn f() { let x = 1 + 1; }\n",
"slop-assertion-free-should-panic",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn test_with_assert_free_closure_is_flagged() {
let findings = findings_for(
"#[test]\nfn f() { let closure = || { let y = 1 + 1; }; closure(); }\n",
"slop-assertion-free-closure",
);
assert_eq!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).len(), 1);
}
#[test]
fn test_with_qualified_insta_snapshot_macro_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { insta::assert_snapshot!(\"value\"); }\n",
"slop-assertion-free-insta-snapshot",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn test_with_qualified_insta_json_snapshot_macro_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { insta::assert_json_snapshot!(value); }\n",
"slop-assertion-free-insta-json-snapshot",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn test_with_qualified_pretty_assertions_assert_eq_is_not_flagged() {
let findings = findings_for(
"#[test]\nfn f() { pretty_assertions::assert_eq!(1 + 1, 2); }\n",
"slop-assertion-free-pretty-assertions",
);
assert!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).is_empty());
}
#[test]
fn generic_naming_numeric_suffix_locals_are_flagged() {
let findings = findings_for(
"fn f() { let data1 = 1; let data2 = 2; }\n",
"slop-generic-naming-numeric-suffix",
);
let hits = rule_findings(&findings, GENERIC_NAMING_RULE);
assert_eq!(hits.len(), 2);
for hit in &hits {
assert_eq!(hit.evidence_class, EvidenceClass::DerivedFact);
}
}
#[test]
fn generic_naming_private_bare_local_is_not_flagged() {
let findings = findings_for(
"fn g() -> Result<i32, String> { let result = do_thing()?; Ok(result) }\nfn do_thing() -> Result<i32, String> { Ok(1) }\n",
"slop-generic-naming-private-result",
);
assert!(rule_findings(&findings, GENERIC_NAMING_RULE).is_empty());
}
#[test]
fn generic_naming_pub_struct_exact_word_is_flagged() {
let findings = findings_for("pub struct Manager;\n", "slop-generic-naming-struct");
let hits = rule_findings(&findings, GENERIC_NAMING_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn generic_naming_pub_struct_substring_is_not_flagged() {
let findings = findings_for(
"pub struct ConnectionManager;\n",
"slop-generic-naming-struct-substring",
);
assert!(rule_findings(&findings, GENERIC_NAMING_RULE).is_empty());
}
#[test]
fn generic_naming_pub_fn_exact_word_is_flagged() {
let findings = findings_for("pub fn helper() {}\n", "slop-generic-naming-fn");
let hits = rule_findings(&findings, GENERIC_NAMING_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn generic_naming_pub_impl_method_is_not_flagged() {
let findings = findings_for(
"struct S;\nimpl S {\n pub fn helper(&self) {}\n}\n",
"slop-generic-naming-impl-method",
);
assert!(rule_findings(&findings, GENERIC_NAMING_RULE).is_empty());
}
#[test]
fn doc_restates_signature_pure_echo_is_flagged() {
let findings = findings_for(
"/// Returns the result.\npub fn f() -> Result<(), String> { Ok(()) }\n",
"slop-doc-restates-signature-echo",
);
let hits = rule_findings(&findings, DOC_RESTATES_SIGNATURE_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn doc_restates_signature_real_prose_is_not_flagged() {
let findings = findings_for(
"/// Parses the config file and validates required fields.\nfn f() {}\n",
"slop-doc-restates-signature-prose",
);
assert!(rule_findings(&findings, DOC_RESTATES_SIGNATURE_RULE).is_empty());
}
#[test]
fn conversational_artifact_tier1_phrase_is_flagged() {
let findings = findings_for(
"fn f() {\n // As an AI, I can't do that.\n}\n",
"slop-conversational-artifact-tier1",
);
let hits = rule_findings(&findings, CONVERSATIONAL_ARTIFACT_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn conversational_artifact_doc_comment_is_excluded() {
let findings = findings_for(
"/// In a real implementation, this would also validate input.\nfn f() {}\n",
"slop-conversational-artifact-doc",
);
assert!(rule_findings(&findings, CONVERSATIONAL_ARTIFACT_RULE).is_empty());
}
#[test]
fn conversational_artifact_tier2_phrase_past_word_eight_is_not_flagged() {
let findings = findings_for(
"fn f() {\n // This example function shows some basic arithmetic logic in a real implementation context.\n let _ = 1;\n}\n",
"slop-conversational-artifact-tier2-position",
);
assert!(rule_findings(&findings, CONVERSATIONAL_ARTIFACT_RULE).is_empty());
}
#[test]
fn conversational_artifact_tier2_phrase_is_not_flagged_inside_an_unrelated_word() {
let findings = findings_for(
"fn f() {\n // There is no other way to express this invariant.\n}\n",
"slop-conversational-artifact-tier2-word-boundary",
);
assert!(rule_findings(&findings, CONVERSATIONAL_ARTIFACT_RULE).is_empty());
}
#[test]
fn step_comment_inflation_three_step_chain_is_flagged() {
let findings = findings_for(
"fn f() {\n // Step 1: initialize\n let x = 1;\n // Step 2: compute\n let y = x + 1;\n // Step 3: finish\n let _ = y;\n}\n",
"slop-step-comment-inflation-chain",
);
let hits = rule_findings(&findings, STEP_COMMENT_INFLATION_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(
hits[0].evidence,
Some(serde_json::json!({ "chain_length": 3, "lines": [2, 4, 6] }))
);
}
#[test]
fn step_comment_inflation_single_step_is_not_flagged() {
let findings = findings_for(
"fn f() {\n // Step 1: initialize\n let x = 1;\n let _ = x;\n}\n",
"slop-step-comment-inflation-single",
);
assert!(rule_findings(&findings, STEP_COMMENT_INFLATION_RULE).is_empty());
}
#[test]
fn restating_comment_short_comment_is_not_flagged() {
let findings = findings_for(
"fn f(counter: &mut i32) {\n // increment counter\n *counter += 1;\n}\n",
"slop-restating-comment-short",
);
assert!(rule_findings(&findings, RESTATING_COMMENT_RULE).is_empty());
}
#[test]
fn restating_comment_verbose_paraphrase_is_flagged() {
let findings = findings_for(
"struct S { user_name_field: String }\nimpl S {\n fn set(&mut self, given_value: String) {\n // set the user name field to the given value\n self.user_name_field = given_value;\n }\n}\n",
"slop-restating-comment-verbose",
);
let hits = rule_findings(&findings, RESTATING_COMMENT_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
}
#[test]
fn merged_stub_inside_macro_body_produces_no_finding() {
let findings = findings_for(
"macro_rules! define_it {\n () => {\n fn generated() { todo!() }\n };\n}\ndefine_it!();\n",
"slop-merged-stub-macro-body",
);
assert!(rule_findings(&findings, MERGED_STUB_RULE).is_empty());
}
#[test]
fn empty_impl_inside_macro_body_produces_no_finding() {
let findings = findings_for(
"macro_rules! define_stub {\n () => {\n /// Does nothing yet.\n fn generated() {}\n };\n}\ndefine_stub!();\n",
"slop-empty-impl-macro-body",
);
assert!(rule_findings(&findings, EMPTY_IMPL_RULE).is_empty());
}
#[test]
fn test_using_custom_assertion_wrapper_macro_is_still_flagged() {
let findings = findings_for(
"macro_rules! check {\n ($cond:expr) => {\n assert!($cond);\n };\n}\n#[test]\nfn f() { check!(1 + 1 == 2); }\n",
"slop-assertion-free-custom-wrapper",
);
let hits = rule_findings(&findings, ASSERTION_FREE_TEST_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn assert_eq_macro_generated_const_is_flagged_syntactically() {
let findings = findings_for(
"macro_rules! define_const {\n ($name:ident, $val:expr) => {\n const $name: i32 = $val;\n };\n}\ndefine_const!(VALUE, 42);\nfn f() { assert_eq!(VALUE, VALUE); }\n",
"slop-tautological-macro-const",
);
let hits = rule_findings(&findings, TAUTOLOGICAL_TEST_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn ignored_test_inside_macro_body_is_not_counted() {
let findings = findings_for(
"macro_rules! define_ignored_test {\n () => {\n #[test]\n #[ignore]\n fn generated() { assert!(true); }\n };\n}\ndefine_ignored_test!();\n",
"slop-ignored-test-macro-body",
);
assert!(rule_findings(&findings, IGNORED_TEST_ACCUMULATION_RULE).is_empty());
}
#[test]
fn generic_naming_pub_fn_inside_cfg_gated_mod_still_flagged() {
let findings = findings_for(
"#[cfg(feature = \"wip\")]\nmod m {\n pub fn manager() {}\n}\n",
"slop-generic-naming-cfg-gated",
);
let hits = rule_findings(&findings, GENERIC_NAMING_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn doc_restates_signature_inside_macro_body_produces_no_finding() {
let findings = findings_for(
"macro_rules! define_getter {\n () => {\n /// Returns the result.\n pub fn f() -> Result<(), String> { Ok(()) }\n };\n}\ndefine_getter!();\n",
"slop-doc-restates-signature-macro-body",
);
assert!(rule_findings(&findings, DOC_RESTATES_SIGNATURE_RULE).is_empty());
}
#[test]
fn empty_error_arm_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(EMPTY_ERROR_ARM_RULE)
.expect("empty-error-arm has a registry entry")
.example
.expect("empty-error-arm has a curated example")
.before;
let findings = findings_for(example, "slop-empty-error-arm-registry-example");
assert_eq!(rule_findings(&findings, EMPTY_ERROR_ARM_RULE).len(), 1);
}
#[test]
fn catch_all_error_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(CATCH_ALL_ERROR_RULE)
.expect("catch-all-error has a registry entry")
.example
.expect("catch-all-error has a curated example")
.before;
let findings = findings_for(example, "slop-catch-all-error-registry-example");
assert_eq!(rule_findings(&findings, CATCH_ALL_ERROR_RULE).len(), 1);
}
#[test]
fn suppression_debt_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(SUPPRESSION_DEBT_RULE)
.expect("suppression-debt has a registry entry")
.example
.expect("suppression-debt has a curated example")
.before;
let findings = findings_for(example, "slop-suppression-debt-registry-example");
assert_eq!(rule_findings(&findings, SUPPRESSION_DEBT_RULE).len(), 1);
}
#[test]
fn merged_stub_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(MERGED_STUB_RULE)
.expect("merged-stub has a registry entry")
.example
.expect("merged-stub has a curated example")
.before;
let findings = findings_for(example, "slop-merged-stub-registry-example");
assert_eq!(rule_findings(&findings, MERGED_STUB_RULE).len(), 1);
}
#[test]
fn empty_impl_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(EMPTY_IMPL_RULE)
.expect("empty-impl has a registry entry")
.example
.expect("empty-impl has a curated example")
.before;
let findings = findings_for(example, "slop-empty-impl-registry-example");
assert_eq!(rule_findings(&findings, EMPTY_IMPL_RULE).len(), 1);
}
#[test]
fn assertion_free_test_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(ASSERTION_FREE_TEST_RULE)
.expect("assertion-free-test has a registry entry")
.example
.expect("assertion-free-test has a curated example")
.before;
let findings = findings_for(example, "slop-assertion-free-test-registry-example");
assert_eq!(rule_findings(&findings, ASSERTION_FREE_TEST_RULE).len(), 1);
}
#[test]
fn tautological_test_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(TAUTOLOGICAL_TEST_RULE)
.expect("tautological-test has a registry entry")
.example
.expect("tautological-test has a curated example")
.before;
let findings = findings_for(example, "slop-tautological-test-registry-example");
assert_eq!(rule_findings(&findings, TAUTOLOGICAL_TEST_RULE).len(), 1);
}
#[test]
fn ignored_test_accumulation_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(IGNORED_TEST_ACCUMULATION_RULE)
.expect("ignored-test-accumulation has a registry entry")
.example
.expect("ignored-test-accumulation has a curated example")
.before;
let findings = findings_for(example, "slop-ignored-test-accumulation-registry-example");
assert_eq!(
rule_findings(&findings, IGNORED_TEST_ACCUMULATION_RULE).len(),
1
);
}
#[test]
fn conversational_artifact_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(CONVERSATIONAL_ARTIFACT_RULE)
.expect("conversational-artifact has a registry entry")
.example
.expect("conversational-artifact has a curated example")
.before;
let findings = findings_for(example, "slop-conversational-artifact-registry-example");
assert_eq!(
rule_findings(&findings, CONVERSATIONAL_ARTIFACT_RULE).len(),
1
);
}
#[test]
fn restating_comment_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(RESTATING_COMMENT_RULE)
.expect("restating-comment has a registry entry")
.example
.expect("restating-comment has a curated example")
.before;
let findings = findings_for(example, "slop-restating-comment-registry-example");
assert_eq!(rule_findings(&findings, RESTATING_COMMENT_RULE).len(), 1);
}
#[test]
fn step_comment_inflation_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(STEP_COMMENT_INFLATION_RULE)
.expect("step-comment-inflation has a registry entry")
.example
.expect("step-comment-inflation has a curated example")
.before;
let findings = findings_for(example, "slop-step-comment-inflation-registry-example");
assert_eq!(
rule_findings(&findings, STEP_COMMENT_INFLATION_RULE).len(),
1
);
}
#[test]
fn generic_naming_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(GENERIC_NAMING_RULE)
.expect("generic-naming has a registry entry")
.example
.expect("generic-naming has a curated example")
.before;
let findings = findings_for(example, "slop-generic-naming-registry-example");
assert_eq!(rule_findings(&findings, GENERIC_NAMING_RULE).len(), 1);
}
#[test]
fn doc_restates_signature_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(DOC_RESTATES_SIGNATURE_RULE)
.expect("doc-restates-signature has a registry entry")
.example
.expect("doc-restates-signature has a curated example")
.before;
let findings = findings_for(example, "slop-doc-restates-signature-registry-example");
assert_eq!(
rule_findings(&findings, DOC_RESTATES_SIGNATURE_RULE).len(),
1
);
}
#[test]
fn unwrap_or_default_without_error_observation_is_flagged() {
let findings = findings_for(
"fn f(input: Option<i32>) -> i32 {\n input.unwrap_or_default()\n}\n",
"slop-silent-default-unwrap-or-default",
);
let hits = rule_findings(&findings, SILENT_DEFAULT_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::Heuristic);
assert_eq!(
hits[0].evidence.as_ref().unwrap()["method"],
"unwrap_or_default"
);
}
#[test]
fn unwrap_or_else_default_closure_without_error_observation_is_flagged() {
let findings = findings_for(
"fn f(input: Result<i32, String>) -> i32 {\n input.unwrap_or_else(|_| Default::default())\n}\n",
"slop-silent-default-unwrap-or-else",
);
let hits = rule_findings(&findings, SILENT_DEFAULT_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(
hits[0].evidence.as_ref().unwrap()["method"],
"unwrap_or_else"
);
}
#[test]
fn unwrap_or_else_non_default_closure_is_not_flagged() {
let findings = findings_for(
"fn f(input: Result<i32, String>) -> i32 {\n input.unwrap_or_else(|_| 0)\n}\n",
"slop-silent-default-unwrap-or-else-non-default",
);
assert!(rule_findings(&findings, SILENT_DEFAULT_RULE).is_empty());
}
#[test]
fn unwrap_or_default_with_error_observation_elsewhere_in_function_is_not_flagged() {
let findings = findings_for(
"fn f(input: Option<i32>, other: Result<i32, String>) -> i32 {\n if let Err(e) = other {\n tracing::warn!(\"failed: {:?}\", e);\n }\n input.unwrap_or_default()\n}\n",
"slop-silent-default-observed-elsewhere",
);
assert!(rule_findings(&findings, SILENT_DEFAULT_RULE).is_empty());
}
#[test]
fn silent_default_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(SILENT_DEFAULT_RULE)
.expect("silent-default has a registry entry")
.example
.expect("silent-default has a curated example")
.before;
let findings = findings_for(example, "slop-silent-default-registry-example");
assert_eq!(rule_findings(&findings, SILENT_DEFAULT_RULE).len(), 1);
}
#[test]
fn two_distinct_try_sites_with_no_context_calls_on_anyhow_result_is_flagged() {
let findings = findings_for(
"pub fn load(path: &str) -> anyhow::Result<String> {\n let raw = std::fs::read_to_string(path)?;\n let parsed = raw.parse::<i32>()?;\n Ok(parsed.to_string())\n}\n",
"slop-context-free-propagation-anyhow",
);
let hits = rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::Heuristic);
assert_eq!(hits[0].evidence.as_ref().unwrap()["try_site_count"], 2);
}
#[test]
fn single_try_site_is_not_flagged() {
let findings = findings_for(
"pub fn load(path: &str) -> anyhow::Result<String> {\n let raw = std::fs::read_to_string(path)?;\n Ok(raw)\n}\n",
"slop-context-free-propagation-single-site",
);
assert!(rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE).is_empty());
}
#[test]
fn two_distinct_try_sites_with_a_context_call_on_only_one_is_not_flagged() {
let findings = findings_for(
"pub fn load(path: &str) -> anyhow::Result<String> {\n let raw = std::fs::read_to_string(path).context(\"failed to read\")?;\n let parsed = raw.parse::<i32>()?;\n Ok(parsed.to_string())\n}\n",
"slop-context-free-propagation-partial-context",
);
assert!(rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE).is_empty());
}
#[test]
fn same_call_site_twice_counts_as_one_distinct_try_site_and_is_not_flagged() {
let findings = findings_for(
"pub fn load(path: &str) -> anyhow::Result<String> {\n let a = std::fs::read_to_string(path)?;\n let b = std::fs::read_to_string(path)?;\n Ok(a + &b)\n}\n",
"slop-context-free-propagation-duplicate-call",
);
assert!(rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE).is_empty());
}
#[test]
fn two_distinct_try_sites_on_boxed_dyn_error_return_is_flagged() {
let findings = findings_for(
"fn load(path: &str) -> Result<String, Box<dyn std::error::Error>> {\n let raw = std::fs::read_to_string(path)?;\n let parsed = raw.parse::<i32>()?;\n Ok(parsed.to_string())\n}\n",
"slop-context-free-propagation-boxed",
);
let hits = rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn two_distinct_try_sites_on_concrete_error_return_is_not_flagged() {
let findings = findings_for(
"struct MyError;\nfn load(path: &str) -> Result<String, MyError> {\n let raw = std::fs::read_to_string(path)?;\n let parsed = raw.parse::<i32>()?;\n Ok(parsed.to_string())\n}\n",
"slop-context-free-propagation-concrete",
);
assert!(rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE).is_empty());
}
#[test]
fn context_free_propagation_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(CONTEXT_FREE_PROPAGATION_RULE)
.expect("context-free-propagation has a registry entry")
.example
.expect("context-free-propagation has a curated example")
.before;
let findings = findings_for(example, "slop-context-free-propagation-registry-example");
assert_eq!(
rule_findings(&findings, CONTEXT_FREE_PROPAGATION_RULE).len(),
1
);
}
#[test]
fn debug_placeholder_inside_display_write_is_flagged() {
let findings = findings_for(
"struct Money(i64);\nimpl std::fmt::Display for Money {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n write!(f, \"{:?}\", self.0)\n }\n}\n",
"slop-debug-format-leak-write",
);
let hits = rule_findings(&findings, DEBUG_FORMAT_LEAK_RULE);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::Heuristic);
assert_eq!(hits[0].evidence.as_ref().unwrap()["type_name"], "Money");
}
#[test]
fn debug_placeholder_inside_display_format_macro_is_flagged() {
let findings = findings_for(
"struct Money(i64);\nimpl std::fmt::Display for Money {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n f.write_str(&format!(\"{:#?}\", self.0))\n }\n}\n",
"slop-debug-format-leak-format-macro",
);
let hits = rule_findings(&findings, DEBUG_FORMAT_LEAK_RULE);
assert_eq!(hits.len(), 1);
}
#[test]
fn debug_placeholder_inside_debug_impl_is_not_flagged() {
let findings = findings_for(
"struct Money(i64);\nimpl std::fmt::Debug for Money {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n write!(f, \"{:?}\", self.0)\n }\n}\n",
"slop-debug-format-leak-debug-impl",
);
assert!(rule_findings(&findings, DEBUG_FORMAT_LEAK_RULE).is_empty());
}
#[test]
fn display_impl_without_debug_placeholder_is_not_flagged() {
let findings = findings_for(
"struct Money(i64);\nimpl std::fmt::Display for Money {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n write!(f, \"{}\", self.0)\n }\n}\n",
"slop-debug-format-leak-no-placeholder",
);
assert!(rule_findings(&findings, DEBUG_FORMAT_LEAK_RULE).is_empty());
}
#[test]
fn debug_format_leak_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(DEBUG_FORMAT_LEAK_RULE)
.expect("debug-format-leak has a registry entry")
.example
.expect("debug-format-leak has a curated example")
.before;
let findings = findings_for(example, "slop-debug-format-leak-registry-example");
assert_eq!(rule_findings(&findings, DEBUG_FORMAT_LEAK_RULE).len(), 1);
}
}