Skip to content

Commit 91080f3

Browse files
committed
Allow cargo-fmt to handle formatting individual files.
Adds an additional command line option ``--src-file`` / ``-s`` It should be noted that this does not allow cargo-fmt to format an arbitrary rust file. The file must be contained with a target specified in a Cargo.toml file. Adjustments were Also made to prevent cargo-fmt from passing along certain arguments to rustfmt. For example, users can no longer pass rust files to rustfmt through cargo-fmt. Additionally, users cannot pass --edition to cargo-fmt, since editions are read from the Cargo.toml. Tests were added in ``src/cargo-fmt/test/mod.rs`` and integration tests were added to ``tests/cargo-fmt/main.rs``
1 parent 4389a4c commit 91080f3

File tree

3 files changed

+184
-3
lines changed

3 files changed

+184
-3
lines changed

src/cargo-fmt/main.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ pub struct Opts {
4444
#[structopt(short = "p", long = "package", value_name = "package")]
4545
packages: Vec<String>,
4646

47+
/// Specify a source file to format
48+
#[structopt(short = "s", long = "src-file", value_name = "src-file")]
49+
src_file: Option<PathBuf>,
50+
4751
/// Specify path to Cargo.toml
4852
#[structopt(long = "manifest-path", value_name = "manifest-path")]
4953
manifest_path: Option<String>,
@@ -89,6 +93,28 @@ fn execute() -> i32 {
8993

9094
let opts = Opts::from_iter(args);
9195

96+
if opts.src_file.is_some() & !opts.packages.is_empty() {
97+
print_usage_to_stderr("cannot format source files and packages at the same time");
98+
return FAILURE;
99+
}
100+
101+
if opts.src_file.is_some() & opts.format_all {
102+
print_usage_to_stderr("cannot format all packages when specifying source files");
103+
return FAILURE;
104+
}
105+
106+
if opts.rustfmt_options.iter().any(|s| s.ends_with(".rs")) {
107+
print_usage_to_stderr(
108+
"cannot pass rust files to rustfmt through cargo-fmt. Use '--src-file' instead",
109+
);
110+
return FAILURE;
111+
}
112+
113+
if opts.rustfmt_options.iter().any(|s| s.contains("--edition")) {
114+
print_usage_to_stderr("cannot pass '--edition' to rustfmt through cargo-fmt");
115+
return FAILURE;
116+
}
117+
92118
let verbosity = match (opts.verbose, opts.quiet) {
93119
(false, false) => Verbosity::Normal,
94120
(false, true) => Verbosity::Quiet,
@@ -314,17 +340,23 @@ pub enum CargoFmtStrategy {
314340
/// Format every packages and dependencies.
315341
All,
316342
/// Format packages that are specified by the command line argument.
317-
Some(Vec<String>),
343+
Packages(Vec<String>),
318344
/// Format the root packages only.
319345
Root,
346+
/// Format individual source files specified by the command line arguments.
347+
SourceFile(PathBuf),
320348
}
321349

322350
impl CargoFmtStrategy {
323351
pub fn from_opts(opts: &Opts) -> CargoFmtStrategy {
352+
if let Some(ref src_file) = opts.src_file {
353+
return CargoFmtStrategy::SourceFile(src_file.clone());
354+
}
355+
324356
match (opts.format_all, opts.packages.is_empty()) {
325357
(false, true) => CargoFmtStrategy::Root,
326358
(true, _) => CargoFmtStrategy::All,
327-
(false, false) => CargoFmtStrategy::Some(opts.packages.clone()),
359+
(false, false) => CargoFmtStrategy::Packages(opts.packages.clone()),
328360
}
329361
}
330362
}
@@ -341,9 +373,12 @@ fn get_targets(
341373
CargoFmtStrategy::All => {
342374
get_targets_recursive(manifest_path, &mut targets, &mut BTreeSet::new())?
343375
}
344-
CargoFmtStrategy::Some(ref hitlist) => {
376+
CargoFmtStrategy::Packages(ref hitlist) => {
345377
get_targets_with_hitlist(manifest_path, hitlist, &mut targets)?
346378
}
379+
CargoFmtStrategy::SourceFile(ref src_file) => {
380+
get_target_from_src_file(manifest_path, &src_file, &mut targets)?
381+
}
347382
}
348383

349384
if targets.is_empty() {
@@ -356,6 +391,57 @@ fn get_targets(
356391
}
357392
}
358393

394+
fn get_target_from_src_file(
395+
manifest_path: Option<&Path>,
396+
src_file: &PathBuf,
397+
targets: &mut BTreeSet<Target>,
398+
) -> Result<(), io::Error> {
399+
let metadata = get_cargo_metadata(manifest_path)?;
400+
401+
let get_target = |src_path: &Path| {
402+
metadata
403+
.packages
404+
.iter()
405+
.map(|p| p.targets.iter())
406+
.flatten()
407+
.filter(|t| {
408+
let kind = &t.kind[0];
409+
// to prevent formatting any arbitrary file within the root of our
410+
// project we special case the build.rs script, becuase it's parent
411+
// is the root of the project and we would always select the custom-build
412+
// target in the event that we couldn't find a better target to associate
413+
// with our file.
414+
if kind == "custom-build" && !src_path.ends_with("build.rs") {
415+
return false;
416+
}
417+
418+
if let Ok(target_path) = fs::canonicalize(&t.src_path) {
419+
let target_dir = target_path
420+
.parent()
421+
.expect("Target src_path should have a parent directory");
422+
src_path.starts_with(target_dir)
423+
} else {
424+
false
425+
}
426+
})
427+
.max_by(|t1, t2| {
428+
let t1_len = t1.src_path.components().count();
429+
let t2_len = t2.src_path.components().count();
430+
t1_len.cmp(&t2_len)
431+
})
432+
};
433+
434+
let canonicalize = fs::canonicalize(&src_file)?;
435+
if let Some(target) = get_target(&canonicalize) {
436+
targets.insert(Target {
437+
path: src_file.to_owned(),
438+
kind: target.kind[0].clone(),
439+
edition: target.edition.clone(),
440+
});
441+
}
442+
Ok(())
443+
}
444+
359445
fn get_targets_root_only(
360446
manifest_path: Option<&Path>,
361447
targets: &mut BTreeSet<Target>,

src/cargo-fmt/test/mod.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use std::path::PathBuf;
23

34
mod message_format;
45
mod targets;
@@ -16,6 +17,7 @@ fn default_options() {
1617
assert_eq!(false, o.format_all);
1718
assert_eq!(None, o.manifest_path);
1819
assert_eq!(None, o.message_format);
20+
assert_eq!(None, o.src_file);
1921
}
2022

2123
#[test]
@@ -27,6 +29,8 @@ fn good_options() {
2729
"p1",
2830
"-p",
2931
"p2",
32+
"-s",
33+
"file.rs",
3034
"--message-format",
3135
"short",
3236
"--check",
@@ -39,6 +43,7 @@ fn good_options() {
3943
assert_eq!(false, o.version);
4044
assert_eq!(true, o.check);
4145
assert_eq!(vec!["p1", "p2"], o.packages);
46+
assert_eq!(Some(PathBuf::from("file.rs")), o.src_file);
4247
assert_eq!(vec!["--edition", "2018"], o.rustfmt_options);
4348
assert_eq!(false, o.format_all);
4449
assert_eq!(Some(String::from("short")), o.message_format);
@@ -90,6 +95,25 @@ fn multiple_packages_one_by_one() {
9095
assert_eq!(3, o.packages.len());
9196
}
9297

98+
#[test]
99+
fn cant_list_more_than_one_source_file() {
100+
assert!(
101+
Opts::clap()
102+
.get_matches_from_safe(&["test", "-s", "src/a.rs", "--src-file", "src/b.rs"])
103+
.is_err()
104+
);
105+
assert!(
106+
!Opts::clap()
107+
.get_matches_from_safe(&["test", "-s", "src/a.rs"])
108+
.is_err()
109+
);
110+
assert!(
111+
!Opts::clap()
112+
.get_matches_from_safe(&["test", "--src-file", "src/b.rs"])
113+
.is_err()
114+
);
115+
}
116+
93117
#[test]
94118
fn multiple_packages_grouped() {
95119
let o = Opts::from_iter(&[
@@ -135,3 +159,35 @@ fn empty_packages_4() {
135159
.is_err()
136160
);
137161
}
162+
163+
#[test]
164+
fn empty_source_files_1() {
165+
assert!(Opts::clap().get_matches_from_safe(&["test", "-s"]).is_err());
166+
}
167+
168+
#[test]
169+
fn empty_source_files_2() {
170+
assert!(
171+
Opts::clap()
172+
.get_matches_from_safe(&["test", "-s", "--", "--check"])
173+
.is_err()
174+
);
175+
}
176+
177+
#[test]
178+
fn empty_source_files_3() {
179+
assert!(
180+
Opts::clap()
181+
.get_matches_from_safe(&["test", "-s", "--verbose"])
182+
.is_err()
183+
);
184+
}
185+
186+
#[test]
187+
fn empty_source_files_4() {
188+
assert!(
189+
Opts::clap()
190+
.get_matches_from_safe(&["test", "-s", "--check"])
191+
.is_err()
192+
);
193+
}

tests/cargo-fmt/main.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,42 @@ fn rustfmt_help() {
7171
assert_that!(&["--", "-h"], contains("Format Rust code"));
7272
assert_that!(&["--", "--help=config"], contains("Configuration Options:"));
7373
}
74+
75+
#[test]
76+
fn cannot_pass_rust_files_to_rustfmt_through_caro_fmt() {
77+
let (_, stderr) = cargo_fmt(&["--", "src/main.rs"]);
78+
assert!(stderr.starts_with(
79+
"cannot pass rust files to rustfmt through cargo-fmt. Use '--src-file' instead"
80+
))
81+
}
82+
83+
#[test]
84+
fn cannot_pass_edition_to_rustfmt_through_caro_fmt() {
85+
let (_, stderr) = cargo_fmt(&["--", "--edition", "2021"]);
86+
assert!(stderr.starts_with("cannot pass '--edition' to rustfmt through cargo-fmt"))
87+
}
88+
89+
#[test]
90+
fn specify_source_files_when_running_cargo_fmt() {
91+
let path = "tests/cargo-fmt/source/workspaces/path-dep-above/e/src/main.rs";
92+
// test that we can run cargo-fmt on a single src file
93+
assert_that!(&["-v", "--check", "-s", path], contains(path));
94+
}
95+
96+
#[test]
97+
fn specify_source_files_in_a_workspace_when_running_cargo_fmt() {
98+
let path = "tests/cargo-fmt/source/workspaces/path-dep-above/ws/a/src/main.rs";
99+
assert_that!(&["-v", "--check", "-s", path], contains(path));
100+
}
101+
102+
#[test]
103+
fn formatting_source_files_and_packages_at_the_same_time_is_not_supported() {
104+
let (_, stderr) = cargo_fmt(&["--check", "-s", "src/main.rs", "-p", "p1"]);
105+
assert!(stderr.starts_with("cannot format source files and packages at the same time"))
106+
}
107+
108+
#[test]
109+
fn formatting_source_files_and_using_package_related_arguments_is_not_supported() {
110+
let (_, stderr) = cargo_fmt(&["--check", "--all", "-s", "src/main.rs"]);
111+
assert!(stderr.starts_with("cannot format all packages when specifying source files"))
112+
}

0 commit comments

Comments
 (0)