mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
Make ruff_cli
binary a small wrapper around lib
(#3398)
This commit is contained in:
parent
d9dfec30eb
commit
a3de791f0a
9 changed files with 457 additions and 453 deletions
|
@ -14,8 +14,6 @@ license = "MIT"
|
|||
|
||||
[[bin]]
|
||||
name = "ruff"
|
||||
path = "src/main.rs"
|
||||
doctest = false
|
||||
|
||||
# Since the name of the binary is the same as the name of the `ruff` crate
|
||||
# running `cargo doc --no-deps --all` results in an `output filename collision`
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
64
crates/ruff_cli/src/bin/ruff.rs
Normal file
64
crates/ruff_cli/src/bin/ruff.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use std::process::ExitCode;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_cli::args::{Args, Command};
|
||||
use ruff_cli::{run, ExitStatus};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "openbsd"),
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
pub fn main() -> ExitCode {
|
||||
let mut args: Vec<_> = std::env::args().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(arg) = args.get(1) {
|
||||
if !Command::has_subcommand(rewrite_legacy_subcommand(arg))
|
||||
&& arg != "-h"
|
||||
&& arg != "--help"
|
||||
&& arg != "-V"
|
||||
&& arg != "--version"
|
||||
&& arg != "help"
|
||||
{
|
||||
args.insert(1, "check".into());
|
||||
}
|
||||
}
|
||||
|
||||
let args = Args::parse_from(args);
|
||||
|
||||
match run(args) {
|
||||
Ok(code) => code.into(),
|
||||
Err(err) => {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("{}{} {err:?}", "error".red().bold(), ":".bold());
|
||||
}
|
||||
ExitStatus::Error.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
|
||||
match cmd {
|
||||
"--explain" => "rule",
|
||||
"--clean" => "clean",
|
||||
"--generate-shell-completion" => "generate-shell-completion",
|
||||
cmd => cmd,
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
use crate::ExitStatus;
|
||||
use ruff::settings::{
|
||||
options::Options,
|
||||
options_base::{ConfigurationOptions, OptionEntry, OptionField},
|
||||
};
|
||||
|
||||
use crate::ExitStatus;
|
||||
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>) -> ExitStatus {
|
||||
let Some(entry) = Options::get(key) else {
|
||||
|
|
|
@ -1,27 +1,288 @@
|
|||
//! This library only exists to enable the Ruff internal tooling (`ruff_dev`)
|
||||
//! to automatically update the `ruff help` output in the `README.md`.
|
||||
//!
|
||||
//! For the actual Ruff library, see [`ruff`].
|
||||
|
||||
mod args;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::CommandFactory;
|
||||
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
/// Returns the output of `ruff help`.
|
||||
pub fn command_help() -> String {
|
||||
args::Args::command().render_help().to_string()
|
||||
use crate::args::{Args, CheckArgs, Command};
|
||||
use crate::printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
use ruff::logging::{set_up_logging, LogLevel};
|
||||
use ruff::settings::types::SerializationFormat;
|
||||
use ruff::settings::CliSettings;
|
||||
use ruff::{fix, fs, warn_user_once};
|
||||
|
||||
pub mod args;
|
||||
mod cache;
|
||||
mod commands;
|
||||
mod diagnostics;
|
||||
mod iterators;
|
||||
mod printer;
|
||||
mod resolve;
|
||||
|
||||
pub enum ExitStatus {
|
||||
/// Linting was successful and there were no linting errors.
|
||||
Success,
|
||||
/// Linting was successful but there were linting errors.
|
||||
Failure,
|
||||
/// Linting failed.
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Returns the output of `ruff help check`.
|
||||
pub fn subcommand_help() -> String {
|
||||
let mut cmd = args::Args::command();
|
||||
|
||||
// The build call is necessary for the help output to contain `Usage: ruff
|
||||
// check` instead of `Usage: check` see https://github.com/clap-rs/clap/issues/4685
|
||||
cmd.build();
|
||||
|
||||
cmd.find_subcommand_mut("check")
|
||||
.expect("`check` subcommand not found")
|
||||
.render_help()
|
||||
.to_string()
|
||||
impl From<ExitStatus> for ExitCode {
|
||||
fn from(status: ExitStatus) -> Self {
|
||||
match status {
|
||||
ExitStatus::Success => ExitCode::from(0),
|
||||
ExitStatus::Failure => ExitCode::from(1),
|
||||
ExitStatus::Error => ExitCode::from(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
Args {
|
||||
command,
|
||||
log_level_args,
|
||||
}: Args,
|
||||
) -> Result<ExitStatus> {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let default_panic_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
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::Rule { rule, format } => commands::rule::rule(&rule, format)?,
|
||||
Command::Config { option } => return Ok(commands::config::config(option.as_deref())),
|
||||
Command::Linter { format } => commands::linter::linter(format)?,
|
||||
Command::Clean => commands::clean::clean(log_level)?,
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut io::stdout());
|
||||
}
|
||||
Command::Check(args) => return check(args, log_level),
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
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::show_settings(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files::show_files(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Extract options that are included in `Settings`, but only apply at the top
|
||||
// level.
|
||||
let CliSettings {
|
||||
fix,
|
||||
fix_only,
|
||||
format,
|
||||
show_fixes,
|
||||
update_check,
|
||||
..
|
||||
} = pyproject_strategy.top_level_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 cache = !cli.no_cache;
|
||||
let noqa = !cli.ignore_noqa;
|
||||
let mut printer_flags = PrinterFlags::empty();
|
||||
if !(cli.diff || fix_only) {
|
||||
printer_flags |= PrinterFlags::SHOW_VIOLATIONS;
|
||||
}
|
||||
if show_fixes {
|
||||
printer_flags |= PrinterFlags::SHOW_FIXES;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
if !matches!(autofix, fix::FixMode::None) {
|
||||
warn_user_once!("--fix is incompatible with --add-noqa.");
|
||||
}
|
||||
let modifications =
|
||||
commands::add_noqa::add_noqa(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
let s = if modifications == 1 { "" } else { "s" };
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("Added {modifications} noqa directive{s}.");
|
||||
}
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let printer = Printer::new(format, log_level, autofix, printer_flags);
|
||||
|
||||
if cli.watch {
|
||||
if !matches!(autofix, fix::FixMode::None) {
|
||||
warn_user_once!("--fix is unsupported 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::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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::run_stdin(
|
||||
cli.stdin_filename.map(fs::normalize_path).as_deref(),
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
noqa.into(),
|
||||
autofix,
|
||||
)?
|
||||
} else {
|
||||
commands::run::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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)) {
|
||||
if cli.statistics {
|
||||
printer.write_statistics(&diagnostics)?;
|
||||
} else {
|
||||
printer.write_once(&diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
if update_check {
|
||||
warn_user_once!(
|
||||
"update-check has been removed; setting it will cause an error in a future \
|
||||
version."
|
||||
);
|
||||
}
|
||||
|
||||
if !cli.exit_zero {
|
||||
if cli.diff || fix_only {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
} else if cli.exit_non_zero_on_fix {
|
||||
if !diagnostics.fixed.is_empty() || !diagnostics.messages.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
} else {
|
||||
if !diagnostics.messages.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
use std::io::{self};
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use notify::{recommended_watcher, RecursiveMode, Watcher};
|
||||
|
||||
use ::ruff::logging::{set_up_logging, LogLevel};
|
||||
use ::ruff::settings::types::SerializationFormat;
|
||||
use ::ruff::settings::CliSettings;
|
||||
use ::ruff::{fix, fs, warn_user_once};
|
||||
use args::{Args, CheckArgs, Command};
|
||||
use printer::{Flags as PrinterFlags, Printer};
|
||||
|
||||
pub(crate) mod args;
|
||||
mod cache;
|
||||
mod commands;
|
||||
mod diagnostics;
|
||||
mod iterators;
|
||||
mod printer;
|
||||
mod resolve;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "openbsd"),
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
enum ExitStatus {
|
||||
/// Linting was successful and there were no linting errors.
|
||||
Success,
|
||||
/// Linting was successful but there were linting errors.
|
||||
Failure,
|
||||
/// Linting failed.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for ExitCode {
|
||||
fn from(status: ExitStatus) -> Self {
|
||||
match status {
|
||||
ExitStatus::Success => ExitCode::from(0),
|
||||
ExitStatus::Failure => ExitCode::from(1),
|
||||
ExitStatus::Error => ExitCode::from(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_main() -> Result<ExitStatus> {
|
||||
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(arg) = args.get(1).and_then(|s| s.to_str()) {
|
||||
if !Command::has_subcommand(rewrite_legacy_subcommand(arg))
|
||||
&& arg != "-h"
|
||||
&& arg != "--help"
|
||||
&& arg != "-V"
|
||||
&& arg != "--version"
|
||||
&& arg != "help"
|
||||
{
|
||||
args.insert(1, "check".into());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract command-line arguments.
|
||||
let Args {
|
||||
command,
|
||||
log_level_args,
|
||||
} = Args::parse_from(args);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let default_panic_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
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::Rule { rule, format } => commands::rule::rule(&rule, format)?,
|
||||
Command::Config { option } => return Ok(commands::config::config(option.as_deref())),
|
||||
Command::Linter { format } => commands::linter::linter(format)?,
|
||||
Command::Clean => commands::clean::clean(log_level)?,
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut io::stdout());
|
||||
}
|
||||
Command::Check(args) => return check(args, log_level),
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
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::show_settings(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
if cli.show_files {
|
||||
commands::show_files::show_files(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
// Extract options that are included in `Settings`, but only apply at the top
|
||||
// level.
|
||||
let CliSettings {
|
||||
fix,
|
||||
fix_only,
|
||||
format,
|
||||
show_fixes,
|
||||
update_check,
|
||||
..
|
||||
} = pyproject_strategy.top_level_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 cache = !cli.no_cache;
|
||||
let noqa = !cli.ignore_noqa;
|
||||
let mut printer_flags = PrinterFlags::empty();
|
||||
if !(cli.diff || fix_only) {
|
||||
printer_flags |= PrinterFlags::SHOW_VIOLATIONS;
|
||||
}
|
||||
if show_fixes {
|
||||
printer_flags |= PrinterFlags::SHOW_FIXES;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
if !matches!(autofix, fix::FixMode::None) {
|
||||
warn_user_once!("--fix is incompatible with --add-noqa.");
|
||||
}
|
||||
let modifications =
|
||||
commands::add_noqa::add_noqa(&cli.files, &pyproject_strategy, &overrides)?;
|
||||
if modifications > 0 && log_level >= LogLevel::Default {
|
||||
let s = if modifications == 1 { "" } else { "s" };
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("Added {modifications} noqa directive{s}.");
|
||||
}
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let printer = Printer::new(format, log_level, autofix, printer_flags);
|
||||
|
||||
if cli.watch {
|
||||
if !matches!(autofix, fix::FixMode::None) {
|
||||
warn_user_once!("--fix is unsupported 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::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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::run_stdin(
|
||||
cli.stdin_filename.map(fs::normalize_path).as_deref(),
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
noqa.into(),
|
||||
autofix,
|
||||
)?
|
||||
} else {
|
||||
commands::run::run(
|
||||
&cli.files,
|
||||
&pyproject_strategy,
|
||||
&overrides,
|
||||
cache.into(),
|
||||
noqa.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)) {
|
||||
if cli.statistics {
|
||||
printer.write_statistics(&diagnostics)?;
|
||||
} else {
|
||||
printer.write_once(&diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
if update_check {
|
||||
warn_user_once!(
|
||||
"update-check has been removed; setting it will cause an error in a future \
|
||||
version."
|
||||
);
|
||||
}
|
||||
|
||||
if !cli.exit_zero {
|
||||
if cli.diff || fix_only {
|
||||
if !diagnostics.fixed.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
} else if cli.exit_non_zero_on_fix {
|
||||
if !diagnostics.fixed.is_empty() || !diagnostics.messages.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
} else {
|
||||
if !diagnostics.messages.is_empty() {
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
|
||||
match cmd {
|
||||
"--explain" => "rule",
|
||||
"--clean" => "clean",
|
||||
"--generate-shell-completion" => "generate-shell-completion",
|
||||
cmd => cmd,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn main() -> ExitCode {
|
||||
match inner_main() {
|
||||
Ok(code) => code.into(),
|
||||
Err(err) => {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("{}{} {err:?}", "error".red().bold(), ":".bold());
|
||||
}
|
||||
ExitStatus::Error.into()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,28 +8,39 @@ pub const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
|
|||
|
||||
#[derive(clap::Args)]
|
||||
pub struct Args {
|
||||
/// Write the generated artifacts to stdout (rather than to the filesystem).
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)]
|
||||
pub enum Mode {
|
||||
/// Update the content in the `configuration.md`
|
||||
#[default]
|
||||
Write,
|
||||
|
||||
/// Don't write to the file, check if the file is up-to-date and error if not
|
||||
#[arg(long)]
|
||||
check: bool,
|
||||
Check,
|
||||
|
||||
/// Write the generated help to stdout (rather than to `docs/configuration.md`).
|
||||
DryRun,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
const fn is_check(self) -> bool {
|
||||
matches!(self, Mode::Check)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_dry_run(self) -> bool {
|
||||
matches!(self, Mode::DryRun)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main(args: &Args) -> Result<()> {
|
||||
// Not checked in
|
||||
if !args.check {
|
||||
generate_docs::main(&generate_docs::Args {
|
||||
dry_run: args.dry_run,
|
||||
})?;
|
||||
if !args.mode.is_check() {
|
||||
generate_docs::main(&generate_docs::Args { dry_run: true })?;
|
||||
}
|
||||
generate_json_schema::main(&generate_json_schema::Args {
|
||||
dry_run: args.dry_run,
|
||||
check: args.check,
|
||||
})?;
|
||||
generate_cli_help::main(&generate_cli_help::Args {
|
||||
dry_run: args.dry_run,
|
||||
check: args.check,
|
||||
})?;
|
||||
generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
|
||||
generate_cli_help::main(&generate_cli_help::Args { mode: args.mode })?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
//! Generate CLI help.
|
||||
#![allow(clippy::print_stdout, clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, str};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::CommandFactory;
|
||||
use pretty_assertions::StrComparison;
|
||||
use ruff_cli::args;
|
||||
|
||||
use crate::generate_all::REGENERATE_ALL_COMMAND;
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
const COMMAND_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated command help. -->\n";
|
||||
|
@ -18,12 +20,8 @@ const SUBCOMMAND_HELP_END_PRAGMA: &str = "<!-- End auto-generated subcommand hel
|
|||
|
||||
#[derive(clap::Args)]
|
||||
pub struct Args {
|
||||
/// Write the generated help to stdout (rather than to `docs/configuration.md`).
|
||||
#[arg(long)]
|
||||
pub(crate) dry_run: bool,
|
||||
/// Don't write to the file, check if the file is up-to-date and error if not
|
||||
#[arg(long)]
|
||||
pub(crate) check: bool,
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
fn trim_lines(s: &str) -> String {
|
||||
|
@ -36,59 +34,63 @@ fn replace_docs_section(
|
|||
section: &str,
|
||||
begin_pragma: &str,
|
||||
end_pragma: &str,
|
||||
) -> String {
|
||||
) -> Result<String> {
|
||||
// Extract the prefix.
|
||||
let index = existing
|
||||
.find(begin_pragma)
|
||||
.expect("Unable to find begin pragma");
|
||||
.with_context(|| "Unable to find begin pragma")?;
|
||||
let prefix = &existing[..index + begin_pragma.len()];
|
||||
|
||||
// Extract the suffix.
|
||||
let index = existing
|
||||
.find(end_pragma)
|
||||
.expect("Unable to find end pragma");
|
||||
.with_context(|| "Unable to find end pragma")?;
|
||||
let suffix = &existing[index..];
|
||||
|
||||
format!("{prefix}\n{section}{suffix}")
|
||||
Ok(format!("{prefix}\n{section}{suffix}"))
|
||||
}
|
||||
|
||||
pub fn main(args: &Args) -> Result<()> {
|
||||
pub(super) fn main(args: &Args) -> Result<()> {
|
||||
// Generate `ruff help`.
|
||||
let command_help = trim_lines(ruff_cli::command_help().trim());
|
||||
let command_help = trim_lines(&help_text());
|
||||
|
||||
// Generate `ruff help check`.
|
||||
let subcommand_help = trim_lines(ruff_cli::subcommand_help().trim());
|
||||
let subcommand_help = trim_lines(&check_help_text());
|
||||
|
||||
if args.dry_run {
|
||||
if args.mode.is_dry_run() {
|
||||
print!("{command_help}");
|
||||
print!("{subcommand_help}");
|
||||
} else {
|
||||
// Read the existing file.
|
||||
let filename = "docs/configuration.md";
|
||||
let file = PathBuf::from(ROOT_DIR).join(filename);
|
||||
let existing = fs::read_to_string(&file)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new = replace_docs_section(
|
||||
&existing,
|
||||
&format!("```text\n{command_help}\n```\n\n"),
|
||||
COMMAND_HELP_BEGIN_PRAGMA,
|
||||
COMMAND_HELP_END_PRAGMA,
|
||||
);
|
||||
let new = replace_docs_section(
|
||||
&new,
|
||||
&format!("```text\n{subcommand_help}\n```\n\n"),
|
||||
SUBCOMMAND_HELP_BEGIN_PRAGMA,
|
||||
SUBCOMMAND_HELP_END_PRAGMA,
|
||||
);
|
||||
// Read the existing file.
|
||||
let filename = "docs/configuration.md";
|
||||
let file = PathBuf::from(ROOT_DIR).join(filename);
|
||||
let existing = fs::read_to_string(&file)?;
|
||||
|
||||
if args.check {
|
||||
let new = replace_docs_section(
|
||||
&existing,
|
||||
&format!("```text\n{command_help}\n```\n\n"),
|
||||
COMMAND_HELP_BEGIN_PRAGMA,
|
||||
COMMAND_HELP_END_PRAGMA,
|
||||
)?;
|
||||
let new = replace_docs_section(
|
||||
&new,
|
||||
&format!("```text\n{subcommand_help}\n```\n\n"),
|
||||
SUBCOMMAND_HELP_BEGIN_PRAGMA,
|
||||
SUBCOMMAND_HELP_END_PRAGMA,
|
||||
)?;
|
||||
|
||||
match args.mode {
|
||||
Mode::Check => {
|
||||
if existing == new {
|
||||
println!("up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(&existing, &new);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
} else {
|
||||
}
|
||||
_ => {
|
||||
fs::write(file, &new)?;
|
||||
}
|
||||
}
|
||||
|
@ -96,16 +98,33 @@ pub fn main(args: &Args) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the output of `ruff help`.
|
||||
fn help_text() -> String {
|
||||
args::Args::command().render_help().to_string()
|
||||
}
|
||||
|
||||
/// Returns the output of `ruff help check`.
|
||||
fn check_help_text() -> String {
|
||||
let mut cmd = args::Args::command();
|
||||
|
||||
// The build call is necessary for the help output to contain `Usage: ruff
|
||||
// check` instead of `Usage: check` see https://github.com/clap-rs/clap/issues/4685
|
||||
cmd.build();
|
||||
|
||||
cmd.find_subcommand_mut("check")
|
||||
.expect("`check` subcommand not found")
|
||||
.render_help()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{main, Args};
|
||||
use crate::generate_all::Mode;
|
||||
use anyhow::Result;
|
||||
|
||||
#[test]
|
||||
fn test_generate_json_schema() -> Result<()> {
|
||||
main(&Args {
|
||||
dry_run: false,
|
||||
check: true,
|
||||
})
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::generate_all::REGENERATE_ALL_COMMAND;
|
||||
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
|
||||
use anyhow::{bail, Result};
|
||||
use pretty_assertions::StrComparison;
|
||||
use ruff::settings::options::Options;
|
||||
|
@ -14,11 +14,8 @@ use crate::ROOT_DIR;
|
|||
#[derive(clap::Args)]
|
||||
pub struct Args {
|
||||
/// Write the generated table to stdout (rather than to `ruff.schema.json`).
|
||||
#[arg(long)]
|
||||
pub(crate) dry_run: bool,
|
||||
/// Don't write to the file, check if the file is up-to-date and error if not
|
||||
#[arg(long)]
|
||||
pub(crate) check: bool,
|
||||
#[arg(long, default_value_t, value_enum)]
|
||||
pub(crate) mode: Mode,
|
||||
}
|
||||
|
||||
pub fn main(args: &Args) -> Result<()> {
|
||||
|
@ -27,33 +24,36 @@ pub fn main(args: &Args) -> Result<()> {
|
|||
let filename = "ruff.schema.json";
|
||||
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
|
||||
|
||||
if args.dry_run {
|
||||
println!("{schema_string}");
|
||||
} else if args.check {
|
||||
let current = fs::read_to_string(schema_path)?;
|
||||
if current == schema_string {
|
||||
println!("up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &schema_string);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
match args.mode {
|
||||
Mode::DryRun => {
|
||||
println!("{schema_string}");
|
||||
}
|
||||
Mode::Check => {
|
||||
let current = fs::read_to_string(schema_path)?;
|
||||
if current == schema_string {
|
||||
println!("up-to-date: {filename}");
|
||||
} else {
|
||||
let comparison = StrComparison::new(¤t, &schema_string);
|
||||
bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
|
||||
}
|
||||
}
|
||||
Mode::Write => {
|
||||
let file = schema_path;
|
||||
fs::write(file, schema_string.as_bytes())?;
|
||||
}
|
||||
} else {
|
||||
let file = schema_path;
|
||||
fs::write(file, schema_string.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{main, Args};
|
||||
use crate::generate_all::Mode;
|
||||
use anyhow::Result;
|
||||
|
||||
#[test]
|
||||
fn test_generate_json_schema() -> Result<()> {
|
||||
main(&Args {
|
||||
dry_run: false,
|
||||
check: true,
|
||||
})
|
||||
main(&Args { mode: Mode::Check })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue