Skip to main content

sniff/
ref_count_flags.rs

1use crate::report_types::StaticFlag;
2use crate::roles::{
3    is_analysis_finding_support_module, is_contract_type_module, is_data_catalog_module,
4    is_intentional_surface_record, is_protocol_stub_method, is_protocol_surface_module,
5    is_thin_wrapper_export, is_utility_helper_name, is_wrapper_only_module,
6};
7use crate::types::{FileRecord, FindingTier};
8
9fn should_skip_ref_flag(method_name: &str) -> bool {
10    matches!(
11        method_name,
12        "new"
13            | "add_file"
14            | "parse_file"
15            | "parse_file_symbols"
16            | "adapter"
17            | "visit"
18            | "visit_stmt"
19            | "reset_model_request_interval"
20    )
21}
22
23fn has_explicit_public_api(file_record: &FileRecord) -> bool {
24    let normalized = file_record.file_path.replace('\\', "/").to_lowercase();
25    normalized.ends_with("/__init__.py") || file_record.source.contains("__all__")
26}
27
28fn make_ref_flag(method: &crate::types::MethodRecord, reason: String) -> StaticFlag {
29    StaticFlag {
30        flag_type: "method".to_string(),
31        file_path: method.file_path.clone(),
32        method_name: Some(method.name.clone()),
33        reasons: vec![reason],
34        tier: FindingTier::KindaSlop,
35        gate: "ref_count".to_string(),
36        loc: method.loc,
37        start_line: method.start_line,
38        end_line: method.end_line,
39    }
40}
41
42fn should_skip_file_record(file_record: &FileRecord) -> bool {
43    is_wrapper_only_module(file_record)
44        || is_intentional_surface_record(file_record)
45        || is_protocol_surface_module(file_record)
46        || is_data_catalog_module(file_record)
47        || is_contract_type_module(file_record)
48        || is_analysis_finding_support_module(&file_record.file_path)
49}
50
51fn should_skip_method_record(
52    file_record: &FileRecord,
53    method: &crate::types::MethodRecord,
54) -> bool {
55    should_skip_ref_flag(&method.name)
56        || is_utility_helper_name(&method.name)
57        || is_protocol_stub_method(method)
58        || is_thin_wrapper_export(method)
59        || is_intentional_surface_record(file_record)
60        || is_data_catalog_module(file_record)
61        || is_contract_type_module(file_record)
62        || is_analysis_finding_support_module(&file_record.file_path)
63}
64
65fn ref_flag_reason(method: &crate::types::MethodRecord) -> Option<String> {
66    if method.real_ref_count == 0 && method.is_exported {
67        return Some("orphaned export (never referenced)".to_string());
68    }
69
70    if (method.real_ref_count == 1 || method.real_ref_count == 2)
71        && method.is_exported
72        && method.loc >= 5
73    {
74        return Some(format!(
75            "overbuilt helper (only {} references)",
76            method.real_ref_count
77        ));
78    }
79
80    None
81}
82
83pub fn build_ref_count_flags(file_records: &[FileRecord]) -> Vec<StaticFlag> {
84    file_records
85        .iter()
86        .filter(|file_record| !should_skip_file_record(file_record))
87        .flat_map(|file_record| {
88            file_record.methods.iter().filter_map(move |method| {
89                if should_skip_method_record(file_record, method) {
90                    return None;
91                }
92
93                let reason = ref_flag_reason(method)?;
94                if reason == "orphaned export (never referenced)"
95                    && has_explicit_public_api(file_record)
96                {
97                    return None;
98                }
99
100                Some(make_ref_flag(method, reason))
101            })
102        })
103        .collect()
104}