Handle io errors gracefully (#5611)

## Summary

It can happen that we can't read a file (a python file, a jupyter
notebook or pyproject.toml), which needs to be handled and handled
consistently for all file types. Instead of using `Err` or `error!`, we
emit E602 with the io error as message and continue. This PR makes sure
we handle all three cases consistently, emit E602.

I'm not convinced that it should be possible to disable io errors, but
we now handle the regular case consistently and at least print warning
consistently.

I went with `warn!` but i can change them all to `error!`, too.

It also checks the error case when a pyproject.toml is not readable. The
error message is not very helpful, but it's now a bit clearer that
actually ruff itself failed instead vs this being a diagnostic.

## Examples

This is how an Err of `run` looks now:


![image](890f7ab2-2309-4b6f-a4b3-67161947cc83)

With an unreadable file and `IOError` disabled:


![image](fd3d6959-fa23-4ddf-b2e5-8d6022df54b1)

(we lint zero files but count files before linting not during so we exit
0)

I'm not sure if it should (or if we should take a different path with
manual ExitStatus), but this currently also triggers when `files` is
empty:


![image](f7ede301-41b5-4743-97fd-49149f750337)

## Test Plan

Unix only: Create a temporary directory with files with permissions
`000` (not readable by the owner) and run on that directory. Since this
breaks the assumptions of most of the test code (single file, `ruff`
instead of `ruff_cli`), the test code is rather cumbersome and looks a
bit misplaced; i'm happy about suggestions to fit it in closer with the
other tests or streamline it in other ways. I added another test for
when the entire directory is not readable.
This commit is contained in:
konsti 2023-07-20 11:30:14 +02:00 committed by GitHub
parent 029fe05a5f
commit 92f471a666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 250 additions and 37 deletions

View file

@ -125,7 +125,7 @@ pub(crate) fn run(
(Some(path.to_owned()), {
let mut error = e.to_string();
for cause in e.chain() {
write!(&mut error, "\n Caused by: {cause}").unwrap();
write!(&mut error, "\n Cause: {cause}").unwrap();
}
error
})
@ -143,30 +143,30 @@ pub(crate) fn run(
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
error!(
"{}{}{} {message}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
let settings = resolver.resolve(path, pyproject_config);
if settings.rules.enabled(Rule::IOError) {
let file =
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![Message::from_diagnostic(
Diagnostic::new(IOError { message }, TextRange::default()),
file,
dummy,
TextSize::default(),
)],
ImportMap::default(),
)
} else {
warn!(
"{}{}{} {message}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
Diagnostics::default()
}
} else {
error!("{} {message}", "Encountered error:".bold());
warn!("{} {message}", "Encountered error:".bold());
Diagnostics::default()
}
})
@ -226,3 +226,85 @@ with the relevant file contents, the `pyproject.toml` settings, and the followin
}
}
}
#[cfg(test)]
#[cfg(unix)]
mod test {
use super::run;
use crate::args::Overrides;
use anyhow::Result;
use ruff::message::{Emitter, EmitterContext, TextEmitter};
use ruff::registry::Rule;
use ruff::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy};
use ruff::settings::{flags, AllSettings, CliSettings, Settings};
use rustc_hash::FxHashMap;
use std::fs;
use std::os::unix::fs::OpenOptionsExt;
use tempfile::TempDir;
/// We check that regular python files, pyproject.toml and jupyter notebooks all handle io
/// errors gracefully
#[test]
fn unreadable_files() -> Result<()> {
let path = "E902.py";
let rule_code = Rule::IOError;
// Create inaccessible files
let tempdir = TempDir::new()?;
let pyproject_toml = tempdir.path().join("pyproject.toml");
let python_file = tempdir.path().join("code.py");
let notebook = tempdir.path().join("notebook.ipynb");
for file in [&pyproject_toml, &python_file, &notebook] {
fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o000)
.open(file)?;
}
// Configure
let snapshot = format!("{}_{}", rule_code.noqa_code(), path);
let settings = AllSettings {
cli: CliSettings::default(),
// invalid pyproject.toml is not active by default
lib: Settings::for_rules(vec![rule_code, Rule::InvalidPyprojectToml]),
};
let pyproject_config =
PyprojectConfig::new(PyprojectDiscoveryStrategy::Fixed, settings, None);
// Run
let diagnostics = run(
// Notebooks are not included by default
&[tempdir.path().to_path_buf(), notebook],
&pyproject_config,
&Overrides::default(),
flags::Cache::Disabled,
flags::Noqa::Disabled,
flags::FixMode::Generate,
)
.unwrap();
let mut output = Vec::new();
TextEmitter::default()
.with_show_fix_status(true)
.emit(
&mut output,
&diagnostics.messages,
&EmitterContext::new(&FxHashMap::default()),
)
.unwrap();
let messages = String::from_utf8(output).unwrap();
insta::with_settings!({
omit_expression => true,
filters => vec![
// The tempdir is always different (and platform dependent)
(tempdir.path().to_str().unwrap(), "/home/ferris/project"),
]
}, {
insta::assert_snapshot!(snapshot, messages);
});
Ok(())
}
}

View file

@ -0,0 +1,7 @@
---
source: crates/ruff_cli/src/commands/run.rs
---
/home/ferris/project/code.py:1:1: E902 Permission denied (os error 13)
/home/ferris/project/notebook.ipynb:1:1: E902 Permission denied (os error 13)
/home/ferris/project/pyproject.toml:1:1: E902 Permission denied (os error 13)