1mod cargo_toml_editor;
30mod context;
31mod diagnostics;
32mod manifest;
33mod output;
34mod package_analyzer;
35mod package_processor;
36mod source_parser;
37#[cfg(test)]
38mod tests;
39pub mod util;
40
41use std::{
42 env, fs,
43 io::Write,
44 path::{Path, PathBuf},
45 process::ExitCode,
46 str::FromStr,
47};
48
49use anyhow::Result;
50use bpaf::Bpaf;
51use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package};
52use owo_colors::OwoColorize;
53use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
54use rustc_hash::FxHashSet;
55use toml_edit::DocumentMut;
56
57pub use crate::output::{ColorMode, OutputFormat};
58use crate::{
59 cargo_toml_editor::CargoTomlEditor,
60 context::{PackageContext, WorkspaceContext},
61 diagnostics::ShearAnalysis,
62 output::Renderer,
63 package_processor::{PackageAnalysis, PackageProcessor, WorkspaceAnalysis},
64 util::read_to_string,
65};
66
67const VERSION: &str = match option_env!("SHEAR_VERSION") {
68 Some(v) => v,
69 None => "dev",
70};
71
72#[derive(Debug, Clone, Bpaf)]
79#[bpaf(options("shear"), version(VERSION))]
80pub struct CargoShearOptions {
81 #[bpaf(long)]
86 fix: bool,
87
88 #[bpaf(long)]
93 expand: bool,
94
95 #[bpaf(long("deny-warnings"))]
99 deny_warnings: bool,
100
101 locked: bool,
103
104 offline: bool,
106
107 frozen: bool,
109
110 #[bpaf(long, short, argument("SPEC"))]
115 package: Vec<String>,
116
117 exclude: Vec<String>,
121
122 #[bpaf(long, fallback(OutputFormat::Auto))]
124 format: OutputFormat,
125
126 #[bpaf(long, fallback(ColorMode::Auto))]
128 color: ColorMode,
129
130 #[bpaf(positional("PATH"), fallback_with(default_path))]
134 path: PathBuf,
135}
136
137impl CargoShearOptions {
138 #[must_use]
140 pub fn new(path: PathBuf) -> Self {
141 Self {
142 path,
143 fix: false,
144 expand: false,
145 deny_warnings: false,
146 locked: false,
147 offline: false,
148 frozen: false,
149 package: vec![],
150 exclude: vec![],
151 format: OutputFormat::default(),
152 color: ColorMode::default(),
153 }
154 }
155
156 #[must_use]
158 pub const fn with_fix(mut self) -> Self {
159 self.fix = true;
160 self
161 }
162
163 #[must_use]
165 pub const fn with_expand(mut self) -> Self {
166 self.expand = true;
167 self
168 }
169
170 #[must_use]
172 pub const fn with_deny_warnings(mut self) -> Self {
173 self.deny_warnings = true;
174 self
175 }
176
177 #[must_use]
179 pub const fn with_locked(mut self) -> Self {
180 self.locked = true;
181 self
182 }
183
184 #[must_use]
186 pub const fn with_offline(mut self) -> Self {
187 self.offline = true;
188 self
189 }
190
191 #[must_use]
193 pub const fn with_frozen(mut self) -> Self {
194 self.frozen = true;
195 self
196 }
197
198 #[must_use]
200 pub fn with_packages(mut self, packages: Vec<String>) -> Self {
201 self.package = packages;
202 self
203 }
204
205 #[must_use]
207 pub fn with_excludes(mut self, excludes: Vec<String>) -> Self {
208 self.exclude = excludes;
209 self
210 }
211
212 #[must_use]
214 pub const fn with_format(mut self, format: OutputFormat) -> Self {
215 self.format = format;
216 self
217 }
218
219 #[must_use]
221 pub const fn with_color(mut self, color: ColorMode) -> Self {
222 self.color = color;
223 self
224 }
225
226 #[must_use]
231 pub fn resolve(mut self) -> Self {
232 self.format = self.format.resolve();
233 self
234 }
235}
236
237pub(crate) fn default_path() -> Result<PathBuf> {
238 Ok(env::current_dir()?)
239}
240
241pub struct CargoShear<W> {
246 writer: W,
248
249 options: CargoShearOptions,
251
252 analysis: ShearAnalysis,
254}
255
256impl<W: Write> CargoShear<W> {
257 #[must_use]
274 pub fn new(writer: W, options: CargoShearOptions) -> Self {
275 let analysis = ShearAnalysis::new(options.clone());
276 Self { writer, options, analysis }
277 }
278
279 #[must_use]
294 pub fn run(mut self) -> ExitCode {
295 match self.shear() {
296 Ok(()) => {
297 let color = self.options.color.enabled();
298 let mut renderer = Renderer::new(&mut self.writer, self.options.format, color);
299
300 if let Err(err) = renderer.render(&self.analysis) {
301 let _ = writeln!(self.writer, "error rendering report: {err:?}");
302 return ExitCode::from(2);
303 }
304
305 self.determine_exit_code()
306 }
307 Err(err) => {
308 let _ = writeln!(self.writer, "error: {err:?}");
309 ExitCode::from(2)
310 }
311 }
312 }
313
314 const fn determine_exit_code(&self) -> ExitCode {
316 if self.options.fix && self.analysis.fixed > 0 && self.analysis.errors == 0 {
318 return ExitCode::SUCCESS;
319 }
320
321 let has_errors = self.analysis.errors > 0;
323 let has_warnings = self.options.deny_warnings && self.analysis.warnings > 0;
324
325 if has_errors || has_warnings { ExitCode::FAILURE } else { ExitCode::SUCCESS }
326 }
327
328 fn shear(&mut self) -> Result<()> {
329 let mut extra_opts = Vec::new();
330 if self.options.locked {
331 extra_opts.push("--locked".to_owned());
332 }
333 if self.options.offline {
334 extra_opts.push("--offline".to_owned());
335 }
336 if self.options.frozen {
337 extra_opts.push("--frozen".to_owned());
338 }
339
340 let metadata = MetadataCommand::new()
341 .features(CargoOpt::AllFeatures)
342 .current_dir(&self.options.path)
343 .other_options(extra_opts)
344 .verbose(true)
345 .exec()
346 .map_err(|e| anyhow::anyhow!("Metadata error: {e}"))?;
347
348 let processor = PackageProcessor::new(self.options.expand);
349 let workspace_ctx = WorkspaceContext::new(&metadata)?;
350
351 let packages = metadata.workspace_packages();
352 let packages: Vec<_> = packages
353 .into_iter()
354 .filter(|package| {
355 if self.options.exclude.iter().any(|name| name == package.name.as_str()) {
357 return false;
358 }
359
360 if !self.options.package.is_empty()
362 && !self.options.package.iter().any(|name| name == package.name.as_str())
363 {
364 return false;
365 }
366
367 true
368 })
369 .collect();
370
371 let total = packages.len();
372 let results: Vec<_> = if self.options.expand {
373 packages
375 .iter()
376 .enumerate()
377 .map(|(index, package)| {
378 eprintln!(
379 "{:>12} {} [{}/{}]",
380 "Expanding".bright_cyan().bold(),
381 package.name,
382 index + 1,
383 total
384 );
385
386 Self::process_package(&processor, &workspace_ctx, package, &metadata)
387 })
388 .collect::<Result<Vec<_>>>()?
389 } else {
390 packages
392 .par_iter()
393 .map(|package| {
394 Self::process_package(&processor, &workspace_ctx, package, &metadata)
395 })
396 .collect::<Result<Vec<_>>>()?
397 };
398
399 let mut used_workspace_ignore_paths: FxHashSet<String> = FxHashSet::default();
400 for (ctx, result) in results {
401 let fixed = self.fix_package_issues(&ctx.manifest_path, &result)?;
402 used_workspace_ignore_paths.extend(result.used_workspace_ignore_paths.iter().cloned());
403 self.analysis.add_package_result(&ctx, &result, fixed);
404 }
405
406 if self.options.package.is_empty() && self.options.exclude.is_empty() {
408 let workspace_result = PackageProcessor::process_workspace(
409 &workspace_ctx,
410 &self.analysis.packages,
411 &used_workspace_ignore_paths,
412 );
413
414 let fixed =
415 self.fix_workspace_issues(&workspace_ctx.manifest_path, &workspace_result)?;
416 self.analysis.add_workspace_result(&workspace_ctx, &workspace_result, fixed);
417 }
418
419 Ok(())
420 }
421
422 fn process_package<'a>(
423 processor: &PackageProcessor,
424 workspace_ctx: &'a WorkspaceContext,
425 package: &Package,
426 metadata: &'a Metadata,
427 ) -> Result<(PackageContext<'a>, PackageAnalysis)> {
428 let ctx = PackageContext::new(workspace_ctx, package, metadata)?;
429 let result = processor.process_package(&ctx)?;
430 Ok((ctx, result))
431 }
432
433 fn fix_package_issues(&self, manifest_path: &Path, result: &PackageAnalysis) -> Result<usize> {
434 if !self.options.fix {
435 return Ok(0);
436 }
437
438 if !result.has_fixable_issues() {
439 return Ok(0);
440 }
441
442 let content = read_to_string(manifest_path)?;
443 let mut manifest = DocumentMut::from_str(&content)?;
444
445 let fixed_unused =
446 CargoTomlEditor::remove_dependencies(&mut manifest, &result.unused_dependencies);
447 let fixed_misplaced = CargoTomlEditor::move_to_dev_dependencies(
448 &mut manifest,
449 &result.misplaced_dependencies,
450 );
451 let mut flag_fixes = 0usize;
452 if !result.test_disabled_with_tests.is_empty() {
453 flag_fixes += usize::from(CargoTomlEditor::remove_lib_flag(&mut manifest, "test"));
454 }
455 if !result.test_enabled_without_tests.is_empty() {
456 CargoTomlEditor::set_lib_flag_false(&mut manifest, "test");
457 flag_fixes += 1;
458 }
459 if !result.doctest_disabled_with_doctests.is_empty() {
460 flag_fixes += usize::from(CargoTomlEditor::remove_lib_flag(&mut manifest, "doctest"));
461 }
462 if !result.doctest_enabled_without_doctests.is_empty() {
463 CargoTomlEditor::set_lib_flag_false(&mut manifest, "doctest");
464 flag_fixes += 1;
465 }
466
467 fs::write(manifest_path, manifest.to_string())?;
468 Ok(fixed_unused + fixed_misplaced + flag_fixes)
469 }
470
471 fn fix_workspace_issues(
472 &self,
473 manifest_path: &Path,
474 result: &WorkspaceAnalysis,
475 ) -> Result<usize> {
476 if !self.options.fix {
477 return Ok(0);
478 }
479
480 if result.unused_dependencies.is_empty() {
481 return Ok(0);
482 }
483
484 let content = read_to_string(manifest_path)?;
485 let mut manifest = DocumentMut::from_str(&content)?;
486
487 let fixed =
488 CargoTomlEditor::remove_workspace_deps(&mut manifest, &result.unused_dependencies);
489
490 fs::write(manifest_path, manifest.to_string())?;
491 Ok(fixed)
492 }
493}