1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::Write;
5
6#[allow(unused_imports)]
7use crate::analyze::{AnalysisOutput, FileAnalysisOutput, FocusedAnalysisOutput};
8
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11pub struct PaginationParams {
12 pub cursor: Option<String>,
14 #[schemars(schema_with = "crate::schema_helpers::option_page_size_schema")]
16 pub page_size: Option<usize>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
21pub struct OutputControlParams {
22 pub force: Option<bool>,
24 pub summary: Option<bool>,
26 pub verbose: Option<bool>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
31pub struct AnalyzeDirectoryParams {
32 pub path: String,
34
35 #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
37 pub max_depth: Option<u32>,
38
39 #[serde(flatten)]
40 pub pagination: PaginationParams,
41
42 #[serde(flatten)]
43 pub output_control: OutputControlParams,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47pub struct AnalyzeFileParams {
48 pub path: String,
50
51 #[schemars(schema_with = "crate::schema_helpers::option_ast_limit_schema")]
53 pub ast_recursion_limit: Option<usize>,
54
55 #[serde(flatten)]
56 pub pagination: PaginationParams,
57
58 #[serde(flatten)]
59 pub output_control: OutputControlParams,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63pub struct AnalyzeModuleParams {
64 pub path: String,
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "snake_case")]
71pub enum SymbolMatchMode {
72 #[default]
74 Exact,
75 Insensitive,
77 Prefix,
79 Contains,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
84pub struct AnalyzeSymbolParams {
85 pub path: String,
87
88 pub symbol: String,
90
91 pub match_mode: Option<SymbolMatchMode>,
93
94 #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
96 pub follow_depth: Option<u32>,
97
98 #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
100 pub max_depth: Option<u32>,
101
102 #[schemars(schema_with = "crate::schema_helpers::option_ast_limit_schema")]
104 pub ast_recursion_limit: Option<usize>,
105
106 #[serde(flatten)]
107 pub pagination: PaginationParams,
108
109 #[serde(flatten)]
110 pub output_control: OutputControlParams,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
114pub struct AnalysisResult {
115 pub path: String,
116 pub mode: AnalysisMode,
117 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
118 pub import_count: usize,
119 #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
120 pub main_line: Option<usize>,
121 pub files: Vec<FileInfo>,
122 pub functions: Vec<FunctionInfo>,
123 pub classes: Vec<ClassInfo>,
124 pub references: Vec<ReferenceInfo>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
128pub struct FileInfo {
129 pub path: String,
130 pub language: String,
131 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
132 pub line_count: usize,
133 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
134 pub function_count: usize,
135 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
136 pub class_count: usize,
137 pub is_test: bool,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142pub struct FunctionInfo {
143 pub name: String,
144 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
145 pub line: usize,
146 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
147 pub end_line: usize,
148 pub parameters: Vec<String>,
150 pub return_type: Option<String>,
151}
152
153impl FunctionInfo {
154 const MAX_PARAMS_DISPLAY_LEN: usize = 80;
156 const TRUNCATION_POINT: usize = 77;
158
159 pub fn compact_signature(&self) -> String {
163 let mut sig = String::with_capacity(self.name.len() + 40);
164 sig.push_str(&self.name);
165 sig.push('(');
166
167 if !self.parameters.is_empty() {
168 let params_str = self.parameters.join(", ");
169 if params_str.len() > Self::MAX_PARAMS_DISPLAY_LEN {
170 let truncate_at = params_str.floor_char_boundary(Self::TRUNCATION_POINT);
172 sig.push_str(¶ms_str[..truncate_at]);
173 sig.push_str("...");
174 } else {
175 sig.push_str(¶ms_str);
176 }
177 }
178
179 sig.push(')');
180
181 if let Some(ret_type) = &self.return_type {
182 sig.push_str(" -> ");
183 sig.push_str(ret_type);
184 }
185
186 write!(sig, " :{}-{}", self.line, self.end_line).ok();
187 sig
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
192pub struct ClassInfo {
193 pub name: String,
194 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
195 pub line: usize,
196 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
197 pub end_line: usize,
198 pub methods: Vec<FunctionInfo>,
199 pub fields: Vec<String>,
200 #[schemars(description = "Inherited types (parent classes, interfaces, trait bounds)")]
202 pub inherits: Vec<String>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
206pub struct CallInfo {
207 pub caller: String,
208 pub callee: String,
209 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
210 pub line: usize,
211 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
212 pub column: usize,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
216 pub arg_count: Option<usize>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
220pub struct AssignmentInfo {
221 pub variable: String,
223 pub value: String,
225 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
227 pub line: usize,
228 pub scope: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
233pub struct FieldAccessInfo {
234 pub object: String,
236 pub field: String,
238 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
240 pub line: usize,
241 pub scope: String,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
246pub struct ReferenceInfo {
247 pub symbol: String,
248 pub reference_type: ReferenceType,
249 pub location: String,
250 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
251 pub line: usize,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
255#[serde(rename_all = "lowercase")]
256pub enum ReferenceType {
257 Definition,
258 Usage,
259 Import,
260 Export,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
264#[serde(rename_all = "lowercase")]
265pub enum EntryType {
266 File,
267 Directory,
268 Function,
269 Class,
270 Variable,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
275#[serde(rename_all = "snake_case")]
276pub enum AnalysisMode {
277 Overview,
279 FileDetails,
281 SymbolFocus,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
286pub struct CallChain {
287 pub chain: Vec<CallInfo>,
288 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
289 pub depth: u32,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
293pub struct FocusedAnalysisData {
294 pub symbol: String,
295 pub definition: Option<FunctionInfo>,
296 pub call_chains: Vec<CallChain>,
297 pub references: Vec<ReferenceInfo>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
301pub struct ElementQueryResult {
302 pub query: String,
303 pub results: Vec<String>,
304 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
305 pub count: usize,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
309pub struct ImportInfo {
310 pub module: String,
312 pub items: Vec<String>,
314 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
316 pub line: usize,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
320pub struct SemanticAnalysis {
321 pub functions: Vec<FunctionInfo>,
322 pub classes: Vec<ClassInfo>,
323 pub imports: Vec<ImportInfo>,
325 pub references: Vec<ReferenceInfo>,
326 #[serde(skip)]
328 #[schemars(skip)]
329 pub call_frequency: HashMap<String, usize>,
330 pub calls: Vec<CallInfo>,
332 #[serde(skip)]
334 #[schemars(skip)]
335 pub assignments: Vec<AssignmentInfo>,
336 #[serde(skip)]
338 #[schemars(skip)]
339 pub field_accesses: Vec<FieldAccessInfo>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
344pub struct ModuleFunctionInfo {
345 pub name: String,
347 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
349 pub line: usize,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
354pub struct ModuleImportInfo {
355 pub module: String,
357 pub items: Vec<String>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
363pub struct ModuleInfo {
364 pub name: String,
366 #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
368 pub line_count: usize,
369 pub language: String,
371 pub functions: Vec<ModuleFunctionInfo>,
373 pub imports: Vec<ModuleImportInfo>,
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_compact_signature_short_params() {
383 let func = FunctionInfo {
384 name: "add".to_string(),
385 line: 10,
386 end_line: 12,
387 parameters: vec!["a: i32".to_string(), "b: i32".to_string()],
388 return_type: Some("i32".to_string()),
389 };
390
391 let sig = func.compact_signature();
392 assert_eq!(sig, "add(a: i32, b: i32) -> i32 :10-12");
393 }
394
395 #[test]
396 fn test_compact_signature_long_params_truncation() {
397 let func = FunctionInfo {
398 name: "process".to_string(),
399 line: 20,
400 end_line: 50,
401 parameters: vec![
402 "config: ComplexConfigType".to_string(),
403 "data: VeryLongDataStructureNameThatExceedsEightyCharacters".to_string(),
404 "callback: Fn(Result) -> ()".to_string(),
405 ],
406 return_type: Some("Result<Output>".to_string()),
407 };
408
409 let sig = func.compact_signature();
410 assert!(sig.contains("process("));
411 assert!(sig.contains("..."));
412 assert!(sig.contains("-> Result<Output>"));
413 assert!(sig.contains(":20-50"));
414 }
415
416 #[test]
417 fn test_compact_signature_empty_params() {
418 let func = FunctionInfo {
419 name: "main".to_string(),
420 line: 1,
421 end_line: 5,
422 parameters: vec![],
423 return_type: None,
424 };
425
426 let sig = func.compact_signature();
427 assert_eq!(sig, "main() :1-5");
428 }
429
430 #[test]
431 fn schema_flatten_inline() {
432 use schemars::schema_for;
433
434 let dir_schema = schema_for!(AnalyzeDirectoryParams);
436 let dir_props = dir_schema
437 .as_object()
438 .and_then(|o| o.get("properties"))
439 .and_then(|v| v.as_object())
440 .expect("AnalyzeDirectoryParams must have properties");
441
442 assert!(
443 dir_props.contains_key("cursor"),
444 "cursor must be top-level in AnalyzeDirectoryParams schema"
445 );
446 assert!(
447 dir_props.contains_key("page_size"),
448 "page_size must be top-level in AnalyzeDirectoryParams schema"
449 );
450 assert!(
451 dir_props.contains_key("force"),
452 "force must be top-level in AnalyzeDirectoryParams schema"
453 );
454 assert!(
455 dir_props.contains_key("summary"),
456 "summary must be top-level in AnalyzeDirectoryParams schema"
457 );
458
459 let file_schema = schema_for!(AnalyzeFileParams);
461 let file_props = file_schema
462 .as_object()
463 .and_then(|o| o.get("properties"))
464 .and_then(|v| v.as_object())
465 .expect("AnalyzeFileParams must have properties");
466
467 assert!(
468 file_props.contains_key("cursor"),
469 "cursor must be top-level in AnalyzeFileParams schema"
470 );
471 assert!(
472 file_props.contains_key("page_size"),
473 "page_size must be top-level in AnalyzeFileParams schema"
474 );
475 assert!(
476 file_props.contains_key("force"),
477 "force must be top-level in AnalyzeFileParams schema"
478 );
479 assert!(
480 file_props.contains_key("summary"),
481 "summary must be top-level in AnalyzeFileParams schema"
482 );
483
484 let symbol_schema = schema_for!(AnalyzeSymbolParams);
486 let symbol_props = symbol_schema
487 .as_object()
488 .and_then(|o| o.get("properties"))
489 .and_then(|v| v.as_object())
490 .expect("AnalyzeSymbolParams must have properties");
491
492 assert!(
493 symbol_props.contains_key("cursor"),
494 "cursor must be top-level in AnalyzeSymbolParams schema"
495 );
496 assert!(
497 symbol_props.contains_key("page_size"),
498 "page_size must be top-level in AnalyzeSymbolParams schema"
499 );
500 assert!(
501 symbol_props.contains_key("force"),
502 "force must be top-level in AnalyzeSymbolParams schema"
503 );
504 assert!(
505 symbol_props.contains_key("summary"),
506 "summary must be top-level in AnalyzeSymbolParams schema"
507 );
508
509 let file_ast = file_props
511 .get("ast_recursion_limit")
512 .expect("ast_recursion_limit must be present in AnalyzeFileParams schema");
513 assert_eq!(
514 file_ast.get("minimum").and_then(|v| v.as_u64()),
515 Some(1),
516 "ast_recursion_limit in AnalyzeFileParams must have minimum: 1"
517 );
518 let symbol_ast = symbol_props
519 .get("ast_recursion_limit")
520 .expect("ast_recursion_limit must be present in AnalyzeSymbolParams schema");
521 assert_eq!(
522 symbol_ast.get("minimum").and_then(|v| v.as_u64()),
523 Some(1),
524 "ast_recursion_limit in AnalyzeSymbolParams must have minimum: 1"
525 );
526 }
527}
528
529#[derive(Debug, serde::Serialize)]
532#[serde(rename_all = "camelCase")]
533pub struct ErrorMeta {
534 pub error_category: &'static str,
535 pub is_retryable: bool,
536 pub suggested_action: &'static str,
537}
538
539#[cfg(test)]
540mod error_meta_tests {
541 use super::*;
542
543 #[test]
544 fn test_error_meta_serialization_camel_case() {
545 let meta = ErrorMeta {
546 error_category: "validation",
547 is_retryable: false,
548 suggested_action: "fix input",
549 };
550 let v = serde_json::to_value(&meta).unwrap();
551 assert_eq!(v["errorCategory"], "validation");
552 assert_eq!(v["isRetryable"], false);
553 assert_eq!(v["suggestedAction"], "fix input");
554 }
555
556 #[test]
557 fn test_error_meta_validation_not_retryable() {
558 let meta = ErrorMeta {
559 error_category: "validation",
560 is_retryable: false,
561 suggested_action: "use summary=true",
562 };
563 assert!(!meta.is_retryable);
564 }
565
566 #[test]
567 fn test_error_meta_transient_retryable() {
568 let meta = ErrorMeta {
569 error_category: "transient",
570 is_retryable: true,
571 suggested_action: "retry the request",
572 };
573 assert!(meta.is_retryable);
574 }
575}