workspacer_linting/
linting.rs

1// ---------------- [ File: workspacer-linting/src/linting.rs ]
2crate::ix!();
3
4/// The `RunLinting` trait remains the same.
5#[async_trait]
6pub trait RunLinting {
7    type Report;
8    type Error;
9    async fn run_linting(&self) -> Result<Self::Report, Self::Error>;
10}
11
12/// Implementation for the entire workspace.
13/// (unchanged from your original approach).
14#[async_trait]
15impl<P, H> RunLinting for Workspace<P,H>
16where
17    H: CrateHandleInterface<P>,
18    for<'async_trait> P: From<PathBuf> + AsRef<Path> + Send + Sync + 'async_trait,
19{
20    type Report = LintReport;
21    type Error  = LintingError;
22
23    async fn run_linting(&self) -> Result<Self::Report, Self::Error> {
24        let workspace_path = self.as_ref(); 
25
26        let output = tokio::process::Command::new("cargo")
27            .arg("clippy")
28            .arg("--all-targets")
29            .arg("--message-format=short")
30            .arg("--quiet")
31            .arg("--")
32            .arg("-D")
33            .arg("warnings")
34            .current_dir(workspace_path)
35            .output()
36            .await
37            .map_err(|e| LintingError::CommandError { io: e.into() })?;
38
39        let report = LintReport::from(output);
40        report.maybe_throw()?;
41        Ok(report)
42    }
43}
44
45/// Now **do not** implement `RunLinting` in a generic way for all `C: CrateHandleInterface`.
46/// Instead, implement it for your **actual concrete crate type**—for example, `CrateHandle`.
47///
48/// That ensures there is no overlap with `Workspace<P,H>` in the compiler's eyes.
49///
50#[async_trait]
51impl RunLinting for CrateHandle {
52    type Report = LintReport;
53    type Error  = LintingError;
54
55    async fn run_linting(&self) -> Result<Self::Report, Self::Error> {
56        // 1) We lock CargoToml to find the actual `Cargo.toml` path
57        let cargo_toml_arc = self.cargo_toml_direct(); 
58        let cargo_toml_guard = cargo_toml_arc.lock().await;
59        let manifest_path = cargo_toml_guard.as_ref().to_path_buf();
60
61        // 2) Run cargo clippy with `--manifest-path` ...
62        let output = tokio::process::Command::new("cargo")
63            .arg("clippy")
64            .arg("--manifest-path")
65            .arg(&manifest_path)
66            .arg("--all-targets")
67            .arg("--message-format=short")
68            .arg("--quiet")
69            .arg("--")
70            .arg("-D")
71            .arg("warnings")
72            .output()
73            .await
74            .map_err(|io_err| {
75                error!(
76                    "Failed to spawn cargo clippy for crate='{}': {io_err}",
77                    self.name()
78                );
79                LintingError::CommandError { io: io_err.into() }
80            })?;
81
82        let report = LintReport::from(output);
83        if !report.success() {
84            warn!(
85                "Lint failed for crate='{}'. Stderr:\n{}",
86                self.name(),
87                report.stderr()
88            );
89            return Err(LintingError::UnknownError {
90                stderr: Some(report.stderr().to_owned()),
91                stdout: Some(report.stdout().to_owned()),
92            });
93        }
94
95        debug!(
96            "Lint successful for crate='{}' => {} bytes stdout, {} bytes stderr",
97            self.name(),
98            report.stdout().len(),
99            report.stderr().len()
100        );
101        Ok(report)
102    }
103}
104
105#[cfg(test)]
106mod test_run_linting_real {
107    use super::*;
108    use std::path::PathBuf;
109    use tempfile::tempdir;
110    use workspacer_3p::tokio;
111    use workspacer_3p::tokio::process::Command;
112
113    // If you already have a real `Workspace<P,H>` for your environment, you can use that directly.
114    // For demonstration, we define a minimal "MockWorkspace" or "TestWorkspace" that implements
115    // your real `RunLinting` snippet, or we rely on the real code if accessible.
116
117    #[derive(Debug)]
118    struct MockWorkspace {
119        root: PathBuf,
120    }
121
122    impl AsRef<std::path::Path> for MockWorkspace {
123        fn as_ref(&self) -> &std::path::Path {
124            &self.root
125        }
126    }
127
128    // We'll replicate the run_linting code or rely on the trait if it's default-implemented.
129    // For demonstration, let's do a direct impl:
130    #[async_trait]
131    impl RunLinting for MockWorkspace {
132        type Report = LintReport;
133        type Error = LintingError;
134
135        async fn run_linting(&self) -> Result<Self::Report, Self::Error> {
136            let workspace_path = self.as_ref();
137
138            let output = Command::new("cargo")
139                .arg("clippy")
140                .arg("--all-targets")
141                .arg("--message-format=short")
142                .arg("--quiet")
143                .arg("--")
144                .arg("-D")
145                .arg("warnings")
146                .current_dir(workspace_path)
147                .output()
148                .await
149                .map_err(|e| LintingError::CommandError { io: e.into() })?;
150
151            let report = LintReport::from(output);
152            report.maybe_throw()?;
153            Ok(report)
154        }
155    }
156
157    // -----------------------------------------------------------------------
158    // Actual tests:
159    // -----------------------------------------------------------------------
160
161    /// 1) If we have a valid, clean Cargo project with no lint warnings, `run_linting` should succeed.
162    #[tokio::test]
163    async fn test_run_linting_succeeds_no_warnings() {
164        let tmp_dir = tempdir().expect("create temp dir");
165        let root = tmp_dir.path();
166
167        // We'll initialize a new cargo project:
168        //   cargo init --vcs none --bin
169        let init_output = Command::new("cargo")
170            .arg("init")
171            .arg("--vcs")
172            .arg("none")
173            .arg("--bin")
174            .arg("--name")
175            .arg("lint_test_proj")
176            .current_dir(root)
177            .output()
178            .await
179            .expect("Failed to run cargo init");
180        assert!(
181            init_output.status.success(),
182            "cargo init must succeed for the test to proceed"
183        );
184
185        // Optionally write code that has no lint warnings:
186        let main_rs = root.join("src").join("main.rs");
187        tokio::fs::write(&main_rs, b"fn main(){ println!(\"Hello!\"); }")
188            .await
189            .expect("write main.rs");
190
191        // Build our mock workspace
192        let ws = MockWorkspace {
193            root: root.to_path_buf(),
194        };
195
196        // 2) run run_linting
197        let result = ws.run_linting().await;
198        // 3) Because there's no warnings, we expect success:
199        assert!(result.is_ok(), "Clippy should succeed without warnings");
200        let report = result.unwrap();
201        assert!(report.success(), "LintReport should show success");
202        // Optionally check stdout/stderr
203        println!("stdout:\n{}", report.stdout());
204        println!("stderr:\n{}", report.stderr());
205    }
206
207    /// 2) If the code has a lint warning or error, we expect a failure `UnknownError` with the output.
208    #[tokio::test]
209    async fn test_run_linting_fails_on_warnings() {
210        let tmp_dir = tempdir().expect("tempdir");
211        let root = tmp_dir.path();
212
213        // cargo init --vcs none
214        let init_output = Command::new("cargo")
215            .arg("init")
216            .arg("--vcs")
217            .arg("none")
218            .arg("--bin")
219            .arg("--name")
220            .arg("lint_warn_proj")
221            .current_dir(root)
222            .output()
223            .await
224            .expect("cargo init");
225        assert!(init_output.status.success());
226
227        // Insert a code snippet that triggers a clippy warning
228        // For example: an unused variable or something. Let "x" be unused
229        let main_rs = root.join("src").join("main.rs");
230        let code_with_warning = b"
231            fn main() {
232                let x = 42; // unused
233                println!(\"Hello\");
234            }
235        ";
236        tokio::fs::write(&main_rs, code_with_warning)
237            .await
238            .expect("write main with warning");
239
240        let ws = MockWorkspace {
241            root: root.to_path_buf(),
242        };
243
244        let result = ws.run_linting().await;
245        match result {
246            Err(LintingError::UnknownError { stderr, stdout }) => {
247                // We'll see clippy's warning => it fails because we pass `-D warnings`.
248                // Possibly check if "warning:" or something is in `stderr`.
249                let stde = stderr.unwrap_or_default();
250                println!("clippy stderr: {}", stde);
251                assert!(
252                    stde.contains("warning") || stde.contains("error"),
253                    "Should mention a lint warning or error"
254                );
255            }
256            Ok(report) => {
257                panic!("Expected clippy to fail with a warning, but it succeeded: {:?}", report)
258            }
259            other => panic!("Expected UnknownError, got {:?}", other),
260        }
261    }
262
263    /// 3) If the environment has no cargo/clippy, or if we can't spawn the process, we get `CommandError`.
264    #[tokio::test]
265    async fn test_run_linting_command_error() {
266        // We'll not create a real cargo project. We'll rely on the environment missing cargo or something.
267        // In many systems cargo is installed, so you'll get a different error. 
268        // We can forcibly rename cargo or do partial checks:
269
270        let ws = MockWorkspace {
271            root: PathBuf::from("/non/existent/directory"),
272        };
273        let result = ws.run_linting().await;
274        match result {
275            Err(LintingError::CommandError { .. }) => {
276                // Good
277            }
278            other => {
279                println!("We got something else: {:?}", other);
280            }
281        }
282    }
283}