]>
git.proxmox.com Git - rustc.git/blob - src/tools/rustfmt/src/cargo-fmt/main.rs
759b21218c353e70787f853a6c09725bf7132b10
1 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/).
4 #![allow(clippy::match_like_matches_macro)]
6 use std
::cmp
::Ordering
;
7 use std
::collections
::{BTreeMap, BTreeSet}
;
11 use std
::hash
::{Hash, Hasher}
;
12 use std
::io
::{self, Write}
;
13 use std
::iter
::FromIterator
;
14 use std
::path
::{Path, PathBuf}
;
15 use std
::process
::Command
;
18 use structopt
::StructOpt
;
20 #[path = "test/mod.rs"]
24 #[derive(StructOpt, Debug)]
26 bin_name
= "cargo fmt",
27 about
= "This utility formats all bin and lib files of \
28 the current crate using rustfmt."
31 /// No output printed to stdout
32 #[structopt(short = "q", long = "quiet")]
35 /// Use verbose output
36 #[structopt(short = "v", long = "verbose")]
39 /// Print rustfmt version and exit
40 #[structopt(long = "version")]
43 /// Specify package to format
44 #[structopt(short = "p", long = "package", value_name = "package")]
45 packages
: Vec
<String
>,
47 /// Specify path to Cargo.toml
48 #[structopt(long = "manifest-path", value_name = "manifest-path")]
49 manifest_path
: Option
<String
>,
51 /// Specify message-format: short|json|human
52 #[structopt(long = "message-format", value_name = "message-format")]
53 message_format
: Option
<String
>,
55 /// Options passed to rustfmt
56 // 'raw = true' to make `--` explicit.
57 #[structopt(name = "rustfmt_options", raw(true))]
58 rustfmt_options
: Vec
<String
>,
60 /// Format all packages, and also their local path-based dependencies
61 #[structopt(long = "all")]
64 /// Run rustfmt in check mode
65 #[structopt(long = "check")]
70 let exit_status
= execute();
71 std
::io
::stdout().flush().unwrap();
72 std
::process
::exit(exit_status
);
75 const SUCCESS
: i32 = 0;
76 const FAILURE
: i32 = 1;
79 // Drop extra `fmt` argument provided by `cargo`.
80 let mut found_fmt
= false;
81 let args
= env
::args().filter(|x
| {
85 found_fmt
= x
== "fmt";
90 let opts
= Opts
::from_iter(args
);
92 let verbosity
= match (opts
.verbose
, opts
.quiet
) {
93 (false, false) => Verbosity
::Normal
,
94 (false, true) => Verbosity
::Quiet
,
95 (true, false) => Verbosity
::Verbose
,
97 print_usage_to_stderr("quiet mode and verbose mode are not compatible");
103 return handle_command_status(get_rustfmt_info(&[String
::from("--version")]));
105 if opts
.rustfmt_options
.iter().any(|s
| {
106 ["--print-config", "-h", "--help", "-V", "--version"].contains(&s
.as_str())
107 || s
.starts_with("--help=")
108 || s
.starts_with("--print-config=")
110 return handle_command_status(get_rustfmt_info(&opts
.rustfmt_options
));
113 let strategy
= CargoFmtStrategy
::from_opts(&opts
);
114 let mut rustfmt_args
= opts
.rustfmt_options
;
116 let check_flag
= "--check";
117 if !rustfmt_args
.iter().any(|o
| o
== check_flag
) {
118 rustfmt_args
.push(check_flag
.to_owned());
121 if let Some(message_format
) = opts
.message_format
{
122 if let Err(msg
) = convert_message_format_to_rustfmt_args(&message_format
, &mut rustfmt_args
)
124 print_usage_to_stderr(&msg
);
129 if let Some(specified_manifest_path
) = opts
.manifest_path
{
130 if !specified_manifest_path
.ends_with("Cargo.toml") {
131 print_usage_to_stderr("the manifest-path must be a path to a Cargo.toml file");
134 let manifest_path
= PathBuf
::from(specified_manifest_path
);
135 handle_command_status(format_crate(
139 Some(&manifest_path
),
142 handle_command_status(format_crate(verbosity
, &strategy
, rustfmt_args
, None
))
146 fn rustfmt_command() -> Command
{
147 let rustfmt_var
= env
::var_os("RUSTFMT");
148 let rustfmt
= match &rustfmt_var
{
149 Some(rustfmt
) => rustfmt
,
150 None
=> OsStr
::new("rustfmt"),
152 Command
::new(rustfmt
)
155 fn convert_message_format_to_rustfmt_args(
156 message_format
: &str,
157 rustfmt_args
: &mut Vec
<String
>,
158 ) -> Result
<(), String
> {
159 let mut contains_emit_mode
= false;
160 let mut contains_check
= false;
161 let mut contains_list_files
= false;
162 for arg
in rustfmt_args
.iter() {
163 if arg
.starts_with("--emit") {
164 contains_emit_mode
= true;
166 if arg
== "--check" {
167 contains_check
= true;
169 if arg
== "-l" || arg
== "--files-with-diff" {
170 contains_list_files
= true;
173 match message_format
{
175 if !contains_list_files
{
176 rustfmt_args
.push(String
::from("-l"));
181 if contains_emit_mode
{
182 return Err(String
::from(
183 "cannot include --emit arg when --message-format is set to json",
187 return Err(String
::from(
188 "cannot include --check arg when --message-format is set to json",
191 rustfmt_args
.push(String
::from("--emit"));
192 rustfmt_args
.push(String
::from("json"));
198 "invalid --message-format value: {}. Allowed values are: short|json|human",
205 fn print_usage_to_stderr(reason
: &str) {
206 eprintln
!("{}", reason
);
207 let app
= Opts
::clap();
209 .write_help(&mut io
::stderr())
210 .expect("failed to write to stderr");
213 #[derive(Debug, Clone, Copy, PartialEq)]
220 fn handle_command_status(status
: Result
<i32, io
::Error
>) -> i32 {
223 print_usage_to_stderr(&e
.to_string());
226 Ok(status
) => status
,
230 fn get_rustfmt_info(args
: &[String
]) -> Result
<i32, io
::Error
> {
231 let mut command
= rustfmt_command()
232 .stdout(std
::process
::Stdio
::inherit())
235 .map_err(|e
| match e
.kind() {
236 io
::ErrorKind
::NotFound
=> io
::Error
::new(
237 io
::ErrorKind
::Other
,
238 "Could not run rustfmt, please make sure it is in your PATH.",
242 let result
= command
.wait()?
;
243 if result
.success() {
246 Ok(result
.code().unwrap_or(SUCCESS
))
251 verbosity
: Verbosity
,
252 strategy
: &CargoFmtStrategy
,
253 rustfmt_args
: Vec
<String
>,
254 manifest_path
: Option
<&Path
>,
255 ) -> Result
<i32, io
::Error
> {
256 let targets
= get_targets(strategy
, manifest_path
)?
;
258 // Currently only bin and lib files get formatted.
259 run_rustfmt(&targets
, &rustfmt_args
, verbosity
)
262 /// Target uses a `path` field for equality and hashing.
265 /// A path to the main source file of the target.
267 /// A kind of target (e.g., lib, bin, example, ...).
269 /// Rust edition for this target.
274 pub fn from_target(target
: &cargo_metadata
::Target
) -> Self {
275 let path
= PathBuf
::from(&target
.src_path
);
276 let canonicalized
= fs
::canonicalize(&path
).unwrap_or(path
);
280 kind
: target
.kind
[0].clone(),
281 edition
: target
.edition
.clone(),
286 impl PartialEq
for Target
{
287 fn eq(&self, other
: &Target
) -> bool
{
288 self.path
== other
.path
292 impl PartialOrd
for Target
{
293 fn partial_cmp(&self, other
: &Target
) -> Option
<Ordering
> {
294 Some(self.path
.cmp(&other
.path
))
298 impl Ord
for Target
{
299 fn cmp(&self, other
: &Target
) -> Ordering
{
300 self.path
.cmp(&other
.path
)
304 impl Eq
for Target {}
306 impl Hash
for Target
{
307 fn hash
<H
: Hasher
>(&self, state
: &mut H
) {
308 self.path
.hash(state
);
312 #[derive(Debug, PartialEq, Eq)]
313 pub enum CargoFmtStrategy
{
314 /// Format every packages and dependencies.
316 /// Format packages that are specified by the command line argument.
318 /// Format the root packages only.
322 impl CargoFmtStrategy
{
323 pub fn from_opts(opts
: &Opts
) -> CargoFmtStrategy
{
324 match (opts
.format_all
, opts
.packages
.is_empty()) {
325 (false, true) => CargoFmtStrategy
::Root
,
326 (true, _
) => CargoFmtStrategy
::All
,
327 (false, false) => CargoFmtStrategy
::Some(opts
.packages
.clone()),
332 /// Based on the specified `CargoFmtStrategy`, returns a set of main source files.
334 strategy
: &CargoFmtStrategy
,
335 manifest_path
: Option
<&Path
>,
336 ) -> Result
<BTreeSet
<Target
>, io
::Error
> {
337 let mut targets
= BTreeSet
::new();
340 CargoFmtStrategy
::Root
=> get_targets_root_only(manifest_path
, &mut targets
)?
,
341 CargoFmtStrategy
::All
=> {
342 get_targets_recursive(manifest_path
, &mut targets
, &mut BTreeSet
::new())?
344 CargoFmtStrategy
::Some(ref hitlist
) => {
345 get_targets_with_hitlist(manifest_path
, hitlist
, &mut targets
)?
349 if targets
.is_empty() {
351 io
::ErrorKind
::Other
,
352 "Failed to find targets".to_owned(),
359 fn get_targets_root_only(
360 manifest_path
: Option
<&Path
>,
361 targets
: &mut BTreeSet
<Target
>,
362 ) -> Result
<(), io
::Error
> {
363 let metadata
= get_cargo_metadata(manifest_path
)?
;
364 let workspace_root_path
= PathBuf
::from(&metadata
.workspace_root
).canonicalize()?
;
365 let (in_workspace_root
, current_dir_manifest
) = if let Some(target_manifest
) = manifest_path
{
367 workspace_root_path
== target_manifest
,
368 target_manifest
.canonicalize()?
,
371 let current_dir
= env
::current_dir()?
.canonicalize()?
;
373 workspace_root_path
== current_dir
,
374 current_dir
.join("Cargo.toml"),
378 let package_targets
= match metadata
.packages
.len() {
379 1 => metadata
.packages
.into_iter().next().unwrap().targets
,
385 || PathBuf
::from(&p
.manifest_path
)
388 == current_dir_manifest
395 for target
in package_targets
{
396 targets
.insert(Target
::from_target(&target
));
402 fn get_targets_recursive(
403 manifest_path
: Option
<&Path
>,
404 targets
: &mut BTreeSet
<Target
>,
405 visited
: &mut BTreeSet
<String
>,
406 ) -> Result
<(), io
::Error
> {
407 let metadata
= get_cargo_metadata(manifest_path
)?
;
408 for package
in &metadata
.packages
{
409 add_targets(&package
.targets
, targets
);
411 // Look for local dependencies using information available since cargo v1.51
412 // It's theoretically possible someone could use a newer version of rustfmt with
413 // a much older version of `cargo`, but we don't try to explicitly support that scenario.
414 // If someone reports an issue with path-based deps not being formatted, be sure to
415 // confirm their version of `cargo` (not `cargo-fmt`) is >= v1.51
416 // https://github.com/rust-lang/cargo/pull/8994
417 for dependency
in &package
.dependencies
{
418 if dependency
.path
.is_none() || visited
.contains(&dependency
.name
) {
422 let manifest_path
= PathBuf
::from(dependency
.path
.as_ref().unwrap()).join("Cargo.toml");
423 if manifest_path
.exists()
427 .any(|p
| p
.manifest_path
.eq(&manifest_path
))
429 visited
.insert(dependency
.name
.to_owned());
430 get_targets_recursive(Some(&manifest_path
), targets
, visited
)?
;
438 fn get_targets_with_hitlist(
439 manifest_path
: Option
<&Path
>,
441 targets
: &mut BTreeSet
<Target
>,
442 ) -> Result
<(), io
::Error
> {
443 let metadata
= get_cargo_metadata(manifest_path
)?
;
444 let mut workspace_hitlist
: BTreeSet
<&String
> = BTreeSet
::from_iter(hitlist
);
446 for package
in metadata
.packages
{
447 if workspace_hitlist
.remove(&package
.name
) {
448 for target
in package
.targets
{
449 targets
.insert(Target
::from_target(&target
));
454 if workspace_hitlist
.is_empty() {
457 let package
= workspace_hitlist
.iter().next().unwrap();
459 io
::ErrorKind
::InvalidInput
,
460 format
!("package `{}` is not a member of the workspace", package
),
465 fn add_targets(target_paths
: &[cargo_metadata
::Target
], targets
: &mut BTreeSet
<Target
>) {
466 for target
in target_paths
{
467 targets
.insert(Target
::from_target(target
));
472 targets
: &BTreeSet
<Target
>,
474 verbosity
: Verbosity
,
475 ) -> Result
<i32, io
::Error
> {
476 let by_edition
= targets
479 if verbosity
== Verbosity
::Verbose
{
480 println
!("[{} ({})] {:?}", t
.kind
, t
.edition
, t
.path
)
483 .fold(BTreeMap
::new(), |mut h
, t
| {
484 h
.entry(&t
.edition
).or_insert_with(Vec
::new
).push(&t
.path
);
488 let mut status
= vec
![];
489 for (edition
, files
) in by_edition
{
490 let stdout
= if verbosity
== Verbosity
::Quiet
{
491 std
::process
::Stdio
::null()
493 std
::process
::Stdio
::inherit()
496 if verbosity
== Verbosity
::Verbose
{
498 print
!(" --edition {}", edition
);
499 fmt_args
.iter().for_each(|f
| print
!(" {}", f
));
500 files
.iter().for_each(|f
| print
!(" {}", f
.display()));
504 let mut command
= rustfmt_command()
507 .args(&["--edition", edition
])
510 .map_err(|e
| match e
.kind() {
511 io
::ErrorKind
::NotFound
=> io
::Error
::new(
512 io
::ErrorKind
::Other
,
513 "Could not run rustfmt, please make sure it is in your PATH.",
518 status
.push(command
.wait()?
);
523 .filter_map(|s
| if s
.success() { None }
else { s.code() }
)
528 fn get_cargo_metadata(manifest_path
: Option
<&Path
>) -> Result
<cargo_metadata
::Metadata
, io
::Error
> {
529 let mut cmd
= cargo_metadata
::MetadataCommand
::new();
531 if let Some(manifest_path
) = manifest_path
{
532 cmd
.manifest_path(manifest_path
);
534 cmd
.other_options(vec
![String
::from("--offline")]);
537 Ok(metadata
) => Ok(metadata
),
539 cmd
.other_options(vec
![]);
541 Ok(metadata
) => Ok(metadata
),
542 Err(error
) => Err(io
::Error
::new(io
::ErrorKind
::Other
, error
.to_string())),