1use crate::analyze::{AnalysisOutput, FileAnalysisOutput, FocusedAnalysisOutput};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt::Write;
6
7#[derive(Debug)]
10pub enum ModeResult {
11 Overview(AnalysisOutput),
12 FileDetails(FileAnalysisOutput),
13 SymbolFocus(FocusedAnalysisOutput),
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
17pub struct AnalyzeParams {
18 #[schemars(description = "File or directory path to analyze")]
19 pub path: String,
20
21 #[schemars(
22 description = "Analysis mode. Auto-detected: directory path without focus -> overview; file path -> file_details; focus parameter present -> symbol_focus. Override by setting explicitly."
23 )]
24 #[serde(default)]
25 pub mode: Option<AnalysisMode>,
26
27 #[schemars(
28 description = "Maximum directory traversal depth for overview mode. Unset means unlimited. Use 2-3 for large monorepos to limit output size."
29 )]
30 pub max_depth: Option<u32>,
31
32 #[schemars(
33 description = "Symbol name for symbol_focus mode (required for symbol_focus). Case-sensitive function or method name. Triggers symbol_focus auto-detection when mode is not set explicitly."
34 )]
35 pub focus: Option<String>,
36
37 #[schemars(
38 description = "Call graph traversal depth for symbol_focus mode. Default 1 (callers and callees one level out). Increase for deeper dependency traces; each level multiplies output size."
39 )]
40 pub follow_depth: Option<u32>,
41
42 #[schemars(
43 description = "Maximum AST node recursion depth for tree-sitter queries. Default is sufficient for all standard source files; increase only for pathologically deep nesting in generated code."
44 )]
45 pub ast_recursion_limit: Option<usize>,
46
47 #[schemars(
48 description = "Return full output even when it exceeds the 50K char limit. Prefer summary=true (overview) or narrowing scope over force=true; force=true can produce very large responses."
49 )]
50 pub force: Option<bool>,
51
52 #[schemars(
53 description = "Overview mode primarily; file_details has same 3-way logic (true/false/auto) but size-error still triggers if output exceeds 50K even after summary is applied. true = compact summary (totals plus directory tree, no per-file function lists); false = full output; unset = auto-summarize when output exceeds 50K chars. Use true proactively on large codebases to avoid the size threshold and reduce token consumption."
54 )]
55 pub summary: Option<bool>,
56
57 #[schemars(
58 description = "Pagination cursor from a previous response's next_cursor field. Pass unchanged to retrieve the next page of files (overview) or functions (file_details). Omit on the first call."
59 )]
60 pub cursor: Option<String>,
61
62 #[schemars(
63 description = "Items per page for pagination (default: 100). Items are files in overview mode and functions in file_details mode. Reduce below 100 to limit response size; increase above 100 to reduce round trips."
64 )]
65 pub page_size: Option<usize>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
69pub struct AnalysisResult {
70 pub path: String,
71 pub mode: AnalysisMode,
72 pub import_count: usize,
73 pub main_line: Option<usize>,
74 pub files: Vec<FileInfo>,
75 pub functions: Vec<FunctionInfo>,
76 pub classes: Vec<ClassInfo>,
77 pub references: Vec<ReferenceInfo>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
81pub struct FileInfo {
82 pub path: String,
83 pub language: String,
84 pub line_count: usize,
85 pub function_count: usize,
86 pub class_count: usize,
87 #[schemars(description = "Whether this file is a test file")]
88 pub is_test: bool,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
92pub struct FunctionInfo {
93 pub name: String,
94 pub line: usize,
95 pub end_line: usize,
96 pub parameters: Vec<String>,
97 pub return_type: Option<String>,
98}
99
100impl FunctionInfo {
101 const MAX_PARAMS_DISPLAY_LEN: usize = 80;
103 const TRUNCATION_POINT: usize = 77;
105
106 pub fn compact_signature(&self) -> String {
110 let mut sig = String::with_capacity(self.name.len() + 40);
111 sig.push_str(&self.name);
112 sig.push('(');
113
114 if !self.parameters.is_empty() {
115 let params_str = self.parameters.join(", ");
116 if params_str.len() > Self::MAX_PARAMS_DISPLAY_LEN {
117 let truncate_at = params_str.floor_char_boundary(Self::TRUNCATION_POINT);
119 sig.push_str(¶ms_str[..truncate_at]);
120 sig.push_str("...");
121 } else {
122 sig.push_str(¶ms_str);
123 }
124 }
125
126 sig.push(')');
127
128 if let Some(ret_type) = &self.return_type {
129 sig.push_str(" -> ");
130 sig.push_str(ret_type);
131 }
132
133 write!(sig, " :{}-{}", self.line, self.end_line).ok();
134 sig
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139pub struct ClassInfo {
140 pub name: String,
141 pub line: usize,
142 pub end_line: usize,
143 pub methods: Vec<FunctionInfo>,
144 pub fields: Vec<String>,
145 #[schemars(description = "Inherited types (parent classes, interfaces, trait bounds)")]
146 pub inherits: Vec<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
150pub struct CallInfo {
151 pub caller: String,
152 pub callee: String,
153 pub line: usize,
154 pub column: usize,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 #[schemars(description = "Number of arguments passed at the call site")]
157 pub arg_count: Option<usize>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161pub struct AssignmentInfo {
162 #[schemars(description = "Variable name being assigned")]
163 pub variable: String,
164 #[schemars(description = "Value expression being assigned")]
165 pub value: String,
166 #[schemars(description = "Line number where assignment occurs")]
167 pub line: usize,
168 #[schemars(description = "Enclosing function scope or 'global'")]
169 pub scope: String,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
173pub struct FieldAccessInfo {
174 #[schemars(description = "Object expression being accessed")]
175 pub object: String,
176 #[schemars(description = "Field name being accessed")]
177 pub field: String,
178 #[schemars(description = "Line number where field access occurs")]
179 pub line: usize,
180 #[schemars(description = "Enclosing function scope or 'global'")]
181 pub scope: String,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
185pub struct ReferenceInfo {
186 pub symbol: String,
187 pub reference_type: ReferenceType,
188 pub location: String,
189 pub line: usize,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
193#[serde(rename_all = "lowercase")]
194pub enum ReferenceType {
195 Definition,
196 Usage,
197 Import,
198 Export,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
202#[serde(rename_all = "lowercase")]
203pub enum EntryType {
204 File,
205 Directory,
206 Function,
207 Class,
208 Variable,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
212#[serde(rename_all = "snake_case")]
213pub enum AnalysisMode {
214 Overview,
215 FileDetails,
216 SymbolFocus,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
220pub struct CallChain {
221 pub chain: Vec<CallInfo>,
222 pub depth: u32,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226pub struct FocusedAnalysisData {
227 pub symbol: String,
228 pub definition: Option<FunctionInfo>,
229 pub call_chains: Vec<CallChain>,
230 pub references: Vec<ReferenceInfo>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
234pub struct ElementQueryResult {
235 pub query: String,
236 pub results: Vec<String>,
237 pub count: usize,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
241pub struct ImportInfo {
242 #[schemars(
243 description = "Full module path excluding the imported symbol (e.g., 'std::collections' for 'use std::collections::HashMap')"
244 )]
245 pub module: String,
246 #[schemars(
247 description = "Imported symbols (e.g., ['HashMap'] for 'use std::collections::HashMap')"
248 )]
249 pub items: Vec<String>,
250 #[schemars(description = "Line number where import appears")]
251 pub line: usize,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
255pub struct SemanticAnalysis {
256 #[schemars(description = "Functions with parameters and return types")]
257 pub functions: Vec<FunctionInfo>,
258 #[schemars(description = "Classes/structs")]
259 pub classes: Vec<ClassInfo>,
260 #[schemars(
261 description = "Flat list of imports; each entry carries its full module path and imported symbols"
262 )]
263 pub imports: Vec<ImportInfo>,
264 #[schemars(description = "Type references with location information")]
265 pub references: Vec<ReferenceInfo>,
266 #[schemars(description = "Call frequency map (function name -> count)")]
267 pub call_frequency: HashMap<String, usize>,
268 #[schemars(description = "Caller-callee pairs extracted from call expressions")]
269 pub calls: Vec<CallInfo>,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
271 #[schemars(description = "Variable assignments and reassignments")]
272 pub assignments: Vec<AssignmentInfo>,
273 #[serde(default, skip_serializing_if = "Vec::is_empty")]
274 #[schemars(description = "Field access patterns")]
275 pub field_accesses: Vec<FieldAccessInfo>,
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_compact_signature_short_params() {
284 let func = FunctionInfo {
285 name: "add".to_string(),
286 line: 10,
287 end_line: 12,
288 parameters: vec!["a: i32".to_string(), "b: i32".to_string()],
289 return_type: Some("i32".to_string()),
290 };
291
292 let sig = func.compact_signature();
293 assert_eq!(sig, "add(a: i32, b: i32) -> i32 :10-12");
294 }
295
296 #[test]
297 fn test_compact_signature_long_params_truncation() {
298 let func = FunctionInfo {
299 name: "process".to_string(),
300 line: 20,
301 end_line: 50,
302 parameters: vec![
303 "config: ComplexConfigType".to_string(),
304 "data: VeryLongDataStructureNameThatExceedsEightyCharacters".to_string(),
305 "callback: Fn(Result) -> ()".to_string(),
306 ],
307 return_type: Some("Result<Output>".to_string()),
308 };
309
310 let sig = func.compact_signature();
311 assert!(sig.contains("process("));
312 assert!(sig.contains("..."));
313 assert!(sig.contains("-> Result<Output>"));
314 assert!(sig.contains(":20-50"));
315 }
316
317 #[test]
318 fn test_compact_signature_empty_params() {
319 let func = FunctionInfo {
320 name: "main".to_string(),
321 line: 1,
322 end_line: 5,
323 parameters: vec![],
324 return_type: None,
325 };
326
327 let sig = func.compact_signature();
328 assert_eq!(sig, "main() :1-5");
329 }
330}