]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/fmt.rs
Auto merge of #4625 - phansch:rollup-qp7ki0h, r=phansch
[rust.git] / clippy_dev / src / fmt.rs
1 use shell_escape::escape;
2 use std::ffi::OsStr;
3 use std::io;
4 use std::path::{Path, PathBuf};
5 use std::process::{self, Command};
6 use walkdir::WalkDir;
7
8 #[derive(Debug)]
9 pub enum CliError {
10     CommandFailed(String),
11     IoError(io::Error),
12     ProjectRootNotFound,
13     RustfmtNotInstalled,
14     WalkDirError(walkdir::Error),
15 }
16
17 impl From<io::Error> for CliError {
18     fn from(error: io::Error) -> Self {
19         Self::IoError(error)
20     }
21 }
22
23 impl From<walkdir::Error> for CliError {
24     fn from(error: walkdir::Error) -> Self {
25         Self::WalkDirError(error)
26     }
27 }
28
29 struct FmtContext {
30     check: bool,
31     verbose: bool,
32 }
33
34 pub fn run(check: bool, verbose: bool) {
35     fn try_run(context: &FmtContext) -> Result<bool, CliError> {
36         let mut success = true;
37
38         let project_root = project_root()?;
39
40         rustfmt_test(context)?;
41
42         success &= cargo_fmt(context, project_root.as_path())?;
43         success &= cargo_fmt(context, &project_root.join("clippy_dev"))?;
44         success &= cargo_fmt(context, &project_root.join("rustc_tools_util"))?;
45
46         for entry in WalkDir::new(project_root.join("tests")) {
47             let entry = entry?;
48             let path = entry.path();
49
50             if path.extension() != Some("rs".as_ref())
51                 || entry.file_name() == "ice-3891.rs"
52                 // Avoid rustfmt bug rust-lang/rustfmt#1873
53                 || cfg!(windows) && entry.file_name() == "implicit_hasher.rs"
54             {
55                 continue;
56             }
57
58             success &= rustfmt(context, &path)?;
59         }
60
61         Ok(success)
62     }
63
64     fn output_err(err: CliError) {
65         match err {
66             CliError::CommandFailed(command) => {
67                 eprintln!("error: A command failed! `{}`", command);
68             },
69             CliError::IoError(err) => {
70                 eprintln!("error: {}", err);
71             },
72             CliError::ProjectRootNotFound => {
73                 eprintln!("error: Can't determine root of project. Please run inside a Clippy working dir.");
74             },
75             CliError::RustfmtNotInstalled => {
76                 eprintln!("error: rustfmt nightly is not installed.");
77             },
78             CliError::WalkDirError(err) => {
79                 eprintln!("error: {}", err);
80             },
81         }
82     }
83
84     let context = FmtContext { check, verbose };
85     let result = try_run(&context);
86     let code = match result {
87         Ok(true) => 0,
88         Ok(false) => {
89             eprintln!();
90             eprintln!("Formatting check failed.");
91             eprintln!("Run `./util/dev fmt` to update formatting.");
92             1
93         },
94         Err(err) => {
95             output_err(err);
96             1
97         },
98     };
99     process::exit(code);
100 }
101
102 fn format_command(program: impl AsRef<OsStr>, dir: impl AsRef<Path>, args: &[impl AsRef<OsStr>]) -> String {
103     let arg_display: Vec<_> = args.iter().map(|a| escape(a.as_ref().to_string_lossy())).collect();
104
105     format!(
106         "cd {} && {} {}",
107         escape(dir.as_ref().to_string_lossy()),
108         escape(program.as_ref().to_string_lossy()),
109         arg_display.join(" ")
110     )
111 }
112
113 fn exec(
114     context: &FmtContext,
115     program: impl AsRef<OsStr>,
116     dir: impl AsRef<Path>,
117     args: &[impl AsRef<OsStr>],
118 ) -> Result<bool, CliError> {
119     if context.verbose {
120         println!("{}", format_command(&program, &dir, args));
121     }
122
123     let mut child = Command::new(&program).current_dir(&dir).args(args.iter()).spawn()?;
124     let code = child.wait()?;
125     let success = code.success();
126
127     if !context.check && !success {
128         return Err(CliError::CommandFailed(format_command(&program, &dir, args)));
129     }
130
131     Ok(success)
132 }
133
134 fn cargo_fmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
135     let mut args = vec!["+nightly", "fmt", "--all"];
136     if context.check {
137         args.push("--");
138         args.push("--check");
139     }
140     let success = exec(context, &bin_path("cargo"), path, &args)?;
141
142     Ok(success)
143 }
144
145 fn rustfmt_test(context: &FmtContext) -> Result<(), CliError> {
146     let program = bin_path("rustfmt");
147     let dir = std::env::current_dir()?;
148     let args = &["+nightly", "--version"];
149
150     if context.verbose {
151         println!("{}", format_command(&program, &dir, args));
152     }
153
154     let output = Command::new(&program).current_dir(&dir).args(args.iter()).output()?;
155
156     if output.status.success() {
157         Ok(())
158     } else if std::str::from_utf8(&output.stderr)
159         .unwrap_or("")
160         .starts_with("error: 'rustfmt' is not installed")
161     {
162         Err(CliError::RustfmtNotInstalled)
163     } else {
164         Err(CliError::CommandFailed(format_command(&program, &dir, args)))
165     }
166 }
167
168 fn rustfmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
169     let mut args = vec!["+nightly".as_ref(), path.as_os_str()];
170     if context.check {
171         args.push("--check".as_ref());
172     }
173     let success = exec(context, &bin_path("rustfmt"), std::env::current_dir()?, &args)?;
174     if !success {
175         eprintln!("rustfmt failed on {}", path.display());
176     }
177     Ok(success)
178 }
179
180 fn project_root() -> Result<PathBuf, CliError> {
181     let current_dir = std::env::current_dir()?;
182     for path in current_dir.ancestors() {
183         let result = std::fs::read_to_string(path.join("Cargo.toml"));
184         if let Err(err) = &result {
185             if err.kind() == io::ErrorKind::NotFound {
186                 continue;
187             }
188         }
189
190         let content = result?;
191         if content.contains("[package]\nname = \"clippy\"") {
192             return Ok(path.to_path_buf());
193         }
194     }
195
196     Err(CliError::ProjectRootNotFound)
197 }
198
199 // Workaround for https://github.com/rust-lang/cargo/issues/7475.
200 // FIXME: replace `&bin_path("command")` with `"command"` once the issue is fixed
201 fn bin_path(bin: &str) -> String {
202     let mut p = home::cargo_home().unwrap();
203     p.push("bin");
204     p.push(bin);
205     p.display().to_string()
206 }