sniff/
roles_surface_presentation.rs1use crate::roles::{contains_any, file_name, normalize_path};
2use crate::types::FileRecord;
3
4fn is_route_surface_path(normalized: &str, name: &str) -> bool {
5 if !normalized.contains("/app/") {
6 return false;
7 }
8
9 matches!(
10 name,
11 "page.tsx"
12 | "layout.tsx"
13 | "template.tsx"
14 | "loading.tsx"
15 | "error.tsx"
16 | "not-found.tsx"
17 | "default.tsx"
18 | "route.tsx"
19 | "page.jsx"
20 | "layout.jsx"
21 | "template.jsx"
22 | "loading.jsx"
23 | "error.jsx"
24 | "not-found.jsx"
25 | "default.jsx"
26 | "route.jsx"
27 ) || (normalized.contains("/pages/")
28 && matches!(name, "index.tsx" | "index.jsx" | "page.tsx" | "page.jsx"))
29}
30
31fn is_component_surface_path(normalized: &str) -> bool {
32 contains_any(
33 normalized,
34 &[
35 "/components/",
36 "/ui/",
37 "/views/",
38 "/screens/",
39 "/sections/",
40 "/modals/",
41 "/templates/",
42 "/layouts/",
43 ],
44 )
45}
46
47fn is_root_app_shell_path(normalized: &str, name: &str) -> bool {
48 matches!(name, "app.tsx" | "app.jsx")
49 && (normalized.starts_with("src/") || normalized.contains("/src/"))
50}
51
52fn source_looks_like_jsx_surface(source: &str) -> bool {
53 let lowered = source.to_lowercase();
54 (lowered.contains("return (") || lowered.contains("return <") || lowered.contains("return\n<"))
55 && source.contains('<')
56}
57
58fn is_small_kotlin_compose_surface(file: &FileRecord, normalized: &str) -> bool {
59 if !(normalized.ends_with(".kt") || normalized.ends_with(".kts")) {
60 return false;
61 }
62 if !normalized.contains("/screens/")
63 && !normalized.contains("/components/")
64 && !normalized.contains("/ui-compose/")
65 {
66 return false;
67 }
68 if !file.source.contains("@Composable") {
69 return false;
70 }
71 if file.methods.len() == 1 {
72 let max_method_loc = file
73 .methods
74 .iter()
75 .map(|method| method.loc)
76 .max()
77 .unwrap_or(0);
78 return max_method_loc <= 60;
79 }
80
81 if file.methods.len() < 4 || file.methods.len() > 20 {
82 return false;
83 }
84
85 let max_method_loc = file
86 .methods
87 .iter()
88 .map(|method| method.loc)
89 .max()
90 .unwrap_or(0);
91 max_method_loc <= 120
92}
93
94pub fn is_presentation_surface_module(file: &FileRecord) -> bool {
95 let normalized = normalize_path(&file.file_path);
96 let name = file_name(&normalized);
97
98 if is_small_kotlin_compose_surface(file, &normalized) {
99 return true;
100 }
101
102 if !(normalized.ends_with(".tsx") || normalized.ends_with(".jsx")) {
103 return false;
104 }
105
106 if is_route_surface_path(&normalized, name) {
107 return true;
108 }
109
110 if is_root_app_shell_path(&normalized, name)
111 && file.methods.len() <= 4
112 && source_looks_like_jsx_surface(&file.source)
113 {
114 return true;
115 }
116
117 if file.methods.len() > 4 {
118 return false;
119 }
120
121 is_component_surface_path(&normalized) && source_looks_like_jsx_surface(&file.source)
122}