1mod cfg_target_arch;
7mod copyright;
8mod crate_name_nodash;
9mod orphaned_rs;
10mod package_info;
11mod repr_packed;
12mod trailing_newline;
13mod unsafe_code_comment;
14mod unused_deps;
15mod workspaced;
16
17use crate::fs_helpers::git_diffed;
18use crate::tasks::fmt::FmtCtx;
19use crate::tasks::fmt::FmtPass;
20use std::fmt::Display;
21use std::ops::Deref;
22use std::path::Path;
23use std::path::PathBuf;
24use std::sync::atomic::AtomicBool;
25use toml_edit::DocumentMut;
26
27pub struct LintCtx {
29 only_diffed: bool,
32}
33
34pub trait Lint {
36 fn new(ctx: &LintCtx) -> Self
38 where
39 Self: Sized;
40
41 fn enter_workspace(&mut self, content: &Lintable<DocumentMut>);
43
44 fn enter_crate(&mut self, content: &Lintable<DocumentMut>);
46
47 fn visit_file(&mut self, content: &mut Lintable<String>);
49
50 fn exit_crate(&mut self, content: &mut Lintable<DocumentMut>);
52
53 fn exit_workspace(&mut self, content: &mut Lintable<DocumentMut>);
55
56 fn visit_nonrust_file(&mut self, extension: &str, content: &mut Lintable<String>) {
62 let _ = (extension, content);
63 }
64}
65
66pub struct Lintable<T> {
71 content: T,
72 raw: Option<String>,
73 fix: bool,
74 path: PathBuf,
75 workspace_dir: PathBuf,
76 modified: bool,
77 failed: AtomicBool,
80}
81
82impl<T> Deref for Lintable<T> {
83 type Target = T;
84
85 fn deref(&self) -> &Self::Target {
86 &self.content
87 }
88}
89
90impl Lintable<String> {
91 fn from_file(path: &Path, ctx: &FmtCtx, workspace_dir: &Path) -> anyhow::Result<Option<Self>> {
95 let bytes = fs_err::read(path)?;
96 let content = match String::from_utf8(bytes) {
97 Ok(s) => s,
98 Err(_) => return Ok(None),
99 };
100 Ok(Some(Self {
101 content,
102 raw: None,
103 fix: ctx.fix,
104 path: path.strip_prefix(workspace_dir).unwrap().to_owned(),
105 workspace_dir: workspace_dir.to_owned(),
106 modified: false,
107 failed: AtomicBool::new(false),
108 }))
109 }
110}
111
112impl Lintable<DocumentMut> {
113 fn from_file(path: &Path, ctx: &FmtCtx, workspace_dir: &Path) -> anyhow::Result<Self> {
117 let raw = fs_err::read_to_string(path)?;
118 Ok(Self {
119 content: raw.parse()?,
120 raw: Some(raw),
121 fix: ctx.fix,
122 path: path.strip_prefix(workspace_dir).unwrap().to_owned(),
123 workspace_dir: workspace_dir.to_owned(),
124 modified: false,
125 failed: AtomicBool::new(false),
126 })
127 }
128}
129
130impl<T> Lintable<T> {
131 pub fn path(&self) -> &Path {
133 &self.path
134 }
135
136 pub fn raw(&self) -> Option<&str> {
141 self.raw.as_deref()
142 }
143
144 pub fn fix(&mut self, description: &str, op: impl FnOnce(&mut T)) {
147 if self.fix {
148 op(&mut self.content);
149 self.modified = true;
150 } else {
151 log::error!("{}: {}", self.path.display(), description);
152 self.failed
153 .store(true, std::sync::atomic::Ordering::Relaxed);
154 }
155 }
156
157 pub fn unfixable(&self, description: &str) {
159 log::error!("{}: {}", self.path.display(), description);
160 self.failed
161 .store(true, std::sync::atomic::Ordering::Relaxed);
162 }
163
164 fn finalize(self) -> anyhow::Result<bool>
166 where
167 T: Display,
168 {
169 if self.modified {
170 let full_path = self.workspace_dir.join(&self.path);
171 fs_err::write(full_path, self.content.to_string())?;
172 }
173 Ok(self.failed.into_inner())
174 }
175}
176
177pub struct Lints;
178
179impl FmtPass for Lints {
180 fn run(self, ctx: FmtCtx) -> anyhow::Result<()> {
181 let mut workspace_dirs = Vec::new();
184 let mut all_crate_dirs = Vec::new();
185 let mut all_files = Vec::new();
186 for entry in ignore::Walk::new(&ctx.ctx.root) {
187 let entry = entry?;
188 if entry.file_name() == "Cargo.toml" {
189 let raw = fs_err::read_to_string(entry.path())?;
191 let doc: DocumentMut = raw.parse()?;
192 if doc.contains_key("workspace") {
193 workspace_dirs.push(entry.path().parent().unwrap().to_owned());
194 } else {
195 all_crate_dirs.push(entry.path().parent().unwrap().to_owned());
198 }
199 } else if entry.file_type().is_some_and(|ft| ft.is_file()) {
200 all_files.push(entry.into_path());
201 }
202 }
203
204 let mut any_failed = false;
205
206 for workspace_dir in &workspace_dirs {
208 let nested_workspace_dirs: Vec<_> = workspace_dirs
210 .iter()
211 .filter(|other| *other != workspace_dir && other.starts_with(workspace_dir))
212 .collect();
213
214 let mut crate_dirs: Vec<_> = all_crate_dirs
217 .iter()
218 .filter(|crate_dir| {
219 crate_dir.starts_with(workspace_dir)
220 && !nested_workspace_dirs
221 .iter()
222 .any(|nested| crate_dir.starts_with(*nested))
223 })
224 .collect();
225
226 let workspace_files: Vec<_> = all_files
229 .iter()
230 .filter(|f| {
231 f.starts_with(workspace_dir)
232 && !nested_workspace_dirs
233 .iter()
234 .any(|nested| f.starts_with(*nested))
235 })
236 .collect();
237
238 let mut non_crate_files: Vec<_> = workspace_files
240 .iter()
241 .filter(|f| {
242 f.extension().and_then(|e| e.to_str()) != Some("rs")
243 && !crate_dirs.iter().any(|crate_dir| f.starts_with(crate_dir))
244 })
245 .copied()
246 .collect();
247
248 if ctx.only_diffed {
250 let diffed = git_diffed(ctx.ctx.in_git_hook)?;
251 crate_dirs.retain(|crate_dir| {
254 let crate_dir = crate_dir.strip_prefix(&ctx.ctx.root).unwrap();
255 diffed.iter().any(|f| f.starts_with(crate_dir))
256 });
257 non_crate_files.retain(|f| {
258 let f = f.strip_prefix(&ctx.ctx.root).unwrap().to_owned();
259 diffed.contains(&f)
260 });
261 }
262
263 any_failed |= lint_workspace(
264 workspace_dir,
265 &crate_dirs,
266 &non_crate_files,
267 &workspace_files,
268 &ctx,
269 )?;
270 }
271
272 if any_failed {
273 anyhow::bail!("one or more lint checks failed");
274 }
275
276 Ok(())
277 }
278}
279
280fn lint_workspace(
282 workspace_dir: &Path,
283 crate_dirs: &[&PathBuf],
284 non_crate_files: &[&PathBuf],
285 all_files: &[&PathBuf],
286 ctx: &FmtCtx,
287) -> anyhow::Result<bool> {
288 let lint_ctx = LintCtx {
289 only_diffed: ctx.only_diffed,
290 };
291
292 let mut lints: Vec<Box<dyn Lint>> = vec![
293 Box::new(cfg_target_arch::CfgTargetArch::new(&lint_ctx)),
294 Box::new(copyright::Copyright::new(&lint_ctx)),
295 Box::new(crate_name_nodash::CrateNameNoDash::new(&lint_ctx)),
296 Box::new(orphaned_rs::OrphanedRustFiles::new(&lint_ctx)),
297 Box::new(package_info::PackageInfo::new(&lint_ctx)),
298 Box::new(repr_packed::ReprPacked::new(&lint_ctx)),
299 Box::new(trailing_newline::TrailingNewline::new(&lint_ctx)),
300 Box::new(unsafe_code_comment::UnsafeCodeComment::new(&lint_ctx)),
301 Box::new(unused_deps::UnusedDeps::new(&lint_ctx)),
302 Box::new(workspaced::WorkspacedManifest::new(&lint_ctx)),
303 ];
304
305 let workspace_manifest_path = workspace_dir.join("Cargo.toml");
306 let mut workspace_manifest =
307 Lintable::<DocumentMut>::from_file(&workspace_manifest_path, ctx, workspace_dir)?;
308
309 log::debug!(
310 "Linting workspace {} with {} crates and {} non-crate files",
311 workspace_dir.display(),
312 crate_dirs.len(),
313 non_crate_files.len()
314 );
315 for lint in lints.iter_mut() {
316 lint.enter_workspace(&workspace_manifest);
317 }
318
319 let mut any_failed = false;
320
321 for crate_dir in crate_dirs {
322 let manifest_path = crate_dir.join("Cargo.toml");
323 let mut crate_manifest =
324 Lintable::<DocumentMut>::from_file(&manifest_path, ctx, workspace_dir)?;
325
326 log::debug!("Linting crate {}", crate_dir.display());
327 for lint in lints.iter_mut() {
328 lint.enter_crate(&crate_manifest);
329 }
330
331 let nested_crate_dirs: Vec<_> = crate_dirs
334 .iter()
335 .filter(|other| *other != crate_dir && other.starts_with(crate_dir))
336 .collect();
337
338 for path in all_files.iter().filter(|f| {
341 f.starts_with(crate_dir)
342 && !nested_crate_dirs.iter().any(|nested| f.starts_with(nested))
343 }) {
344 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
345 let Some(mut file) = Lintable::<String>::from_file(path, ctx, workspace_dir)? else {
346 continue;
348 };
349
350 for lint in lints.iter_mut() {
351 if ext == "rs" {
352 lint.visit_file(&mut file);
353 } else {
354 lint.visit_nonrust_file(ext, &mut file);
355 }
356 }
357 any_failed |= file.finalize()?;
358 }
359
360 for lint in lints.iter_mut() {
361 lint.exit_crate(&mut crate_manifest);
362 }
363 any_failed |= crate_manifest.finalize()?;
364 }
365
366 for path in non_crate_files {
368 log::debug!("Linting non-crate file {}", path.display());
369 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
370 let Some(mut file) = Lintable::<String>::from_file(path, ctx, workspace_dir)? else {
371 log::debug!("Skipping binary file {}", path.display());
373 continue;
374 };
375 for lint in lints.iter_mut() {
376 lint.visit_nonrust_file(ext, &mut file);
377 }
378 any_failed |= file.finalize()?;
379 }
380
381 for lint in lints.iter_mut() {
382 lint.exit_workspace(&mut workspace_manifest);
383 }
384 any_failed |= workspace_manifest.finalize()?;
385
386 Ok(any_failed)
387}