]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/fmt.rs
Auto merge of #9574 - Alexendoo:unused-fixed, r=Jarcho
[rust.git] / clippy_dev / src / fmt.rs
1 use crate::clippy_project_root;
2 use itertools::Itertools;
3 use shell_escape::escape;
4 use std::ffi::{OsStr, OsString};
5 use std::path::Path;
6 use std::process::{self, Command, Stdio};
7 use std::{fs, io};
8 use walkdir::WalkDir;
9
10 #[derive(Debug)]
11 pub enum CliError {
12     CommandFailed(String, String),
13     IoError(io::Error),
14     RustfmtNotInstalled,
15     WalkDirError(walkdir::Error),
16     IntellijSetupActive,
17 }
18
19 impl From<io::Error> for CliError {
20     fn from(error: io::Error) -> Self {
21         Self::IoError(error)
22     }
23 }
24
25 impl From<walkdir::Error> for CliError {
26     fn from(error: walkdir::Error) -> Self {
27         Self::WalkDirError(error)
28     }
29 }
30
31 struct FmtContext {
32     check: bool,
33     verbose: bool,
34     rustfmt_path: String,
35 }
36
37 // the "main" function of cargo dev fmt
38 pub fn run(check: bool, verbose: bool) {
39     fn try_run(context: &FmtContext) -> Result<bool, CliError> {
40         let mut success = true;
41
42         let project_root = clippy_project_root();
43
44         // if we added a local rustc repo as path dependency to clippy for rust analyzer, we do NOT want to
45         // format because rustfmt would also format the entire rustc repo as it is a local
46         // dependency
47         if fs::read_to_string(project_root.join("Cargo.toml"))
48             .expect("Failed to read clippy Cargo.toml")
49             .contains("[target.'cfg(NOT_A_PLATFORM)'.dependencies]")
50         {
51             return Err(CliError::IntellijSetupActive);
52         }
53
54         rustfmt_test(context)?;
55
56         success &= cargo_fmt(context, project_root.as_path())?;
57         success &= cargo_fmt(context, &project_root.join("clippy_dev"))?;
58         success &= cargo_fmt(context, &project_root.join("rustc_tools_util"))?;
59         success &= cargo_fmt(context, &project_root.join("lintcheck"))?;
60
61         let chunks = WalkDir::new(project_root.join("tests"))
62             .into_iter()
63             .filter_map(|entry| {
64                 let entry = entry.expect("failed to find tests");
65                 let path = entry.path();
66
67                 if path.extension() != Some("rs".as_ref()) || entry.file_name() == "ice-3891.rs" {
68                     None
69                 } else {
70                     Some(entry.into_path().into_os_string())
71                 }
72             })
73             .chunks(250);
74
75         for chunk in &chunks {
76             success &= rustfmt(context, chunk)?;
77         }
78
79         Ok(success)
80     }
81
82     fn output_err(err: CliError) {
83         match err {
84             CliError::CommandFailed(command, stderr) => {
85                 eprintln!("error: A command failed! `{command}`\nstderr: {stderr}");
86             },
87             CliError::IoError(err) => {
88                 eprintln!("error: {err}");
89             },
90             CliError::RustfmtNotInstalled => {
91                 eprintln!("error: rustfmt nightly is not installed.");
92             },
93             CliError::WalkDirError(err) => {
94                 eprintln!("error: {err}");
95             },
96             CliError::IntellijSetupActive => {
97                 eprintln!(
98                     "error: a local rustc repo is enabled as path dependency via `cargo dev setup intellij`.
99 Not formatting because that would format the local repo as well!
100 Please revert the changes to Cargo.tomls with `cargo dev remove intellij`."
101                 );
102             },
103         }
104     }
105
106     let output = Command::new("rustup")
107         .args(["which", "rustfmt"])
108         .stderr(Stdio::inherit())
109         .output()
110         .expect("error running `rustup which rustfmt`");
111     if !output.status.success() {
112         eprintln!("`rustup which rustfmt` did not execute successfully");
113         process::exit(1);
114     }
115     let mut rustfmt_path = String::from_utf8(output.stdout).expect("invalid rustfmt path");
116     rustfmt_path.truncate(rustfmt_path.trim_end().len());
117
118     let context = FmtContext {
119         check,
120         verbose,
121         rustfmt_path,
122     };
123     let result = try_run(&context);
124     let code = match result {
125         Ok(true) => 0,
126         Ok(false) => {
127             eprintln!();
128             eprintln!("Formatting check failed.");
129             eprintln!("Run `cargo dev fmt` to update formatting.");
130             1
131         },
132         Err(err) => {
133             output_err(err);
134             1
135         },
136     };
137     process::exit(code);
138 }
139
140 fn format_command(program: impl AsRef<OsStr>, dir: impl AsRef<Path>, args: &[impl AsRef<OsStr>]) -> String {
141     let arg_display: Vec<_> = args.iter().map(|a| escape(a.as_ref().to_string_lossy())).collect();
142
143     format!(
144         "cd {} && {} {}",
145         escape(dir.as_ref().to_string_lossy()),
146         escape(program.as_ref().to_string_lossy()),
147         arg_display.join(" ")
148     )
149 }
150
151 fn exec(
152     context: &FmtContext,
153     program: impl AsRef<OsStr>,
154     dir: impl AsRef<Path>,
155     args: &[impl AsRef<OsStr>],
156 ) -> Result<bool, CliError> {
157     if context.verbose {
158         println!("{}", format_command(&program, &dir, args));
159     }
160
161     let output = Command::new(&program)
162         .env("RUSTFMT", &context.rustfmt_path)
163         .current_dir(&dir)
164         .args(args.iter())
165         .output()
166         .unwrap();
167     let success = output.status.success();
168
169     if !context.check && !success {
170         let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
171         return Err(CliError::CommandFailed(
172             format_command(&program, &dir, args),
173             String::from(stderr),
174         ));
175     }
176
177     Ok(success)
178 }
179
180 fn cargo_fmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
181     let mut args = vec!["fmt", "--all"];
182     if context.check {
183         args.push("--check");
184     }
185     let success = exec(context, "cargo", path, &args)?;
186
187     Ok(success)
188 }
189
190 fn rustfmt_test(context: &FmtContext) -> Result<(), CliError> {
191     let program = "rustfmt";
192     let dir = std::env::current_dir()?;
193     let args = &["--version"];
194
195     if context.verbose {
196         println!("{}", format_command(program, &dir, args));
197     }
198
199     let output = Command::new(program).current_dir(&dir).args(args.iter()).output()?;
200
201     if output.status.success() {
202         Ok(())
203     } else if std::str::from_utf8(&output.stderr)
204         .unwrap_or("")
205         .starts_with("error: 'rustfmt' is not installed")
206     {
207         Err(CliError::RustfmtNotInstalled)
208     } else {
209         Err(CliError::CommandFailed(
210             format_command(program, &dir, args),
211             std::str::from_utf8(&output.stderr).unwrap_or("").to_string(),
212         ))
213     }
214 }
215
216 fn rustfmt(context: &FmtContext, paths: impl Iterator<Item = OsString>) -> Result<bool, CliError> {
217     let mut args = Vec::new();
218     if context.check {
219         args.push(OsString::from("--check"));
220     }
221     args.extend(paths);
222
223     let success = exec(context, &context.rustfmt_path, std::env::current_dir()?, &args)?;
224
225     Ok(success)
226 }