sniff/
roles_surface_facade.rs1use crate::roles::file_name;
2use crate::types::{FileRecord, MethodRecord};
3
4pub fn is_detector_facade_module(file: &FileRecord) -> bool {
5 let normalized = crate::roles::normalize_path(&file.file_path);
6 let name = file_name(&normalized);
7 let looks_like_detector = name.ends_with("_detector.py")
8 || name.ends_with("_finder.py")
9 || name.ends_with("_pipeline.py");
10 looks_like_detector
11 && file.methods.len() == 1
12 && file
13 .methods
14 .iter()
15 .all(|method| method.name.starts_with("run_") || method.name.starts_with("detect_"))
16}
17
18pub fn is_thin_wrapper_export(method: &MethodRecord) -> bool {
19 let source = method.source.trim();
20 if source.is_empty() || method.loc > 30 {
21 return false;
22 }
23
24 let lowered = source.to_lowercase();
25 let has_import = lowered.contains("from ") || lowered.contains(" import ");
26 let has_return = lowered.contains("return ");
27 let returns_alias = lowered.contains("return _");
28 let has_control_flow = lowered.contains("if ")
29 || lowered.contains("match ")
30 || lowered.contains("for ")
31 || lowered.contains("while ");
32
33 has_import && has_return && returns_alias && !has_control_flow
34}
35
36pub fn is_call_like_delegation_expression(expr: &str) -> bool {
37 let lowered = expr.to_lowercase();
38 if lowered.is_empty()
39 || lowered.contains(" if ")
40 || lowered.contains(" for ")
41 || lowered.contains(" while ")
42 || lowered.contains(" match ")
43 || lowered.contains(" try ")
44 || lowered.contains(" except ")
45 || lowered.contains(" catch ")
46 || lowered.contains(" lambda ")
47 || lowered.contains("buildstring")
48 || lowered.contains("buildlist")
49 || lowered.contains("buildmap")
50 || lowered.contains("buildset")
51 || lowered.contains("apply {")
52 || lowered.contains("also {")
53 || lowered.contains("let {")
54 || lowered.contains("run {")
55 || lowered.contains("with(")
56 || expr.contains('{')
57 || expr.contains('}')
58 {
59 return false;
60 }
61
62 expr.contains('(') || expr.contains("::")
63}
64
65pub fn is_thin_delegation_method(method: &MethodRecord) -> bool {
66 let source = method.source.trim();
67 if source.is_empty() || method.loc > 50 {
68 return false;
69 }
70
71 let meaningful_lines: Vec<&str> = source
72 .lines()
73 .map(str::trim)
74 .filter(|line| {
75 !line.is_empty()
76 && !line.starts_with('#')
77 && !line.starts_with("\"\"\"")
78 && !line.starts_with("'''")
79 })
80 .collect();
81
82 if meaningful_lines.len() == 1 {
83 let line = meaningful_lines[0].trim();
84 if let Some(return_expr) = line.strip_prefix("return ") {
85 return is_call_like_delegation_expression(return_expr.trim().trim_end_matches(';'));
86 }
87
88 if let Some(eq_index) = line.rfind('=') {
89 let Some(fun_index) = line.find("fun ") else {
90 return false;
91 };
92 let Some(close_paren_index) = line.rfind(')') else {
93 return false;
94 };
95 if eq_index < close_paren_index && eq_index > fun_index {
96 let expression = line[eq_index + 1..].trim().trim_end_matches(';');
97 return is_call_like_delegation_expression(expression);
98 }
99 }
100
101 return false;
102 }
103
104 if let Some(return_line) = meaningful_lines
105 .iter()
106 .rev()
107 .find(|line| line.trim_start().starts_with("return "))
108 .copied()
109 && let Some(return_expr) = return_line.strip_prefix("return ")
110 && is_call_like_delegation_expression(return_expr.trim().trim_end_matches(';'))
111 {
112 return true;
113 }
114
115 let lowered = source.to_lowercase();
116 if lowered.contains("fun ")
117 && let Some(eq_index) = source.rfind('=')
118 {
119 let expression = source[eq_index + 1..].replace('\n', " ");
120 if is_call_like_delegation_expression(expression.trim().trim_end_matches(';')) {
121 return true;
122 }
123 }
124
125 false
126}