1pub mod ast_cache;
2pub mod command_parser;
3pub mod dependency_graph;
4pub mod struct_parser;
5pub mod type_resolver;
6pub mod validator_parser;
7
8use crate::models::{CommandInfo, StructInfo};
9use std::collections::{HashMap, HashSet};
10use std::path::{Path, PathBuf};
11
12use ast_cache::AstCache;
13use command_parser::CommandParser;
14use dependency_graph::TypeDependencyGraph;
15use struct_parser::StructParser;
16use type_resolver::TypeResolver;
17
18pub struct CommandAnalyzer {
20 ast_cache: AstCache,
22 command_parser: CommandParser,
24 struct_parser: StructParser,
26 type_resolver: TypeResolver,
28 dependency_graph: TypeDependencyGraph,
30 discovered_structs: HashMap<String, StructInfo>,
32}
33
34impl CommandAnalyzer {
35 pub fn new() -> Self {
36 Self {
37 ast_cache: AstCache::new(),
38 command_parser: CommandParser::new(),
39 struct_parser: StructParser::new(),
40 type_resolver: TypeResolver::new(),
41 dependency_graph: TypeDependencyGraph::new(),
42 discovered_structs: HashMap::new(),
43 }
44 }
45
46 pub fn analyze_project(
48 &mut self,
49 project_path: &str,
50 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
51 self.analyze_project_with_verbose(project_path, false)
52 }
53
54 pub fn analyze_project_with_verbose(
56 &mut self,
57 project_path: &str,
58 verbose: bool,
59 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
60 self.ast_cache
62 .parse_and_cache_all_files(project_path, verbose)?;
63
64 let file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
66 let mut commands = Vec::new();
67 let mut type_names_to_discover = HashSet::new();
68
69 for file_path in file_paths {
71 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
72 if verbose {
73 println!("🔍 Analyzing file: {}", parsed_file.path.display());
74 }
75
76 let file_commands = self.command_parser.extract_commands_from_ast(
78 &parsed_file.ast,
79 parsed_file.path.as_path(),
80 &mut self.type_resolver,
81 )?;
82
83 file_commands.iter().for_each(|cmd| {
85 cmd.parameters.iter().for_each(|param| {
86 self.extract_type_names(¶m.rust_type, &mut type_names_to_discover);
87 });
88 self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
89 });
90
91 commands.extend(file_commands);
92
93 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
95 }
96 }
97
98 if verbose {
99 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
100 }
101
102 self.resolve_types_lazily(&type_names_to_discover)?;
104
105 if verbose {
106 println!(
107 "🏗️ Discovered {} structs total",
108 self.discovered_structs.len()
109 );
110 for (name, info) in &self.discovered_structs {
111 println!(" - {}: {} fields", name, info.fields.len());
112 }
113 }
114
115 Ok(commands)
116 }
117
118 pub fn analyze_file(
120 &mut self,
121 file_path: &std::path::Path,
122 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
123 let path_buf = file_path.to_path_buf();
124
125 match self.ast_cache.parse_and_cache_file(&path_buf) {
127 Ok(_) => {
128 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
130 self.command_parser.extract_commands_from_ast(
131 &parsed_file.ast,
132 path_buf.as_path(),
133 &mut self.type_resolver,
134 )
135 } else {
136 Ok(vec![])
137 }
138 }
139 Err(_) => {
140 Ok(vec![])
142 }
143 }
144 }
145
146 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
148 for item in &ast.items {
149 match item {
150 syn::Item::Struct(item_struct) => {
151 if self.struct_parser.should_include_struct(item_struct) {
152 let struct_name = item_struct.ident.to_string();
153 self.dependency_graph
154 .add_type_definition(struct_name, file_path.to_path_buf());
155 }
156 }
157 syn::Item::Enum(item_enum) => {
158 if self.struct_parser.should_include_enum(item_enum) {
159 let enum_name = item_enum.ident.to_string();
160 self.dependency_graph
161 .add_type_definition(enum_name, file_path.to_path_buf());
162 }
163 }
164 _ => {}
165 }
166 }
167 }
168
169 fn resolve_types_lazily(
171 &mut self,
172 initial_types: &HashSet<String>,
173 ) -> Result<(), Box<dyn std::error::Error>> {
174 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
175 let mut resolved_types = HashSet::new();
176
177 while let Some(type_name) = types_to_resolve.pop() {
178 if resolved_types.contains(&type_name)
180 || self.discovered_structs.contains_key(&type_name)
181 {
182 continue;
183 }
184
185 if let Some(file_path) = self
187 .dependency_graph
188 .get_type_definition_path(&type_name)
189 .cloned()
190 {
191 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
192 if let Some(struct_info) = self.extract_type_from_ast(
194 &parsed_file.ast,
195 &type_name,
196 file_path.as_path(),
197 ) {
198 let mut type_dependencies = HashSet::new();
200 for field in &struct_info.fields {
201 self.extract_type_names(&field.rust_type, &mut type_dependencies);
202 }
203
204 for dep_type in &type_dependencies {
206 if !resolved_types.contains(dep_type)
207 && !self.discovered_structs.contains_key(dep_type)
208 && self.dependency_graph.has_type_definition(dep_type)
209 {
210 types_to_resolve.push(dep_type.clone());
211 }
212 }
213
214 self.dependency_graph
216 .add_dependencies(type_name.clone(), type_dependencies.clone());
217 self.dependency_graph
218 .add_resolved_type(type_name.clone(), struct_info.clone());
219 self.discovered_structs
220 .insert(type_name.clone(), struct_info);
221 resolved_types.insert(type_name);
222 }
223 }
224 }
225 }
226
227 Ok(())
228 }
229
230 fn extract_type_from_ast(
232 &mut self,
233 ast: &syn::File,
234 type_name: &str,
235 file_path: &Path,
236 ) -> Option<StructInfo> {
237 for item in &ast.items {
238 match item {
239 syn::Item::Struct(item_struct) => {
240 if item_struct.ident == type_name
241 && self.struct_parser.should_include_struct(item_struct)
242 {
243 return self.struct_parser.parse_struct(
244 item_struct,
245 file_path,
246 &mut self.type_resolver,
247 );
248 }
249 }
250 syn::Item::Enum(item_enum) => {
251 if item_enum.ident == type_name
252 && self.struct_parser.should_include_enum(item_enum)
253 {
254 return self.struct_parser.parse_enum(
255 item_enum,
256 file_path,
257 &mut self.type_resolver,
258 );
259 }
260 }
261 _ => {}
262 }
263 }
264 None
265 }
266
267 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
269 self.extract_type_names_recursive(rust_type, type_names);
270 }
271
272 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
274 let rust_type = rust_type.trim();
275
276 if rust_type.starts_with("Result<") {
278 if let Some(inner) = rust_type
279 .strip_prefix("Result<")
280 .and_then(|s| s.strip_suffix(">"))
281 {
282 if let Some(comma_pos) = inner.find(',') {
283 let ok_type = inner[..comma_pos].trim();
284 let err_type = inner[comma_pos + 1..].trim();
285 self.extract_type_names_recursive(ok_type, type_names);
286 self.extract_type_names_recursive(err_type, type_names);
287 }
288 }
289 return;
290 }
291
292 if rust_type.starts_with("Option<") {
294 if let Some(inner) = rust_type
295 .strip_prefix("Option<")
296 .and_then(|s| s.strip_suffix(">"))
297 {
298 self.extract_type_names_recursive(inner, type_names);
299 }
300 return;
301 }
302
303 if rust_type.starts_with("Vec<") {
305 if let Some(inner) = rust_type
306 .strip_prefix("Vec<")
307 .and_then(|s| s.strip_suffix(">"))
308 {
309 self.extract_type_names_recursive(inner, type_names);
310 }
311 return;
312 }
313
314 if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
316 let prefix = if rust_type.starts_with("HashMap<") {
317 "HashMap<"
318 } else {
319 "BTreeMap<"
320 };
321 if let Some(inner) = rust_type
322 .strip_prefix(prefix)
323 .and_then(|s| s.strip_suffix(">"))
324 {
325 if let Some(comma_pos) = inner.find(',') {
326 let key_type = inner[..comma_pos].trim();
327 let value_type = inner[comma_pos + 1..].trim();
328 self.extract_type_names_recursive(key_type, type_names);
329 self.extract_type_names_recursive(value_type, type_names);
330 }
331 }
332 return;
333 }
334
335 if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
337 let prefix = if rust_type.starts_with("HashSet<") {
338 "HashSet<"
339 } else {
340 "BTreeSet<"
341 };
342 if let Some(inner) = rust_type
343 .strip_prefix(prefix)
344 .and_then(|s| s.strip_suffix(">"))
345 {
346 self.extract_type_names_recursive(inner, type_names);
347 }
348 return;
349 }
350
351 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
353 let inner = &rust_type[1..rust_type.len() - 1];
354 for part in inner.split(',') {
355 self.extract_type_names_recursive(part.trim(), type_names);
356 }
357 return;
358 }
359
360 if rust_type.starts_with('&') {
362 let without_ref = rust_type.trim_start_matches('&');
363 self.extract_type_names_recursive(without_ref, type_names);
364 return;
365 }
366
367 if !rust_type.is_empty()
369 && !self.type_resolver.get_type_mappings().contains_key(rust_type)
370 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
372 && !rust_type.contains('<')
373 {
375 type_names.insert(rust_type.to_string());
376 }
377 }
378
379 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
381 &self.discovered_structs
382 }
383
384 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
386 &self.dependency_graph
387 }
388
389 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
391 self.dependency_graph.topological_sort_types(types)
392 }
393
394 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
396 self.dependency_graph.visualize_dependencies(commands)
397 }
398
399 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
401 self.dependency_graph.generate_dot_graph(commands)
402 }
403
404 pub fn map_rust_type_to_typescript(&mut self, rust_type: &str) -> String {
406 self.type_resolver.map_rust_type_to_typescript(rust_type)
407 }
408}
409
410impl Default for CommandAnalyzer {
411 fn default() -> Self {
412 Self::new()
413 }
414}