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}