Write unchanged, excluded files to stdout when read via stdin (#8596)

## Summary

When you run Ruff via stdin, and pass `format` or `check --fix`, we
typically write the changed or unchanged contents to stdout. It turns
out we forgot to do this when the file is _excluded_, so if you run
`ruff format /path/to/excluded/file.py`, we don't write _anything_ to
`stdout`. This led to a bug in the LSP whereby we deleted file contents
for third-party files.

The right thing to do here is write back the unchanged contents, as it
should always be safe to write the output of stdout back to a file.
This commit is contained in:
Charlie Marsh 2023-11-09 20:15:01 -08:00 committed by GitHub
parent 346a828db2
commit 7968e190dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 32 additions and 4 deletions

View file

@ -8,7 +8,7 @@ use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectCo
use crate::args::CliOverrides; use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics}; use crate::diagnostics::{lint_stdin, Diagnostics};
use crate::stdin::read_from_stdin; use crate::stdin::{parrot_stdin, read_from_stdin};
/// Run the linter over a single file, read from `stdin`. /// Run the linter over a single file, read from `stdin`.
pub(crate) fn check_stdin( pub(crate) fn check_stdin(
@ -21,6 +21,9 @@ pub(crate) fn check_stdin(
if pyproject_config.settings.file_resolver.force_exclude { if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = filename { if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? { if !python_file_at_path(filename, pyproject_config, overrides)? {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
@ -29,14 +32,17 @@ pub(crate) fn check_stdin(
.file_name() .file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude)) .is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{ {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
} }
} }
let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| { let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages) packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
}); });
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin( let mut diagnostics = lint_stdin(
filename, filename,
package_root, package_root,

View file

@ -15,7 +15,7 @@ use crate::commands::format::{
FormatResult, FormattedSource, FormatResult, FormattedSource,
}; };
use crate::resolve::resolve; use crate::resolve::resolve;
use crate::stdin::read_from_stdin; use crate::stdin::{parrot_stdin, read_from_stdin};
use crate::ExitStatus; use crate::ExitStatus;
/// Run the formatter over a single file, read from `stdin`. /// Run the formatter over a single file, read from `stdin`.
@ -34,6 +34,9 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
if pyproject_config.settings.file_resolver.force_exclude { if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = cli.stdin_filename.as_deref() { if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? { if !python_file_at_path(filename, &pyproject_config, overrides)? {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
@ -42,6 +45,9 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
.file_name() .file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude)) .is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{ {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
} }
@ -50,6 +56,9 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
let path = cli.stdin_filename.as_deref(); let path = cli.stdin_filename.as_deref();
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else { let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
}; };

View file

@ -1,5 +1,5 @@
use std::io; use std::io;
use std::io::Read; use std::io::{Read, Write};
/// Read a string from `stdin`. /// Read a string from `stdin`.
pub(crate) fn read_from_stdin() -> Result<String, io::Error> { pub(crate) fn read_from_stdin() -> Result<String, io::Error> {
@ -7,3 +7,11 @@ pub(crate) fn read_from_stdin() -> Result<String, io::Error> {
io::stdin().lock().read_to_string(&mut buffer)?; io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer) Ok(buffer)
} }
/// Read bytes from `stdin` and write them to `stdout`.
pub(crate) fn parrot_stdin() -> Result<(), io::Error> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
io::stdout().write_all(buffer.as_bytes())?;
Ok(())
}

View file

@ -320,6 +320,11 @@ if __name__ == '__main__':
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
from test import say_hy
if __name__ == '__main__':
say_hy("dear Ruff contributor")
----- stderr ----- ----- stderr -----
"###); "###);
Ok(()) Ok(())