]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/fmt.rs
Use home::cargo_home
[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
104         .iter()
105         .map(|a| escape(a.as_ref().to_string_lossy()).to_owned())
106         .collect();
107
108     format!(
109         "cd {} && {} {}",
110         escape(dir.as_ref().to_string_lossy()),
111         escape(program.as_ref().to_string_lossy()),
112         arg_display.join(" ")
113     )
114 }
115
116 fn exec(
117     context: &FmtContext,
118     program: impl AsRef<OsStr>,
119     dir: impl AsRef<Path>,
120     args: &[impl AsRef<OsStr>],
121 ) -> Result<bool, CliError> {
122     if context.verbose {
123         println!("{}", format_command(&program, &dir, args));
124     }
125
126     let mut child = Command::new(&program).current_dir(&dir).args(args.iter()).spawn()?;
127     let code = child.wait()?;
128     let success = code.success();
129
130     if !context.check && !success {
131         return Err(CliError::CommandFailed(format_command(&program, &dir, args)));
132     }
133
134     Ok(success)
135 }
136
137 fn cargo_fmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
138     let mut args = vec!["+nightly", "fmt", "--all"];
139     if context.check {
140         args.push("--");
141         args.push("--check");
142     }
143     let success = exec(context, &bin_path("cargo"), path, &args)?;
144
145     Ok(success)
146 }
147
148 fn rustfmt_test(context: &FmtContext) -> Result<(), CliError> {
149     let program = bin_path("rustfmt");
150     let dir = std::env::current_dir()?;
151     let args = &["+nightly", "--version"];
152
153     if context.verbose {
154         println!("{}", format_command(&program, &dir, args));
155     }
156
157     let output = Command::new(&program).current_dir(&dir).args(args.iter()).output()?;
158
159     if output.status.success() {
160         Ok(())
161     } else if std::str::from_utf8(&output.stderr)
162         .unwrap_or("")
163         .starts_with("error: 'rustfmt' is not installed")
164     {
165         Err(CliError::RustfmtNotInstalled)
166     } else {
167         Err(CliError::CommandFailed(format_command(&program, &dir, args)))
168     }
169 }
170
171 fn rustfmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
172     let mut args = vec!["+nightly".as_ref(), path.as_os_str()];
173     if context.check {
174         args.push("--check".as_ref());
175     }
176     let success = exec(context, &bin_path("rustfmt"), std::env::current_dir()?, &args)?;
177     if !success {
178         eprintln!("rustfmt failed on {}", path.display());
179     }
180     Ok(success)
181 }
182
183 fn project_root() -> Result<PathBuf, CliError> {
184     let current_dir = std::env::current_dir()?;
185     for path in current_dir.ancestors() {
186         let result = std::fs::read_to_string(path.join("Cargo.toml"));
187         if let Err(err) = &result {
188             if err.kind() == io::ErrorKind::NotFound {
189                 continue;
190             }
191         }
192
193         let content = result?;
194         if content.contains("[package]\nname = \"clippy\"") {
195             return Ok(path.to_path_buf());
196         }
197     }
198
199     Err(CliError::ProjectRootNotFound)
200 }
201
202 // Workaround for https://github.com/rust-lang/cargo/issues/7475.
203 // FIXME: replace `&bin_path("command")` with `"command"` once the issue is fixed
204 fn bin_path(bin: &str) -> String {
205     let mut p = home::cargo_home().unwrap();
206     p.push("bin");
207     p.push(bin);
208     p.display().to_string()
209 }