mirror of https://github.com/astral-sh/ruff
287 lines
9.0 KiB
Rust
287 lines
9.0 KiB
Rust
#![forbid(unsafe_code)]
|
|
#![warn(clippy::pedantic)]
|
|
#![allow(
|
|
clippy::match_same_arms,
|
|
clippy::missing_errors_doc,
|
|
clippy::module_name_repetitions,
|
|
clippy::too_many_lines
|
|
)]
|
|
|
|
use std::io::{self};
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
use std::sync::mpsc::channel;
|
|
|
|
use ::ruff::logging::{set_up_logging, LogLevel};
|
|
use ::ruff::resolver::PyprojectDiscovery;
|
|
use ::ruff::settings::types::SerializationFormat;
|
|
use ::ruff::{fix, fs, warn_user_once};
|
|
use anyhow::Result;
|
|
use args::{Args, CheckArgs, Command};
|
|
use clap::{CommandFactory, Parser, Subcommand};
|
|
use colored::Colorize;
|
|
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
|
use printer::{Printer, Violations};
|
|
use ruff::settings::CliSettings;
|
|
|
|
pub(crate) mod args;
|
|
mod cache;
|
|
mod commands;
|
|
mod diagnostics;
|
|
mod iterators;
|
|
mod printer;
|
|
mod resolve;
|
|
#[cfg(all(feature = "update-informer"))]
|
|
pub mod updates;
|
|
|
|
fn inner_main() -> Result<ExitCode> {
|
|
let mut args: Vec<_> = std::env::args_os().collect();
|
|
|
|
// Clap doesn't support default subcommands but we want to run `check` by
|
|
// default for convenience and backwards-compatibility, so we just
|
|
// preprocess the arguments accordingly before passing them to clap.
|
|
if let Some(arg1) = args.get(1).and_then(|s| s.to_str()) {
|
|
if !Command::has_subcommand(arg1)
|
|
&& !arg1
|
|
.strip_prefix("--")
|
|
.map(Command::has_subcommand)
|
|
.unwrap_or_default()
|
|
&& arg1 != "-h"
|
|
&& arg1 != "--help"
|
|
&& arg1 != "-v"
|
|
&& arg1 != "--version"
|
|
&& arg1 != "help"
|
|
{
|
|
args.insert(1, "check".into());
|
|
}
|
|
}
|
|
|
|
// Extract command-line arguments.
|
|
let Args {
|
|
command,
|
|
log_level_args,
|
|
} = Args::parse_from(args);
|
|
|
|
let default_panic_hook = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
eprintln!(
|
|
r#"
|
|
{}: `ruff` crashed. This indicates a bug in `ruff`. If you could open an issue at:
|
|
|
|
https://github.com/charliermarsh/ruff/issues/new?title=%5BPanic%5D
|
|
|
|
quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
|
|
"#,
|
|
"error".red().bold(),
|
|
);
|
|
default_panic_hook(info);
|
|
}));
|
|
|
|
let log_level: LogLevel = (&log_level_args).into();
|
|
set_up_logging(&log_level)?;
|
|
|
|
match command {
|
|
Command::Explain { rule, format } => commands::explain(rule, format)?,
|
|
Command::Clean => commands::clean(log_level)?,
|
|
Command::GenerateShellCompletion { shell } => {
|
|
shell.generate(&mut Args::command(), &mut io::stdout());
|
|
}
|
|
|
|
Command::Check(args) => return check(args, log_level),
|
|
}
|
|
|
|
Ok(ExitCode::SUCCESS)
|
|
}
|
|
|
|
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitCode> {
|
|
let (cli, overrides) = args.partition();
|
|
|
|
// Construct the "default" settings. These are used when no `pyproject.toml`
|
|
// files are present, or files are injected from outside of the hierarchy.
|
|
let pyproject_strategy = resolve::resolve(
|
|
cli.isolated,
|
|
cli.config.as_deref(),
|
|
&overrides,
|
|
cli.stdin_filename.as_deref(),
|
|
)?;
|
|
|
|
if cli.show_settings {
|
|
commands::show_settings(&cli.files, &pyproject_strategy, &overrides)?;
|
|
return Ok(ExitCode::SUCCESS);
|
|
}
|
|
if cli.show_files {
|
|
commands::show_files(&cli.files, &pyproject_strategy, &overrides)?;
|
|
}
|
|
|
|
// Extract options that are included in `Settings`, but only apply at the top
|
|
// level.
|
|
let CliSettings {
|
|
fix,
|
|
fix_only,
|
|
format,
|
|
update_check,
|
|
..
|
|
} = match &pyproject_strategy {
|
|
PyprojectDiscovery::Fixed(settings) => settings.cli.clone(),
|
|
PyprojectDiscovery::Hierarchical(settings) => settings.cli.clone(),
|
|
};
|
|
|
|
// Autofix rules are as follows:
|
|
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
|
|
// print them to stdout, if we're reading from stdin).
|
|
// - Otherwise, if `--format json` is set, generate the fixes (so we print them
|
|
// out as part of the JSON payload), but don't write them to disk.
|
|
// - If `--diff` or `--fix-only` are set, don't print any violations (only
|
|
// fixes).
|
|
// TODO(charlie): Consider adding ESLint's `--fix-dry-run`, which would generate
|
|
// but not apply fixes. That would allow us to avoid special-casing JSON
|
|
// here.
|
|
let autofix = if cli.diff {
|
|
fix::FixMode::Diff
|
|
} else if fix || fix_only {
|
|
fix::FixMode::Apply
|
|
} else if matches!(format, SerializationFormat::Json) {
|
|
fix::FixMode::Generate
|
|
} else {
|
|
fix::FixMode::None
|
|
};
|
|
let violations = if cli.diff || fix_only {
|
|
Violations::Hide
|
|
} else {
|
|
Violations::Show
|
|
};
|
|
let cache = !cli.no_cache;
|
|
|
|
#[cfg(debug_assertions)]
|
|
if cache {
|
|
// `--no-cache` doesn't respect code changes, and so is often confusing during
|
|
// development.
|
|
warn_user_once!("Detected debug build without --no-cache.");
|
|
}
|
|
|
|
if cli.add_noqa {
|
|
let modifications = commands::add_noqa(&cli.files, &pyproject_strategy, &overrides)?;
|
|
if modifications > 0 && log_level >= LogLevel::Default {
|
|
println!("Added {modifications} noqa directives.");
|
|
}
|
|
return Ok(ExitCode::SUCCESS);
|
|
}
|
|
|
|
let printer = Printer::new(&format, &log_level, &autofix, &violations);
|
|
|
|
if cli.watch {
|
|
if !matches!(autofix, fix::FixMode::None) {
|
|
warn_user_once!("--fix is not enabled in watch mode.");
|
|
}
|
|
if format != SerializationFormat::Text {
|
|
warn_user_once!("--format 'text' is used in watch mode.");
|
|
}
|
|
|
|
// Perform an initial run instantly.
|
|
Printer::clear_screen()?;
|
|
printer.write_to_user("Starting linter in watch mode...\n");
|
|
|
|
let messages = commands::run(
|
|
&cli.files,
|
|
&pyproject_strategy,
|
|
&overrides,
|
|
cache.into(),
|
|
fix::FixMode::None,
|
|
)?;
|
|
printer.write_continuously(&messages)?;
|
|
|
|
// Configure the file watcher.
|
|
let (tx, rx) = channel();
|
|
let mut watcher = recommended_watcher(tx)?;
|
|
for file in &cli.files {
|
|
watcher.watch(file, RecursiveMode::Recursive)?;
|
|
}
|
|
|
|
loop {
|
|
match rx.recv() {
|
|
Ok(event) => {
|
|
let paths = event?.paths;
|
|
let py_changed = paths.iter().any(|path| {
|
|
path.extension()
|
|
.map(|ext| ext == "py" || ext == "pyi")
|
|
.unwrap_or_default()
|
|
});
|
|
if py_changed {
|
|
Printer::clear_screen()?;
|
|
printer.write_to_user("File change detected...\n");
|
|
|
|
let messages = commands::run(
|
|
&cli.files,
|
|
&pyproject_strategy,
|
|
&overrides,
|
|
cache.into(),
|
|
fix::FixMode::None,
|
|
)?;
|
|
printer.write_continuously(&messages)?;
|
|
}
|
|
}
|
|
Err(err) => return Err(err.into()),
|
|
}
|
|
}
|
|
} else {
|
|
let is_stdin = cli.files == vec![PathBuf::from("-")];
|
|
|
|
// Generate lint violations.
|
|
let diagnostics = if is_stdin {
|
|
commands::run_stdin(
|
|
cli.stdin_filename.map(fs::normalize_path).as_deref(),
|
|
&pyproject_strategy,
|
|
&overrides,
|
|
autofix,
|
|
)?
|
|
} else {
|
|
commands::run(
|
|
&cli.files,
|
|
&pyproject_strategy,
|
|
&overrides,
|
|
cache.into(),
|
|
autofix,
|
|
)?
|
|
};
|
|
|
|
// Always try to print violations (the printer itself may suppress output),
|
|
// unless we're writing fixes via stdin (in which case, the transformed
|
|
// source code goes to stdout).
|
|
if !(is_stdin && matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff)) {
|
|
printer.write_once(&diagnostics)?;
|
|
}
|
|
|
|
// Check for updates if we're in a non-silent log level.
|
|
#[cfg(feature = "update-informer")]
|
|
if update_check
|
|
&& !is_stdin
|
|
&& log_level >= LogLevel::Default
|
|
&& atty::is(atty::Stream::Stdout)
|
|
{
|
|
drop(updates::check_for_updates());
|
|
}
|
|
|
|
if !cli.exit_zero {
|
|
if cli.diff || fix_only {
|
|
if diagnostics.fixed > 0 {
|
|
return Ok(ExitCode::FAILURE);
|
|
}
|
|
} else if !diagnostics.messages.is_empty() {
|
|
return Ok(ExitCode::FAILURE);
|
|
}
|
|
}
|
|
}
|
|
Ok(ExitCode::SUCCESS)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn main() -> ExitCode {
|
|
match inner_main() {
|
|
Ok(code) => code,
|
|
Err(err) => {
|
|
eprintln!("{}{} {err:?}", "error".red().bold(), ":".bold());
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|