Skip to content

Commit 2a3ed2c

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 2a3ed2c

File tree

6 files changed

+192
-2
lines changed

6 files changed

+192
-2
lines changed

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: 66 additions & 2 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,16 +32,45 @@ 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 {
64+
use super::*;
65+
use crate::config::Config;
3566
use rustfmt_config_proc_macro::nightly_only_test;
67+
use std::path::{Path, PathBuf};
3668

3769
#[nightly_only_test]
3870
#[test]
3971
fn test_ignore_path_set() {
40-
use crate::config::{Config, FileName};
72+
use crate::config::FileName;
4173
use crate::ignore_path::IgnorePathSet;
42-
use std::path::{Path, PathBuf};
4374

4475
let config =
4576
Config::from_toml(r#"ignore = ["foo.rs", "bar_dir/*"]"#, Path::new("")).unwrap();
@@ -49,4 +80,37 @@ mod test {
4980
assert!(ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz.rs"))));
5081
assert!(!ignore_path_set.is_match(&FileName::Real(PathBuf::from("src/bar.rs"))));
5182
}
83+
84+
#[test]
85+
fn test_is_std_ignored() {
86+
let config =
87+
Config::from_toml(r#"ignore = ["foo.rs", "bar_dir/*"]"#, Path::new("")).unwrap();
88+
89+
assert!(is_std_ignored(
90+
Some(PathBuf::from("foo.rs")),
91+
&config.ignore()
92+
));
93+
assert!(is_std_ignored(
94+
Some(PathBuf::from("src/foo.rs")),
95+
&config.ignore()
96+
));
97+
assert!(is_std_ignored(
98+
Some(PathBuf::from("bar_dir/bar/bar.rs")),
99+
&config.ignore()
100+
));
101+
102+
assert!(!is_std_ignored(
103+
Some(PathBuf::from("baz.rs")),
104+
&config.ignore()
105+
));
106+
assert!(!is_std_ignored(
107+
Some(PathBuf::from("src/baz.rs")),
108+
&config.ignore()
109+
));
110+
assert!(!is_std_ignored(
111+
Some(PathBuf::from("baz_dir/baz/baz.rs")),
112+
&config.ignore()
113+
));
114+
assert!(!is_std_ignored(None, &config.ignore()));
115+
}
52116
}

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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,67 @@ r#"fn main() {
215215
let (stdout, _stderr) = rustfmt_std_input(&args, source);
216216
assert!(stdout.trim_end() == source)
217217
}
218+
219+
#[test]
220+
fn input_ignored_when_stdin_file_hint_is_ignored() {
221+
// NOTE: the source is not properly formatted, but we're giving rustfmt a hint that
222+
// the input actually corresponds to `src/lib.rs`, which is ignored in the given config file
223+
let args = [
224+
"--stdin-file-hint",
225+
"src/lib.rs",
226+
"--config-path",
227+
"tests/config/stdin-file-hint-ignore.toml",
228+
];
229+
let source = "fn main () { println!(\"hello world!\"); }";
230+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
231+
assert!(stdout.trim_end() == source)
232+
}
233+
234+
#[rustfmt::skip]
235+
#[test]
236+
fn input_formatted_when_stdin_file_hint_is_not_ignored() {
237+
// NOTE: `src/bin/main.rs` is not ignored in the config file so the input is formatted.
238+
let args = [
239+
"--stdin-file-hint",
240+
"src/bin/main.rs",
241+
"--config-path",
242+
"tests/config/stdin-file-hint-ignore.toml",
243+
];
244+
let source = "fn main () { println!(\"hello world!\"); }";
245+
let (stdout, _stderr) = rustfmt_std_input(&args, source);
246+
let expected_output =
247+
r#"fn main() {
248+
println!("hello world!");
249+
}"#;
250+
assert!(stdout.contains(expected_output))
251+
}
252+
}
253+
254+
mod stdin_file_hint {
255+
use super::{rustfmt, rustfmt_std_input};
256+
257+
#[test]
258+
fn error_not_a_rust_file() {
259+
let args = ["--stdin-file-hint", "README.md"];
260+
let source = "fn main() {}";
261+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
262+
assert!(stderr.contains("`--std-file-hint=\"README.md\"` is not a rust file"))
263+
}
264+
265+
#[test]
266+
fn error_file_not_found() {
267+
let args = ["--stdin-file-hint", "does_not_exist.rs"];
268+
let source = "fn main() {}";
269+
let (_stdout, stderr) = rustfmt_std_input(&args, source);
270+
assert!(stderr.contains("`--std-file-hint=\"does_not_exist.rs\"` could not be found"))
271+
}
272+
273+
#[test]
274+
fn cant_use_stdin_file_hint_with_rustfmt_file_format() {
275+
let args = ["--stdin-file-hint", "src/lib.rs", "src/lib.rs"];
276+
let (_stdout, stderr) = rustfmt(&args);
277+
assert!(
278+
stderr.contains("Cannot use `--std-file-hint` without formatting input from stdin.")
279+
);
280+
}
218281
}

0 commit comments

Comments
 (0)