diff --git a/Cargo.lock b/Cargo.lock index 8dc7b7c92e..f3e9319d4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,20 @@ dependencies = [ "term", ] +[[package]] +name = "assert_cmd" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-channel" version = "1.7.1" @@ -580,6 +594,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.8.1" @@ -658,6 +678,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "either" version = "1.8.0" @@ -1685,6 +1711,33 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +dependencies = [ + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" + +[[package]] +name = "predicates-tree" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1910,6 +1963,7 @@ name = "ruff" version = "0.0.68" dependencies = [ "anyhow", + "assert_cmd", "bincode", "cacache", "chrono", @@ -2345,6 +2399,12 @@ dependencies = [ "phf_codegen 0.8.0", ] +[[package]] +name = "termtree" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" + [[package]] name = "thiserror" version = "1.0.37" @@ -2593,6 +2653,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e76fae08f03f96e166d2dfda232190638c10e0383841252416f9cfe2ae60e6" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 79ef425d7a..dfb8ac21bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ strum_macros = "0.24.3" num-bigint = "0.4.3" [dev-dependencies] +assert_cmd = "2.0.4" insta = { version = "1.19.1", features = ["yaml"] } [features] diff --git a/src/cli.rs b/src/cli.rs index 7e71aa05b1..66407842eb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,9 @@ pub struct Cli { // TODO(charlie): This should be a sub-command. #[arg(long, hide = true)] pub autoformat: bool, + /// The name of the file when passing it through stdin. + #[arg(long)] + pub stdin_filename: Option, } pub enum Warnable { diff --git a/src/linter.rs b/src/linter.rs index fb8ed4aa33..2ddc6dedc2 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -83,6 +83,36 @@ pub(crate) fn check_path( Ok(checks) } +pub fn lint_stdin(path: &Path, stdin: &str, settings: &Settings) -> Result> { + // Tokenize once. + let tokens: Vec = tokenize(stdin); + + // Determine the noqa line for every line in the source. + let noqa_line_for = noqa::extract_noqa_line_for(&tokens); + + // Generate checks. + let checks = check_path( + path, + stdin, + tokens, + &noqa_line_for, + settings, + &fixer::Mode::None, + )?; + + // Convert to messages. + Ok(checks + .into_iter() + .map(|check| Message { + kind: check.kind, + fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(), + location: check.location, + end_location: check.end_location, + filename: path.to_string_lossy().to_string(), + }) + .collect()) +} + pub fn lint_path( path: &Path, settings: &Settings, diff --git a/src/main.rs b/src/main.rs index 25cdb1f78a..adf420caa7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::io; +use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::sync::mpsc::channel; @@ -19,7 +19,7 @@ use ruff::cli::{warn_on, Cli, Warnable}; use ruff::fs::iter_python_files; use ruff::linter::add_noqa_to_path; use ruff::linter::autoformat_path; -use ruff::linter::lint_path; +use ruff::linter::{lint_path, lint_stdin}; use ruff::logging::set_up_logging; use ruff::message::Message; use ruff::printer::{Printer, SerializationFormat}; @@ -75,6 +75,19 @@ fn show_files(files: &[PathBuf], settings: &Settings) { } } +fn read_from_stdin() -> Result { + let mut buffer = String::new(); + io::stdin().lock().read_to_string(&mut buffer)?; + Ok(buffer) +} + +fn run_once_stdin(settings: &Settings, filename: &Path) -> Result> { + let stdin = read_from_stdin()?; + let mut messages = lint_stdin(filename, &stdin, settings)?; + messages.sort_unstable(); + Ok(messages) +} + fn run_once( files: &[PathBuf], settings: &Settings, @@ -352,7 +365,17 @@ fn inner_main() -> Result { println!("Formatted {modifications} files."); } } else { - let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?; + let messages = if cli.files == vec![PathBuf::from("-")] { + if cli.fix { + eprintln!("Warning: --fix is not enabled when reading from stdin."); + } + + let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); + let path = Path::new(&filename); + run_once_stdin(&settings, path)? + } else { + run_once(&cli.files, &settings, !cli.no_cache, cli.fix)? + }; if !cli.quiet { printer.write_once(&messages)?; } diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000000..a6749972c4 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,47 @@ +use std::str; + +use anyhow::Result; +use assert_cmd::{crate_name, Command}; + +#[test] +fn test_stdin_success() -> Result<()> { + let mut cmd = Command::cargo_bin(crate_name!())?; + cmd.args(&["-"]).write_stdin("").assert().success(); + Ok(()) +} + +#[test] +fn test_stdin_error() -> Result<()> { + let mut cmd = Command::cargo_bin(crate_name!())?; + let output = cmd + .args(&["-"]) + .write_stdin("import os\n") + .assert() + .failure(); + assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:1: F401")); + Ok(()) +} + +#[test] +fn test_stdin_filename() -> Result<()> { + let mut cmd = Command::cargo_bin(crate_name!())?; + let output = cmd + .args(&["-", "--stdin-filename", "F401.py"]) + .write_stdin("import os\n") + .assert() + .failure(); + assert!(str::from_utf8(&output.get_output().stdout)?.contains("F401.py:1:1: F401")); + Ok(()) +} + +#[test] +fn test_stdin_autofix() -> Result<()> { + let mut cmd = Command::cargo_bin(crate_name!())?; + let output = cmd + .args(&["-", "--fix"]) + .write_stdin("import os\n") + .assert() + .failure(); + assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:1: F401")); + Ok(()) +}