dry4rust/cli/
entry_point.rs1use std::env::current_dir;
7use std::io;
8use std::io::Write as _;
9use std::path::PathBuf;
10use std::process::ExitCode;
11
12use clap::Error as ClapError;
13use clap::Parser;
14
15use crate::cli::cli_error::CliError;
16use crate::cli::cli_overrides::CliOverrides;
17use crate::cli::command::Command;
18use crate::cli::output_format::OutputFormat;
19use crate::command_dispatcher::CommandDispatcher;
20use crate::rust::rust_analyzer::RustAnalyzer;
21
22#[derive(Parser)]
28#[command(
32 name = "cargo-dry4rust",
33 version,
34 about = "Detect duplicate code in Rust codebases",
35 long_about = None
36)]
37pub struct EntryPoint {
38 #[arg(hide = true, default_value = "")]
40 _cargo_subcommand: String,
41
42 #[command(subcommand)]
43 command: Option<Command>,
44
45 #[arg(short, long, global = true)]
47 path: Option<PathBuf>,
48
49 #[arg(long, global = true)]
51 min_nodes: Option<usize>,
52
53 #[arg(long, global = true)]
55 min_lines: Option<usize>,
56
57 #[arg(long, global = true)]
59 threshold: Option<f64>,
60
61 #[arg(long, global = true, default_value = "text")]
63 format: OutputFormat,
64
65 #[arg(long, global = true)]
67 exclude: Vec<String>,
68
69 #[arg(long, global = true)]
71 exclude_tests: bool,
72
73 #[arg(long, short = 's', global = true)]
75 sub_function: bool,
76
77 #[arg(long, global = true)]
79 min_sub_nodes: Option<usize>,
80
81 #[arg(long, global = true)]
84 baseline: Option<PathBuf>,
85}
86
87impl EntryPoint {
88 #[must_use]
93 pub fn run(args: Vec<String>) -> ExitCode {
94 match Self::try_parse_from(args) {
95 Ok(entry) => entry.execute(),
96 Err(error) => Self::parsing_failed(&error),
97 }
98 }
99
100 fn execute(self) -> ExitCode {
101 let analyzer = RustAnalyzer::new();
102 let root = self.root();
103 let dispatcher = CommandDispatcher::new(&analyzer, &root, self.format, self.overrides());
104 match dispatcher.dispatch(&self.command()) {
105 Ok(outcome) => {
106 let stdout = io::stdout();
107 let mut writer = stdout.lock();
108 match writer
109 .write_all(outcome.text.as_bytes())
110 .and_then(|()| writer.flush())
111 {
112 Ok(()) => match outcome.result {
113 Ok(()) => ExitCode::SUCCESS,
114 Err(e) => Self::reported(&e),
115 },
116 Err(e) => Self::reported(&CliError::Io(e)),
117 }
118 }
119 Err(e) => Self::reported(&e),
120 }
121 }
122
123 fn parsing_failed(error: &ClapError) -> ExitCode {
126 let _ = error.print();
127 if error.use_stderr() {
128 ExitCode::from(2)
129 } else {
130 ExitCode::SUCCESS
131 }
132 }
133
134 #[must_use]
136 pub fn root(&self) -> PathBuf {
137 self.path
138 .clone()
139 .unwrap_or_else(|| current_dir().unwrap_or_else(|_| PathBuf::from(".")))
140 }
141
142 #[must_use]
144 pub fn command(&self) -> Command {
145 self.command.clone().unwrap_or(Command::Report)
146 }
147
148 #[must_use]
155 pub fn overrides(&self) -> CliOverrides {
156 CliOverrides {
157 min_nodes: self.min_nodes,
158 min_lines: self.min_lines,
159 threshold: self.threshold,
160 exclude: self.exclude.clone(),
161 exclude_tests: self.exclude_tests.then_some(true),
162 sub_function: self.sub_function.then_some(true),
163 min_sub_nodes: self.min_sub_nodes,
164 baseline: self.baseline.clone(),
165 }
166 }
167
168 #[must_use]
170 pub const fn format(&self) -> OutputFormat {
171 self.format
172 }
173
174 fn reported(error: &CliError) -> ExitCode {
175 if !matches!(error, CliError::CheckFailed) {
176 eprintln!("Error: {error}");
177 }
178 ExitCode::from(error.exit_code() as u8)
179 }
180}