Skip to main content

sniff/
roles_surface_presentation.rs

1use 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}