use crate::analysis::ParsedFile;
use crate::model::{Finding, Severity};
pub(crate) const BINDING_LOCATION: &str = file!();
use super::{file_attributes, is_main_like_file, is_test_like};
pub(crate) const RULE_DEFINITIONS: &[crate::rules::catalog::RuleDefinition] = &[
crate::rules::catalog::RuleDefinition {
id: "rust_oversized_module_file",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Rust module files that grow too large and mix too many responsibilities.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_pub_use_glob_surface",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Public glob re-exports that flatten the crate surface.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_root_reexport_wall",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Crate roots that expose too many public re-exports at once.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_mod_rs_catchall",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "mod.rs files that look like catch-all subsystem dumps.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_duplicate_bootstrap_sequence",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Repeated startup or bootstrap wiring in multiple functions within the same file.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_redundant_path_attribute",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Same-directory #[path = \"...\"] module attributes that standard resolution could replace.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
crate::rules::catalog::RuleDefinition {
id: "rust_broad_allow_dead_code",
language: crate::rules::catalog::RuleLanguage::Rust,
family: "module_surface",
default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
status: crate::rules::catalog::RuleStatus::Stable,
configurability: &[
crate::rules::catalog::RuleConfigurability::Disable,
crate::rules::catalog::RuleConfigurability::Ignore,
crate::rules::catalog::RuleConfigurability::SeverityOverride,
],
description: "Broad dead_code suppression that can hide real wiring or maintenance gaps.",
binding_location: crate::rules::catalog::bindings::RUST_MODULE_SURFACE,
},
];
pub(crate) fn module_surface_file_findings(file: &ParsedFile) -> Vec<Finding> {
if is_test_like(file, None) {
return Vec::new();
}
let mut findings = Vec::new();
findings.extend(oversized_module_findings(file));
findings.extend(public_surface_findings(file));
findings.extend(mod_rs_findings(file));
findings.extend(bootstrap_sequence_findings(file));
findings.extend(path_attribute_findings(file));
findings.extend(dead_code_allow_findings(file));
findings
}
fn oversized_module_findings(file: &ParsedFile) -> Vec<Finding> {
let top_level_count = file
.functions
.iter()
.filter(|function| !function.is_test_function)
.count()
+ file
.rust_data()
.map(|data| {
data.structs.len()
+ data.rust_enums.len()
+ data.rust_statics.len()
+ data.module_declarations.len()
})
.unwrap_or(0);
if top_level_count < 18 || file.line_count < 40 {
return Vec::new();
}
if !(file.path.ends_with("lib.rs")
|| file.path.ends_with("main.rs")
|| file.path.file_name().and_then(|name| name.to_str()) == Some("mod.rs"))
{
return Vec::new();
}
vec![Finding {
rule_id: "rust_oversized_module_file".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: 1,
end_line: 1,
message: format!(
"module file {} is large enough to justify a closer ownership split",
file.path.display()
),
evidence: vec![
format!("top_level_items={top_level_count}"),
format!("line_count={}", file.line_count),
],
}]
}
fn public_surface_findings(file: &ParsedFile) -> Vec<Finding> {
let public_globs = file
.imports
.iter()
.filter(|import| import.is_public && import.alias == "*")
.collect::<Vec<_>>();
let public_reexports = file
.imports
.iter()
.filter(|import| import.is_public && import.alias != "*")
.collect::<Vec<_>>();
let mut findings = Vec::new();
if !public_globs.is_empty() {
findings.push(Finding {
rule_id: "rust_pub_use_glob_surface".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: public_globs[0].line,
end_line: public_globs[0].line,
message: format!("file {} uses a public glob re-export", file.path.display()),
evidence: vec![public_globs[0].path.clone()],
});
}
if file.path.ends_with("lib.rs") && public_reexports.len() >= 5 {
findings.push(Finding {
rule_id: "rust_root_reexport_wall".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: public_reexports[0].line,
end_line: public_reexports[0].line,
message: format!(
"file {} exposes many public re-exports from the root surface",
file.path.display()
),
evidence: vec![format!("public_reexports={}", public_reexports.len())],
});
}
findings
}
fn mod_rs_findings(file: &ParsedFile) -> Vec<Finding> {
let Some(file_name) = file.path.file_name().and_then(|name| name.to_str()) else {
return Vec::new();
};
if file_name != "mod.rs" {
return Vec::new();
}
let top_level_count = file
.functions
.iter()
.filter(|function| !function.is_test_function)
.count()
+ file
.rust_data()
.map(|data| {
data.structs.len()
+ data.rust_enums.len()
+ data.rust_statics.len()
+ data.module_declarations.len()
})
.unwrap_or(0);
if top_level_count < 10 || file.line_count < 40 {
return Vec::new();
}
vec![Finding {
rule_id: "rust_mod_rs_catchall".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: 1,
end_line: 1,
message: format!(
"mod.rs file {} is acting as a catch-all module",
file.path.display()
),
evidence: vec![format!("top_level_items={top_level_count}")],
}]
}
fn bootstrap_sequence_findings(file: &ParsedFile) -> Vec<Finding> {
let mut bootstrap_hits = Vec::new();
for function in &file.functions {
let body = function.body_text.as_str();
let markers = [
"Router::new(",
"Runtime::new(",
"Builder::new_multi_thread(",
"Builder::new_current_thread(",
"Client::new(",
".connect().await",
];
if markers.iter().any(|marker| body.contains(marker)) {
bootstrap_hits.push((
function.fingerprint.name.clone(),
function.fingerprint.start_line,
));
}
}
if bootstrap_hits.len() < 2 {
return Vec::new();
}
vec![Finding {
rule_id: "rust_duplicate_bootstrap_sequence".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: Some(bootstrap_hits[0].0.clone()),
start_line: bootstrap_hits[0].1,
end_line: bootstrap_hits[0].1,
message: format!(
"file {} contains repeated bootstrap-style setup functions",
file.path.display()
),
evidence: bootstrap_hits
.iter()
.map(|(name, line)| format!("{name} at line {line}"))
.collect(),
}]
}
fn path_attribute_findings(file: &ParsedFile) -> Vec<Finding> {
let Some(file_name) = file.path.file_name().and_then(|name| name.to_str()) else {
return Vec::new();
};
if file_name != "mod.rs" {
return Vec::new();
}
let Some(attribute) = file_attributes(file)
.iter()
.find(|attribute| attribute.text.contains("#[path="))
else {
return Vec::new();
};
if attribute.text.contains("../") {
return Vec::new();
}
vec![Finding {
rule_id: "rust_redundant_path_attribute".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: attribute.line,
end_line: attribute.line,
message: format!(
"file {} uses a redundant #[path] module attribute",
file.path.display()
),
evidence: vec![attribute.text.clone()],
}]
}
fn dead_code_allow_findings(file: &ParsedFile) -> Vec<Finding> {
let Some(attribute) = file_attributes(file)
.iter()
.find(|attribute| attribute.text.contains("allow(dead_code)"))
else {
return Vec::new();
};
if is_main_like_file(file) || file.is_test_file {
return Vec::new();
}
vec![Finding {
rule_id: "rust_broad_allow_dead_code".to_string(),
severity: Severity::Info,
path: file.path.clone(),
function_name: None,
start_line: attribute.line,
end_line: attribute.line,
message: format!("file {} suppresses dead_code broadly", file.path.display()),
evidence: vec![attribute.text.clone()],
}]
}