aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/linter/cmd_mlint.rs
blob: 1dfd02ba5763a125608b20ec0d0acb103bde8c97 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use crate::linter::mlint_report::{MlintReport, StateLintReports};
use cargo_metadata::Metadata;
use mingling::LazyRes;
use mingling::consts::REMAINS;
use mingling::macros::{arg, chain, dispatcher, pack};
use mingling::picker::EntryPicker;
use tokio::task::JoinSet;

dispatcher!("lint", CMDLint => EntryLint);

/// Main linting function that processes all packages in the metadata.
///
/// Iterates through all packages and their targets (e.g., binaries, libraries, tests),
/// reads Rust source files (`.rs`), parses them into ASTs, runs lint checks,
/// and enriches each report with metadata information.
async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> {
    let mut join_set = JoinSet::new();

    for package in &metadata.packages {
        for target in &package.targets {
            let path = &target.src_path;
            // Only process Rust source files (with `.rs` extension)
            if !path.as_str().ends_with(".rs") {
                continue;
            }

            // Clone/move all data needed inside the blocking closure
            let path_str = path.as_str().to_string();
            let package_id = package.id.to_string();
            let target_name = target.name.clone();
            let target_kind = target.kind.first().map(|k| k.to_string());

            join_set.spawn_blocking(move || {
                // Read the source file content
                let source = std::fs::read_to_string(&path_str).ok()?;
                // Parse the source file into an AST
                let ast = syn::parse_file(&source).ok()?;
                // Run all lint checks and collect reports
                let reports = crate::lints::run_all_lints(&ast, &source);

                // Enrich each report with metadata information
                let enriched: Vec<MlintReport> = reports
                    .into_iter()
                    .map(|mut r| {
                        r.file_name = path_str.clone();
                        r.source_code = source.clone();
                        r.package_id = Some(package_id.clone());
                        r.target_name = Some(target_name.clone());
                        r.target_kind = target_kind.clone();
                        r.target_src_path = Some(path_str.clone());
                        r
                    })
                    .collect();

                Some(enriched)
            });
        }
    }

    let mut all_reports = Vec::new();
    while let Some(res) = join_set.join_next().await {
        // `spawn_blocking` panics are propagated, `None` means task skipped (read/parse failure)
        if let Ok(Some(reports)) = res {
            all_reports.extend(reports);
        }
    }

    all_reports
}

pack!(StateBeginLinter = ());

#[chain]
pub fn handle_lint(args: EntryLint) -> StateBeginLinter {
    let (with_checker, checker_args) = args
        .pick_or(&arg![with_checker: Option<String>], || {
            Some("cargo,check".to_string())
        })
        .pick(&REMAINS)
        .unwrap();

    // If with_checker is not set, proceed directly to the mingling lint phase
    let Some(with_checker) = with_checker else {
        return StateBeginLinter::new(());
    };

    let with_checker: Vec<&str> = with_checker.split(',').collect();
    let checker_args: Vec<String> = checker_args.into();

    // Run the outer checker (e.g. cargo check) with output passed through directly
    execute_checker(&with_checker, checker_args.as_slice());

    StateBeginLinter::new(())
}

/// Run the outer checker (e.g. cargo check) with output passed through directly.
fn execute_checker(with_checker: &[&str], checker_args: &[String]) {
    if with_checker.is_empty() {
        return;
    }

    let checker_str = with_checker.join(" ");
    let args_str = checker_args.join(" ");
    let full_cmd = if args_str.is_empty() {
        checker_str
    } else {
        format!("{} {}", checker_str, args_str)
    };

    let mut cmd = if cfg!(target_os = "windows") {
        let mut c = std::process::Command::new("cmd");
        c.args(["/C", &full_cmd]);
        c
    } else {
        let mut c = std::process::Command::new("sh");
        c.args(["-c", &full_cmd]);
        c
    };

    // Pass through stdin/stdout/stderr so the user sees everything
    let _ = cmd.status();
}

#[chain]
pub async fn handle_state_begin_linter(
    _: StateBeginLinter,
    metadata: &mut LazyRes<crate::metadata::setup::ResMetadata>,
) -> StateLintReports {
    let metadata = metadata.get_ref().data();
    let reports = linter_main(metadata).await;
    StateLintReports::new(reports)
}