Skip to content

Commit e7668b4

Browse files
committed
Add --stdin-file-hint rustfmt command line option
When formatting files via stdin rustfmt didn't have a way to ignore stdin input. Now, when passing input to rustfmt via stdin one can also provide the `--stdin-file-hint` option to inform rustfmt that the input is actually from the hinted at file. rustfmt now uses this hint to determine if it can ignore formatting stdin. Note: This option is intended for text editor plugins that call rustfmt by passing input via stdin (e.g. rust-analyzer).
1 parent c981e59 commit e7668b4

File tree

6 files changed

+224
-5
lines changed

6 files changed

+224
-5
lines changed

src/bin/main.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ pub enum OperationError {
9090
/// supported with standard input.
9191
#[error("Emit mode {0} not supported with standard output.")]
9292
StdinBadEmit(EmitMode),
93+
/// Using `--std-file-hint` incorrectly
94+
#[error("{0}")]
95+
StdInFileHint(StdInFileHintError),
96+
}
97+
98+
#[derive(Error, Debug)]
99+
pub enum StdInFileHintError {
100+
/// The file hint does not exist
101+
#[error("`--std-file-hint={0:?}` could not be found")]
102+
NotFound(PathBuf),
103+
/// The file hint isn't a rust file
104+
#[error("`--std-file-hint={0:?}` is not a rust file")]
105+
NotRustFile(PathBuf),
106+
/// Attempted to pass --std-file-hint without passing input through stdin
107+
#[error("Cannot use `--std-file-hint` without formatting input from stdin.")]
108+
NotFormttingWithStdIn,
93109
}
94110

