Skip to content

Commit bcf190d

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 d71f22d commit bcf190d

6 files changed

Lines changed: 224 additions & 3 deletions

File tree

src/bin/main.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ pub enum OperationError {
7878
/// supported with stdandard input.
7979
#[error("Emit mode {0} not supported with standard output.")]
8080
StdinBadEmit(EmitMode),
81+
/// Using `--std-file-hint` incorrectly
82+
#[error("{0}")]
83+
StdInFileHint(StdInFileHintError),
84+
}
85+
86+
#[derive(Error, Debug)]
87+
pub enum StdInFileHintError {
88+
/// The file hint does not exist
89+
#[error("`--std-file-hint={0:?}` could not be found")]
90+
NotFound(PathBuf),
91+
/// The file hint isn't a rust file
92+
#[error("`--std-file-hint={0:?}` is not a rust file")]
93+
NotRustFile(PathBuf),
94+
/// Attempted to pass --std-file-hint without passing input through stdin
95+
#[error("Cannot use `--std-file-hint` without formatting input from stdin.")]
96+
NotFormttingWithStdIn,
8197
}
8298

8399
impl From<IoError> for OperationError {
@@ -144,6 +160,14 @@ fn make_opts() -> Options {
144160
"Set options from command line. These settings take priority over .rustfmt.toml",
145161
"[key1=val1,key2=val2...]",
146162
);
163+
opts.optopt(
164+
"",
165+
"stdin-file-hint",
166+
"Inform rustfmt that the text passed to stdin is from the given file. \
167+
This option can only be passed when formatting text via stdin, \
168+
and the file name is used to determine if rustfmt can skip formatting the input.",
169+
"[Path to a rust file.]",
170+
);
147171

148172
if is_nightly {
149173
opts.optflag(
@@ -250,6 +274,11 @@ fn format_string(input: String, options: GetOptsOptions) -> Result<i32> {
250274
// try to read config from local directory
251275
let (mut config, _) = load_config(Some(Path::new(".")), Some(options.clone()))?;
252276

277+
if rustfmt::is_std_ignored(options.stdin_file_hint, &config.ignore()) {
278+
io::stdout().write_all(input.as_bytes())?;
279+
return Ok(0);
280+
}
281+
253282
if options.check {
254283
config.set().emit_mode(EmitMode::Diff);
255284
} else {
@@ -490,6 +519,13 @@ fn determine_operation(matches: &Matches) -> Result<Operation, OperationError> {
490519
return Ok(Operation::Stdin { input: buffer });
491520
}
492521

522+
// User's can only pass `--stdin-file-hint` when formating files via stdin.
523+
if matches.opt_present("stdin-file-hint") {
524+
return Err(OperationError::StdInFileHint(
525+
StdInFileHintError::NotFormttingWithStdIn,
526+
));
527+
}
528+
493529
Ok(Operation::Format {
494530
files,
495531
minimal_config_path,
@@ -515,6 +551,7 @@ struct GetOptsOptions {
515551
unstable_features: bool,
516552
error_on_unformatted: Option<bool>,
517553
print_misformatted_file_names: bool,
554+
stdin_file_hint: Option<PathBuf>,
518555
}
519556

520557
impl GetOptsOptions {
@@ -564,6 +601,20 @@ impl GetOptsOptions {
564601
}
565602

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

568619
options.inline_config = matches
569620
.opt_strs("config")

src/config/options.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,14 @@ impl IgnoreList {
391391
pub fn rustfmt_toml_path(&self) -> &Path {
392392
&self.rustfmt_toml_path
393393
}
394+
395+
pub fn is_empty(&self) -> bool {
396+
self.path_set.is_empty()
397+
}
398+
399+
pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
400+
self.path_set.contains(path.as_ref())
401+
}
394402
}
395403

396404
impl FromStr for IgnoreList {

src/ignore_path.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::{Path, PathBuf};
2+
13
use ignore::{self, gitignore};
24

35
use crate::config::{FileName, IgnoreList};
@@ -30,6 +32,33 @@ impl IgnorePathSet {
3032
}
3133
}
3234

35+
/// Determine if input from stdin should be ignored by rustfmt.
36+
/// See the `ignore` configuration options for details on specifying ignore files.
37+
pub fn is_std_ignored(file_hint: Option<PathBuf>, ignore_list: &IgnoreList) -> bool {
38+
// trivially return false, because no files are ignored
39+
if ignore_list.is_empty() {
40+
return false;
41+
}
42+
43+
// trivially return true, because everything is ignored when "/" is in the ignore list
44+
if ignore_list.contains(Path::new("/")) {
45+
return true;
46+
}
47+
48+
// See if the hinted stdin input is an ignored file.
49+
if let Some(std_file_hint) = file_hint {
50+
let file = FileName::Real(std_file_hint);
51+
match IgnorePathSet::from_ignore_list(ignore_list) {
52+
Ok(ignore_set) if ignore_set.is_match(&file) => {
53+
debug!("{:?} is ignored", file);
54+
return true;
55+
}
56+
_ => {}
57+
}
58+
}
59+
false
60+
}
61+
3362
#[cfg(test)]
3463
mod test {
3564
use rustfmt_config_proc_macro::nightly_only_test;
@@ -49,4 +78,35 @@ mod test {
4978
assert!(ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz.rs"))));
5079
assert!(!ignore_path_set.is_match(&FileName::Real(PathBuf::from("src/bar.rs"))));
5180
}
81+
82+
#[test]
83+
fn test_is_std_ignored() {
84+
use serde_json;
85+
use std::path::PathBuf;
86+
87+
use super::is_std_ignored;
88+
use crate::config::IgnoreList;
89+
90+
let ignore_list: IgnoreList = serde_json::from_str(r#"["foo.rs","bar_dir/*"]"#).unwrap();
91+
assert!(is_std_ignored(Some(PathBuf::from("foo.rs")), &ignore_list));
92+
assert!(is_std_ignored(
93+
Some(PathBuf::from("src/foo.rs")),
94+
&ignore_list
95+
));
96+
assert!(is_std_ignored(
97+
Some(PathBuf::from("bar_dir/bar/bar.rs")),
98+
&ignore_list
99+
));
100+
101+
assert!(!is_std_ignored(Some(PathBuf::from("baz.rs")), &ignore_list));
102+
assert!(!is_std_ignored(
103+
Some(PathBuf::from("src/baz.rs")),
104+
&ignore_list
105+
));
106+
assert!(!is_std_ignored(
107+
Some(PathBuf::from("baz_dir/baz/baz.rs")),
108+
&ignore_list
109+
));
110+
assert!(!is_std_ignored(None, &ignore_list));
111+
}
52112
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ mod expr;
6868
mod format_report_formatter;
6969
pub(crate) mod formatting;
7070
mod ignore_path;
71+
pub use ignore_path::is_std_ignored;
7172
mod imports;
7273
mod issues;
7374
mod items;
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
@@ -193,18 +193,21 @@ fn mod_resolution_error_path_attribute_does_not_exist() {
193193

194194
mod rustfmt_stdin_formatting {
195195
use super::rustfmt_std_input;
196+
use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};
196197

197198
#[rustfmt::skip]
198199
#[test]
199200
fn changes_are_output_to_stdout() {
200-
let args = [];
201+
// line endings are normalized to '\n' to avoid platform differences
202+
let args = ["--config", "newline_style=Unix"];
201203
let source = "fn main () { println!(\"hello world!\"); }";
202204
let (stdout, _stderr) = rustfmt_std_input(&args, source);
203205
let expected_output =
204206
r#"fn main() {
205207
println!("hello world!");
206-
}"#;
207-
assert!(stdout.contains(expected_output))
208+
}
209+
"#;
210+
assert!(stdout == expected_output);
208211
}
209212

210213
#[test]
@@ -215,4 +218,99 @@ r#"fn main() {
215218
let (stdout, _stderr) = rustfmt_std_input(&args, source);
216219
assert!(stdout.trim_end() == source)
217220
}
221+
222+
#[nightly_only_test]
223+
#[test]
224+
fn input_ignored_when_stdin_file_hint_is_ignored() {
225+
// NOTE: the source is not properly formatted, but we're giving rustfmt a hint that
226+
// the input actually corresponds to `src/lib.rs`, which is ignored in the given config file
227+
let args = [
228+
"--stdin-file-hint",
229+
"src/lib.rs",
230+
"--config-path",
231+
"tests/config/stdin-file-hint-ignore.toml",
232+
];
233+
let source = "fn main () { println!(\"hello world!\"); }";
234+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
235+
assert!(stdout.trim_end() == source)
236+
}
237+
238+
#[rustfmt::skip]
239+
#[nightly_only_test]
240+
#[test]
241+
fn input_formatted_when_stdin_file_hint_is_not_ignored() {
242+
// NOTE: `src/bin/main.rs` is not ignored in the config file so the input is formatted.
243+
// line endings are normalized to '\n' to avoid platform differences
244+
let args = [
245+
"--stdin-file-hint",
246+
"src/bin/main.rs",
247+
"--config-path",
248+
"tests/config/stdin-file-hint-ignore.toml",
249+
"--config",
250+
"newline_style=Unix",
251+
];
252+
let source = "fn main () { println!(\"hello world!\"); }";
253+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
254+
let expected_output =
255+
r#"fn main() {
256+
println!("hello world!");
257+
}
258+
"#;
259+
assert!(stdout == expected_output);
260+
}
261+
262+
#[rustfmt::skip]
263+
#[stable_only_test]
264+
#[test]
265+
fn ignore_list_is_not_set_on_stable_channel_and_therefore_stdin_file_hint_does_nothing() {
266+
// NOTE: the source is not properly formatted, and although the file is `ignored` we
267+
// can't set the `ignore` list on the stable channel so the input is formatted
268+
// line endings are normalized to '\n' to avoid platform differences
269+
let args = [
270+
"--stdin-file-hint",
271+
"src/lib.rs",
272+
"--config-path",
273+
"tests/config/stdin-file-hint-ignore.toml",
274+
"--config",
275+
"newline_style=Unix",
276+
];
277+
let source = "fn main () { println!(\"hello world!\"); }";
278+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
279+
let expected_output =
280+
r#"fn main() {
281+
println!("hello world!");
282+
}
283+
"#;
284+
assert!(stdout == expected_output);
285+
286+
}
287+
}
288+
289+
mod stdin_file_hint {
290+
use super::{rustfmt, rustfmt_std_input};
291+
292+
#[test]
293+
fn error_not_a_rust_file() {
294+
let args = ["--stdin-file-hint", "README.md"];
295+
let source = "fn main() {}";
296+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
297+
assert!(stderr.contains("`--std-file-hint=\"README.md\"` is not a rust file"))
298+
}
299+
300+
#[test]
301+
fn error_file_not_found() {
302+
let args = ["--stdin-file-hint", "does_not_exist.rs"];
303+
let source = "fn main() {}";
304+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
305+
assert!(stderr.contains("`--std-file-hint=\"does_not_exist.rs\"` could not be found"))
306+
}
307+
308+
#[test]
309+
fn cant_use_stdin_file_hint_if_input_not_passed_to_rustfmt_via_stdin() {
310+
let args = ["--stdin-file-hint", "src/lib.rs", "src/lib.rs"];
311+
let (_stdout, stderr) = rustfmt(&args);
312+
assert!(
313+
stderr.contains("Cannot use `--std-file-hint` without formatting input from stdin.")
314+
);
315+
}
218316
}

0 commit comments

Comments
 (0)