543865c96b introduced
RuleCode::origin() -> RuleOrigin generation via a macro, while that
signature now has been renamed to Rule::origin() -> Linter we actually
want to get rid of it since rules and linters shouldn't be this tightly
coupled (since one rule can exist in multiple linters).
Another disadvantage of the previous approach was that the prefixes
had to be defined in ruff_macros/src/prefixes.rs, which was easy to
miss when defining new linters in src/*, case in point
INP001 => violations::ImplicitNamespacePackage has in the meantime been
added without ruff_macros/src/prefixes.rs being updated accordingly
which resulted in `ruff --explain INP001` mistakenly reporting that the
rule belongs to isort (since INP001 starts with the isort prefix "I").
The derive proc macro introduced in this commit requires every variant
to have at least one #[prefix = "..."], eliminating such mistakes.
357 lines
12 KiB
Rust
357 lines
12 KiB
Rust
use std::fs::remove_dir_all;
|
|
use std::io::{self, Read};
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Instant;
|
|
|
|
use anyhow::{bail, Result};
|
|
use colored::Colorize;
|
|
use ignore::Error;
|
|
use itertools::Itertools;
|
|
use log::{debug, error};
|
|
use path_absolutize::path_dedot;
|
|
#[cfg(not(target_family = "wasm"))]
|
|
use rayon::prelude::*;
|
|
use ruff::cache::CACHE_DIR_NAME;
|
|
use ruff::linter::add_noqa_to_path;
|
|
use ruff::logging::LogLevel;
|
|
use ruff::message::{Location, Message};
|
|
use ruff::registry::{Linter, ParseCode, Rule};
|
|
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
|
|
use ruff::settings::flags;
|
|
use ruff::settings::types::SerializationFormat;
|
|
use ruff::{fix, fs, packaging, resolver, warn_user_once, AutofixAvailability, IOError};
|
|
use serde::Serialize;
|
|
use walkdir::WalkDir;
|
|
|
|
use crate::cache;
|
|
use crate::cli::Overrides;
|
|
use crate::diagnostics::{lint_path, lint_stdin, Diagnostics};
|
|
use crate::iterators::par_iter;
|
|
|
|
/// Run the linter over a collection of files.
|
|
pub fn run(
|
|
files: &[PathBuf],
|
|
pyproject_strategy: &PyprojectDiscovery,
|
|
file_strategy: &FileDiscovery,
|
|
overrides: &Overrides,
|
|
cache: flags::Cache,
|
|
autofix: fix::FixMode,
|
|
) -> Result<Diagnostics> {
|
|
// Collect all the Python files to check.
|
|
let start = Instant::now();
|
|
let (paths, resolver) =
|
|
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
|
let duration = start.elapsed();
|
|
debug!("Identified files to lint in: {:?}", duration);
|
|
|
|
if paths.is_empty() {
|
|
warn_user_once!("No Python files found under the given path(s)");
|
|
return Ok(Diagnostics::default());
|
|
}
|
|
|
|
// Validate the `Settings` and return any errors.
|
|
resolver.validate(pyproject_strategy)?;
|
|
|
|
// Initialize the cache.
|
|
if matches!(cache, flags::Cache::Enabled) {
|
|
match &pyproject_strategy {
|
|
PyprojectDiscovery::Fixed(settings) => {
|
|
if let Err(e) = cache::init(&settings.cli.cache_dir) {
|
|
error!(
|
|
"Failed to initialize cache at {}: {e:?}",
|
|
settings.cli.cache_dir.to_string_lossy()
|
|
);
|
|
}
|
|
}
|
|
PyprojectDiscovery::Hierarchical(default) => {
|
|
for settings in std::iter::once(default).chain(resolver.iter()) {
|
|
if let Err(e) = cache::init(&settings.cli.cache_dir) {
|
|
error!(
|
|
"Failed to initialize cache at {}: {e:?}",
|
|
settings.cli.cache_dir.to_string_lossy()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Discover the package root for each Python file.
|
|
let package_roots = packaging::detect_package_roots(
|
|
&paths
|
|
.iter()
|
|
.flatten()
|
|
.map(ignore::DirEntry::path)
|
|
.collect::<Vec<_>>(),
|
|
&resolver,
|
|
pyproject_strategy,
|
|
);
|
|
|
|
let start = Instant::now();
|
|
let mut diagnostics: Diagnostics = par_iter(&paths)
|
|
.map(|entry| {
|
|
match entry {
|
|
Ok(entry) => {
|
|
let path = entry.path();
|
|
let package = path
|
|
.parent()
|
|
.and_then(|parent| package_roots.get(parent))
|
|
.and_then(|package| *package);
|
|
let settings = resolver.resolve_all(path, pyproject_strategy);
|
|
lint_path(path, package, settings, cache, autofix)
|
|
.map_err(|e| (Some(path.to_owned()), e.to_string()))
|
|
}
|
|
Err(e) => Err((
|
|
if let Error::WithPath { path, .. } = e {
|
|
Some(path.clone())
|
|
} else {
|
|
None
|
|
},
|
|
e.io_error()
|
|
.map_or_else(|| e.to_string(), io::Error::to_string),
|
|
)),
|
|
}
|
|
.unwrap_or_else(|(path, message)| {
|
|
if let Some(path) = &path {
|
|
let settings = resolver.resolve(path, pyproject_strategy);
|
|
if settings.rules.enabled(&Rule::IOError) {
|
|
Diagnostics::new(vec![Message {
|
|
kind: IOError(message).into(),
|
|
location: Location::default(),
|
|
end_location: Location::default(),
|
|
fix: None,
|
|
filename: path.to_string_lossy().to_string(),
|
|
source: None,
|
|
}])
|
|
} else {
|
|
error!("Failed to check {}: {message}", path.to_string_lossy());
|
|
Diagnostics::default()
|
|
}
|
|
} else {
|
|
error!("{message}");
|
|
Diagnostics::default()
|
|
}
|
|
})
|
|
})
|
|
.reduce(Diagnostics::default, |mut acc, item| {
|
|
acc += item;
|
|
acc
|
|
});
|
|
|
|
diagnostics.messages.sort_unstable();
|
|
let duration = start.elapsed();
|
|
debug!("Checked files in: {:?}", duration);
|
|
|
|
Ok(diagnostics)
|
|
}
|
|
|
|
/// Read a `String` from `stdin`.
|
|
fn read_from_stdin() -> Result<String> {
|
|
let mut buffer = String::new();
|
|
io::stdin().lock().read_to_string(&mut buffer)?;
|
|
Ok(buffer)
|
|
}
|
|
|
|
/// Run the linter over a single file, read from `stdin`.
|
|
pub fn run_stdin(
|
|
filename: Option<&Path>,
|
|
pyproject_strategy: &PyprojectDiscovery,
|
|
file_strategy: &FileDiscovery,
|
|
overrides: &Overrides,
|
|
autofix: fix::FixMode,
|
|
) -> Result<Diagnostics> {
|
|
if let Some(filename) = filename {
|
|
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
|
|
return Ok(Diagnostics::default());
|
|
}
|
|
}
|
|
let settings = match pyproject_strategy {
|
|
PyprojectDiscovery::Fixed(settings) => settings,
|
|
PyprojectDiscovery::Hierarchical(settings) => settings,
|
|
};
|
|
let package_root = filename
|
|
.and_then(Path::parent)
|
|
.and_then(|path| packaging::detect_package_root(path, &settings.lib.namespace_packages));
|
|
let stdin = read_from_stdin()?;
|
|
let mut diagnostics = lint_stdin(filename, package_root, &stdin, &settings.lib, autofix)?;
|
|
diagnostics.messages.sort_unstable();
|
|
Ok(diagnostics)
|
|
}
|
|
|
|
/// Add `noqa` directives to a collection of files.
|
|
pub fn add_noqa(
|
|
files: &[PathBuf],
|
|
pyproject_strategy: &PyprojectDiscovery,
|
|
file_strategy: &FileDiscovery,
|
|
overrides: &Overrides,
|
|
) -> Result<usize> {
|
|
// Collect all the files to check.
|
|
let start = Instant::now();
|
|
let (paths, resolver) =
|
|
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
|
let duration = start.elapsed();
|
|
debug!("Identified files to lint in: {:?}", duration);
|
|
|
|
if paths.is_empty() {
|
|
warn_user_once!("No Python files found under the given path(s)");
|
|
return Ok(0);
|
|
}
|
|
|
|
// Validate the `Settings` and return any errors.
|
|
resolver.validate(pyproject_strategy)?;
|
|
|
|
let start = Instant::now();
|
|
let modifications: usize = par_iter(&paths)
|
|
.flatten()
|
|
.filter_map(|entry| {
|
|
let path = entry.path();
|
|
let settings = resolver.resolve(path, pyproject_strategy);
|
|
match add_noqa_to_path(path, settings) {
|
|
Ok(count) => Some(count),
|
|
Err(e) => {
|
|
error!("Failed to add noqa to {}: {e}", path.to_string_lossy());
|
|
None
|
|
}
|
|
}
|
|
})
|
|
.sum();
|
|
|
|
let duration = start.elapsed();
|
|
debug!("Added noqa to files in: {:?}", duration);
|
|
|
|
Ok(modifications)
|
|
}
|
|
|
|
/// Print the user-facing configuration settings.
|
|
pub fn show_settings(
|
|
files: &[PathBuf],
|
|
pyproject_strategy: &PyprojectDiscovery,
|
|
file_strategy: &FileDiscovery,
|
|
overrides: &Overrides,
|
|
) -> Result<()> {
|
|
// Collect all files in the hierarchy.
|
|
let (paths, resolver) =
|
|
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
|
|
|
// Validate the `Settings` and return any errors.
|
|
resolver.validate(pyproject_strategy)?;
|
|
|
|
// Print the list of files.
|
|
let Some(entry) = paths
|
|
.iter()
|
|
.flatten()
|
|
.sorted_by(|a, b| a.path().cmp(b.path())).next() else {
|
|
bail!("No files found under the given path");
|
|
};
|
|
let path = entry.path();
|
|
let settings = resolver.resolve(path, pyproject_strategy);
|
|
println!("Resolved settings for: {path:?}");
|
|
println!("{settings:#?}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Show the list of files to be checked based on current settings.
|
|
pub fn show_files(
|
|
files: &[PathBuf],
|
|
pyproject_strategy: &PyprojectDiscovery,
|
|
file_strategy: &FileDiscovery,
|
|
overrides: &Overrides,
|
|
) -> Result<()> {
|
|
// Collect all files in the hierarchy.
|
|
let (paths, resolver) =
|
|
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
|
|
|
if paths.is_empty() {
|
|
warn_user_once!("No Python files found under the given path(s)");
|
|
return Ok(());
|
|
}
|
|
|
|
// Validate the `Settings` and return any errors.
|
|
resolver.validate(pyproject_strategy)?;
|
|
|
|
// Print the list of files.
|
|
for entry in paths
|
|
.iter()
|
|
.flatten()
|
|
.sorted_by(|a, b| a.path().cmp(b.path()))
|
|
{
|
|
println!("{}", entry.path().to_string_lossy());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct Explanation<'a> {
|
|
code: &'a str,
|
|
linter: &'a str,
|
|
summary: &'a str,
|
|
}
|
|
|
|
/// Explain a `Rule` to the user.
|
|
pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
|
|
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
|
match format {
|
|
SerializationFormat::Text | SerializationFormat::Grouped => {
|
|
println!("{}\n", rule.as_ref());
|
|
println!("Code: {} ({})\n", rule.code(), linter.name());
|
|
|
|
if let Some(autofix) = rule.autofixable() {
|
|
println!(
|
|
"{}",
|
|
match autofix.available {
|
|
AutofixAvailability::Sometimes => "Autofix is sometimes available.\n",
|
|
AutofixAvailability::Always => "Autofix is always available.\n",
|
|
}
|
|
);
|
|
}
|
|
println!("Message formats:\n");
|
|
for format in rule.message_formats() {
|
|
println!("* {format}");
|
|
}
|
|
}
|
|
SerializationFormat::Json => {
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&Explanation {
|
|
code: rule.code(),
|
|
linter: linter.name(),
|
|
summary: rule.message_formats()[0],
|
|
})?
|
|
);
|
|
}
|
|
SerializationFormat::Junit => {
|
|
bail!("`--explain` does not support junit format")
|
|
}
|
|
SerializationFormat::Github => {
|
|
bail!("`--explain` does not support GitHub format")
|
|
}
|
|
SerializationFormat::Gitlab => {
|
|
bail!("`--explain` does not support GitLab format")
|
|
}
|
|
SerializationFormat::Pylint => {
|
|
bail!("`--explain` does not support pylint format")
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Clear any caches in the current directory or any subdirectories.
|
|
pub fn clean(level: &LogLevel) -> Result<()> {
|
|
for entry in WalkDir::new(&*path_dedot::CWD)
|
|
.into_iter()
|
|
.filter_map(Result::ok)
|
|
.filter(|entry| entry.file_type().is_dir())
|
|
{
|
|
let cache = entry.path().join(CACHE_DIR_NAME);
|
|
if cache.is_dir() {
|
|
if level >= &LogLevel::Default {
|
|
eprintln!("Removing cache at: {}", fs::relativize_path(&cache).bold());
|
|
}
|
|
remove_dir_all(&cache)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|