95111
impl From<IoError> for OperationError {
@@ -156,6 +172,14 @@ fn make_opts() -> Options {
156172
"Set options from command line. These settings take priority over .rustfmt.toml",
157173
"[key1=val1,key2=val2...]",
158174
);
175+
opts.optopt(
176+
"",
177+
"stdin-file-hint",
178+
"Inform rustfmt that the text passed to stdin is from the given file. \
179+
This option can only be passed when formatting text via stdin, \
180+
and the file name is used to determine if rustfmt can skip formatting the input.",
181+
"[Path to a rust file.]",
182+
);
159183

160184
if is_nightly {
161185
opts.optflag(
@@ -262,6 +286,11 @@ fn format_string(input: String, options: GetOptsOptions) -> Result<i32> {
262286
// try to read config from local directory
263287
let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
264288

289+
if rustfmt::is_std_ignored(options.stdin_file_hint, &config.ignore()) {
290+
io::stdout().write_all(input.as_bytes())?;
291+
return Ok(0);
292+
}
293+
265294
if options.check {
266295
config.set().emit_mode(EmitMode::Diff);
267296
} else {
@@ -494,6 +523,13 @@ fn determine_operation(matches: &Matches) -> Result<Operation, OperationError> {
494523
return Ok(Operation::Stdin { input: buffer });
495524
}
496525

526+
// User's can only pass `--stdin-file-hint` when formating files via stdin.
527+
if matches.opt_present("stdin-file-hint") {
528+
return Err(OperationError::StdInFileHint(
529+
StdInFileHintError::NotFormttingWithStdIn,
530+
));
531+
}
532+
497533
Ok(Operation::Format {
498534
files,
499535
minimal_config_path,
@@ -519,6 +555,7 @@ struct GetOptsOptions {
519555
unstable_features: bool,
520556
error_on_unformatted: Option<bool>,
521557
print_misformatted_file_names: bool,
558+
stdin_file_hint: Option<PathBuf>,
522559
}
523560

524561
impl GetOptsOptions {
@@ -568,6 +605,20 @@ impl GetOptsOptions {
568605
}
569606

570607
options.config_path = matches.opt_str("config-path").map(PathBuf::from);
608+
options.stdin_file_hint = matches.opt_str("stdin-file-hint").map(PathBuf::from);
609+
610+
// return early if there are issues with the file hint specified
611+
if let Some(file_hint) = &options.stdin_file_hint {
612+
if !file_hint.exists() {
613+
return Err(StdInFileHintError::NotFound(file_hint.to_owned()))?;
614+
}
615+
616+
if let Some(ext) = file_hint.extension() {
617+
if ext != "rs" {
618+
return Err(StdInFileHintError::NotRustFile(file_hint.to_owned()))?;
619+
}
620+
}
621+
}
571622

572623
options.inline_config = matches
573624
.opt_strs("config")

src/config/options.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@ impl IgnoreList {
399399
pub fn rustfmt_toml_path(&self) -> &Path {
400400
&self.rustfmt_toml_path
401401
}
402+
403+
pub fn is_empty(&self) -> bool {
404+
self.path_set.is_empty()
405+
}
406+
407+
pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
408+
self.path_set.contains(path.as_ref())
409+
}
402410
}
403411

404412
impl FromStr for IgnoreList {

src/ignore_path.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use ignore::gitignore;
2-
31
use crate::config::{FileName, IgnoreList};
2+
use ignore::gitignore;
3+
use std::path::{Path, PathBuf};
44

55
pub(crate) struct IgnorePathSet {
66
ignore_set: gitignore::Gitignore,
@@ -30,6 +30,33 @@ impl IgnorePathSet {
3030
}
3131
}
3232

33+
/// Determine if input from stdin should be ignored by rustfmt.
34+
/// See the `ignore` configuration options for details on specifying ignore files.
35+
pub fn is_std_ignored(file_hint: Option<PathBuf>, ignore_list: &IgnoreList) -> bool {
36+
// trivially return false, because no files are ignored
37+
if ignore_list.is_empty() {
38+
return false;
39+
}
40+
41+
// trivially return true, because everything is ignored when "/" is in the ignore list
42+
if ignore_list.contains(Path::new("/")) {
43+
return true;
44+
}
45+
46+
// See if the hinted stdin input is an ignored file.
47+
if let Some(std_file_hint) = file_hint {
48+
let file = FileName::Real(std_file_hint);
49+
match IgnorePathSet::from_ignore_list(ignore_list) {
50+
Ok(ignore_set) if ignore_set.is_match(&file) => {
51+
debug!("{:?} is ignored", file);
52+
return true;
53+
}
54+
_ => {}
55+
}
56+
}
57+
false
58+
}
59+
3360
#[cfg(test)]
3461
mod test {
3562
use rustfmt_config_proc_macro::nightly_only_test;
@@ -67,4 +94,35 @@ mod test {
6794
assert!(ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz/a.rs"))));
6895
assert!(!ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz/what.rs"))));
6996
}
97+
98+
#[test]
99+
fn test_is_std_ignored() {
100+
use serde_json;
101+
use std::path::PathBuf;
102+
103+
use super::is_std_ignored;
104+
use crate::config::IgnoreList;
105+
106+
let ignore_list: IgnoreList = serde_json::from_str(r#"["foo.rs","bar_dir/*"]"#).unwrap();
107+
assert!(is_std_ignored(Some(PathBuf::from("foo.rs")), &ignore_list));
108+
assert!(is_std_ignored(
109+
Some(PathBuf::from("src/foo.rs")),
110+
&ignore_list
111+
));
112+
assert!(is_std_ignored(
113+
Some(PathBuf::from("bar_dir/bar/bar.rs")),
114+
&ignore_list
115+
));
116+
117+
assert!(!is_std_ignored(Some(PathBuf::from("baz.rs")), &ignore_list));
118+
assert!(!is_std_ignored(
119+
Some(PathBuf::from("src/baz.rs")),
120+
&ignore_list
121+
));
122+
assert!(!is_std_ignored(
123+
Some(PathBuf::from("baz_dir/baz/baz.rs")),
124+
&ignore_list
125+
));
126+
assert!(!is_std_ignored(None, &ignore_list));
127+
}
70128
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ mod expr;
7676
mod format_report_formatter;
7777
pub(crate) mod formatting;
7878
mod ignore_path;
79+
pub use ignore_path::is_std_ignored;
7980
mod imports;
8081
mod items;
8182
mod lists;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ignore = [
2+
"src/lib.rs"
3+
]

tests/rustfmt/main.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,21 @@ fn rustfmt_emits_error_when_control_brace_style_is_always_next_line() {
242242
}
243243
mod rustfmt_stdin_formatting {
244244
use super::rustfmt_std_input;
245+
use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};
245246

246247
#[rustfmt::skip]
247248
#[test]
248249
fn changes_are_output_to_stdout() {
249-
let args = [];
250+
// line endings are normalized to '\n' to avoid platform differences
251+
let args = ["--config", "newline_style=Unix"];
250252
let source = "fn main () { println!(\"hello world!\"); }";
251253
let (stdout, _stderr) = rustfmt_std_input(&args, source);
252254
let expected_output =
253255
r#"fn main() {
254256
println!("hello world!");
255-
}"#;
256-
assert!(stdout.contains(expected_output))
257+
}
258+
"#;
259+
assert!(stdout == expected_output);
257260
}
258261

259262
#[test]
@@ -264,4 +267,99 @@ r#"fn main() {
264267
let (stdout, _stderr) = rustfmt_std_input(&args, source);
265268
assert!(stdout.trim_end() == source)
266269
}
270+
271+
#[nightly_only_test]
272+
#[test]
273+
fn input_ignored_when_stdin_file_hint_is_ignored() {
274+
// NOTE: the source is not properly formatted, but we're giving rustfmt a hint that
275+
// the input actually corresponds to `src/lib.rs`, which is ignored in the given config file
276+
let args = [
277+
"--stdin-file-hint",
278+
"src/lib.rs",
279+
"--config-path",
280+
"tests/config/stdin-file-hint-ignore.toml",
281+
];
282+
let source = "fn main () { println!(\"hello world!\"); }";
283+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
284+
assert!(stdout.trim_end() == source)
285+
}
286+
287+
#[rustfmt::skip]
288+
#[nightly_only_test]
289+
#[test]
290+
fn input_formatted_when_stdin_file_hint_is_not_ignored() {
291+
// NOTE: `src/bin/main.rs` is not ignored in the config file so the input is formatted.
292+
// line endings are normalized to '\n' to avoid platform differences
293+
let args = [
294+
"--stdin-file-hint",
295+
"src/bin/main.rs",
296+
"--config-path",
297+
"tests/config/stdin-file-hint-ignore.toml",
298+
"--config",
299+
"newline_style=Unix",
300+
];
301+
let source = "fn main () { println!(\"hello world!\"); }";
302+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
303+
let expected_output =
304+
r#"fn main() {
305+
println!("hello world!");
306+
}
307+
"#;
308+
assert!(stdout == expected_output);
309+
}
310+
311+
#[rustfmt::skip]
312+
#[stable_only_test]
313+
#[test]
314+
fn ignore_list_is_not_set_on_stable_channel_and_therefore_stdin_file_hint_does_nothing() {
315+
// NOTE: the source is not properly formatted, and although the file is `ignored` we
316+
// can't set the `ignore` list on the stable channel so the input is formatted
317+
// line endings are normalized to '\n' to avoid platform differences
318+
let args = [
319+
"--stdin-file-hint",
320+
"src/lib.rs",
321+
"--config-path",
322+
"tests/config/stdin-file-hint-ignore.toml",
323+
"--config",
324+
"newline_style=Unix",
325+
];
326+
let source = "fn main () { println!(\"hello world!\"); }";
327+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
328+
let expected_output =
329+
r#"fn main() {
330+
println!("hello world!");
331+
}
332+
"#;
333+
assert!(stdout == expected_output);
334+
335+
}
336+
}
337+
338+
mod stdin_file_hint {
339+
use super::{rustfmt, rustfmt_std_input};
340+
341+
#[test]
342+
fn error_not_a_rust_file() {
343+
let args = ["--stdin-file-hint", "README.md"];
344+
let source = "fn main() {}";
345+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
346+
assert!(stderr.contains("`--std-file-hint=\"README.md\"` is not a rust file"))
347+
}
348+
349+
#[test]
350+
fn error_file_not_found() {
351+
let args = ["--stdin-file-hint", "does_not_exist.rs"];
352+
let source = "fn main() {}";
353+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
354+
assert!(stderr.contains("`--std-file-hint=\"does_not_exist.rs\"` could not be found"))
355+
}
356+
357+
#[test]
358+
fn cant_use_stdin_file_hint_if_input_not_passed_to_rustfmt_via_stdin() {
359+
let args = ["--stdin-file-hint", "src/lib.rs", "src/lib.rs"];
360+
let (_stdout, stderr) = rustfmt(&args);
361+
assert!(
362+
stderr.contains("Cannot use `--std-file-hint` without formatting input from stdin.")
363+
);
364+
}
267365
}

0 commit comments

Comments
 (0)