loctree/analyzer/
report.rs

1use serde::Serialize;
2
3/// Confidence level for unused handler detection.
4/// HIGH = no string literal matches found (likely truly unused)
5/// LOW = string literal matches found (may be used dynamically)
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
7pub enum Confidence {
8    High,
9    Low,
10}
11
12impl std::fmt::Display for Confidence {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            Confidence::High => write!(f, "HIGH"),
16            Confidence::Low => write!(f, "LOW"),
17        }
18    }
19}
20
21/// A string literal match in frontend code that might indicate dynamic usage.
22#[derive(Clone, Debug, Serialize)]
23pub struct StringLiteralMatch {
24    pub file: String,
25    pub line: usize,
26    pub context: String, // "allowlist", "const", "object_key", "array_item"
27}
28
29#[derive(Clone, Serialize)]
30pub struct CommandGap {
31    pub name: String,
32    pub implementation_name: Option<String>,
33    pub locations: Vec<(String, usize)>,
34    /// Confidence level (None for missing handlers, Some for unused handlers)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub confidence: Option<Confidence>,
37    /// String literal matches that may indicate dynamic usage
38    #[serde(skip_serializing_if = "Vec::is_empty", default)]
39    pub string_literal_matches: Vec<StringLiteralMatch>,
40}
41
42#[derive(Clone, Serialize)]
43pub struct AiInsight {
44    pub title: String,
45    pub severity: String,
46    pub message: String,
47}
48
49#[derive(Clone, Serialize)]
50pub struct GraphNode {
51    pub id: String,
52    pub label: String,
53    pub loc: usize,
54    pub x: f32,
55    pub y: f32,
56    pub component: usize,
57    pub degree: usize,
58    pub detached: bool,
59}
60
61#[derive(Clone, Serialize)]
62pub struct GraphComponent {
63    pub id: usize,
64    pub size: usize,
65    #[serde(rename = "edges")]
66    pub edge_count: usize,
67    pub nodes: Vec<String>,
68    pub isolated_count: usize,
69    pub sample: String,
70    pub loc_sum: usize,
71    pub detached: bool,
72    pub tauri_frontend: usize,
73    pub tauri_backend: usize,
74}
75
76#[derive(Clone, Serialize)]
77pub struct GraphData {
78    pub nodes: Vec<GraphNode>,
79    pub edges: Vec<(String, String, String)>, // from, to, kind
80    pub components: Vec<GraphComponent>,
81    pub main_component_id: usize,
82    /// Whether this graph was truncated due to size limits
83    #[serde(default)]
84    pub truncated: bool,
85    /// Total number of nodes before truncation (same as nodes.len() if not truncated)
86    #[serde(default)]
87    pub total_nodes: usize,
88    /// Total number of edges before truncation (same as edges.len() if not truncated)
89    #[serde(default)]
90    pub total_edges: usize,
91    /// Reason for truncation, if any
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub truncation_reason: Option<String>,
94}
95
96/// Location of a duplicate export with line number
97#[derive(Clone, Serialize)]
98pub struct DupLocation {
99    pub file: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub line: Option<usize>,
102}
103
104#[derive(Clone, Serialize)]
105pub struct RankedDup {
106    pub name: String,
107    pub files: Vec<String>,
108    /// Locations with line numbers (file, line)
109    #[serde(skip_serializing_if = "Vec::is_empty", default)]
110    pub locations: Vec<DupLocation>,
111    pub score: usize,
112    pub prod_count: usize,
113    pub dev_count: usize,
114    pub canonical: String,
115    /// Line number in canonical file
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub canonical_line: Option<usize>,
118    pub refactors: Vec<String>,
119}
120
121/// Full command bridge for FE↔BE comparison table.
122/// Represents a single command with all its frontend calls and backend handler.
123#[derive(Clone, Serialize)]
124pub struct CommandBridge {
125    /// Command name (exposed_name from Tauri)
126    pub name: String,
127    /// Frontend call locations (file, line)
128    pub fe_locations: Vec<(String, usize)>,
129    /// Backend handler location (file, line, impl_symbol) - None if missing
130    pub be_location: Option<(String, usize, String)>,
131    /// Status: "ok", "missing_handler", "unused_handler", "unregistered_handler"
132    pub status: String,
133    /// Language (ts, rs, etc.)
134    pub language: String,
135}
136
137#[derive(Clone, Default, Serialize)]
138pub struct TreeNode {
139    pub path: String,
140    pub loc: usize,
141    #[serde(default)]
142    pub children: Vec<TreeNode>,
143}
144
145#[derive(Serialize)]
146pub struct ReportSection {
147    pub root: String,
148    pub files_analyzed: usize,
149    pub total_loc: usize,
150    pub reexport_files_count: usize,
151    pub dynamic_imports_count: usize,
152    pub ranked_dups: Vec<RankedDup>,
153    pub cascades: Vec<(String, String)>,
154    /// Actual circular import components (normalized)
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub circular_imports: Vec<Vec<String>>,
157    pub dynamic: Vec<(String, Vec<String>)>,
158    pub analyze_limit: usize,
159    pub missing_handlers: Vec<CommandGap>,
160    /// Backend handlers that exist (`#[tauri::command]`) but are never
161    /// registered via `tauri::generate_handler![...]`.
162    pub unregistered_handlers: Vec<CommandGap>,
163    pub unused_handlers: Vec<CommandGap>,
164    pub command_counts: (usize, usize),
165    /// Full command bridges for FE↔BE comparison table
166    pub command_bridges: Vec<CommandBridge>,
167    pub open_base: Option<String>,
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub tree: Option<Vec<TreeNode>>,
170    pub graph: Option<GraphData>,
171    pub graph_warning: Option<String>,
172    pub insights: Vec<AiInsight>,
173    pub git_branch: Option<String>,
174    pub git_commit: Option<String>,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::snapshot::CommandBridge;
181
182    #[test]
183    fn confidence_display_high() {
184        assert_eq!(format!("{}", Confidence::High), "HIGH");
185    }
186
187    #[test]
188    fn confidence_display_low() {
189        assert_eq!(format!("{}", Confidence::Low), "LOW");
190    }
191
192    #[test]
193    fn confidence_equality() {
194        assert_eq!(Confidence::High, Confidence::High);
195        assert_eq!(Confidence::Low, Confidence::Low);
196        assert_ne!(Confidence::High, Confidence::Low);
197    }
198
199    #[test]
200    fn string_literal_match_creation() {
201        let m = StringLiteralMatch {
202            file: "test.ts".to_string(),
203            line: 42,
204            context: "allowlist".to_string(),
205        };
206        assert_eq!(m.file, "test.ts");
207        assert_eq!(m.line, 42);
208        assert_eq!(m.context, "allowlist");
209    }
210
211    #[test]
212    fn command_gap_creation() {
213        let gap = CommandGap {
214            name: "test_cmd".to_string(),
215            implementation_name: Some("testCmd".to_string()),
216            locations: vec![("test.ts".to_string(), 10)],
217            confidence: Some(Confidence::High),
218            string_literal_matches: vec![],
219        };
220        assert_eq!(gap.name, "test_cmd");
221        assert_eq!(gap.implementation_name, Some("testCmd".to_string()));
222        assert_eq!(gap.locations.len(), 1);
223        assert_eq!(gap.confidence, Some(Confidence::High));
224    }
225
226    #[test]
227    fn ai_insight_creation() {
228        let insight = AiInsight {
229            title: "Test Insight".to_string(),
230            severity: "warning".to_string(),
231            message: "Some message".to_string(),
232        };
233        assert_eq!(insight.title, "Test Insight");
234        assert_eq!(insight.severity, "warning");
235    }
236
237    #[test]
238    fn graph_node_creation() {
239        let node = GraphNode {
240            id: "src/main.ts".to_string(),
241            label: "main.ts".to_string(),
242            loc: 100,
243            x: 0.5,
244            y: 0.5,
245            component: 0,
246            degree: 3,
247            detached: false,
248        };
249        assert_eq!(node.id, "src/main.ts");
250        assert_eq!(node.loc, 100);
251        assert!(!node.detached);
252    }
253
254    #[test]
255    fn command_bridge_creation() {
256        let bridge = CommandBridge {
257            name: "get_user".to_string(),
258            frontend_calls: vec![("src/app.ts".to_string(), 10)],
259            backend_handler: Some(("src-tauri/src/lib.rs".to_string(), 20)),
260            has_handler: true,
261            is_called: true,
262        };
263        assert_eq!(bridge.name, "get_user");
264        assert!(bridge.has_handler);
265        assert!(bridge.backend_handler.is_some());
266    }
267}