Skip to main content

code_analyze_mcp/
types.rs

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/// Pagination parameters shared across all tools.
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11pub struct PaginationParams {
12    /// Pagination cursor from a previous response's next_cursor field. Pass unchanged to retrieve the next page. Omit on the first call.
13    pub cursor: Option<String>,
14    /// Files per page for pagination (default: 100). Reduce below 100 to limit response size; increase above 100 to reduce round trips.
15    pub page_size: Option<usize>,
16}
17
18/// Output control parameters shared across all tools.
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
20pub struct OutputControlParams {
21    /// Return full output even when it exceeds the 50K char limit. Prefer summary=true or narrowing scope over force=true; force=true can produce very large responses.
22    pub force: Option<bool>,
23    /// true = compact summary (totals plus directory tree, no per-file function lists); false = full output; unset = auto-summarize when output exceeds 50K chars.
24    pub summary: Option<bool>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
28pub struct AnalyzeDirectoryParams {
29    /// Directory path to analyze
30    pub path: String,
31
32    /// Maximum directory traversal depth for overview mode only. 0 or unset = unlimited depth. Use 1-3 for large monorepos to manage output size. Ignored in other modes.
33    pub max_depth: Option<u32>,
34
35    #[serde(flatten)]
36    pub pagination: PaginationParams,
37
38    #[serde(flatten)]
39    pub output_control: OutputControlParams,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
43pub struct AnalyzeFileParams {
44    /// File path to analyze
45    pub path: String,
46
47    /// Maximum AST node depth for tree-sitter queries. Internal tuning parameter; leave unset in normal use. Increase only if query results are missing constructs in deeply nested or generated code.
48    pub ast_recursion_limit: Option<usize>,
49
50    #[serde(flatten)]
51    pub pagination: PaginationParams,
52
53    #[serde(flatten)]
54    pub output_control: OutputControlParams,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58pub struct AnalyzeSymbolParams {
59    /// Directory path to search for the symbol
60    pub path: String,
61
62    /// Symbol name to build call graph for (function or method). Exact case-sensitive match required; searched across all files in the specified directory. Example: 'parse_config' finds all callers and callees of that function.
63    pub symbol: String,
64
65    /// Call graph traversal depth for this tool (default 1). Level 1 = direct callers and callees; level 2 = one more hop, etc. Output size grows exponentially with graph branching. Warn user on levels above 2.
66    pub follow_depth: Option<u32>,
67
68    /// Maximum directory traversal depth. Unset means unlimited. Use 2-3 for large monorepos.
69    pub max_depth: Option<u32>,
70
71    /// Maximum AST node depth for tree-sitter queries. Internal tuning parameter; leave unset in normal use. Increase only if query results are missing constructs in deeply nested or generated code.
72    pub ast_recursion_limit: Option<usize>,
73
74    #[serde(flatten)]
75    pub pagination: PaginationParams,
76
77    #[serde(flatten)]
78    pub output_control: OutputControlParams,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82pub struct AnalysisResult {
83    pub path: String,
84    pub mode: AnalysisMode,
85    pub import_count: usize,
86    pub main_line: Option<usize>,
87    pub files: Vec<FileInfo>,
88    pub functions: Vec<FunctionInfo>,
89    pub classes: Vec<ClassInfo>,
90    pub references: Vec<ReferenceInfo>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
94pub struct FileInfo {
95    pub path: String,
96    pub language: String,
97    pub line_count: usize,
98    pub function_count: usize,
99    pub class_count: usize,
100    /// Whether this file is a test file.
101    pub is_test: bool,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
105pub struct FunctionInfo {
106    pub name: String,
107    pub line: usize,
108    pub end_line: usize,
109    /// Parameter list as string representations (e.g., ["x: i32", "y: String"]).
110    pub parameters: Vec<String>,
111    pub return_type: Option<String>,
112}
113
114impl FunctionInfo {
115    /// Maximum length for parameter display before truncation.
116    const MAX_PARAMS_DISPLAY_LEN: usize = 80;
117    /// Truncation point when parameters exceed MAX_PARAMS_DISPLAY_LEN.
118    const TRUNCATION_POINT: usize = 77;
119
120    /// Format function signature as a single-line string with truncation.
121    /// Returns: `name(param1, param2, ...) -> return_type :start-end`
122    /// Parameters are truncated to ~80 chars with '...' if needed.
123    pub fn compact_signature(&self) -> String {
124        let mut sig = String::with_capacity(self.name.len() + 40);
125        sig.push_str(&self.name);
126        sig.push('(');
127
128        if !self.parameters.is_empty() {
129            let params_str = self.parameters.join(", ");
130            if params_str.len() > Self::MAX_PARAMS_DISPLAY_LEN {
131                // Truncate at a safe char boundary to avoid panicking on multibyte UTF-8.
132                let truncate_at = params_str.floor_char_boundary(Self::TRUNCATION_POINT);
133                sig.push_str(&params_str[..truncate_at]);
134                sig.push_str("...");
135            } else {
136                sig.push_str(&params_str);
137            }
138        }
139
140        sig.push(')');
141
142        if let Some(ret_type) = &self.return_type {
143            sig.push_str(" -> ");
144            sig.push_str(ret_type);
145        }
146
147        write!(sig, " :{}-{}", self.line, self.end_line).ok();
148        sig
149    }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
153pub struct ClassInfo {
154    pub name: String,
155    pub line: usize,
156    pub end_line: usize,
157    pub methods: Vec<FunctionInfo>,
158    pub fields: Vec<String>,
159    /// Inherited types (parent classes, interfaces, trait bounds).
160    #[schemars(description = "Inherited types (parent classes, interfaces, trait bounds)")]
161    pub inherits: Vec<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
165pub struct CallInfo {
166    pub caller: String,
167    pub callee: String,
168    pub line: usize,
169    pub column: usize,
170    /// Number of arguments passed at the call site.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub arg_count: Option<usize>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
176pub struct AssignmentInfo {
177    /// Variable name being assigned
178    pub variable: String,
179    /// Value expression being assigned
180    pub value: String,
181    /// Line number where assignment occurs
182    pub line: usize,
183    /// Enclosing function scope or 'global'
184    pub scope: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
188pub struct FieldAccessInfo {
189    /// Object expression being accessed
190    pub object: String,
191    /// Field name being accessed
192    pub field: String,
193    /// Line number where field access occurs
194    pub line: usize,
195    /// Enclosing function scope or 'global'
196    pub scope: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
200pub struct ReferenceInfo {
201    pub symbol: String,
202    pub reference_type: ReferenceType,
203    pub location: String,
204    pub line: usize,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208#[serde(rename_all = "lowercase")]
209pub enum ReferenceType {
210    Definition,
211    Usage,
212    Import,
213    Export,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
217#[serde(rename_all = "lowercase")]
218pub enum EntryType {
219    File,
220    Directory,
221    Function,
222    Class,
223    Variable,
224}
225
226/// Analysis mode for generating output.
227#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
228#[serde(rename_all = "snake_case")]
229pub enum AnalysisMode {
230    /// High-level directory structure and file counts.
231    Overview,
232    /// Detailed semantic analysis of functions, classes, and references within a file.
233    FileDetails,
234    /// Call graph and dataflow analysis focused on a specific symbol.
235    SymbolFocus,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
239pub struct CallChain {
240    pub chain: Vec<CallInfo>,
241    pub depth: u32,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
245pub struct FocusedAnalysisData {
246    pub symbol: String,
247    pub definition: Option<FunctionInfo>,
248    pub call_chains: Vec<CallChain>,
249    pub references: Vec<ReferenceInfo>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
253pub struct ElementQueryResult {
254    pub query: String,
255    pub results: Vec<String>,
256    pub count: usize,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
260pub struct ImportInfo {
261    /// Full module path excluding the imported symbol (e.g., 'std::collections' for 'use std::collections::HashMap').
262    pub module: String,
263    /// Imported symbols (e.g., ['HashMap'] for 'use std::collections::HashMap').
264    pub items: Vec<String>,
265    /// Line number where import appears.
266    pub line: usize,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
270pub struct SemanticAnalysis {
271    pub functions: Vec<FunctionInfo>,
272    pub classes: Vec<ClassInfo>,
273    /// Flat list of imports; each entry carries its full module path and imported symbols.
274    pub imports: Vec<ImportInfo>,
275    pub references: Vec<ReferenceInfo>,
276    /// Call frequency map (function name -> count).
277    pub call_frequency: HashMap<String, usize>,
278    /// Caller-callee pairs extracted from call expressions.
279    pub calls: Vec<CallInfo>,
280    /// Variable assignments and reassignments.
281    #[serde(default, skip_serializing_if = "Vec::is_empty")]
282    pub assignments: Vec<AssignmentInfo>,
283    /// Field access patterns.
284    #[serde(default, skip_serializing_if = "Vec::is_empty")]
285    pub field_accesses: Vec<FieldAccessInfo>,
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_compact_signature_short_params() {
294        let func = FunctionInfo {
295            name: "add".to_string(),
296            line: 10,
297            end_line: 12,
298            parameters: vec!["a: i32".to_string(), "b: i32".to_string()],
299            return_type: Some("i32".to_string()),
300        };
301
302        let sig = func.compact_signature();
303        assert_eq!(sig, "add(a: i32, b: i32) -> i32 :10-12");
304    }
305
306    #[test]
307    fn test_compact_signature_long_params_truncation() {
308        let func = FunctionInfo {
309            name: "process".to_string(),
310            line: 20,
311            end_line: 50,
312            parameters: vec![
313                "config: ComplexConfigType".to_string(),
314                "data: VeryLongDataStructureNameThatExceedsEightyCharacters".to_string(),
315                "callback: Fn(Result) -> ()".to_string(),
316            ],
317            return_type: Some("Result<Output>".to_string()),
318        };
319
320        let sig = func.compact_signature();
321        assert!(sig.contains("process("));
322        assert!(sig.contains("..."));
323        assert!(sig.contains("-> Result<Output>"));
324        assert!(sig.contains(":20-50"));
325    }
326
327    #[test]
328    fn test_compact_signature_empty_params() {
329        let func = FunctionInfo {
330            name: "main".to_string(),
331            line: 1,
332            end_line: 5,
333            parameters: vec![],
334            return_type: None,
335        };
336
337        let sig = func.compact_signature();
338        assert_eq!(sig, "main() :1-5");
339    }
340
341    #[test]
342    fn schema_flatten_inline() {
343        use schemars::schema_for;
344
345        // Test AnalyzeDirectoryParams: cursor, page_size, force, summary must be top-level
346        let dir_schema = schema_for!(AnalyzeDirectoryParams);
347        let dir_props = dir_schema
348            .as_object()
349            .and_then(|o| o.get("properties"))
350            .and_then(|v| v.as_object())
351            .expect("AnalyzeDirectoryParams must have properties");
352
353        assert!(
354            dir_props.contains_key("cursor"),
355            "cursor must be top-level in AnalyzeDirectoryParams schema"
356        );
357        assert!(
358            dir_props.contains_key("page_size"),
359            "page_size must be top-level in AnalyzeDirectoryParams schema"
360        );
361        assert!(
362            dir_props.contains_key("force"),
363            "force must be top-level in AnalyzeDirectoryParams schema"
364        );
365        assert!(
366            dir_props.contains_key("summary"),
367            "summary must be top-level in AnalyzeDirectoryParams schema"
368        );
369
370        // Test AnalyzeFileParams
371        let file_schema = schema_for!(AnalyzeFileParams);
372        let file_props = file_schema
373            .as_object()
374            .and_then(|o| o.get("properties"))
375            .and_then(|v| v.as_object())
376            .expect("AnalyzeFileParams must have properties");
377
378        assert!(
379            file_props.contains_key("cursor"),
380            "cursor must be top-level in AnalyzeFileParams schema"
381        );
382        assert!(
383            file_props.contains_key("page_size"),
384            "page_size must be top-level in AnalyzeFileParams schema"
385        );
386        assert!(
387            file_props.contains_key("force"),
388            "force must be top-level in AnalyzeFileParams schema"
389        );
390        assert!(
391            file_props.contains_key("summary"),
392            "summary must be top-level in AnalyzeFileParams schema"
393        );
394
395        // Test AnalyzeSymbolParams
396        let symbol_schema = schema_for!(AnalyzeSymbolParams);
397        let symbol_props = symbol_schema
398            .as_object()
399            .and_then(|o| o.get("properties"))
400            .and_then(|v| v.as_object())
401            .expect("AnalyzeSymbolParams must have properties");
402
403        assert!(
404            symbol_props.contains_key("cursor"),
405            "cursor must be top-level in AnalyzeSymbolParams schema"
406        );
407        assert!(
408            symbol_props.contains_key("page_size"),
409            "page_size must be top-level in AnalyzeSymbolParams schema"
410        );
411        assert!(
412            symbol_props.contains_key("force"),
413            "force must be top-level in AnalyzeSymbolParams schema"
414        );
415        assert!(
416            symbol_props.contains_key("summary"),
417            "summary must be top-level in AnalyzeSymbolParams schema"
418        );
419    }
420}