1pub mod ast_cache;
2pub mod channel_parser;
3pub mod command_parser;
4pub mod dependency_graph;
5pub mod event_parser;
6pub mod serde_parser;
7pub mod struct_parser;
8pub mod type_resolver;
9pub mod validator_parser;
10
11use crate::models::{ChannelInfo, CommandInfo, EventInfo, StructInfo};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14
15use ast_cache::AstCache;
16use channel_parser::ChannelParser;
17use command_parser::CommandParser;
18use dependency_graph::TypeDependencyGraph;
19use event_parser::EventParser;
20use struct_parser::StructParser;
21use type_resolver::TypeResolver;
22
23pub struct CommandAnalyzer {
25 ast_cache: AstCache,
27 command_parser: CommandParser,
29 channel_parser: ChannelParser,
31 event_parser: EventParser,
33 struct_parser: StructParser,
35 type_resolver: TypeResolver,
37 dependency_graph: TypeDependencyGraph,
39 discovered_structs: HashMap<String, StructInfo>,
41 discovered_events: Vec<EventInfo>,
43}
44
45impl CommandAnalyzer {
46 pub fn new() -> Self {
47 Self {
48 ast_cache: AstCache::new(),
49 command_parser: CommandParser::new(),
50 channel_parser: ChannelParser::new(),
51 event_parser: EventParser::new(),
52 struct_parser: StructParser::new(),
53 type_resolver: TypeResolver::new(),
54 dependency_graph: TypeDependencyGraph::new(),
55 discovered_structs: HashMap::new(),
56 discovered_events: Vec::new(),
57 }
58 }
59
60 pub fn analyze_project(
62 &mut self,
63 project_path: &str,
64 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
65 self.analyze_project_with_verbose(project_path, false)
66 }
67
68 pub fn analyze_project_with_verbose(
70 &mut self,
71 project_path: &str,
72 verbose: bool,
73 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
74 self.ast_cache
76 .parse_and_cache_all_files(project_path, verbose)?;
77
78 let file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
80 let mut commands = Vec::new();
81 let mut type_names_to_discover = HashSet::new();
82
83 for file_path in file_paths {
85 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
86 if verbose {
87 println!("🔍 Analyzing file: {}", parsed_file.path.display());
88 }
89
90 let mut file_commands = self.command_parser.extract_commands_from_ast(
92 &parsed_file.ast,
93 parsed_file.path.as_path(),
94 &mut self.type_resolver,
95 )?;
96
97 for command in &mut file_commands {
99 if let Some(func) = self.find_function_in_ast(&parsed_file.ast, &command.name) {
100 let channels = self.channel_parser.extract_channels_from_command(
101 func,
102 &command.name,
103 parsed_file.path.as_path(),
104 &mut self.type_resolver,
105 )?;
106
107 channels.iter().for_each(|ch| {
109 self.extract_type_names(&ch.message_type, &mut type_names_to_discover);
110 });
111
112 command.channels = channels;
113 }
114 }
115
116 let file_events = self.event_parser.extract_events_from_ast(
118 &parsed_file.ast,
119 parsed_file.path.as_path(),
120 &mut self.type_resolver,
121 )?;
122
123 file_commands.iter().for_each(|cmd| {
125 cmd.parameters.iter().for_each(|param| {
126 self.extract_type_names(¶m.rust_type, &mut type_names_to_discover);
127 });
128 self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
129 });
130
131 file_events.iter().for_each(|event| {
133 self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
134 });
135
136 commands.extend(file_commands);
137 self.discovered_events.extend(file_events);
138
139 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
141 }
142 }
143
144 if verbose {
145 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
146 }
147
148 self.resolve_types_lazily(&type_names_to_discover)?;
150
151 if verbose {
152 println!(
153 "🏗️ Discovered {} structs total",
154 self.discovered_structs.len()
155 );
156 for (name, info) in &self.discovered_structs {
157 println!(" - {}: {} fields", name, info.fields.len());
158 }
159 println!(
160 "📡 Discovered {} events total",
161 self.discovered_events.len()
162 );
163 for event in &self.discovered_events {
164 println!(" - '{}': {}", event.event_name, event.payload_type);
165 }
166 let all_channels = self.get_all_discovered_channels(&commands);
167 println!("📞 Discovered {} channels total", all_channels.len());
168 for channel in &all_channels {
169 println!(
170 " - '{}' in {}: {}",
171 channel.parameter_name, channel.command_name, channel.message_type
172 );
173 }
174 }
175
176 Ok(commands)
177 }
178
179 pub fn analyze_file(
181 &mut self,
182 file_path: &std::path::Path,
183 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
184 let path_buf = file_path.to_path_buf();
185
186 match self.ast_cache.parse_and_cache_file(&path_buf) {
188 Ok(_) => {
189 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
191 let file_events = self.event_parser.extract_events_from_ast(
193 &parsed_file.ast,
194 path_buf.as_path(),
195 &mut self.type_resolver,
196 )?;
197 self.discovered_events.extend(file_events);
198
199 let mut commands = self.command_parser.extract_commands_from_ast(
201 &parsed_file.ast,
202 path_buf.as_path(),
203 &mut self.type_resolver,
204 )?;
205
206 for command in &mut commands {
208 if let Some(func) =
209 self.find_function_in_ast(&parsed_file.ast, &command.name)
210 {
211 let channels = self.channel_parser.extract_channels_from_command(
212 func,
213 &command.name,
214 path_buf.as_path(),
215 &mut self.type_resolver,
216 )?;
217 command.channels = channels;
218 }
219 }
220
221 Ok(commands)
222 } else {
223 Ok(vec![])
224 }
225 }
226 Err(_) => {
227 Ok(vec![])
229 }
230 }
231 }
232
233 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
235 for item in &ast.items {
236 match item {
237 syn::Item::Struct(item_struct) => {
238 if self.struct_parser.should_include_struct(item_struct) {
239 let struct_name = item_struct.ident.to_string();
240 self.dependency_graph
241 .add_type_definition(struct_name, file_path.to_path_buf());
242 }
243 }
244 syn::Item::Enum(item_enum) => {
245 if self.struct_parser.should_include_enum(item_enum) {
246 let enum_name = item_enum.ident.to_string();
247 self.dependency_graph
248 .add_type_definition(enum_name, file_path.to_path_buf());
249 }
250 }
251 _ => {}
252 }
253 }
254 }
255
256 fn resolve_types_lazily(
258 &mut self,
259 initial_types: &HashSet<String>,
260 ) -> Result<(), Box<dyn std::error::Error>> {
261 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
262 let mut resolved_types = HashSet::new();
263
264 while let Some(type_name) = types_to_resolve.pop() {
265 if resolved_types.contains(&type_name)
267 || self.discovered_structs.contains_key(&type_name)
268 {
269 continue;
270 }
271
272 if let Some(file_path) = self
274 .dependency_graph
275 .get_type_definition_path(&type_name)
276 .cloned()
277 {
278 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
279 if let Some(struct_info) = self.extract_type_from_ast(
281 &parsed_file.ast,
282 &type_name,
283 file_path.as_path(),
284 ) {
285 let mut type_dependencies = HashSet::new();
287 for field in &struct_info.fields {
288 self.extract_type_names(&field.rust_type, &mut type_dependencies);
289 }
290
291 for dep_type in &type_dependencies {
293 if !resolved_types.contains(dep_type)
294 && !self.discovered_structs.contains_key(dep_type)
295 && self.dependency_graph.has_type_definition(dep_type)
296 {
297 types_to_resolve.push(dep_type.clone());
298 }
299 }
300
301 self.dependency_graph
303 .add_dependencies(type_name.clone(), type_dependencies.clone());
304 self.dependency_graph
305 .add_resolved_type(type_name.clone(), struct_info.clone());
306 self.discovered_structs
307 .insert(type_name.clone(), struct_info);
308 resolved_types.insert(type_name);
309 }
310 }
311 }
312 }
313
314 Ok(())
315 }
316
317 fn extract_type_from_ast(
319 &mut self,
320 ast: &syn::File,
321 type_name: &str,
322 file_path: &Path,
323 ) -> Option<StructInfo> {
324 for item in &ast.items {
325 match item {
326 syn::Item::Struct(item_struct) => {
327 if item_struct.ident == type_name
328 && self.struct_parser.should_include_struct(item_struct)
329 {
330 return self.struct_parser.parse_struct(
331 item_struct,
332 file_path,
333 &mut self.type_resolver,
334 );
335 }
336 }
337 syn::Item::Enum(item_enum) => {
338 if item_enum.ident == type_name
339 && self.struct_parser.should_include_enum(item_enum)
340 {
341 return self.struct_parser.parse_enum(
342 item_enum,
343 file_path,
344 &mut self.type_resolver,
345 );
346 }
347 }
348 _ => {}
349 }
350 }
351 None
352 }
353
354 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
356 self.extract_type_names_recursive(rust_type, type_names);
357 }
358
359 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
361 let rust_type = rust_type.trim();
362
363 if rust_type.starts_with("Result<") {
365 if let Some(inner) = rust_type
366 .strip_prefix("Result<")
367 .and_then(|s| s.strip_suffix(">"))
368 {
369 if let Some(comma_pos) = inner.find(',') {
370 let ok_type = inner[..comma_pos].trim();
371 let err_type = inner[comma_pos + 1..].trim();
372 self.extract_type_names_recursive(ok_type, type_names);
373 self.extract_type_names_recursive(err_type, type_names);
374 }
375 }
376 return;
377 }
378
379 if rust_type.starts_with("Option<") {
381 if let Some(inner) = rust_type
382 .strip_prefix("Option<")
383 .and_then(|s| s.strip_suffix(">"))
384 {
385 self.extract_type_names_recursive(inner, type_names);
386 }
387 return;
388 }
389
390 if rust_type.starts_with("Vec<") {
392 if let Some(inner) = rust_type
393 .strip_prefix("Vec<")
394 .and_then(|s| s.strip_suffix(">"))
395 {
396 self.extract_type_names_recursive(inner, type_names);
397 }
398 return;
399 }
400
401 if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
403 let prefix = if rust_type.starts_with("HashMap<") {
404 "HashMap<"
405 } else {
406 "BTreeMap<"
407 };
408 if let Some(inner) = rust_type
409 .strip_prefix(prefix)
410 .and_then(|s| s.strip_suffix(">"))
411 {
412 if let Some(comma_pos) = inner.find(',') {
413 let key_type = inner[..comma_pos].trim();
414 let value_type = inner[comma_pos + 1..].trim();
415 self.extract_type_names_recursive(key_type, type_names);
416 self.extract_type_names_recursive(value_type, type_names);
417 }
418 }
419 return;
420 }
421
422 if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
424 let prefix = if rust_type.starts_with("HashSet<") {
425 "HashSet<"
426 } else {
427 "BTreeSet<"
428 };
429 if let Some(inner) = rust_type
430 .strip_prefix(prefix)
431 .and_then(|s| s.strip_suffix(">"))
432 {
433 self.extract_type_names_recursive(inner, type_names);
434 }
435 return;
436 }
437
438 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
440 let inner = &rust_type[1..rust_type.len() - 1];
441 for part in inner.split(',') {
442 self.extract_type_names_recursive(part.trim(), type_names);
443 }
444 return;
445 }
446
447 if rust_type.starts_with('&') {
449 let without_ref = rust_type.trim_start_matches('&');
450 self.extract_type_names_recursive(without_ref, type_names);
451 return;
452 }
453
454 if !rust_type.is_empty()
456 && !self.type_resolver.get_type_mappings().contains_key(rust_type)
457 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
459 && !rust_type.contains('<')
460 {
462 type_names.insert(rust_type.to_string());
463 }
464 }
465
466 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
468 &self.discovered_structs
469 }
470
471 pub fn get_discovered_events(&self) -> &[EventInfo] {
473 &self.discovered_events
474 }
475
476 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
478 commands
479 .iter()
480 .flat_map(|cmd| cmd.channels.clone())
481 .collect()
482 }
483
484 fn find_function_in_ast<'a>(
486 &self,
487 ast: &'a syn::File,
488 function_name: &str,
489 ) -> Option<&'a syn::ItemFn> {
490 for item in &ast.items {
491 if let syn::Item::Fn(func) = item {
492 if func.sig.ident == function_name {
493 return Some(func);
494 }
495 }
496 }
497 None
498 }
499
500 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
502 &self.dependency_graph
503 }
504
505 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
507 self.dependency_graph.topological_sort_types(types)
508 }
509
510 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
512 self.dependency_graph.visualize_dependencies(commands)
513 }
514
515 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
517 self.dependency_graph.generate_dot_graph(commands)
518 }
519
520 pub fn map_rust_type_to_typescript(&mut self, rust_type: &str) -> String {
522 self.type_resolver.map_rust_type_to_typescript(rust_type)
523 }
524}
525
526impl Default for CommandAnalyzer {
527 fn default() -> Self {
528 Self::new()
529 }
530}