mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:53 +00:00
Split commands.rs into separate files (#2792)
This commit is contained in:
parent
d827a9156e
commit
77e65c9ff5
10 changed files with 456 additions and 384 deletions
|
@ -1,383 +0,0 @@
|
||||||
use std::fs::remove_dir_all;
|
|
||||||
use std::io::{self, BufWriter, Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use colored::control::SHOULD_COLORIZE;
|
|
||||||
use colored::Colorize;
|
|
||||||
use ignore::Error;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use log::{debug, error};
|
|
||||||
use mdcat::terminal::{TerminalProgram, TerminalSize};
|
|
||||||
use mdcat::{Environment, ResourceAccess, Settings};
|
|
||||||
use path_absolutize::path_dedot;
|
|
||||||
use pulldown_cmark::{Options, Parser};
|
|
||||||
#[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, Rule, RuleNamespace};
|
|
||||||
use ruff::resolver::PyprojectDiscovery;
|
|
||||||
use ruff::settings::flags;
|
|
||||||
use ruff::{fix, fs, packaging, resolver, warn_user_once, AutofixAvailability, IOError};
|
|
||||||
use serde::Serialize;
|
|
||||||
use syntect::parsing::SyntaxSet;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use crate::args::{HelpFormat, Overrides};
|
|
||||||
use crate::cache;
|
|
||||||
use crate::diagnostics::{lint_path, lint_stdin, Diagnostics};
|
|
||||||
use crate::iterators::par_iter;
|
|
||||||
|
|
||||||
pub mod linter;
|
|
||||||
|
|
||||||
/// Run the linter over a collection of files.
|
|
||||||
pub fn run(
|
|
||||||
files: &[PathBuf],
|
|
||||||
pyproject_strategy: &PyprojectDiscovery,
|
|
||||||
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, 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
error!(
|
|
||||||
"{}{}{} {message}",
|
|
||||||
"Failed to lint ".bold(),
|
|
||||||
fs::relativize_path(path).bold(),
|
|
||||||
":".bold()
|
|
||||||
);
|
|
||||||
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: format!("{}", path.display()),
|
|
||||||
source: None,
|
|
||||||
}])
|
|
||||||
} else {
|
|
||||||
Diagnostics::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("{} {message}", "Encountered error:".bold());
|
|
||||||
Diagnostics::default()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.reduce(Diagnostics::default, |mut acc, item| {
|
|
||||||
acc += item;
|
|
||||||
acc
|
|
||||||
});
|
|
||||||
|
|
||||||
diagnostics.messages.sort_unstable();
|
|
||||||
let duration = start.elapsed();
|
|
||||||
debug!("Checked {:?} files in: {:?}", paths.len(), 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,
|
|
||||||
overrides: &Overrides,
|
|
||||||
autofix: fix::FixMode,
|
|
||||||
) -> Result<Diagnostics> {
|
|
||||||
if let Some(filename) = filename {
|
|
||||||
if !resolver::python_file_at_path(filename, pyproject_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,
|
|
||||||
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, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
overrides: &Overrides,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Collect all files in the hierarchy.
|
|
||||||
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
|
||||||
write!(stdout, "Resolved settings for: {path:?}")?;
|
|
||||||
write!(stdout, "{settings:#?}")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show the list of files to be checked based on current settings.
|
|
||||||
pub fn show_files(
|
|
||||||
files: &[PathBuf],
|
|
||||||
pyproject_strategy: &PyprojectDiscovery,
|
|
||||||
overrides: &Overrides,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Collect all files in the hierarchy.
|
|
||||||
let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
|
|
||||||
|
|
||||||
if paths.is_empty() {
|
|
||||||
warn_user_once!("No Python files found under the given path(s)");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the list of files.
|
|
||||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
|
||||||
for entry in paths
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.sorted_by(|a, b| a.path().cmp(b.path()))
|
|
||||||
{
|
|
||||||
writeln!(stdout, "{}", 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 rule(rule: &Rule, format: HelpFormat) -> Result<()> {
|
|
||||||
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
|
||||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
match format {
|
|
||||||
HelpFormat::Text | HelpFormat::Markdown => {
|
|
||||||
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.code()));
|
|
||||||
output.push('\n');
|
|
||||||
output.push('\n');
|
|
||||||
|
|
||||||
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
|
||||||
output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
|
|
||||||
output.push('\n');
|
|
||||||
output.push('\n');
|
|
||||||
|
|
||||||
if let Some(autofix) = rule.autofixable() {
|
|
||||||
output.push_str(match autofix.available {
|
|
||||||
AutofixAvailability::Sometimes => "Autofix is sometimes available.",
|
|
||||||
AutofixAvailability::Always => "Autofix is always available.",
|
|
||||||
});
|
|
||||||
output.push('\n');
|
|
||||||
output.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(explanation) = rule.explanation() {
|
|
||||||
output.push_str(explanation.trim());
|
|
||||||
} else {
|
|
||||||
output.push_str("Message formats:");
|
|
||||||
for format in rule.message_formats() {
|
|
||||||
output.push('\n');
|
|
||||||
output.push_str(&format!("* {format}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HelpFormat::Json => {
|
|
||||||
output.push_str(&serde_json::to_string_pretty(&Explanation {
|
|
||||||
code: rule.code(),
|
|
||||||
linter: linter.name(),
|
|
||||||
summary: rule.message_formats()[0],
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match format {
|
|
||||||
HelpFormat::Json | HelpFormat::Text => {
|
|
||||||
writeln!(stdout, "{output}")?;
|
|
||||||
}
|
|
||||||
HelpFormat::Markdown => {
|
|
||||||
let parser = Parser::new_ext(
|
|
||||||
&output,
|
|
||||||
Options::ENABLE_TASKLISTS | Options::ENABLE_STRIKETHROUGH,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cwd = std::env::current_dir()?;
|
|
||||||
let env = &Environment::for_local_directory(&cwd)?;
|
|
||||||
|
|
||||||
let terminal = if SHOULD_COLORIZE.should_colorize() {
|
|
||||||
TerminalProgram::detect()
|
|
||||||
} else {
|
|
||||||
TerminalProgram::Dumb
|
|
||||||
};
|
|
||||||
|
|
||||||
let settings = &Settings {
|
|
||||||
resource_access: ResourceAccess::LocalOnly,
|
|
||||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
|
||||||
terminal_capabilities: terminal.capabilities(),
|
|
||||||
terminal_size: TerminalSize::detect().unwrap_or_default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mdcat::push_tty(settings, env, &mut stdout, parser)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear any caches in the current directory or any subdirectories.
|
|
||||||
pub fn clean(level: LogLevel) -> Result<()> {
|
|
||||||
let mut stderr = BufWriter::new(io::stderr().lock());
|
|
||||||
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 {
|
|
||||||
writeln!(
|
|
||||||
stderr,
|
|
||||||
"Removing cache at: {}",
|
|
||||||
fs::relativize_path(&cache).bold()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
remove_dir_all(&cache)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
53
crates/ruff_cli/src/commands/add_noqa.rs
Normal file
53
crates/ruff_cli/src/commands/add_noqa.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::{debug, error};
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
use ruff::linter::add_noqa_to_path;
|
||||||
|
use ruff::resolver::PyprojectDiscovery;
|
||||||
|
use ruff::{resolver, warn_user_once};
|
||||||
|
|
||||||
|
use crate::args::Overrides;
|
||||||
|
use crate::iterators::par_iter;
|
||||||
|
|
||||||
|
/// Add `noqa` directives to a collection of files.
|
||||||
|
pub fn add_noqa(
|
||||||
|
files: &[PathBuf],
|
||||||
|
pyproject_strategy: &PyprojectDiscovery,
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
34
crates/ruff_cli/src/commands/clean.rs
Normal file
34
crates/ruff_cli/src/commands/clean.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use std::fs::remove_dir_all;
|
||||||
|
use std::io::{self, BufWriter, Write};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::Colorize;
|
||||||
|
use path_absolutize::path_dedot;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use ruff::cache::CACHE_DIR_NAME;
|
||||||
|
use ruff::fs;
|
||||||
|
use ruff::logging::LogLevel;
|
||||||
|
|
||||||
|
/// Clear any caches in the current directory or any subdirectories.
|
||||||
|
pub fn clean(level: LogLevel) -> Result<()> {
|
||||||
|
let mut stderr = BufWriter::new(io::stderr().lock());
|
||||||
|
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 {
|
||||||
|
writeln!(
|
||||||
|
stderr,
|
||||||
|
"Removing cache at: {}",
|
||||||
|
fs::relativize_path(&cache).bold()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
remove_dir_all(&cache)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
17
crates/ruff_cli/src/commands/mod.rs
Normal file
17
crates/ruff_cli/src/commands/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
pub use add_noqa::add_noqa;
|
||||||
|
pub use clean::clean;
|
||||||
|
pub use linter::linter;
|
||||||
|
pub use rule::rule;
|
||||||
|
pub use run::run;
|
||||||
|
pub use run_stdin::run_stdin;
|
||||||
|
pub use show_files::show_files;
|
||||||
|
pub use show_settings::show_settings;
|
||||||
|
|
||||||
|
mod add_noqa;
|
||||||
|
mod clean;
|
||||||
|
mod linter;
|
||||||
|
mod rule;
|
||||||
|
mod run;
|
||||||
|
mod run_stdin;
|
||||||
|
mod show_files;
|
||||||
|
mod show_settings;
|
98
crates/ruff_cli/src/commands/rule.rs
Normal file
98
crates/ruff_cli/src/commands/rule.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::io::{self, BufWriter, Write};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::control::SHOULD_COLORIZE;
|
||||||
|
use mdcat::terminal::{TerminalProgram, TerminalSize};
|
||||||
|
use mdcat::{Environment, ResourceAccess, Settings};
|
||||||
|
use pulldown_cmark::{Options, Parser};
|
||||||
|
use serde::Serialize;
|
||||||
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
use ruff::registry::{Linter, Rule, RuleNamespace};
|
||||||
|
use ruff::AutofixAvailability;
|
||||||
|
|
||||||
|
use crate::args::HelpFormat;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Explanation<'a> {
|
||||||
|
code: &'a str,
|
||||||
|
linter: &'a str,
|
||||||
|
summary: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explain a `Rule` to the user.
|
||||||
|
pub fn rule(rule: &Rule, format: HelpFormat) -> Result<()> {
|
||||||
|
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
||||||
|
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
match format {
|
||||||
|
HelpFormat::Text | HelpFormat::Markdown => {
|
||||||
|
output.push_str(&format!("# {} ({})", rule.as_ref(), rule.code()));
|
||||||
|
output.push('\n');
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
|
||||||
|
output.push_str(&format!("Derived from the **{}** linter.", linter.name()));
|
||||||
|
output.push('\n');
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
if let Some(autofix) = rule.autofixable() {
|
||||||
|
output.push_str(match autofix.available {
|
||||||
|
AutofixAvailability::Sometimes => "Autofix is sometimes available.",
|
||||||
|
AutofixAvailability::Always => "Autofix is always available.",
|
||||||
|
});
|
||||||
|
output.push('\n');
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(explanation) = rule.explanation() {
|
||||||
|
output.push_str(explanation.trim());
|
||||||
|
} else {
|
||||||
|
output.push_str("Message formats:");
|
||||||
|
for format in rule.message_formats() {
|
||||||
|
output.push('\n');
|
||||||
|
output.push_str(&format!("* {format}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HelpFormat::Json => {
|
||||||
|
output.push_str(&serde_json::to_string_pretty(&Explanation {
|
||||||
|
code: rule.code(),
|
||||||
|
linter: linter.name(),
|
||||||
|
summary: rule.message_formats()[0],
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match format {
|
||||||
|
HelpFormat::Json | HelpFormat::Text => {
|
||||||
|
writeln!(stdout, "{output}")?;
|
||||||
|
}
|
||||||
|
HelpFormat::Markdown => {
|
||||||
|
let parser = Parser::new_ext(
|
||||||
|
&output,
|
||||||
|
Options::ENABLE_TASKLISTS | Options::ENABLE_STRIKETHROUGH,
|
||||||
|
);
|
||||||
|
|
||||||
|
let cwd = std::env::current_dir()?;
|
||||||
|
let env = &Environment::for_local_directory(&cwd)?;
|
||||||
|
|
||||||
|
let terminal = if SHOULD_COLORIZE.should_colorize() {
|
||||||
|
TerminalProgram::detect()
|
||||||
|
} else {
|
||||||
|
TerminalProgram::Dumb
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = &Settings {
|
||||||
|
resource_access: ResourceAccess::LocalOnly,
|
||||||
|
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||||
|
terminal_capabilities: terminal.capabilities(),
|
||||||
|
terminal_size: TerminalSize::detect().unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mdcat::push_tty(settings, env, &mut stdout, parser)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
138
crates/ruff_cli/src/commands/run.rs
Normal file
138
crates/ruff_cli/src/commands/run.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
use std::io::{self};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use colored::Colorize;
|
||||||
|
use ignore::Error;
|
||||||
|
use log::{debug, error};
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
use ruff::message::{Location, Message};
|
||||||
|
use ruff::registry::Rule;
|
||||||
|
use ruff::resolver::PyprojectDiscovery;
|
||||||
|
use ruff::settings::flags;
|
||||||
|
use ruff::{fix, fs, packaging, resolver, warn_user_once, IOError};
|
||||||
|
|
||||||
|
use crate::args::Overrides;
|
||||||
|
use crate::cache;
|
||||||
|
use crate::diagnostics::{lint_path, Diagnostics};
|
||||||
|
use crate::iterators::par_iter;
|
||||||
|
|
||||||
|
/// Run the linter over a collection of files.
|
||||||
|
pub fn run(
|
||||||
|
files: &[PathBuf],
|
||||||
|
pyproject_strategy: &PyprojectDiscovery,
|
||||||
|
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, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
error!(
|
||||||
|
"{}{}{} {message}",
|
||||||
|
"Failed to lint ".bold(),
|
||||||
|
fs::relativize_path(path).bold(),
|
||||||
|
":".bold()
|
||||||
|
);
|
||||||
|
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: format!("{}", path.display()),
|
||||||
|
source: None,
|
||||||
|
}])
|
||||||
|
} else {
|
||||||
|
Diagnostics::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("{} {message}", "Encountered error:".bold());
|
||||||
|
Diagnostics::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.reduce(Diagnostics::default, |mut acc, item| {
|
||||||
|
acc += item;
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
diagnostics.messages.sort_unstable();
|
||||||
|
let duration = start.elapsed();
|
||||||
|
debug!("Checked {:?} files in: {:?}", paths.len(), duration);
|
||||||
|
|
||||||
|
Ok(diagnostics)
|
||||||
|
}
|
42
crates/ruff_cli/src/commands/run_stdin.rs
Normal file
42
crates/ruff_cli/src/commands/run_stdin.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use ruff::resolver::PyprojectDiscovery;
|
||||||
|
use ruff::{fix, packaging, resolver};
|
||||||
|
|
||||||
|
use crate::args::Overrides;
|
||||||
|
use crate::diagnostics::{lint_stdin, 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,
|
||||||
|
overrides: &Overrides,
|
||||||
|
autofix: fix::FixMode,
|
||||||
|
) -> Result<Diagnostics> {
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
if !resolver::python_file_at_path(filename, pyproject_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)
|
||||||
|
}
|
37
crates/ruff_cli/src/commands/show_files.rs
Normal file
37
crates/ruff_cli/src/commands/show_files.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use std::io::{self, BufWriter, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use ruff::resolver::PyprojectDiscovery;
|
||||||
|
use ruff::{resolver, warn_user_once};
|
||||||
|
|
||||||
|
use crate::args::Overrides;
|
||||||
|
|
||||||
|
/// Show the list of files to be checked based on current settings.
|
||||||
|
pub fn show_files(
|
||||||
|
files: &[PathBuf],
|
||||||
|
pyproject_strategy: &PyprojectDiscovery,
|
||||||
|
overrides: &Overrides,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Collect all files in the hierarchy.
|
||||||
|
let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
|
||||||
|
|
||||||
|
if paths.is_empty() {
|
||||||
|
warn_user_once!("No Python files found under the given path(s)");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the list of files.
|
||||||
|
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||||
|
for entry in paths
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.sorted_by(|a, b| a.path().cmp(b.path()))
|
||||||
|
{
|
||||||
|
writeln!(stdout, "{}", entry.path().to_string_lossy())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
36
crates/ruff_cli/src/commands/show_settings.rs
Normal file
36
crates/ruff_cli/src/commands/show_settings.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::io::{self, BufWriter, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use ruff::resolver;
|
||||||
|
use ruff::resolver::PyprojectDiscovery;
|
||||||
|
|
||||||
|
use crate::args::Overrides;
|
||||||
|
|
||||||
|
/// Print the user-facing configuration settings.
|
||||||
|
pub fn show_settings(
|
||||||
|
files: &[PathBuf],
|
||||||
|
pyproject_strategy: &PyprojectDiscovery,
|
||||||
|
overrides: &Overrides,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Collect all files in the hierarchy.
|
||||||
|
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||||
|
write!(stdout, "Resolved settings for: {path:?}")?;
|
||||||
|
write!(stdout, "{settings:#?}")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -101,7 +101,7 @@ quoting the executed command, along with the relevant file contents and `pyproje
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
Command::Rule { rule, format } => commands::rule(&rule, format)?,
|
Command::Rule { rule, format } => commands::rule(&rule, format)?,
|
||||||
Command::Linter { format } => commands::linter::linter(format),
|
Command::Linter { format } => commands::linter(format),
|
||||||
Command::Clean => commands::clean(log_level)?,
|
Command::Clean => commands::clean(log_level)?,
|
||||||
Command::GenerateShellCompletion { shell } => {
|
Command::GenerateShellCompletion { shell } => {
|
||||||
shell.generate(&mut Args::command(), &mut io::stdout());
|
shell.generate(&mut Args::command(), &mut io::stdout());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue