Skip to main content

cargo_mate/tools/
async_lint.rs

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}