Skip to content

Commit

Permalink
Add --stdin-file-hint rustfmt command line option
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
ytmimi committed Jun 27, 2024
1 parent c981e59 commit e7668b4
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 5 deletions.
51 changes: 51 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ pub enum OperationError {
/// supported with standard input.
#[error("Emit mode {0} not supported with standard output.")]
StdinBadEmit(EmitMode),
/// Using `--std-file-hint` incorrectly
#[error("{0}")]
StdInFileHint(StdInFileHintError),
}

#[derive(Error, Debug)]
pub enum StdInFileHintError {
/// The file hint does not exist
#[error("`--std-file-hint={0:?}` could not be found")]
NotFound(PathBuf),
/// The file hint isn't a rust file
#[error("`--std-file-hint={0:?}` is not a rust file")]
NotRustFile(PathBuf),
/// Attempted to pass --std-file-hint without passing input through stdin
#[error("Cannot use `--std-file-hint` without formatting input from stdin.")]
NotFormttingWithStdIn,
}

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

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

if rustfmt::is_std_ignored(options.stdin_file_hint, &config.ignore()) {
io::stdout().write_all(input.as_bytes())?;
return Ok(0);
}

if options.check {
config.set().emit_mode(EmitMode::Diff);
} else {
Expand Down Expand Up @@ -494,6 +523,13 @@ fn determine_operation(matches: &Matches) -> Result<Operation, OperationError> {
return Ok(Operation::Stdin { input: buffer });
}

// User's can only pass `--stdin-file-hint` when formating files via stdin.
if matches.opt_present("stdin-file-hint") {
return Err(OperationError::StdInFileHint(
StdInFileHintError::NotFormttingWithStdIn,
));
}

Ok(Operation::Format {
files,
minimal_config_path,
Expand All @@ -519,6 +555,7 @@ struct GetOptsOptions {
unstable_features: bool,
error_on_unformatted: Option<bool>,
print_misformatted_file_names: bool,
stdin_file_hint: Option<PathBuf>,
}

impl GetOptsOptions {
Expand Down Expand Up @@ -568,6 +605,20 @@ impl GetOptsOptions {
}

options.config_path = matches.opt_str("config-path").map(PathBuf::from);
options.stdin_file_hint = matches.opt_str("stdin-file-hint").map(PathBuf::from);

// return early if there are issues with the file hint specified
if let Some(file_hint) = &options.stdin_file_hint {
if !file_hint.exists() {
return Err(StdInFileHintError::NotFound(file_hint.to_owned()))?;
}

if let Some(ext) = file_hint.extension() {
if ext != "rs" {
return Err(StdInFileHintError::NotRustFile(file_hint.to_owned()))?;
}
}
}

options.inline_config = matches
.opt_strs("config")
Expand Down
8 changes: 8 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,14 @@ impl IgnoreList {
pub fn rustfmt_toml_path(&self) -> &Path {
&self.rustfmt_toml_path
}

pub fn is_empty(&self) -> bool {
self.path_set.is_empty()
}

pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
self.path_set.contains(path.as_ref())
}
}

impl FromStr for IgnoreList {
Expand Down
62 changes: 60 additions & 2 deletions src/ignore_path.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use ignore::gitignore;

use crate::config::{FileName, IgnoreList};
use ignore::gitignore;
use std::path::{Path, PathBuf};

pub(crate) struct IgnorePathSet {
ignore_set: gitignore::Gitignore,
Expand Down Expand Up @@ -30,6 +30,33 @@ impl IgnorePathSet {
}
}

/// Determine if input from stdin should be ignored by rustfmt.
/// See the `ignore` configuration options for details on specifying ignore files.
pub fn is_std_ignored(file_hint: Option<PathBuf>, ignore_list: &IgnoreList) -> bool {
// trivially return false, because no files are ignored
if ignore_list.is_empty() {
return false;
}

// trivially return true, because everything is ignored when "/" is in the ignore list
if ignore_list.contains(Path::new("/")) {
return true;
}

// See if the hinted stdin input is an ignored file.
if let Some(std_file_hint) = file_hint {
let file = FileName::Real(std_file_hint);
match IgnorePathSet::from_ignore_list(ignore_list) {
Ok(ignore_set) if ignore_set.is_match(&file) => {
debug!("{:?} is ignored", file);
return true;
}
_ => {}
}
}
false
}

#[cfg(test)]
mod test {
use rustfmt_config_proc_macro::nightly_only_test;
Expand Down Expand Up @@ -67,4 +94,35 @@ mod test {
assert!(ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz/a.rs"))));
assert!(!ignore_path_set.is_match(&FileName::Real(PathBuf::from("bar_dir/baz/what.rs"))));
}

#[test]
fn test_is_std_ignored() {
use serde_json;
use std::path::PathBuf;

use super::is_std_ignored;
use crate::config::IgnoreList;

let ignore_list: IgnoreList = serde_json::from_str(r#"["foo.rs","bar_dir/*"]"#).unwrap();
assert!(is_std_ignored(Some(PathBuf::from("foo.rs")), &ignore_list));
assert!(is_std_ignored(
Some(PathBuf::from("src/foo.rs")),
&ignore_list
));
assert!(is_std_ignored(
Some(PathBuf::from("bar_dir/bar/bar.rs")),
&ignore_list
));

assert!(!is_std_ignored(Some(PathBuf::from("baz.rs")), &ignore_list));
assert!(!is_std_ignored(
Some(PathBuf::from("src/baz.rs")),
&ignore_list
));
assert!(!is_std_ignored(
Some(PathBuf::from("baz_dir/baz/baz.rs")),
&ignore_list
));
assert!(!is_std_ignored(None, &ignore_list));
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ mod expr;
mod format_report_formatter;
pub(crate) mod formatting;
mod ignore_path;
pub use ignore_path::is_std_ignored;
mod imports;
mod items;
mod lists;
Expand Down
3 changes: 3 additions & 0 deletions tests/config/stdin-file-hint-ignore.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignore = [
"src/lib.rs"
]
104 changes: 101 additions & 3 deletions tests/rustfmt/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,21 @@ fn rustfmt_emits_error_when_control_brace_style_is_always_next_line() {
}
mod rustfmt_stdin_formatting {
use super::rustfmt_std_input;
use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};

#[rustfmt::skip]
#[test]
fn changes_are_output_to_stdout() {
let args = [];
// line endings are normalized to '\n' to avoid platform differences
let args = ["--config", "newline_style=Unix"];
let source = "fn main () { println!(\"hello world!\"); }";
let (stdout, _stderr) = rustfmt_std_input(&args, source);
let expected_output =
r#"fn main() {
println!("hello world!");
}"#;
assert!(stdout.contains(expected_output))
}
"#;
assert!(stdout == expected_output);
}

#[test]
Expand All @@ -264,4 +267,99 @@ r#"fn main() {
let (stdout, _stderr) = rustfmt_std_input(&args, source);
assert!(stdout.trim_end() == source)
}

#[nightly_only_test]
#[test]
fn input_ignored_when_stdin_file_hint_is_ignored() {
// NOTE: the source is not properly formatted, but we're giving rustfmt a hint that
// the input actually corresponds to `src/lib.rs`, which is ignored in the given config file
let args = [
"--stdin-file-hint",
"src/lib.rs",
"--config-path",
"tests/config/stdin-file-hint-ignore.toml",
];
let source = "fn main () { println!(\"hello world!\"); }";
let (stdout, _stderr) = rustfmt_std_input(&args, source);
assert!(stdout.trim_end() == source)
}

#[rustfmt::skip]
#[nightly_only_test]
#[test]
fn input_formatted_when_stdin_file_hint_is_not_ignored() {
// NOTE: `src/bin/main.rs` is not ignored in the config file so the input is formatted.
// line endings are normalized to '\n' to avoid platform differences
let args = [
"--stdin-file-hint",
"src/bin/main.rs",
"--config-path",
"tests/config/stdin-file-hint-ignore.toml",
"--config",
"newline_style=Unix",
];
let source = "fn main () { println!(\"hello world!\"); }";
let (stdout, _stderr) = rustfmt_std_input(&args, source);
let expected_output =
r#"fn main() {
println!("hello world!");
}
"#;
assert!(stdout == expected_output);
}

#[rustfmt::skip]
#[stable_only_test]
#[test]
fn ignore_list_is_not_set_on_stable_channel_and_therefore_stdin_file_hint_does_nothing() {
// NOTE: the source is not properly formatted, and although the file is `ignored` we
// can't set the `ignore` list on the stable channel so the input is formatted
// line endings are normalized to '\n' to avoid platform differences
let args = [
"--stdin-file-hint",
"src/lib.rs",
"--config-path",
"tests/config/stdin-file-hint-ignore.toml",
"--config",
"newline_style=Unix",
];
let source = "fn main () { println!(\"hello world!\"); }";
let (stdout, _stderr) = rustfmt_std_input(&args, source);
let expected_output =
r#"fn main() {
println!("hello world!");
}
"#;
assert!(stdout == expected_output);

}
}

mod stdin_file_hint {
use super::{rustfmt, rustfmt_std_input};

#[test]
fn error_not_a_rust_file() {
let args = ["--stdin-file-hint", "README.md"];
let source = "fn main() {}";
let (_stdout, stderr) = rustfmt_std_input(&args, source);
assert!(stderr.contains("`--std-file-hint=\"README.md\"` is not a rust file"))
}

#[test]
fn error_file_not_found() {
let args = ["--stdin-file-hint", "does_not_exist.rs"];
let source = "fn main() {}";
let (_stdout, stderr) = rustfmt_std_input(&args, source);
assert!(stderr.contains("`--std-file-hint=\"does_not_exist.rs\"` could not be found"))
}

#[test]
fn cant_use_stdin_file_hint_if_input_not_passed_to_rustfmt_via_stdin() {
let args = ["--stdin-file-hint", "src/lib.rs", "src/lib.rs"];
let (_stdout, stderr) = rustfmt(&args);
assert!(
stderr.contains("Cannot use `--std-file-hint` without formatting input from stdin.")
);
}
}

0 comments on commit e7668b4

Please sign in to comment.