workspacer_linting/
linting.rs1crate::ix!();
3
4#[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#[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#[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 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 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 #[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 #[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 #[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 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 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 let ws = MockWorkspace {
193 root: root.to_path_buf(),
194 };
195
196 let result = ws.run_linting().await;
198 assert!(result.is_ok(), "Clippy should succeed without warnings");
200 let report = result.unwrap();
201 assert!(report.success(), "LintReport should show success");
202 println!("stdout:\n{}", report.stdout());
204 println!("stderr:\n{}", report.stderr());
205 }
206
207 #[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 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 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 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 #[tokio::test]
265 async fn test_run_linting_command_error() {
266 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 }
278 other => {
279 println!("We got something else: {:?}", other);
280 }
281 }
282 }
283}