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);
130 });
131
132 file_events.iter().for_each(|event| {
134 self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
135 });
136
137 commands.extend(file_commands);
138 self.discovered_events.extend(file_events);
139
140 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
142 }
143 }
144
145 if verbose {
146 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
147 }
148
149 self.resolve_types_lazily(&type_names_to_discover)?;
151
152 if verbose {
153 println!(
154 "🏗️ Discovered {} structs total",
155 self.discovered_structs.len()
156 );
157 for (name, info) in &self.discovered_structs {
158 println!(" - {}: {} fields", name, info.fields.len());
159 }
160 println!(
161 "📡 Discovered {} events total",
162 self.discovered_events.len()
163 );
164 for event in &self.discovered_events {
165 println!(" - '{}': {}", event.event_name, event.payload_type);
166 }
167 let all_channels = self.get_all_discovered_channels(&commands);
168 println!("📞 Discovered {} channels total", all_channels.len());
169 for channel in &all_channels {
170 println!(
171 " - '{}' in {}: {}",
172 channel.parameter_name, channel.command_name, channel.message_type
173 );
174 }
175 }
176
177 Ok(commands)
178 }
179
180 pub fn analyze_file(
182 &mut self,
183 file_path: &std::path::Path,
184 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
185 let path_buf = file_path.to_path_buf();
186
187 match self.ast_cache.parse_and_cache_file(&path_buf) {
189 Ok(_) => {
190 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
192 let file_events = self.event_parser.extract_events_from_ast(
194 &parsed_file.ast,
195 path_buf.as_path(),
196 &mut self.type_resolver,
197 )?;
198 self.discovered_events.extend(file_events);
199
200 let mut commands = self.command_parser.extract_commands_from_ast(
202 &parsed_file.ast,
203 path_buf.as_path(),
204 &mut self.type_resolver,
205 )?;
206
207 for command in &mut commands {
209 if let Some(func) =
210 self.find_function_in_ast(&parsed_file.ast, &command.name)
211 {
212 let channels = self.channel_parser.extract_channels_from_command(
213 func,
214 &command.name,
215 path_buf.as_path(),
216 &mut self.type_resolver,
217 )?;
218 command.channels = channels;
219 }
220 }
221
222 Ok(commands)
223 } else {
224 Ok(vec![])
225 }
226 }
227 Err(_) => {
228 Ok(vec![])
230 }
231 }
232 }
233
234 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
236 for item in &ast.items {
237 match item {
238 syn::Item::Struct(item_struct) => {
239 if self.struct_parser.should_include_struct(item_struct) {
240 let struct_name = item_struct.ident.to_string();
241 self.dependency_graph
242 .add_type_definition(struct_name, file_path.to_path_buf());
243 }
244 }
245 syn::Item::Enum(item_enum) => {
246 if self.struct_parser.should_include_enum(item_enum) {
247 let enum_name = item_enum.ident.to_string();
248 self.dependency_graph
249 .add_type_definition(enum_name, file_path.to_path_buf());
250 }
251 }
252 _ => {}
253 }
254 }
255 }
256
257 fn resolve_types_lazily(
259 &mut self,
260 initial_types: &HashSet<String>,
261 ) -> Result<(), Box<dyn std::error::Error>> {
262 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
263 let mut resolved_types = HashSet::new();
264
265 while let Some(type_name) = types_to_resolve.pop() {
266 if resolved_types.contains(&type_name)
268 || self.discovered_structs.contains_key(&type_name)
269 {
270 continue;
271 }
272
273 if let Some(file_path) = self
275 .dependency_graph
276 .get_type_definition_path(&type_name)
277 .cloned()
278 {
279 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
280 if let Some(struct_info) = self.extract_type_from_ast(
282 &parsed_file.ast,
283 &type_name,
284 file_path.as_path(),
285 ) {
286 let mut type_dependencies = HashSet::new();
288 for field in &struct_info.fields {
289 self.extract_type_names(&field.rust_type, &mut type_dependencies);
290 }
291
292 for dep_type in &type_dependencies {
294 if !resolved_types.contains(dep_type)
295 && !self.discovered_structs.contains_key(dep_type)
296 && self.dependency_graph.has_type_definition(dep_type)
297 {
298 types_to_resolve.push(dep_type.clone());
299 }
300 }
301
302 self.dependency_graph
304 .add_dependencies(type_name.clone(), type_dependencies.clone());
305 self.dependency_graph
306 .add_resolved_type(type_name.clone(), struct_info.clone());
307 self.discovered_structs
308 .insert(type_name.clone(), struct_info);
309 resolved_types.insert(type_name);
310 }
311 }
312 }
313 }
314
315 Ok(())
316 }
317
318 fn extract_type_from_ast(
320 &mut self,
321 ast: &syn::File,
322 type_name: &str,
323 file_path: &Path,
324 ) -> Option<StructInfo> {
325 for item in &ast.items {
326 match item {
327 syn::Item::Struct(item_struct) => {
328 if item_struct.ident == type_name
329 && self.struct_parser.should_include_struct(item_struct)
330 {
331 return self.struct_parser.parse_struct(
332 item_struct,
333 file_path,
334 &mut self.type_resolver,
335 );
336 }
337 }
338 syn::Item::Enum(item_enum) => {
339 if item_enum.ident == type_name
340 && self.struct_parser.should_include_enum(item_enum)
341 {
342 return self.struct_parser.parse_enum(
343 item_enum,
344 file_path,
345 &mut self.type_resolver,
346 );
347 }
348 }
349 _ => {}
350 }
351 }
352 None
353 }
354
355 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
357 self.extract_type_names_recursive(rust_type, type_names);
358 }
359
360 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
362 let rust_type = rust_type.trim();
363
364 if rust_type.starts_with("Result<") {
366 if let Some(inner) = rust_type
367 .strip_prefix("Result<")
368 .and_then(|s| s.strip_suffix(">"))
369 {
370 if let Some(comma_pos) = inner.find(',') {
371 let ok_type = inner[..comma_pos].trim();
372 let err_type = inner[comma_pos + 1..].trim();
373 self.extract_type_names_recursive(ok_type, type_names);
374 self.extract_type_names_recursive(err_type, type_names);
375 }
376 }
377 return;
378 }
379
380 if rust_type.starts_with("Option<") {
382 if let Some(inner) = rust_type
383 .strip_prefix("Option<")
384 .and_then(|s| s.strip_suffix(">"))
385 {
386 self.extract_type_names_recursive(inner, type_names);
387 }
388 return;
389 }
390
391 if rust_type.starts_with("Vec<") {
393 if let Some(inner) = rust_type
394 .strip_prefix("Vec<")
395 .and_then(|s| s.strip_suffix(">"))
396 {
397 self.extract_type_names_recursive(inner, type_names);
398 }
399 return;
400 }
401
402 if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
404 let prefix = if rust_type.starts_with("HashMap<") {
405 "HashMap<"
406 } else {
407 "BTreeMap<"
408 };
409 if let Some(inner) = rust_type
410 .strip_prefix(prefix)
411 .and_then(|s| s.strip_suffix(">"))
412 {
413 if let Some(comma_pos) = inner.find(',') {
414 let key_type = inner[..comma_pos].trim();
415 let value_type = inner[comma_pos + 1..].trim();
416 self.extract_type_names_recursive(key_type, type_names);
417 self.extract_type_names_recursive(value_type, type_names);
418 }
419 }
420 return;
421 }
422
423 if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
425 let prefix = if rust_type.starts_with("HashSet<") {
426 "HashSet<"
427 } else {
428 "BTreeSet<"
429 };
430 if let Some(inner) = rust_type
431 .strip_prefix(prefix)
432 .and_then(|s| s.strip_suffix(">"))
433 {
434 self.extract_type_names_recursive(inner, type_names);
435 }
436 return;
437 }
438
439 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
441 let inner = &rust_type[1..rust_type.len() - 1];
442 for part in inner.split(',') {
443 self.extract_type_names_recursive(part.trim(), type_names);
444 }
445 return;
446 }
447
448 if rust_type.starts_with('&') {
450 let without_ref = rust_type.trim_start_matches('&');
451 self.extract_type_names_recursive(without_ref, type_names);
452 return;
453 }
454
455 if !rust_type.is_empty()
457 && !self.type_resolver.get_type_mappings().contains_key(rust_type)
458 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
460 && !rust_type.contains('<')
461 {
463 type_names.insert(rust_type.to_string());
464 }
465 }
466
467 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
469 &self.discovered_structs
470 }
471
472 pub fn get_discovered_events(&self) -> &[EventInfo] {
474 &self.discovered_events
475 }
476
477 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
479 commands
480 .iter()
481 .flat_map(|cmd| cmd.channels.clone())
482 .collect()
483 }
484
485 fn find_function_in_ast<'a>(
487 &self,
488 ast: &'a syn::File,
489 function_name: &str,
490 ) -> Option<&'a syn::ItemFn> {
491 for item in &ast.items {
492 if let syn::Item::Fn(func) = item {
493 if func.sig.ident == function_name {
494 return Some(func);
495 }
496 }
497 }
498 None
499 }
500
501 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
503 &self.dependency_graph
504 }
505
506 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
508 self.dependency_graph.topological_sort_types(types)
509 }
510
511 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
513 self.dependency_graph.visualize_dependencies(commands)
514 }
515
516 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
518 self.dependency_graph.generate_dot_graph(commands)
519 }
520
521 pub fn map_rust_type_to_typescript(&mut self, rust_type: &str) -> String {
523 self.type_resolver.map_rust_type_to_typescript(rust_type)
524 }
525}
526
527impl Default for CommandAnalyzer {
528 fn default() -> Self {
529 Self::new()
530 }
531}