diff --git a/README.md b/README.md index 52a2ee79d4..2b2a74ef7d 100644 --- a/README.md +++ b/README.md @@ -1440,6 +1440,25 @@ line-length = 120 --- +#### [`format`](#format) + +The style in which violation messages should be formatted: `"text"` (default), `"grouped"` +(group messages by file), or `"json"` (machine-readable). + +**Default value**: `"text"` + +**Type**: `SerializationFormat` + +**Example usage**: + +```toml +[tool.ruff] +# Group violations by containing file. +format = "grouped" +``` + +--- + #### [`per_file_ignores`](#per_file_ignores) A list of mappings from file pattern to check code prefixes to exclude, when considering any diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index b8099817a3..9a0ab39594 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -251,6 +251,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: None, per_file_ignores: None, @@ -291,6 +292,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: Some(100), per_file_ignores: None, @@ -331,6 +333,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: Some(100), per_file_ignores: None, @@ -371,6 +374,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: None, per_file_ignores: None, @@ -411,6 +415,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: None, per_file_ignores: None, @@ -459,6 +464,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: None, per_file_ignores: None, @@ -534,6 +540,7 @@ mod tests { external: None, fix: None, fixable: None, + format: None, ignore: Some(vec![]), line_length: None, per_file_ignores: None, diff --git a/src/cli.rs b/src/cli.rs index 0314203cfa..b2a2cd4199 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,8 +7,9 @@ use rustc_hash::FxHashMap; use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; use crate::logging::LogLevel; -use crate::printer::SerializationFormat; -use crate::settings::types::{FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion}; +use crate::settings::types::{ + FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat, +}; #[derive(Debug, Parser)] #[command(author, about = "Ruff: An extremely fast Python linter.")] @@ -77,8 +78,8 @@ pub struct Cli { #[arg(long, value_delimiter = ',')] pub per_file_ignores: Vec, /// Output serialization format for error messages. - #[arg(long, value_enum, default_value_t = SerializationFormat::Text)] - pub format: SerializationFormat, + #[arg(long, value_enum)] + pub format: Option, /// Show violations with source code. #[arg(long)] pub show_source: bool, @@ -143,8 +144,6 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel { LogLevel::Quiet } else if cli.verbose { LogLevel::Verbose - } else if matches!(cli.format, SerializationFormat::Json) { - LogLevel::Quiet } else { LogLevel::Default } diff --git a/src/commands.rs b/src/commands.rs index 3b37012bec..e8aea7ffd1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,7 @@ use walkdir::DirEntry; use crate::checks::CheckCode; use crate::fs::iter_python_files; -use crate::printer::SerializationFormat; +use crate::settings::types::SerializationFormat; use crate::{Configuration, Settings}; /// Print the user-facing configuration settings. diff --git a/src/main.rs b/src/main.rs index a6c932919c..e14dc8b5e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,8 +23,9 @@ use ::ruff::fs::iter_python_files; use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics}; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::message::Message; -use ::ruff::printer::{Printer, SerializationFormat}; +use ::ruff::printer::Printer; use ::ruff::settings::configuration::Configuration; +use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::{pyproject, Settings}; #[cfg(feature = "update-informer")] use ::ruff::updates; @@ -189,14 +190,7 @@ fn inner_main() -> Result { // Extract command-line arguments. let cli = Cli::parse(); let fix = cli.fix(); - let log_level = extract_log_level(&cli); - set_up_logging(&log_level)?; - - if let Some(code) = cli.explain { - commands::explain(&code, cli.format)?; - return Ok(ExitCode::SUCCESS); - } if let Some(shell) = cli.generate_shell_completion { shell.generate(&mut Cli::command(), &mut std::io::stdout()); @@ -205,17 +199,9 @@ fn inner_main() -> Result { // Find the project root and pyproject.toml. let project_root = pyproject::find_project_root(&cli.files); - match &project_root { - Some(path) => debug!("Found project root at: {:?}", path), - None => debug!("Unable to identify project root; assuming current directory..."), - }; let pyproject = cli .config .or_else(|| pyproject::find_pyproject_toml(project_root.as_ref())); - match &pyproject { - Some(path) => debug!("Found pyproject.toml at: {:?}", path), - None => debug!("Unable to find pyproject.toml; using default settings..."), - }; // Reconcile configuration from pyproject.toml and command-line arguments. let mut configuration = @@ -247,6 +233,9 @@ fn inner_main() -> Result { if !cli.unfixable.is_empty() { configuration.unfixable = cli.unfixable; } + if let Some(format) = cli.format { + configuration.format = format; + } if let Some(line_length) = cli.line_length { configuration.line_length = line_length; } @@ -279,6 +268,30 @@ fn inner_main() -> Result { let fix_enabled: bool = configuration.fix; let settings = Settings::from_configuration(configuration, project_root.as_ref())?; + // If we're using JSON, override the log level. + let log_level = if matches!(settings.format, SerializationFormat::Json) { + LogLevel::Quiet + } else { + log_level + }; + set_up_logging(&log_level)?; + + // Now that we've inferred the appropriate log level, add some debug + // information. + match &project_root { + Some(path) => debug!("Found project root at: {:?}", path), + None => debug!("Unable to identify project root; assuming current directory..."), + }; + match &pyproject { + Some(path) => debug!("Found pyproject.toml at: {:?}", path), + None => debug!("Unable to find pyproject.toml; using default settings..."), + }; + + if let Some(code) = cli.explain { + commands::explain(&code, settings.format)?; + return Ok(ExitCode::SUCCESS); + } + if cli.show_files { commands::show_files(&cli.files, &settings); return Ok(ExitCode::SUCCESS); @@ -291,24 +304,21 @@ fn inner_main() -> Result { cache_enabled = false; } - let printer = Printer::new(&cli.format, &log_level); + let printer = Printer::new(&settings.format, &log_level); if cli.watch { + if settings.format != SerializationFormat::Text { + eprintln!("Warning: --format 'text' is used in watch mode."); + } if fix_enabled { eprintln!("Warning: --fix is not enabled in watch mode."); } - if cli.add_noqa { eprintln!("Warning: --no-qa is not enabled in watch mode."); } - if cli.autoformat { eprintln!("Warning: --autoformat is not enabled in watch mode."); } - if cli.format != SerializationFormat::Text { - eprintln!("Warning: --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"); diff --git a/src/printer.rs b/src/printer.rs index 8623a77b1c..01ef8d3b05 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -4,7 +4,6 @@ use std::path::Path; use annotate_snippets::display_list::{DisplayList, FormatOptions}; use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}; use anyhow::Result; -use clap::ValueEnum; use colored::Colorize; use itertools::iterate; use rustpython_parser::ast::Location; @@ -15,15 +14,9 @@ use crate::fs::relativize_path; use crate::linter::Diagnostics; use crate::logging::LogLevel; use crate::message::Message; +use crate::settings::types::SerializationFormat; use crate::tell_user; -#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)] -pub enum SerializationFormat { - Text, - Json, - Grouped, -} - #[derive(Serialize)] struct ExpandedMessage<'a> { kind: &'a CheckKind, diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 4560fb6293..56704e0329 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -11,7 +11,7 @@ use regex::Regex; use crate::checks_gen::{CheckCodePrefix, CATEGORIES}; use crate::settings::pyproject::load_options; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::{ flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe, pep8_naming, @@ -27,6 +27,7 @@ pub struct Configuration { pub external: Vec, pub fix: bool, pub fixable: Vec, + pub format: SerializationFormat, pub ignore: Vec, pub line_length: usize, pub per_file_ignores: Vec, @@ -121,6 +122,7 @@ impl Configuration { fix: options.fix.unwrap_or_default(), fixable: options.fixable.unwrap_or_else(|| CATEGORIES.to_vec()), unfixable: options.unfixable.unwrap_or_default(), + format: options.format.unwrap_or(SerializationFormat::Text), ignore: options.ignore.unwrap_or_default(), line_length: options.line_length.unwrap_or(88), per_file_ignores: options diff --git a/src/settings/mod.rs b/src/settings/mod.rs index d06ca1590f..60b5d92504 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -15,7 +15,7 @@ use rustc_hash::FxHashSet; use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; use crate::settings::configuration::Configuration; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::{ flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe, pep8_naming, @@ -34,6 +34,7 @@ pub struct Settings { pub extend_exclude: GlobSet, pub external: BTreeSet, pub fixable: FxHashSet, + pub format: SerializationFormat, pub line_length: usize, pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, BTreeSet)>, pub show_source: bool, @@ -72,6 +73,7 @@ impl Settings { extend_exclude: resolve_globset(config.extend_exclude, project_root)?, external: BTreeSet::from_iter(config.external), fixable: resolve_codes(&config.fixable, &config.unfixable), + format: config.format, flake8_annotations: config.flake8_annotations, flake8_bugbear: config.flake8_bugbear, flake8_quotes: config.flake8_quotes, @@ -91,12 +93,14 @@ impl Settings { Self { dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), enabled: FxHashSet::from_iter([check_code.clone()]), - fixable: FxHashSet::from_iter([check_code]), exclude: GlobSet::empty(), extend_exclude: GlobSet::empty(), external: BTreeSet::default(), + fixable: FxHashSet::from_iter([check_code]), + format: SerializationFormat::Text, line_length: 88, per_file_ignores: vec![], + show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, flake8_annotations: flake8_annotations::settings::Settings::default(), @@ -106,7 +110,6 @@ impl Settings { isort: isort::settings::Settings::default(), mccabe: mccabe::settings::Settings::default(), pep8_naming: pep8_naming::settings::Settings::default(), - show_source: false, } } @@ -114,12 +117,14 @@ impl Settings { Self { dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), enabled: FxHashSet::from_iter(check_codes.clone()), - fixable: FxHashSet::from_iter(check_codes), exclude: GlobSet::empty(), extend_exclude: GlobSet::empty(), external: BTreeSet::default(), + fixable: FxHashSet::from_iter(check_codes), + format: SerializationFormat::Text, line_length: 88, per_file_ignores: vec![], + show_source: false, src: vec![path_dedot::CWD.clone()], target_version: PythonVersion::Py310, flake8_annotations: flake8_annotations::settings::Settings::default(), @@ -129,7 +134,6 @@ impl Settings { isort: isort::settings::Settings::default(), mccabe: mccabe::settings::Settings::default(), pep8_naming: pep8_naming::settings::Settings::default(), - show_source: false, } } } diff --git a/src/settings/options.rs b/src/settings/options.rs index 40ac20d66e..57a83e74c0 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -4,7 +4,7 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; -use crate::settings::types::PythonVersion; +use crate::settings::types::{PythonVersion, SerializationFormat}; use crate::{ flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe, pep8_naming, @@ -21,6 +21,7 @@ pub struct Options { pub external: Option>, pub fix: Option, pub fixable: Option>, + pub format: Option, pub ignore: Option>, pub line_length: Option, pub select: Option>, diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index 4a3f6e25eb..1a25c6b2e9 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -148,6 +148,7 @@ mod tests { show_source: None, src: None, target_version: None, + format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -186,6 +187,7 @@ line-length = 79 show_source: None, src: None, target_version: None, + format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -219,6 +221,7 @@ exclude = ["foo.py"] ignore: None, extend_ignore: None, fixable: None, + format: None, unfixable: None, per_file_ignores: None, dummy_variable_rgx: None, @@ -262,6 +265,7 @@ select = ["E501"] show_source: None, src: None, target_version: None, + format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -301,6 +305,7 @@ ignore = ["E501"] show_source: None, src: None, target_version: None, + format: None, unfixable: None, flake8_annotations: None, flake8_bugbear: None, @@ -378,6 +383,7 @@ other-attribute = 1 ignore: None, extend_ignore: None, fixable: None, + format: None, unfixable: None, per_file_ignores: Some(FxHashMap::from_iter([( "__init__.py".to_string(), diff --git a/src/settings/types.rs b/src/settings/types.rs index c6b80ddbf5..7a01b6b654 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use anyhow::{anyhow, Result}; +use clap::ValueEnum; use globset::{Glob, GlobSetBuilder}; use serde::{de, Deserialize, Deserializer, Serialize}; @@ -141,3 +142,11 @@ impl FromStr for PatternPrefixPair { Ok(Self { pattern, prefix }) } } + +#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum SerializationFormat { + Text, + Json, + Grouped, +}