1use super::{Tool, ToolError, Result, OutputFormat, parse_output_format};
2use clap::{Arg, ArgMatches, Command};
3use std::path::Path;
4use std::fs;
5use colored::*;
6use syn::{parse_file, visit::Visit, ItemFn, ExprAwait, ExprCall, Type, spanned::Spanned};
7use quote::ToTokens;
8use serde::{Serialize, Deserialize};
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BlockingOperation {
11 pub function: String,
12 pub line: usize,
13 pub column: usize,
14 pub operation: String,
15 pub suggestion: String,
16}
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AwaitIssue {
19 pub function: String,
20 pub line: usize,
21 pub column: usize,
22 pub issue: String,
23 pub code_snippet: String,
24 pub suggestion: String,
25}
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct DeadlockRisk {
28 pub function: String,
29 pub line: usize,
30 pub column: usize,
31 pub risk_type: String,
32 pub description: String,
33 pub suggestion: String,
34}
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AsyncSuggestion {
37 pub category: String,
38 pub description: String,
39 pub impact: String,
40 pub suggestion: String,
41}
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ConcurrencyAnalysis {
44 pub total_async_functions: usize,
45 pub average_await_depth: f64,
46 pub concurrent_operations: usize,
47 pub potential_race_conditions: usize,
48 pub blocking_operations: usize,
49 pub nested_async_blocks: usize,
50}
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AsyncIssue {
53 pub file: String,
54 pub issues: Vec<AsyncIssueType>,
55}
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub enum AsyncIssueType {
58 BlockingOperation(BlockingOperation),
59 AwaitIssue(AwaitIssue),
60 DeadlockRisk(DeadlockRisk),
61}
62pub struct AsyncLintTool;
63impl AsyncLintTool {
64 pub fn new() -> Self {
65 Self
66 }
67 fn analyze_async_patterns(&self, file_path: &str) -> Result<Vec<AsyncIssue>> {
68 if !Path::new(file_path).exists() {
69 return Err(
70 ToolError::InvalidArguments(format!("File not found: {}", file_path)),
71 );
72 }
73 let content = fs::read_to_string(file_path)?;
74 let mut issues = Vec::new();
75 if file_path.ends_with(".rs") {
76 match parse_file(&content) {
77 Ok(ast) => {
78 let mut visitor = AsyncVisitor::new(file_path);
79 visitor.visit_file(&ast);
80 issues
81 .push(AsyncIssue {
82 file: file_path.to_string(),
83 issues: visitor.issues,
84 });
85 }
86 Err(e) => {
87 return Err(
88 ToolError::ExecutionFailed(
89 format!("Failed to parse Rust file: {}", e),
90 ),
91 );
92 }
93 }
94 } else {
95 return Err(
96 ToolError::InvalidArguments(
97 "Only Rust (.rs) files are supported".to_string(),
98 ),
99 );
100 }
101 Ok(issues)
102 }
103 fn detect_blocking_operations(&self, ast: &syn::File) -> Vec<BlockingOperation> {
104 let mut operations = Vec::new();
105 let mut visitor = BlockingOperationVisitor::new();
106 visitor.visit_file(ast);
107 operations.extend(visitor.operations);
108 operations
109 }
110 fn analyze_await_patterns(&self, functions: &[syn::ItemFn]) -> Vec<AwaitIssue> {
111 let mut issues = Vec::new();
112 for function in functions {
113 let mut visitor = AwaitPatternVisitor::new();
114 visitor.visit_item_fn(function);
115 issues
116 .extend(
117 visitor
118 .issues
119 .into_iter()
120 .map(|mut issue| {
121 issue.function = function.sig.ident.to_string();
122 issue
123 }),
124 );
125 }
126 issues
127 }
128 fn detect_deadlock_patterns(&self, ast: &syn::File) -> Vec<DeadlockRisk> {
129 let mut risks = Vec::new();
130 let mut visitor = DeadlockVisitor::new();
131 visitor.visit_file(ast);
132 risks.extend(visitor.risks);
133 risks
134 }
135 fn suggest_async_improvements(
136 &self,
137 all_issues: &[AsyncIssue],
138 ) -> Vec<AsyncSuggestion> {
139 let mut suggestions = Vec::new();
140 let mut total_blocking = 0;
141 let mut unnecessary_awaits = 0;
142 let mut nested_async = 0;
143 let mut select_issues = 0;
144 for issue_set in all_issues {
145 for issue in &issue_set.issues {
146 match issue {
147 AsyncIssueType::BlockingOperation(_) => total_blocking += 1,
148 AsyncIssueType::AwaitIssue(issue) => {
149 if issue.issue.contains("unnecessary") {
150 unnecessary_awaits += 1;
151 } else if issue.issue.contains("nested") {
152 nested_async += 1;
153 }
154 }
155 AsyncIssueType::DeadlockRisk(_) => select_issues += 1,
156 }
157 }
158 }
159 if total_blocking > 0 {
160 suggestions
161 .push(AsyncSuggestion {
162 category: "Blocking Operations".to_string(),
163 description: format!(
164 "Found {} blocking operations in async contexts", total_blocking
165 ),
166 impact: "High".to_string(),
167 suggestion: "Replace std::fs with tokio::fs, std::thread::sleep with tokio::time::sleep"
168 .to_string(),
169 });
170 }
171 if unnecessary_awaits > 0 {
172 suggestions
173 .push(AsyncSuggestion {
174 category: "Unnecessary Awaits".to_string(),
175 description: format!(
176 "Found {} unnecessary await expressions", unnecessary_awaits
177 ),
178 impact: "Low".to_string(),
179 suggestion: "Remove unnecessary async/await for immediate values"
180 .to_string(),
181 });
182 }
183 if nested_async > 0 {
184 suggestions
185 .push(AsyncSuggestion {
186 category: "Nested Async".to_string(),
187 description: format!("Found {} nested async blocks", nested_async),
188 impact: "Medium".to_string(),
189 suggestion: "Flatten nested async blocks for better readability"
190 .to_string(),
191 });
192 }
193 if select_issues > 0 {
194 suggestions
195 .push(AsyncSuggestion {
196 category: "Deadlock Prevention".to_string(),
197 description: format!(
198 "Found {} potential deadlock patterns", select_issues
199 ),
200 impact: "High".to_string(),
201 suggestion: "Use try_select! or restructure concurrent operations"
202 .to_string(),
203 });
204 }
205 suggestions
206 .push(AsyncSuggestion {
207 category: "Performance".to_string(),
208 description: "General async performance improvements".to_string(),
209 impact: "Medium".to_string(),
210 suggestion: "Use JoinSet for concurrent operations, implement proper error handling"
211 .to_string(),
212 });
213 suggestions
214 .push(AsyncSuggestion {
215 category: "Best Practices".to_string(),
216 description: "Async best practices".to_string(),
217 impact: "Low".to_string(),
218 suggestion: "Use async fn consistently, avoid mixing sync and async code"
219 .to_string(),
220 });
221 suggestions
222 }
223 fn analyze_concurrency_patterns(
224 &self,
225 file_path: &str,
226 ) -> Result<ConcurrencyAnalysis> {
227 let content = fs::read_to_string(file_path)?;
228 let async_fn_count = content.matches("async fn").count();
229 let await_count = content.matches("await").count();
230 let select_count = content.matches("select!").count()
231 + content.matches("join!").count();
232 let nested_async_count = content.matches("async {").count();
233 let average_await_depth = if async_fn_count > 0 {
234 await_count as f64 / async_fn_count as f64
235 } else {
236 0.0
237 };
238 let race_condition_patterns = [
239 "Arc<Mutex<",
240 "Arc<RwLock<",
241 "static mut",
242 "RefCell<",
243 ];
244 let mut race_condition_count = 0;
245 for pattern in &race_condition_patterns {
246 race_condition_count += content.matches(pattern).count();
247 }
248 Ok(ConcurrencyAnalysis {
249 total_async_functions: async_fn_count,
250 average_await_depth,
251 concurrent_operations: select_count,
252 potential_race_conditions: race_condition_count,
253 blocking_operations: content.matches("std::fs::").count()
254 + content.matches("std::thread::").count(),
255 nested_async_blocks: nested_async_count,
256 })
257 }
258}
259struct AsyncVisitor {
260 file_path: String,
261 issues: Vec<AsyncIssueType>,
262}
263impl AsyncVisitor {
264 fn new(file_path: &str) -> Self {
265 Self {
266 file_path: file_path.to_string(),
267 issues: Vec::new(),
268 }
269 }
270}
271impl<'ast> Visit<'ast> for AsyncVisitor {
272 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
273 let is_async = node.sig.asyncness.is_some();
274 if is_async {
275 let mut blocking_visitor = BlockingOperationVisitor::new();
276 blocking_visitor.visit_block(&node.block);
277 self.issues
278 .extend(
279 blocking_visitor
280 .operations
281 .into_iter()
282 .map(|mut op| {
283 op.function = node.sig.ident.to_string();
284 AsyncIssueType::BlockingOperation(op)
285 }),
286 );
287 let mut await_visitor = AwaitPatternVisitor::new();
288 await_visitor.visit_block(&node.block);
289 self.issues
290 .extend(
291 await_visitor
292 .issues
293 .into_iter()
294 .map(|mut issue| {
295 issue.function = node.sig.ident.to_string();
296 AsyncIssueType::AwaitIssue(issue)
297 }),
298 );
299 let mut deadlock_visitor = DeadlockVisitor::new();
300 deadlock_visitor.visit_block(&node.block);
301 self.issues
302 .extend(
303 deadlock_visitor
304 .risks
305 .into_iter()
306 .map(|mut risk| {
307 risk.function = node.sig.ident.to_string();
308 AsyncIssueType::DeadlockRisk(risk)
309 }),
310 );
311 }
312 syn::visit::visit_item_fn(self, node);
313 }
314}
315struct BlockingOperationVisitor {
316 operations: Vec<BlockingOperation>,
317}
318impl BlockingOperationVisitor {
319 fn new() -> Self {
320 Self { operations: Vec::new() }
321 }
322}
323impl<'ast> Visit<'ast> for BlockingOperationVisitor {
324 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
325 if let Some(func_name) = self.extract_function_name(node) {
326 let blocking_patterns = [
327 ("std::fs::read", "tokio::fs::read"),
328 ("std::fs::write", "tokio::fs::write"),
329 ("std::fs::File::open", "tokio::fs::File::open"),
330 ("std::thread::sleep", "tokio::time::sleep"),
331 ("std::thread::spawn", "tokio::spawn"),
332 ("reqwest::blocking", "reqwest::get"),
333 ];
334 for (blocking, async_version) in &blocking_patterns {
335 if func_name.contains(blocking) {
336 let line = 0;
337 let col = 0;
338 self.operations
339 .push(BlockingOperation {
340 function: String::new(),
341 line,
342 column: col,
343 operation: func_name.clone(),
344 suggestion: format!("Use {} instead", async_version),
345 });
346 }
347 }
348 }
349 syn::visit::visit_expr_call(self, node);
350 }
351}
352struct AwaitPatternVisitor {
353 issues: Vec<AwaitIssue>,
354}
355impl AwaitPatternVisitor {
356 fn new() -> Self {
357 Self { issues: Vec::new() }
358 }
359}
360impl<'ast> Visit<'ast> for AwaitPatternVisitor {
361 fn visit_expr_await(&mut self, node: &'ast syn::ExprAwait) {
362 if let syn::Expr::Call(call) = &*node.base {
363 if let Some(func_name) = self.extract_function_name(call) {
364 if func_name == "async" {
365 let line = 0;
366 let col = 0;
367 self.issues
368 .push(AwaitIssue {
369 function: String::new(),
370 line,
371 column: col,
372 issue: "Unnecessary await on immediate value".to_string(),
373 code_snippet: "async { 42 }.await".to_string(),
374 suggestion: "Remove async/await: 42".to_string(),
375 });
376 }
377 }
378 }
379 syn::visit::visit_expr_await(self, node);
380 }
381 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
382 if let Some(func_name) = self.extract_function_name(node) {
383 if func_name == "async" {
384 let line = 0;
385 let col = 0;
386 self.issues
387 .push(AwaitIssue {
388 function: String::new(),
389 line,
390 column: col,
391 issue: "Nested async blocks".to_string(),
392 code_snippet: "async { async { work() }.await }.await"
393 .to_string(),
394 suggestion: "Flatten to: async { work() }.await".to_string(),
395 });
396 }
397 }
398 syn::visit::visit_expr_call(self, node);
399 }
400}
401struct DeadlockVisitor {
402 risks: Vec<DeadlockRisk>,
403}
404impl DeadlockVisitor {
405 fn new() -> Self {
406 Self { risks: Vec::new() }
407 }
408}
409impl<'ast> Visit<'ast> for DeadlockVisitor {
410 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
411 if let Some(func_name) = self.extract_function_name(node) {
412 if func_name.contains("select!") {
413 let line = 0;
414 let col = 0;
415 self.risks
416 .push(DeadlockRisk {
417 function: String::new(),
418 line,
419 column: col,
420 risk_type: "select! deadlock".to_string(),
421 description: "Multiple futures competing for same resource"
422 .to_string(),
423 suggestion: "Use try_select! or restructure to avoid resource contention"
424 .to_string(),
425 });
426 }
427 }
428 syn::visit::visit_expr_call(self, node);
429 }
430}
431trait FunctionNameExtractor {
432 fn extract_function_name(&self, node: &syn::ExprCall) -> Option<String>;
433}
434impl<T> FunctionNameExtractor for T {
435 fn extract_function_name(&self, node: &syn::ExprCall) -> Option<String> {
436 match &*node.func {
437 syn::Expr::Path(path) => {
438 Some(
439 path
440 .path
441 .segments
442 .iter()
443 .map(|seg| seg.ident.to_string())
444 .collect::<Vec<_>>()
445 .join("::"),
446 )
447 }
448 _ => None,
449 }
450 }
451}
452impl Tool for AsyncLintTool {
453 fn name(&self) -> &'static str {
454 "async-lint"
455 }
456 fn description(&self) -> &'static str {
457 "Detect common async programming pitfalls and suggest improvements"
458 }
459 fn command(&self) -> Command {
460 Command::new(self.name())
461 .about(self.description())
462 .long_about(
463 "Detect common async programming pitfalls and suggest improvements.\n\
464 \n\
465 This tool analyzes async code for common issues:\n\
466 ⢠Detect blocking operations in async contexts\n\
467 ⢠Find unnecessary async/await usage\n\
468 ⢠Identify potential deadlocks\n\
469 ⢠Analyze async function call graphs\n\
470 \n\
471 EXAMPLES:\n\
472 cm tool async-lint --input src/ --blocking --await --deadlock\n\
473 cm tool async-lint --input src/main.rs --blocking --fix\n\
474 cm tool async-lint --input src/ --strict --ignore async-move,unnecessary-await",
475 )
476 .args(
477 &[
478 Arg::new("input")
479 .long("input")
480 .short('i')
481 .help("Input file or directory to analyze")
482 .default_value("src/"),
483 Arg::new("blocking")
484 .long("blocking")
485 .help("Detect blocking operations in async contexts")
486 .action(clap::ArgAction::SetTrue),
487 Arg::new("await")
488 .long("await")
489 .help("Analyze async/await usage patterns")
490 .action(clap::ArgAction::SetTrue),
491 Arg::new("deadlock")
492 .long("deadlock")
493 .help("Detect potential deadlock patterns")
494 .action(clap::ArgAction::SetTrue),
495 Arg::new("concurrency")
496 .long("concurrency")
497 .help("Analyze concurrent async operations")
498 .action(clap::ArgAction::SetTrue),
499 Arg::new("fix")
500 .long("fix")
501 .help("Generate fix suggestions")
502 .action(clap::ArgAction::SetTrue),
503 Arg::new("strict")
504 .long("strict")
505 .help("Enable strict async linting rules")
506 .action(clap::ArgAction::SetTrue),
507 Arg::new("ignore")
508 .long("ignore")
509 .help("Comma-separated list of rules to ignore")
510 .default_value(""),
511 ],
512 )
513 .args(&super::common_options())
514 }
515 fn execute(&self, matches: &ArgMatches) -> Result<()> {
516 let input = matches.get_one::<String>("input").unwrap();
517 let detect_blocking = matches.get_flag("blocking");
518 let analyze_await = matches.get_flag("await");
519 let detect_deadlock = matches.get_flag("deadlock");
520 let analyze_concurrency = matches.get_flag("concurrency");
521 let generate_fixes = matches.get_flag("fix");
522 let strict_mode = matches.get_flag("strict");
523 let ignore_rules = matches.get_one::<String>("ignore").unwrap();
524 let verbose = matches.get_flag("verbose");
525 let dry_run = matches.get_flag("dry-run");
526 let output_format = parse_output_format(matches);
527 let ignored_rules: Vec<String> = ignore_rules
528 .split(',')
529 .map(|s| s.trim().to_string())
530 .filter(|s| !s.is_empty())
531 .collect();
532 if dry_run {
533 println!("š Would analyze async patterns in: {}", input);
534 return Ok(());
535 }
536 let mut all_issues = Vec::new();
537 if Path::new(input).is_file() {
538 match self.analyze_async_patterns(input) {
539 Ok(issues) => all_issues.extend(issues),
540 Err(e) => {
541 if verbose {
542 println!("ā ļø Failed to analyze {}: {}", input, e);
543 }
544 }
545 }
546 } else if Path::new(input).is_dir() {
547 let rust_files = self.find_rust_files(input)?;
548 for file in rust_files {
549 match self.analyze_async_patterns(&file) {
550 Ok(issues) => all_issues.extend(issues),
551 Err(e) => {
552 if verbose {
553 println!("ā ļø Failed to analyze {}: {}", file, e);
554 }
555 }
556 }
557 }
558 } else {
559 return Err(
560 ToolError::InvalidArguments(format!("Path not found: {}", input)),
561 );
562 }
563 match output_format {
564 OutputFormat::Human => {
565 println!(
566 "ā” {} - {}", "Async Pattern Analysis".bold(), self.description()
567 .cyan()
568 );
569 let mut total_issues = 0;
570 for issue_set in &all_issues {
571 if !issue_set.issues.is_empty() {
572 println!("\nš File: {}", issue_set.file.bold());
573 let mut blocking_count = 0;
574 let mut await_count = 0;
575 let mut deadlock_count = 0;
576 for issue in &issue_set.issues {
577 match issue {
578 AsyncIssueType::BlockingOperation(op) => {
579 if detect_blocking
580 && !ignored_rules.contains(&"blocking".to_string())
581 {
582 blocking_count += 1;
583 println!(
584 " š« Line {}: {} in async function", op.line.to_string()
585 .red(), op.operation.yellow()
586 );
587 println!(" š” {}", op.suggestion.cyan());
588 }
589 }
590 AsyncIssueType::AwaitIssue(issue) => {
591 if analyze_await
592 && !ignored_rules.contains(&"await".to_string())
593 {
594 await_count += 1;
595 println!(
596 " š Line {}: {}", issue.line.to_string().yellow(), issue
597 .issue
598 );
599 println!(" Code: {}", issue.code_snippet.red());
600 println!(" š” {}", issue.suggestion.cyan());
601 }
602 }
603 AsyncIssueType::DeadlockRisk(risk) => {
604 if detect_deadlock
605 && !ignored_rules.contains(&"deadlock".to_string())
606 {
607 deadlock_count += 1;
608 println!(
609 " š Line {}: {}", risk.line.to_string().red(), risk
610 .risk_type
611 );
612 println!(" {}", risk.description.yellow());
613 println!(" š” {}", risk.suggestion.cyan());
614 }
615 }
616 }
617 }
618 if blocking_count + await_count + deadlock_count > 0 {
619 println!(
620 " š Issues in this file: {} blocking, {} await, {} deadlock",
621 blocking_count, await_count, deadlock_count
622 );
623 }
624 total_issues += blocking_count + await_count + deadlock_count;
625 }
626 }
627 if analyze_concurrency {
628 println!("\nš Concurrency Analysis:");
629 for issue_set in &all_issues {
630 match self.analyze_concurrency_patterns(&issue_set.file) {
631 Ok(analysis) => {
632 println!(" File: {}", issue_set.file.bold());
633 println!(
634 " Async functions: {}", analysis.total_async_functions
635 );
636 println!(
637 " Average await depth: {:.1}", analysis
638 .average_await_depth
639 );
640 println!(
641 " Concurrent operations: {}", analysis
642 .concurrent_operations
643 );
644 if analysis.potential_race_conditions > 0 {
645 println!(
646 " Potential race conditions: {}", analysis
647 .potential_race_conditions.to_string().yellow()
648 );
649 }
650 if analysis.blocking_operations > 0 {
651 println!(
652 " Blocking operations: {}", analysis.blocking_operations
653 .to_string().red()
654 );
655 }
656 }
657 Err(e) => {
658 if verbose {
659 println!(" ā ļø Concurrency analysis failed: {}", e);
660 }
661 }
662 }
663 }
664 }
665 if generate_fixes {
666 let suggestions = self.suggest_async_improvements(&all_issues);
667 if !suggestions.is_empty() {
668 println!("\nš” Improvement Suggestions:");
669 for suggestion in suggestions {
670 let impact_color = match suggestion.impact.as_str() {
671 "High" => suggestion.impact.red().bold(),
672 "Medium" => suggestion.impact.yellow().bold(),
673 _ => suggestion.impact.green().bold(),
674 };
675 println!(
676 " ⢠[{}] {}: {}", impact_color, suggestion.category
677 .bold(), suggestion.suggestion
678 );
679 }
680 }
681 }
682 println!(
683 "\nš Summary: {} files analyzed, {} issues found", all_issues
684 .len(), total_issues
685 );
686 }
687 OutputFormat::Json => {
688 let mut json_output = serde_json::json!(
689 { "files_analyzed" : all_issues.len(), "issues" : all_issues, }
690 );
691 if generate_fixes {
692 let suggestions = self.suggest_async_improvements(&all_issues);
693 json_output["suggestions"] = serde_json::to_value(&suggestions)
694 .unwrap();
695 }
696 println!("{}", serde_json::to_string_pretty(& json_output).unwrap());
697 }
698 OutputFormat::Table => {
699 println!(
700 "āā Async Pattern Analysis āāāāāāāāāāāāāāāāāā"
701 );
702 println!("ā Files analyzed: {:<25} ā", all_issues.len());
703 let total_issues: usize = all_issues
704 .iter()
705 .map(|i| i.issues.len())
706 .sum();
707 println!("ā Total issues: {:<26} ā", total_issues);
708 if detect_blocking {
709 println!("ā Blocking ops: {:<25} ā", "ā".green());
710 }
711 if analyze_await {
712 println!("ā Await patterns: {:<23} ā", "ā".green());
713 }
714 if detect_deadlock {
715 println!("ā Deadlock check: {:<23} ā", "ā".green());
716 }
717 println!(
718 "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
719 );
720 }
721 }
722 Ok(())
723 }
724}
725impl AsyncLintTool {
726 fn find_rust_files(&self, dir: &str) -> Result<Vec<String>> {
727 let mut rust_files = Vec::new();
728 fn visit_dir(dir: &str, files: &mut Vec<String>) -> Result<()> {
729 let entries = fs::read_dir(dir)?;
730 for entry in entries {
731 let entry = entry?;
732 let path = entry.path();
733 if path.is_dir() {
734 if let Some(dir_name) = path.file_name() {
735 if dir_name != "target" && dir_name != ".git" {
736 visit_dir(&path.to_string_lossy(), files)?;
737 }
738 }
739 } else if let Some(ext) = path.extension() {
740 if ext == "rs" {
741 files.push(path.to_string_lossy().to_string());
742 }
743 }
744 }
745 Ok(())
746 }
747 visit_dir(dir, &mut rust_files)?;
748 Ok(rust_files)
749 }
750}