From 0c65d0c8a6d796dea101982dcd449bd900b4f80c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 27 Sep 2023 08:46:27 +0200 Subject: [PATCH] Add `lint` section to Ruff configuration ## Summary This PR adds a new `lint` section to the configuration that groups all linter-specific settings. The existing top-level configurations continue to work without any warning because the `lint.*` settings are experimental. The configuration merges the top level and `lint.*` settings where the settings in `lint` have higher precedence (override the top-level settings). The reasoning behind this is that the settings in `lint.` are more specific and more specific settings should override less specific settings. I decided against showing the new `lint.*` options on our website because it would make the page extremely long (it's technically easy to do, just attribute `lint` with `[option_group`]). We may want to explore adding an `alias` field to the `option` attribute and show the alias on the website along with its regular name. ## Test Plan * I added new integration tests * I verified that the generated `options.md` is identical * Verified the default settings in the playground ![Screenshot from 2023-09-22 13-52-23](https://github.com/astral-sh/ruff/assets/1203881/7b4d9689-aa88-402e-9199-9c43c8d8cc2d) --- crates/flake8_to_ruff/src/converter.rs | 105 ++-- crates/ruff_cli/src/args.rs | 6 +- crates/ruff_cli/tests/integration_test.rs | 3 +- crates/ruff_cli/tests/lint.rs | 157 ++++++ crates/ruff_dev/src/generate_options.rs | 45 +- crates/ruff_macros/src/config.rs | 67 ++- crates/ruff_wasm/src/lib.rs | 62 +-- crates/ruff_workspace/src/configuration.rs | 402 +++++++------- crates/ruff_workspace/src/options.rs | 599 +++++++++++---------- crates/ruff_workspace/src/pyproject.rs | 26 +- ruff.schema.json | 434 +++++++++++++++ 11 files changed, 1298 insertions(+), 608 deletions(-) create mode 100644 crates/ruff_cli/tests/lint.rs diff --git a/crates/flake8_to_ruff/src/converter.rs b/crates/flake8_to_ruff/src/converter.rs index 0caafa0b54df5..4b6693d9c4d96 100644 --- a/crates/flake8_to_ruff/src/converter.rs +++ b/crates/flake8_to_ruff/src/converter.rs @@ -16,8 +16,8 @@ use ruff_linter::settings::types::PythonVersion; use ruff_linter::warn_user; use ruff_workspace::options::{ Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions, - Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, McCabeOptions, - Options, Pep8NamingOptions, PydocstyleOptions, + Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions, + McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions, }; use ruff_workspace::pyproject::Pyproject; @@ -103,6 +103,7 @@ pub(crate) fn convert( // Parse each supported option. let mut options = Options::default(); + let mut lint_options = LintOptions::default(); let mut flake8_annotations = Flake8AnnotationsOptions::default(); let mut flake8_bugbear = Flake8BugbearOptions::default(); let mut flake8_builtins = Flake8BuiltinsOptions::default(); @@ -150,7 +151,7 @@ pub(crate) fn convert( "per-file-ignores" | "per_file_ignores" => { match parser::parse_files_to_codes_mapping(value.as_ref()) { Ok(per_file_ignores) => { - options.per_file_ignores = + lint_options.per_file_ignores = Some(parser::collect_per_file_ignores(per_file_ignores)); } Err(e) => { @@ -358,47 +359,47 @@ pub(crate) fn convert( } // Deduplicate and sort. - options.select = Some( + lint_options.select = Some( select .into_iter() .sorted_by_key(RuleSelector::prefix_and_code) .collect(), ); - options.ignore = Some( + lint_options.ignore = Some( ignore .into_iter() .sorted_by_key(RuleSelector::prefix_and_code) .collect(), ); if flake8_annotations != Flake8AnnotationsOptions::default() { - options.flake8_annotations = Some(flake8_annotations); + lint_options.flake8_annotations = Some(flake8_annotations); } if flake8_bugbear != Flake8BugbearOptions::default() { - options.flake8_bugbear = Some(flake8_bugbear); + lint_options.flake8_bugbear = Some(flake8_bugbear); } if flake8_builtins != Flake8BuiltinsOptions::default() { - options.flake8_builtins = Some(flake8_builtins); + lint_options.flake8_builtins = Some(flake8_builtins); } if flake8_errmsg != Flake8ErrMsgOptions::default() { - options.flake8_errmsg = Some(flake8_errmsg); + lint_options.flake8_errmsg = Some(flake8_errmsg); } if flake8_pytest_style != Flake8PytestStyleOptions::default() { - options.flake8_pytest_style = Some(flake8_pytest_style); + lint_options.flake8_pytest_style = Some(flake8_pytest_style); } if flake8_quotes != Flake8QuotesOptions::default() { - options.flake8_quotes = Some(flake8_quotes); + lint_options.flake8_quotes = Some(flake8_quotes); } if flake8_tidy_imports != Flake8TidyImportsOptions::default() { - options.flake8_tidy_imports = Some(flake8_tidy_imports); + lint_options.flake8_tidy_imports = Some(flake8_tidy_imports); } if mccabe != McCabeOptions::default() { - options.mccabe = Some(mccabe); + lint_options.mccabe = Some(mccabe); } if pep8_naming != Pep8NamingOptions::default() { - options.pep8_naming = Some(pep8_naming); + lint_options.pep8_naming = Some(pep8_naming); } if pydocstyle != PydocstyleOptions::default() { - options.pydocstyle = Some(pydocstyle); + lint_options.pydocstyle = Some(pydocstyle); } // Extract any settings from the existing `pyproject.toml`. @@ -436,6 +437,10 @@ pub(crate) fn convert( } } + if lint_options != LintOptions::default() { + options.lint = Some(lint_options); + } + // Create the pyproject.toml. Pyproject::new(options) } @@ -464,7 +469,7 @@ mod tests { use ruff_linter::rules::flake8_quotes; use ruff_linter::rules::pydocstyle::settings::Convention; use ruff_linter::settings::types::PythonVersion; - use ruff_workspace::options::{Flake8QuotesOptions, Options, PydocstyleOptions}; + use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions}; use ruff_workspace::pyproject::Pyproject; use crate::converter::DEFAULT_SELECTORS; @@ -474,8 +479,8 @@ mod tests { use super::super::plugin::Plugin; use super::convert; - fn default_options(plugins: impl IntoIterator) -> Options { - Options { + fn lint_default_options(plugins: impl IntoIterator) -> LintOptions { + LintOptions { ignore: Some(vec![]), select: Some( DEFAULT_SELECTORS @@ -485,7 +490,7 @@ mod tests { .sorted_by_key(RuleSelector::prefix_and_code) .collect(), ), - ..Options::default() + ..LintOptions::default() } } @@ -496,7 +501,10 @@ mod tests { &ExternalConfig::default(), None, ); - let expected = Pyproject::new(default_options([])); + let expected = Pyproject::new(Options { + lint: Some(lint_default_options([])), + ..Options::default() + }); assert_eq!(actual, expected); } @@ -512,7 +520,8 @@ mod tests { ); let expected = Pyproject::new(Options { line_length: Some(LineLength::try_from(100).unwrap()), - ..default_options([]) + lint: Some(lint_default_options([])), + ..Options::default() }); assert_eq!(actual, expected); } @@ -529,7 +538,8 @@ mod tests { ); let expected = Pyproject::new(Options { line_length: Some(LineLength::try_from(100).unwrap()), - ..default_options([]) + lint: Some(lint_default_options([])), + ..Options::default() }); assert_eq!(actual, expected); } @@ -544,7 +554,10 @@ mod tests { &ExternalConfig::default(), Some(vec![]), ); - let expected = Pyproject::new(default_options([])); + let expected = Pyproject::new(Options { + lint: Some(lint_default_options([])), + ..Options::default() + }); assert_eq!(actual, expected); } @@ -559,13 +572,16 @@ mod tests { Some(vec![]), ); let expected = Pyproject::new(Options { - flake8_quotes: Some(Flake8QuotesOptions { - inline_quotes: Some(flake8_quotes::settings::Quote::Single), - multiline_quotes: None, - docstring_quotes: None, - avoid_escape: None, + lint: Some(LintOptions { + flake8_quotes: Some(Flake8QuotesOptions { + inline_quotes: Some(flake8_quotes::settings::Quote::Single), + multiline_quotes: None, + docstring_quotes: None, + avoid_escape: None, + }), + ..lint_default_options([]) }), - ..default_options([]) + ..Options::default() }); assert_eq!(actual, expected); } @@ -584,12 +600,15 @@ mod tests { Some(vec![Plugin::Flake8Docstrings]), ); let expected = Pyproject::new(Options { - pydocstyle: Some(PydocstyleOptions { - convention: Some(Convention::Numpy), - ignore_decorators: None, - property_decorators: None, + lint: Some(LintOptions { + pydocstyle: Some(PydocstyleOptions { + convention: Some(Convention::Numpy), + ignore_decorators: None, + property_decorators: None, + }), + ..lint_default_options([Linter::Pydocstyle.into()]) }), - ..default_options([Linter::Pydocstyle.into()]) + ..Options::default() }); assert_eq!(actual, expected); } @@ -605,13 +624,16 @@ mod tests { None, ); let expected = Pyproject::new(Options { - flake8_quotes: Some(Flake8QuotesOptions { - inline_quotes: Some(flake8_quotes::settings::Quote::Single), - multiline_quotes: None, - docstring_quotes: None, - avoid_escape: None, + lint: Some(LintOptions { + flake8_quotes: Some(Flake8QuotesOptions { + inline_quotes: Some(flake8_quotes::settings::Quote::Single), + multiline_quotes: None, + docstring_quotes: None, + avoid_escape: None, + }), + ..lint_default_options([Linter::Flake8Quotes.into()]) }), - ..default_options([Linter::Flake8Quotes.into()]) + ..Options::default() }); assert_eq!(actual, expected); } @@ -630,7 +652,8 @@ mod tests { ); let expected = Pyproject::new(Options { target_version: Some(PythonVersion::Py38), - ..default_options([]) + lint: Some(lint_default_options([])), + ..Options::default() }); assert_eq!(actual, expected); diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index b3159f4eb6ab5..06eadb86b3e44 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -610,7 +610,7 @@ impl ConfigurationTransformer for CliOverrides { config.cache_dir = Some(cache_dir.clone()); } if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx { - config.dummy_variable_rgx = Some(dummy_variable_rgx.clone()); + config.lint.dummy_variable_rgx = Some(dummy_variable_rgx.clone()); } if let Some(exclude) = &self.exclude { config.exclude = Some(exclude.clone()); @@ -624,7 +624,7 @@ impl ConfigurationTransformer for CliOverrides { if let Some(fix_only) = &self.fix_only { config.fix_only = Some(*fix_only); } - config.rule_selections.push(RuleSelection { + config.lint.rule_selections.push(RuleSelection { select: self.select.clone(), ignore: self .ignore @@ -657,7 +657,7 @@ impl ConfigurationTransformer for CliOverrides { config.preview = Some(*preview); } if let Some(per_file_ignores) = &self.per_file_ignores { - config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone())); + config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone())); } if let Some(respect_gitignore) = &self.respect_gitignore { config.respect_gitignore = Some(*respect_gitignore); diff --git a/crates/ruff_cli/tests/integration_test.rs b/crates/ruff_cli/tests/integration_test.rs index 1a4e538bd2ff8..2c059f1892ecc 100644 --- a/crates/ruff_cli/tests/integration_test.rs +++ b/crates/ruff_cli/tests/integration_test.rs @@ -12,8 +12,7 @@ use std::process::Command; use std::str; #[cfg(unix)] -use anyhow::Context; -use anyhow::Result; +use anyhow::{Context, Result}; #[cfg(unix)] use clap::Parser; use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; diff --git a/crates/ruff_cli/tests/lint.rs b/crates/ruff_cli/tests/lint.rs new file mode 100644 index 0000000000000..2cfae9668738c --- /dev/null +++ b/crates/ruff_cli/tests/lint.rs @@ -0,0 +1,157 @@ +//! Tests the interaction of the `lint` configuration section + +#![cfg(not(target_family = "wasm"))] + +use std::fs; +use std::process::Command; +use std::str; + +use anyhow::Result; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +use tempfile::TempDir; + +const BIN_NAME: &str = "ruff"; +const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "text"]; + +#[test] +fn top_level_options() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +extend-select = ["B", "Q"] + +[flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 potentially fixable with the --fix option. + + ----- stderr ----- + "###); + Ok(()) +} + +#[test] +fn lint_options() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +[lint] +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 potentially fixable with the --fix option. + + ----- stderr ----- + "###); + Ok(()) +} + +/// Tests that configurations from the top-level and `lint` section are merged together. +#[test] +fn mixed_levels() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 potentially fixable with the --fix option. + + ----- stderr ----- + "###); + Ok(()) +} + +/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific). +#[test] +fn precedence() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +[lint] +extend-select = ["B", "Q"] + +[flake8-quotes] +inline-quotes = "double" + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(&ruff_toml) + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r###" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 potentially fixable with the --fix option. + + ----- stderr ----- + "###); + Ok(()) +} diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index ea135d5ba44de..3e73b74f43010 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -14,7 +14,11 @@ pub(crate) fn generate() -> String { } fn generate_set(output: &mut String, set: &Set) { - writeln!(output, "### {title}\n", title = set.title()).unwrap(); + if set.level() < 2 { + writeln!(output, "### {title}\n", title = set.title()).unwrap(); + } else { + writeln!(output, "#### {title}\n", title = set.title()).unwrap(); + } if let Some(documentation) = set.metadata().documentation() { output.push_str(documentation); @@ -32,56 +36,69 @@ fn generate_set(output: &mut String, set: &Set) { // Generate the fields. for (name, field) in &fields { - emit_field(output, name, field, set.name()); + emit_field(output, name, field, set); output.push_str("---\n\n"); } // Generate all the sub-sets. for (set_name, sub_set) in &sets { - generate_set(output, &Set::Named(set_name, *sub_set)); + generate_set(output, &Set::Named(set_name, *sub_set, set.level() + 1)); } } enum Set<'a> { Toplevel(OptionSet), - Named(&'a str, OptionSet), + Named(&'a str, OptionSet, u32), } impl<'a> Set<'a> { fn name(&self) -> Option<&'a str> { match self { Set::Toplevel(_) => None, - Set::Named(name, _) => Some(name), + Set::Named(name, _, _) => Some(name), } } fn title(&self) -> &'a str { match self { Set::Toplevel(_) => "Top-level", - Set::Named(name, _) => name, + Set::Named(name, _, _) => name, } } fn metadata(&self) -> &OptionSet { match self { Set::Toplevel(set) => set, - Set::Named(_, set) => set, + Set::Named(_, set, _) => set, + } + } + + fn level(&self) -> u32 { + match self { + Set::Toplevel(_) => 0, + Set::Named(_, _, level) => *level, } } } -fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) { - // if there's a group name, we need to add it to the anchor - if let Some(group_name) = group_name { +fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set: &Set) { + let header_level = if parent_set.level() < 2 { + "####" + } else { + "#####" + }; + + // if there's a set name, we need to add it to the anchor + if let Some(set_name) = parent_set.name() { // the anchor used to just be the name, but now it's the group name // for backwards compatibility, we need to keep the old anchor output.push_str(&format!("\n")); output.push_str(&format!( - "#### [`{name}`](#{group_name}-{name}) {{: #{group_name}-{name} }}\n" + "{header_level} [`{name}`](#{set_name}-{name}) {{: #{set_name}-{name} }}\n" )); } else { - output.push_str(&format!("#### [`{name}`](#{name})\n")); + output.push_str(&format!("{header_level} [`{name}`](#{name})\n")); } output.push('\n'); output.push_str(field.doc); @@ -92,8 +109,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: output.push('\n'); output.push_str(&format!( "**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n", - if group_name.is_some() { - format!(".{}", group_name.unwrap()) + if let Some(set_name) = parent_set.name() { + format!(".{set_name}") } else { String::new() }, diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 5e48b2f5bfd62..ac7851384e9b4 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -1,14 +1,15 @@ -use ruff_python_trivia::textwrap::dedent; - +use proc_macro2::TokenTree; use quote::{quote, quote_spanned}; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::token::Comma; use syn::{ AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field, - Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath, + Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath, }; +use ruff_python_trivia::textwrap::dedent; + pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { let DeriveInput { ident, @@ -25,34 +26,39 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result = field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("doc")) - .collect(); - - if docs.is_empty() { - return Err(syn::Error::new( - field.span(), - "Missing documentation for field", - )); - } - if let Some(attr) = field .attrs .iter() .find(|attr| attr.path().is_ident("option")) { - output.push(handle_option(field, attr, docs)?); - }; - - if field + output.push(handle_option(field, attr)?); + } else if field .attrs .iter() .any(|attr| attr.path().is_ident("option_group")) { output.push(handle_option_group(field)?); - }; + } else if let Some(serde) = field + .attrs + .iter() + .find(|attr| attr.path().is_ident("serde")) + { + // If a field has the `serde(flatten)` attribute, flatten the options into the parent + // by calling `Type::record` instead of `visitor.visit_set` + if let (Type::Path(ty), Meta::List(list)) = (&field.ty, &serde.meta) { + for token in list.tokens.clone() { + if let TokenTree::Ident(ident) = token { + if ident == "flatten" { + let ty_name = ty.path.require_ident()?; + output.push(quote_spanned!( + ident.span() => (#ty_name::record(visit)) + )); + break; + } + } + } + } + } } let docs: Vec<&Attribute> = struct_attributes @@ -150,11 +156,20 @@ fn parse_doc(doc: &Attribute) -> syn::Result { /// Parse an `#[option(doc="...", default="...", value_type="...", /// example="...")]` attribute and return data in the form of an `OptionField`. -fn handle_option( - field: &Field, - attr: &Attribute, - docs: Vec<&Attribute>, -) -> syn::Result { +fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { + let docs: Vec<&Attribute> = field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .collect(); + + if docs.is_empty() { + return Err(syn::Error::new( + field.span(), + "Missing documentation for field", + )); + } + // Convert the list of `doc` attributes into a single string. let doc = dedent( &docs diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index c1edc621ca2af..67b8e42cfe60f 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -4,7 +4,7 @@ use js_sys::Error; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; -use ruff_formatter::{FormatResult, Formatted}; +use ruff_formatter::{FormatResult, Formatted, IndentStyle}; use ruff_linter::directives; use ruff_linter::line_width::{LineLength, TabSize}; use ruff_linter::linter::{check_path, LinterResult}; @@ -14,7 +14,7 @@ use ruff_linter::settings::{flags, DUMMY_VARIABLE_RGX, PREFIXES}; use ruff_linter::source_kind::SourceKind; use ruff_python_ast::{Mod, PySourceType}; use ruff_python_codegen::Stylist; -use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext}; +use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext, QuoteStyle}; use ruff_python_index::{CommentRangesBuilder, Indexer}; use ruff_python_parser::lexer::LexResult; use ruff_python_parser::{parse_tokens, AsMode, Mode}; @@ -22,7 +22,7 @@ use ruff_python_trivia::CommentRanges; use ruff_source_file::{Locator, SourceLocation}; use ruff_text_size::Ranged; use ruff_workspace::configuration::Configuration; -use ruff_workspace::options::Options; +use ruff_workspace::options::{FormatOptions, FormatOrOutputFormat, LintOptions, Options}; use ruff_workspace::Settings; #[wasm_bindgen(typescript_custom_section)] @@ -119,46 +119,32 @@ impl Workspace { #[wasm_bindgen(js_name = defaultSettings)] pub fn default_settings() -> Result { serde_wasm_bindgen::to_value(&Options { + preview: Some(false), + // Propagate defaults. - allowed_confusables: Some(Vec::default()), builtins: Some(Vec::default()), - dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()), - extend_fixable: Some(Vec::default()), - extend_ignore: Some(Vec::default()), - extend_select: Some(Vec::default()), - extend_unfixable: Some(Vec::default()), - external: Some(Vec::default()), - ignore: Some(Vec::default()), + line_length: Some(LineLength::default()), - preview: Some(false), - select: Some(PREFIXES.to_vec()), + tab_size: Some(TabSize::default()), target_version: Some(PythonVersion::default()), - // Ignore a bunch of options that don't make sense in a single-file editor. - cache_dir: None, - exclude: None, - extend: None, - extend_exclude: None, - extend_include: None, - extend_per_file_ignores: None, - fix: None, - fix_only: None, - fixable: None, - force_exclude: None, - output_format: None, - ignore_init_module_imports: None, - include: None, - logger_objects: None, - namespace_packages: None, - per_file_ignores: None, - required_version: None, - respect_gitignore: None, - show_fixes: None, - show_source: None, - src: None, - task_tags: None, - typing_modules: None, - unfixable: None, + + lint: Some(LintOptions { + allowed_confusables: Some(Vec::default()), + dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()), + ignore: Some(Vec::default()), + select: Some(PREFIXES.to_vec()), + extend_fixable: Some(Vec::default()), + extend_select: Some(Vec::default()), + external: Some(Vec::default()), + + ..LintOptions::default() + }), + format: Some(FormatOrOutputFormat::Format(FormatOptions { + indent_style: Some(IndentStyle::Space), + quote_style: Some(QuoteStyle::Double), + ..FormatOptions::default() + })), ..Options::default() }) .map_err(into_error) diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index afc47401c84db..89d941bb8dbec 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -39,9 +39,9 @@ use crate::options::{ Flake8ComprehensionsOptions, Flake8CopyrightOptions, Flake8ErrMsgOptions, Flake8GetTextOptions, Flake8ImplicitStrConcatOptions, Flake8ImportConventionsOptions, Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions, - Flake8UnusedArgumentsOptions, FormatOptions, FormatOrOutputFormat, IsortOptions, McCabeOptions, - Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, PydocstyleOptions, - PyflakesOptions, PylintOptions, + Flake8UnusedArgumentsOptions, FormatOptions, FormatOrOutputFormat, IsortOptions, LintOptions, + McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, + PydocstyleOptions, PyflakesOptions, PylintOptions, }; use crate::settings::{ FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE, @@ -59,64 +59,36 @@ pub struct RuleSelection { #[derive(Debug, Default)] pub struct Configuration { - pub rule_selections: Vec, - pub per_file_ignores: Option>, - - pub allowed_confusables: Option>, - pub builtins: Option>, + // Global options pub cache_dir: Option, - pub dummy_variable_rgx: Option, - pub exclude: Option>, pub extend: Option, - pub extend_exclude: Vec, - pub extend_include: Vec, - pub extend_per_file_ignores: Vec, - pub external: Option>, pub fix: Option, pub fix_only: Option, - pub force_exclude: Option, pub output_format: Option, - pub ignore_init_module_imports: Option, - pub include: Option>, - pub line_length: Option, - pub logger_objects: Option>, - pub namespace_packages: Option>, pub preview: Option, pub required_version: Option, - pub respect_gitignore: Option, pub show_fixes: Option, pub show_source: Option, + + // File resolver options + pub exclude: Option>, + pub extend_exclude: Vec, + pub extend_include: Vec, + pub force_exclude: Option, + pub include: Option>, + pub respect_gitignore: Option, + + // Generic python options settings + pub builtins: Option>, + pub namespace_packages: Option>, pub src: Option>, - pub tab_size: Option, pub target_version: Option, - pub task_tags: Option>, - pub typing_modules: Option>, - // Plugins - pub flake8_annotations: Option, - pub flake8_bandit: Option, - pub flake8_bugbear: Option, - pub flake8_builtins: Option, - pub flake8_comprehensions: Option, - pub flake8_copyright: Option, - pub flake8_errmsg: Option, - pub flake8_gettext: Option, - pub flake8_implicit_str_concat: Option, - pub flake8_import_conventions: Option, - pub flake8_pytest_style: Option, - pub flake8_quotes: Option, - pub flake8_self: Option, - pub flake8_tidy_imports: Option, - pub flake8_type_checking: Option, - pub flake8_unused_arguments: Option, - pub isort: Option, - pub mccabe: Option, - pub pep8_naming: Option, - pub pycodestyle: Option, - pub pydocstyle: Option, - pub pyflakes: Option, - pub pylint: Option, - pub pyupgrade: Option, + // Global formatting options + pub line_length: Option, + pub tab_size: Option, + + pub lint: LintConfiguration, pub format: FormatConfiguration, } @@ -133,7 +105,6 @@ impl Configuration { } let target_version = self.target_version.unwrap_or_default(); - let rules = self.as_rule_table(); let preview = self.preview.unwrap_or_default(); let format = self.format; @@ -157,6 +128,8 @@ impl Configuration { .unwrap_or(format_defaults.magic_trailing_comma), }; + let lint = self.lint; + Ok(Settings { cache_dir: self .cache_dir @@ -183,135 +156,135 @@ impl Configuration { }, linter: LinterSettings { + rules: lint.as_rule_table(preview), target_version, project_root: project_root.to_path_buf(), - rules, - allowed_confusables: self + allowed_confusables: lint .allowed_confusables .map(FxHashSet::from_iter) .unwrap_or_default(), builtins: self.builtins.unwrap_or_default(), - dummy_variable_rgx: self + dummy_variable_rgx: lint .dummy_variable_rgx .unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()), - external: FxHashSet::from_iter(self.external.unwrap_or_default()), - ignore_init_module_imports: self.ignore_init_module_imports.unwrap_or_default(), + external: FxHashSet::from_iter(lint.external.unwrap_or_default()), + ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(), line_length: self.line_length.unwrap_or_default(), tab_size: self.tab_size.unwrap_or_default(), namespace_packages: self.namespace_packages.unwrap_or_default(), per_file_ignores: resolve_per_file_ignores( - self.per_file_ignores + lint.per_file_ignores .unwrap_or_default() .into_iter() - .chain(self.extend_per_file_ignores) + .chain(lint.extend_per_file_ignores) .collect(), )?, src: self.src.unwrap_or_else(|| vec![project_root.to_path_buf()]), - task_tags: self + task_tags: lint .task_tags .unwrap_or_else(|| TASK_TAGS.iter().map(ToString::to_string).collect()), - logger_objects: self.logger_objects.unwrap_or_default(), + logger_objects: lint.logger_objects.unwrap_or_default(), preview, - typing_modules: self.typing_modules.unwrap_or_default(), + typing_modules: lint.typing_modules.unwrap_or_default(), // Plugins - flake8_annotations: self + flake8_annotations: lint .flake8_annotations .map(Flake8AnnotationsOptions::into_settings) .unwrap_or_default(), - flake8_bandit: self + flake8_bandit: lint .flake8_bandit .map(Flake8BanditOptions::into_settings) .unwrap_or_default(), - flake8_bugbear: self + flake8_bugbear: lint .flake8_bugbear .map(Flake8BugbearOptions::into_settings) .unwrap_or_default(), - flake8_builtins: self + flake8_builtins: lint .flake8_builtins .map(Flake8BuiltinsOptions::into_settings) .unwrap_or_default(), - flake8_comprehensions: self + flake8_comprehensions: lint .flake8_comprehensions .map(Flake8ComprehensionsOptions::into_settings) .unwrap_or_default(), - flake8_copyright: self + flake8_copyright: lint .flake8_copyright .map(Flake8CopyrightOptions::try_into_settings) .transpose()? .unwrap_or_default(), - flake8_errmsg: self + flake8_errmsg: lint .flake8_errmsg .map(Flake8ErrMsgOptions::into_settings) .unwrap_or_default(), - flake8_implicit_str_concat: self + flake8_implicit_str_concat: lint .flake8_implicit_str_concat .map(Flake8ImplicitStrConcatOptions::into_settings) .unwrap_or_default(), - flake8_import_conventions: self + flake8_import_conventions: lint .flake8_import_conventions .map(Flake8ImportConventionsOptions::into_settings) .unwrap_or_default(), - flake8_pytest_style: self + flake8_pytest_style: lint .flake8_pytest_style .map(Flake8PytestStyleOptions::try_into_settings) .transpose()? .unwrap_or_default(), - flake8_quotes: self + flake8_quotes: lint .flake8_quotes .map(Flake8QuotesOptions::into_settings) .unwrap_or_default(), - flake8_self: self + flake8_self: lint .flake8_self .map(Flake8SelfOptions::into_settings) .unwrap_or_default(), - flake8_tidy_imports: self + flake8_tidy_imports: lint .flake8_tidy_imports .map(Flake8TidyImportsOptions::into_settings) .unwrap_or_default(), - flake8_type_checking: self + flake8_type_checking: lint .flake8_type_checking .map(Flake8TypeCheckingOptions::into_settings) .unwrap_or_default(), - flake8_unused_arguments: self + flake8_unused_arguments: lint .flake8_unused_arguments .map(Flake8UnusedArgumentsOptions::into_settings) .unwrap_or_default(), - flake8_gettext: self + flake8_gettext: lint .flake8_gettext .map(Flake8GetTextOptions::into_settings) .unwrap_or_default(), - isort: self + isort: lint .isort .map(IsortOptions::try_into_settings) .transpose()? .unwrap_or_default(), - mccabe: self + mccabe: lint .mccabe .map(McCabeOptions::into_settings) .unwrap_or_default(), - pep8_naming: self + pep8_naming: lint .pep8_naming .map(Pep8NamingOptions::try_into_settings) .transpose()? .unwrap_or_default(), - pycodestyle: self + pycodestyle: lint .pycodestyle .map(PycodestyleOptions::into_settings) .unwrap_or_default(), - pydocstyle: self + pydocstyle: lint .pydocstyle .map(PydocstyleOptions::into_settings) .unwrap_or_default(), - pyflakes: self + pyflakes: lint .pyflakes .map(PyflakesOptions::into_settings) .unwrap_or_default(), - pylint: self + pylint: lint .pylint .map(PylintOptions::into_settings) .unwrap_or_default(), - pyupgrade: self + pyupgrade: lint .pyupgrade .map(PyUpgradeOptions::into_settings) .unwrap_or_default(), @@ -322,26 +295,13 @@ impl Configuration { } pub fn from_options(options: Options, project_root: &Path) -> Result { + let lint = if let Some(lint) = options.lint { + lint.combine(options.lint_top_level) + } else { + options.lint_top_level + }; + Ok(Self { - rule_selections: vec![RuleSelection { - select: options.select, - ignore: options - .ignore - .into_iter() - .flatten() - .chain(options.extend_ignore.into_iter().flatten()) - .collect(), - extend_select: options.extend_select.unwrap_or_default(), - fixable: options.fixable, - unfixable: options - .unfixable - .into_iter() - .flatten() - .chain(options.extend_unfixable.into_iter().flatten()) - .collect(), - extend_fixable: options.extend_fixable.unwrap_or_default(), - }], - allowed_confusables: options.allowed_confusables, builtins: options.builtins, cache_dir: options .cache_dir @@ -351,11 +311,7 @@ impl Configuration { }) .transpose() .map_err(|e| anyhow!("Invalid `cache-dir` value: {e}"))?, - dummy_variable_rgx: options - .dummy_variable_rgx - .map(|pattern| Regex::new(&pattern)) - .transpose() - .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?, + exclude: options.exclude.map(|paths| { paths .into_iter() @@ -397,28 +353,6 @@ impl Configuration { .collect() }) .unwrap_or_default(), - extend_per_file_ignores: options - .extend_per_file_ignores - .map(|per_file_ignores| { - per_file_ignores - .into_iter() - .map(|(pattern, prefixes)| { - PerFileIgnore::new(pattern, &prefixes, Some(project_root)) - }) - .collect() - }) - .unwrap_or_default(), - external: options.external, - fix: options.fix, - fix_only: options.fix_only, - output_format: options.output_format.or_else(|| { - options - .format - .as_ref() - .and_then(FormatOrOutputFormat::as_output_format) - }), - force_exclude: options.force_exclude, - ignore_init_module_imports: options.ignore_init_module_imports, include: options.include.map(|paths| { paths .into_iter() @@ -428,6 +362,15 @@ impl Configuration { }) .collect() }), + fix: options.fix, + fix_only: options.fix_only, + output_format: options.output_format.or_else(|| { + options + .format + .as_ref() + .and_then(FormatOrOutputFormat::as_output_format) + }), + force_exclude: options.force_exclude, line_length: options.line_length, tab_size: options.tab_size, namespace_packages: options @@ -435,14 +378,6 @@ impl Configuration { .map(|namespace_package| resolve_src(&namespace_package, project_root)) .transpose()?, preview: options.preview.map(PreviewMode::from), - per_file_ignores: options.per_file_ignores.map(|per_file_ignores| { - per_file_ignores - .into_iter() - .map(|(pattern, prefixes)| { - PerFileIgnore::new(pattern, &prefixes, Some(project_root)) - }) - .collect() - }), required_version: options.required_version, respect_gitignore: options.respect_gitignore, show_source: options.show_source, @@ -452,6 +387,146 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version, + + lint: LintConfiguration::from_options(lint, project_root)?, + format: if let Some(FormatOrOutputFormat::Format(format)) = options.format { + FormatConfiguration::from_options(format)? + } else { + FormatConfiguration::default() + }, + }) + } + + #[must_use] + pub fn combine(self, config: Self) -> Self { + Self { + builtins: self.builtins.or(config.builtins), + cache_dir: self.cache_dir.or(config.cache_dir), + exclude: self.exclude.or(config.exclude), + extend: self.extend.or(config.extend), + extend_exclude: config + .extend_exclude + .into_iter() + .chain(self.extend_exclude) + .collect(), + extend_include: config + .extend_include + .into_iter() + .chain(self.extend_include) + .collect(), + include: self.include.or(config.include), + fix: self.fix.or(config.fix), + fix_only: self.fix_only.or(config.fix_only), + output_format: self.output_format.or(config.output_format), + force_exclude: self.force_exclude.or(config.force_exclude), + line_length: self.line_length.or(config.line_length), + tab_size: self.tab_size.or(config.tab_size), + namespace_packages: self.namespace_packages.or(config.namespace_packages), + required_version: self.required_version.or(config.required_version), + respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), + show_source: self.show_source.or(config.show_source), + show_fixes: self.show_fixes.or(config.show_fixes), + src: self.src.or(config.src), + target_version: self.target_version.or(config.target_version), + preview: self.preview.or(config.preview), + + lint: self.lint.combine(config.lint), + format: self.format.combine(config.format), + } + } +} + +#[derive(Debug, Default)] +pub struct LintConfiguration { + // Rule selection + pub extend_per_file_ignores: Vec, + pub per_file_ignores: Option>, + pub rule_selections: Vec, + + // Global lint settings + pub allowed_confusables: Option>, + pub dummy_variable_rgx: Option, + pub external: Option>, + pub ignore_init_module_imports: Option, + pub logger_objects: Option>, + pub task_tags: Option>, + pub typing_modules: Option>, + + // Plugins + pub flake8_annotations: Option, + pub flake8_bandit: Option, + pub flake8_bugbear: Option, + pub flake8_builtins: Option, + pub flake8_comprehensions: Option, + pub flake8_copyright: Option, + pub flake8_errmsg: Option, + pub flake8_gettext: Option, + pub flake8_implicit_str_concat: Option, + pub flake8_import_conventions: Option, + pub flake8_pytest_style: Option, + pub flake8_quotes: Option, + pub flake8_self: Option, + pub flake8_tidy_imports: Option, + pub flake8_type_checking: Option, + pub flake8_unused_arguments: Option, + pub isort: Option, + pub mccabe: Option, + pub pep8_naming: Option, + pub pycodestyle: Option, + pub pydocstyle: Option, + pub pyflakes: Option, + pub pylint: Option, + pub pyupgrade: Option, +} + +impl LintConfiguration { + fn from_options(options: LintOptions, project_root: &Path) -> Result { + Ok(LintConfiguration { + rule_selections: vec![RuleSelection { + select: options.select, + ignore: options + .ignore + .into_iter() + .flatten() + .chain(options.extend_ignore.into_iter().flatten()) + .collect(), + extend_select: options.extend_select.unwrap_or_default(), + fixable: options.fixable, + unfixable: options + .unfixable + .into_iter() + .flatten() + .chain(options.extend_unfixable.into_iter().flatten()) + .collect(), + extend_fixable: options.extend_fixable.unwrap_or_default(), + }], + allowed_confusables: options.allowed_confusables, + dummy_variable_rgx: options + .dummy_variable_rgx + .map(|pattern| Regex::new(&pattern)) + .transpose() + .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?, + extend_per_file_ignores: options + .extend_per_file_ignores + .map(|per_file_ignores| { + per_file_ignores + .into_iter() + .map(|(pattern, prefixes)| { + PerFileIgnore::new(pattern, &prefixes, Some(project_root)) + }) + .collect() + }) + .unwrap_or_default(), + external: options.external, + ignore_init_module_imports: options.ignore_init_module_imports, + per_file_ignores: options.per_file_ignores.map(|per_file_ignores| { + per_file_ignores + .into_iter() + .map(|(pattern, prefixes)| { + PerFileIgnore::new(pattern, &prefixes, Some(project_root)) + }) + .collect() + }), task_tags: options.task_tags, logger_objects: options.logger_objects, typing_modules: options.typing_modules, @@ -480,18 +555,10 @@ impl Configuration { pyflakes: options.pyflakes, pylint: options.pylint, pyupgrade: options.pyupgrade, - - format: if let Some(FormatOrOutputFormat::Format(format)) = options.format { - FormatConfiguration::from_options(format)? - } else { - FormatConfiguration::default() - }, }) } - pub fn as_rule_table(&self) -> RuleTable { - let preview = self.preview.unwrap_or_default(); - + fn as_rule_table(&self, preview: PreviewMode) -> RuleTable { // The select_set keeps track of which rules have been selected. let mut select_set: RuleSet = PREFIXES .iter() @@ -731,47 +798,18 @@ impl Configuration { .chain(self.rule_selections) .collect(), allowed_confusables: self.allowed_confusables.or(config.allowed_confusables), - builtins: self.builtins.or(config.builtins), - cache_dir: self.cache_dir.or(config.cache_dir), dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx), - exclude: self.exclude.or(config.exclude), - extend: self.extend.or(config.extend), - extend_exclude: config - .extend_exclude - .into_iter() - .chain(self.extend_exclude) - .collect(), - extend_include: config - .extend_include - .into_iter() - .chain(self.extend_include) - .collect(), extend_per_file_ignores: config .extend_per_file_ignores .into_iter() .chain(self.extend_per_file_ignores) .collect(), external: self.external.or(config.external), - fix: self.fix.or(config.fix), - fix_only: self.fix_only.or(config.fix_only), - output_format: self.output_format.or(config.output_format), - force_exclude: self.force_exclude.or(config.force_exclude), - include: self.include.or(config.include), ignore_init_module_imports: self .ignore_init_module_imports .or(config.ignore_init_module_imports), - line_length: self.line_length.or(config.line_length), logger_objects: self.logger_objects.or(config.logger_objects), - tab_size: self.tab_size.or(config.tab_size), - namespace_packages: self.namespace_packages.or(config.namespace_packages), per_file_ignores: self.per_file_ignores.or(config.per_file_ignores), - required_version: self.required_version.or(config.required_version), - respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), - show_source: self.show_source.or(config.show_source), - show_fixes: self.show_fixes.or(config.show_fixes), - src: self.src.or(config.src), - target_version: self.target_version.or(config.target_version), - preview: self.preview.or(config.preview), task_tags: self.task_tags.or(config.task_tags), typing_modules: self.typing_modules.or(config.typing_modules), // Plugins @@ -809,8 +847,6 @@ impl Configuration { pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), - - format: self.format.combine(config.format), } } } @@ -858,7 +894,6 @@ impl FormatConfiguration { } } } - pub(crate) trait CombinePluginOptions { #[must_use] fn combine(self, other: Self) -> Self; @@ -902,7 +937,7 @@ mod tests { use ruff_linter::settings::types::PreviewMode; use ruff_linter::RuleSelector; - use crate::configuration::{Configuration, RuleSelection}; + use crate::configuration::{LintConfiguration, RuleSelection}; const NURSERY_RULES: &[Rule] = &[ Rule::MissingCopyrightNotice, @@ -966,12 +1001,11 @@ mod tests { selections: impl IntoIterator, preview: Option, ) -> RuleSet { - Configuration { + LintConfiguration { rule_selections: selections.into_iter().collect(), - preview, - ..Configuration::default() + ..LintConfiguration::default() } - .as_rule_table() + .as_rule_table(preview.unwrap_or_default()) .iter_enabled() // Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is off-by-default .filter(|rule| rule.noqa_code() != "RUF014") diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 7be946a7a7259..f81612e6e8d19 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -36,30 +36,6 @@ use crate::settings::LineEnding; #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { - /// A list of allowed "confusable" Unicode characters to ignore when - /// enforcing `RUF001`, `RUF002`, and `RUF003`. - #[option( - default = r#"[]"#, - value_type = "list[str]", - example = r#" - # Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217), - # which could be confused for "-", "p", and "*", respectively. - allowed-confusables = ["−", "ρ", "∗"] - "# - )] - pub allowed_confusables: Option>, - - /// A list of builtins to treat as defined references, in addition to the - /// system builtins. - #[option( - default = r#"[]"#, - value_type = "list[str]", - example = r#" - builtins = ["_"] - "# - )] - pub builtins: Option>, - /// A path to the cache directory. /// /// By default, Ruff stores cache results in a `.ruff_cache` directory in @@ -77,19 +53,98 @@ pub struct Options { )] pub cache_dir: Option, - /// A regular expression used to identify "dummy" variables, or those which - /// should be ignored when enforcing (e.g.) unused-variable rules. The - /// default expression matches `_`, `__`, and `_var`, but not `_var_`. + /// A path to a local `pyproject.toml` file to merge into this + /// configuration. User home directory and environment variables will be + /// expanded. + /// + /// To resolve the current `pyproject.toml` file, Ruff will first resolve + /// this base configuration file, then merge in any properties defined + /// in the current configuration file. #[option( - default = r#""^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$""#, - value_type = "re.Pattern", + default = r#"None"#, + value_type = "str", example = r#" - # Only ignore variables named "_". - dummy-variable-rgx = "^_$" + # Extend the `pyproject.toml` file in the parent directory. + extend = "../pyproject.toml" + # But use a different line length. + line-length = 100 "# )] - pub dummy_variable_rgx: Option, + pub extend: Option, + /// The style in which violation messages should be formatted: `"text"` + /// (default), `"grouped"` (group messages by file), `"json"` + /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub + /// Actions annotations), `"gitlab"` (GitLab CI code quality report), + /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands). + #[option( + default = r#""text""#, + value_type = r#""text" | "json" | "junit" | "github" | "gitlab" | "pylint" | "azure""#, + example = r#" + # Group violations by containing file. + output-format = "grouped" + "# + )] + pub output_format: Option, + + /// Enable autofix behavior by-default when running `ruff` (overridden + /// by the `--fix` and `--no-fix` command-line flags). + #[option(default = "false", value_type = "bool", example = "fix = true")] + pub fix: Option, + + /// Like `fix`, but disables reporting on leftover violation. Implies `fix`. + #[option(default = "false", value_type = "bool", example = "fix-only = true")] + pub fix_only: Option, + + /// Whether to show source code snippets when reporting lint violations + /// (overridden by the `--show-source` command-line flag). + #[option( + default = "false", + value_type = "bool", + example = r#" + # By default, always show source code snippets. + show-source = true + "# + )] + pub show_source: Option, + + /// Whether to show an enumeration of all autofixed lint violations + /// (overridden by the `--show-fixes` command-line flag). + #[option( + default = "false", + value_type = "bool", + example = r#" + # Enumerate all fixed violations. + show-fixes = true + "# + )] + pub show_fixes: Option, + + /// Require a specific version of Ruff to be running (useful for unifying + /// results across many environments, e.g., with a `pyproject.toml` + /// file). + #[option( + default = "None", + value_type = "str", + example = r#" + required-version = "0.0.193" + "# + )] + pub required_version: Option, + + /// Whether to enable preview mode. When preview mode is enabled, Ruff will + /// use unstable rules and fixes. + #[option( + default = "false", + value_type = "bool", + example = r#" + # Enable preview features + preview = true + "# + )] + pub preview: Option, + + // File resolver options /// A list of file patterns to exclude from linting. /// /// Exclusions are based on globs, and can be either: @@ -115,25 +170,6 @@ pub struct Options { )] pub exclude: Option>, - /// A path to a local `pyproject.toml` file to merge into this - /// configuration. User home directory and environment variables will be - /// expanded. - /// - /// To resolve the current `pyproject.toml` file, Ruff will first resolve - /// this base configuration file, then merge in any properties defined - /// in the current configuration file. - #[option( - default = r#"None"#, - value_type = "str", - example = r#" - # Extend the `pyproject.toml` file in the parent directory. - extend = "../pyproject.toml" - # But use a different line length. - line-length = 100 - "# - )] - pub extend: Option, - /// A list of file patterns to omit from linting, in addition to those /// specified by `exclude`. /// @@ -166,14 +202,226 @@ pub struct Options { /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). #[option( - default = "[]", - value_type = "list[str]", + default = "[]", + value_type = "list[str]", + example = r#" + # In addition to the standard set of inclusions, include `.pyw` files. + extend-include = ["*.pyw"] + "# + )] + pub extend_include: Option>, + + /// Whether to enforce `exclude` and `extend-exclude` patterns, even for + /// paths that are passed to Ruff explicitly. Typically, Ruff will lint + /// any paths passed in directly, even if they would typically be + /// excluded. Setting `force-exclude = true` will cause Ruff to + /// respect these exclusions unequivocally. + /// + /// This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all + /// changed files to the [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit) + /// plugin, regardless of whether they're marked as excluded by Ruff's own + /// settings. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + force-exclude = true + "# + )] + pub force_exclude: Option, + + /// A list of file patterns to include when linting. + /// + /// Inclusion are based on globs, and should be single-path patterns, like + /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is + /// included here not for configuration but because we lint whether e.g. the + /// `[project]` matches the schema. + /// + /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). + #[option( + default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#, + value_type = "list[str]", + example = r#" + include = ["*.py"] + "# + )] + pub include: Option>, + + /// Whether to automatically exclude files that are ignored by `.ignore`, + /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files. + /// Enabled by default. + #[option( + default = "true", + value_type = "bool", + example = r#" + respect-gitignore = false + "# + )] + pub respect_gitignore: Option, + + // Generic python options + /// A list of builtins to treat as defined references, in addition to the + /// system builtins. + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + builtins = ["_"] + "# + )] + pub builtins: Option>, + + /// Mark the specified directories as namespace packages. For the purpose of + /// module resolution, Ruff will treat those directories as if they + /// contained an `__init__.py` file. + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + namespace-packages = ["airflow/providers"] + "# + )] + pub namespace_packages: Option>, + + /// The minimum Python version to target, e.g., when considering automatic + /// code upgrades, like rewriting type annotations. Ruff will not propose + /// changes using features that are not available in the given version. + /// + /// For example, to represent supporting Python >=3.10 or ==3.10 + /// specify `target-version = "py310"`. + /// + /// If omitted, and Ruff is configured via a `pyproject.toml` file, the + /// target version will be inferred from its `project.requires-python` + /// field (e.g., `requires-python = ">=3.8"`). If Ruff is configured via + /// `ruff.toml` or `.ruff.toml`, no such inference will be performed. + #[option( + default = r#""py38""#, + value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#, + example = r#" + # Always generate Python 3.7-compatible code. + target-version = "py37" + "# + )] + pub target_version: Option, + + /// The directories to consider when resolving first- vs. third-party + /// imports. + /// + /// As an example: given a Python package structure like: + /// + /// ```text + /// my_project + /// ├── pyproject.toml + /// └── src + /// └── my_package + /// ├── __init__.py + /// ├── foo.py + /// └── bar.py + /// ``` + /// + /// The `./src` directory should be included in the `src` option + /// (e.g., `src = ["src"]`), such that when resolving imports, + /// `my_package.foo` is considered a first-party import. + /// + /// When omitted, the `src` directory will typically default to the + /// directory containing the nearest `pyproject.toml`, `ruff.toml`, or + /// `.ruff.toml` file (the "project root"), unless a configuration file + /// is explicitly provided (e.g., via the `--config` command-line flag). + /// + /// This field supports globs. For example, if you have a series of Python + /// packages in a `python_modules` directory, `src = ["python_modules/*"]` + /// would expand to incorporate all of the packages in that directory. User + /// home directory and environment variables will also be expanded. + #[option( + default = r#"["."]"#, + value_type = "list[str]", + example = r#" + # Allow imports relative to the "src" and "test" directories. + src = ["src", "test"] + "# + )] + pub src: Option>, + + // Global Formatting options + /// The line length to use when enforcing long-lines violations (like + /// `E501`). Must be greater than `0` and less than or equal to `320`. + #[option( + default = "88", + value_type = "int", + example = r#" + # Allow lines to be as long as 120 characters. + line-length = 120 + "# + )] + #[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] + pub line_length: Option, + + /// The tabulation size to calculate line length. + #[option( + default = "4", + value_type = "int", + example = r#" + tab-size = 8 + "# + )] + pub tab_size: Option, + + pub lint: Option, + + /// The lint sections specified at the top level. + #[serde(flatten)] + pub lint_top_level: LintOptions, + + /// Options to configure the code formatting. + /// + /// Previously: + /// The style in which violation messages should be formatted: `"text"` + /// (default), `"grouped"` (group messages by file), `"json"` + /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub + /// Actions annotations), `"gitlab"` (GitLab CI code quality report), + /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands). + /// + /// This option has been **deprecated** in favor of `output-format` + /// to avoid ambiguity with Ruff's upcoming formatter. + #[option_group] + pub format: Option, +} + +/// Experimental section to configure Ruff's linting. This new section will eventually +/// replace the top-level linting options. +/// +/// Options specified in the `lint` section take precedence over the top-level settings. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive( + Debug, PartialEq, Eq, Default, ConfigurationOptions, CombineOptions, Serialize, Deserialize, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct LintOptions { + /// A list of allowed "confusable" Unicode characters to ignore when + /// enforcing `RUF001`, `RUF002`, and `RUF003`. + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + # Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217), + # which could be confused for "-", "p", and "*", respectively. + allowed-confusables = ["−", "ρ", "∗"] + "# + )] + pub allowed_confusables: Option>, + + /// A regular expression used to identify "dummy" variables, or those which + /// should be ignored when enforcing (e.g.) unused-variable rules. The + /// default expression matches `_`, `__`, and `_var`, but not `_var_`. + #[option( + default = r#""^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$""#, + value_type = "re.Pattern", example = r#" - # In addition to the standard set of inclusions, include `.pyw` files. - extend-include = ["*.pyw"] + # Only ignore variables named "_". + dummy-variable-rgx = "^_$" "# )] - pub extend_include: Option>, + pub dummy_variable_rgx: Option, /// A list of rule codes or prefixes to ignore, in addition to those /// specified by `ignore`. @@ -238,15 +486,6 @@ pub struct Options { )] pub external: Option>, - /// Enable autofix behavior by-default when running `ruff` (overridden - /// by the `--fix` and `--no-fix` command-line flags). - #[option(default = "false", value_type = "bool", example = "fix = true")] - pub fix: Option, - - /// Like `fix`, but disables reporting on leftover violation. Implies `fix`. - #[option(default = "false", value_type = "bool", example = "fix-only = true")] - pub fix_only: Option, - /// A list of rule codes or prefixes to consider autofixable. By default, /// all rules are considered autofixable. #[option( @@ -259,47 +498,6 @@ pub struct Options { )] pub fixable: Option>, - /// The style in which violation messages should be formatted: `"text"` - /// (default), `"grouped"` (group messages by file), `"json"` - /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub - /// Actions annotations), `"gitlab"` (GitLab CI code quality report), - /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands). - #[option( - default = r#""text""#, - value_type = r#""text" | "json" | "junit" | "github" | "gitlab" | "pylint" | "azure""#, - example = r#" - # Group violations by containing file. - output-format = "grouped" - "# - )] - pub output_format: Option, - - #[option( - default = r#"false"#, - value_type = "bool", - example = r#" - force-exclude = true - "# - )] - /// Whether to enforce `exclude` and `extend-exclude` patterns, even for - /// paths that are passed to Ruff explicitly. Typically, Ruff will lint - /// any paths passed in directly, even if they would typically be - /// excluded. Setting `force-exclude = true` will cause Ruff to - /// respect these exclusions unequivocally. - /// - /// This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all - /// changed files to the [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit) - /// plugin, regardless of whether they're marked as excluded by Ruff's own - /// settings. - #[option( - default = r#"false"#, - value_type = "bool", - example = r#" - force-exclude = true - "# - )] - pub force_exclude: Option, - /// A list of rule codes or prefixes to ignore. Prefixes can specify exact /// rules (like `F841`), entire categories (like `F`), or anything in /// between. @@ -330,46 +528,6 @@ pub struct Options { )] pub ignore_init_module_imports: Option, - /// A list of file patterns to include when linting. - /// - /// Inclusion are based on globs, and should be single-path patterns, like - /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is - /// included here not for configuration but because we lint whether e.g. the - /// `[project]` matches the schema. - /// - /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). - #[option( - default = r#"["*.py", "*.pyi", "**/pyproject.toml"]"#, - value_type = "list[str]", - example = r#" - include = ["*.py"] - "# - )] - pub include: Option>, - - /// The line length to use when enforcing long-lines violations (like - /// `E501`). Must be greater than `0` and less than or equal to `320`. - #[option( - default = "88", - value_type = "int", - example = r#" - # Allow lines to be as long as 120 characters. - line-length = 120 - "# - )] - #[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] - pub line_length: Option, - - /// The tabulation size to calculate line length. - #[option( - default = "4", - value_type = "int", - example = r#" - tab-size = 8 - "# - )] - pub tab_size: Option, - /// A list of objects that should be treated equivalently to a /// `logging.Logger` object. /// @@ -395,30 +553,6 @@ pub struct Options { )] pub logger_objects: Option>, - /// Require a specific version of Ruff to be running (useful for unifying - /// results across many environments, e.g., with a `pyproject.toml` - /// file). - #[option( - default = "None", - value_type = "str", - example = r#" - required-version = "0.0.193" - "# - )] - pub required_version: Option, - - /// Whether to automatically exclude files that are ignored by `.ignore`, - /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files. - /// Enabled by default. - #[option( - default = "true", - value_type = "bool", - example = r#" - respect-gitignore = false - "# - )] - pub respect_gitignore: Option, - /// A list of rule codes or prefixes to enable. Prefixes can specify exact /// rules (like `F841`), entire categories (like `F`), or anything in /// between. @@ -436,113 +570,6 @@ pub struct Options { )] pub select: Option>, - /// Whether to show source code snippets when reporting lint violations - /// (overridden by the `--show-source` command-line flag). - #[option( - default = "false", - value_type = "bool", - example = r#" - # By default, always show source code snippets. - show-source = true - "# - )] - pub show_source: Option, - - /// Whether to show an enumeration of all autofixed lint violations - /// (overridden by the `--show-fixes` command-line flag). - #[option( - default = "false", - value_type = "bool", - example = r#" - # Enumerate all fixed violations. - show-fixes = true - "# - )] - pub show_fixes: Option, - - /// The directories to consider when resolving first- vs. third-party - /// imports. - /// - /// As an example: given a Python package structure like: - /// - /// ```text - /// my_project - /// ├── pyproject.toml - /// └── src - /// └── my_package - /// ├── __init__.py - /// ├── foo.py - /// └── bar.py - /// ``` - /// - /// The `./src` directory should be included in the `src` option - /// (e.g., `src = ["src"]`), such that when resolving imports, - /// `my_package.foo` is considered a first-party import. - /// - /// When omitted, the `src` directory will typically default to the - /// directory containing the nearest `pyproject.toml`, `ruff.toml`, or - /// `.ruff.toml` file (the "project root"), unless a configuration file - /// is explicitly provided (e.g., via the `--config` command-line flag). - /// - /// This field supports globs. For example, if you have a series of Python - /// packages in a `python_modules` directory, `src = ["python_modules/*"]` - /// would expand to incorporate all of the packages in that directory. User - /// home directory and environment variables will also be expanded. - #[option( - default = r#"["."]"#, - value_type = "list[str]", - example = r#" - # Allow imports relative to the "src" and "test" directories. - src = ["src", "test"] - "# - )] - pub src: Option>, - - /// Mark the specified directories as namespace packages. For the purpose of - /// module resolution, Ruff will treat those directories as if they - /// contained an `__init__.py` file. - #[option( - default = r#"[]"#, - value_type = "list[str]", - example = r#" - namespace-packages = ["airflow/providers"] - "# - )] - pub namespace_packages: Option>, - - /// The minimum Python version to target, e.g., when considering automatic - /// code upgrades, like rewriting type annotations. Ruff will not propose - /// changes using features that are not available in the given version. - /// - /// For example, to represent supporting Python >=3.10 or ==3.10 - /// specify `target-version = "py310"`. - /// - /// If omitted, and Ruff is configured via a `pyproject.toml` file, the - /// target version will be inferred from its `project.requires-python` - /// field (e.g., `requires-python = ">=3.8"`). If Ruff is configured via - /// `ruff.toml` or `.ruff.toml`, no such inference will be performed. - #[option( - default = r#""py38""#, - value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#, - example = r#" - # Always generate Python 3.7-compatible code. - target-version = "py37" - "# - )] - pub target_version: Option, - - /// Whether to enable preview mode. When preview mode is enabled, Ruff will - /// use unstable rules and fixes. - #[option( - default = "false", - value_type = "bool", - example = r#" - # Enable preview features - preview = true - "# - )] - pub preview: Option, - /// A list of task tags to recognize (e.g., "TODO", "FIXME", "XXX"). /// /// Comments starting with these tags will be ignored by commented-out code @@ -551,7 +578,9 @@ pub struct Options { #[option( default = r#"["TODO", "FIXME", "XXX"]"#, value_type = "list[str]", - example = r#"task-tags = ["HACK"]"# + example = r#" + task-tags = ["HACK"] + "# )] pub task_tags: Option>, @@ -677,20 +706,6 @@ pub struct Options { #[option_group] pub pyupgrade: Option, - /// Options to configure the code formatting. - /// - /// Previously: - /// The style in which violation messages should be formatted: `"text"` - /// (default), `"grouped"` (group messages by file), `"json"` - /// (machine-readable), `"junit"` (machine-readable XML), `"github"` (GitHub - /// Actions annotations), `"gitlab"` (GitLab CI code quality report), - /// `"pylint"` (Pylint text format) or `"azure"` (Azure Pipeline logging commands). - /// - /// This option has been **deprecated** in favor of `output-format` - /// to avoid ambiguity with Ruff's upcoming formatter. - #[option_group] - pub format: Option, - // Tables are required to go last. /// A list of mappings from file pattern to rule codes or prefixes to /// exclude, when considering any matching files. diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index cf6fb30893b2e..6488459697226 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -161,7 +161,7 @@ mod tests { use ruff_linter::line_width::LineLength; use ruff_linter::settings::types::PatternPrefixPair; - use crate::options::Options; + use crate::options::{LintOptions, Options}; use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools}; use crate::tests::test_resource_path; @@ -236,7 +236,10 @@ select = ["E501"] pyproject.tool, Some(Tools { ruff: Some(Options { - select: Some(vec![codes::Pycodestyle::E501.into()]), + lint_top_level: LintOptions { + select: Some(vec![codes::Pycodestyle::E501.into()]), + ..LintOptions::default() + }, ..Options::default() }) }) @@ -254,8 +257,11 @@ ignore = ["E501"] pyproject.tool, Some(Tools { ruff: Some(Options { - extend_select: Some(vec![codes::Ruff::_100.into()]), - ignore: Some(vec![codes::Pycodestyle::E501.into()]), + lint_top_level: LintOptions { + extend_select: Some(vec![codes::Ruff::_100.into()]), + ignore: Some(vec![codes::Pycodestyle::E501.into()]), + ..LintOptions::default() + }, ..Options::default() }) }) @@ -308,10 +314,14 @@ other-attribute = 1 "migrations".to_string(), "with_excluded_file/other_excluded_file.py".to_string(), ]), - per_file_ignores: Some(FxHashMap::from_iter([( - "__init__.py".to_string(), - vec![codes::Pyflakes::_401.into()] - )])), + + lint_top_level: LintOptions { + per_file_ignores: Some(FxHashMap::from_iter([( + "__init__.py".to_string(), + vec![codes::Pyflakes::_401.into()] + )])), + ..LintOptions::default() + }, ..Options::default() } ); diff --git a/ruff.schema.json b/ruff.schema.json index 2cef8d57b7cb8..1b0d1db27eca8 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Options", + "description": "Experimental section to configure Ruff's linting. This new section will eventually replace the top-level linting options.\n\nOptions specified in the `lint` section take precedence over the top-level settings.", "type": "object", "properties": { "allowed-confusables": { @@ -388,6 +389,16 @@ "maximum": 320.0, "minimum": 1.0 }, + "lint": { + "anyOf": [ + { + "$ref": "#/definitions/LintOptions" + }, + { + "type": "null" + } + ] + }, "logger-objects": { "description": "A list of objects that should be treated equivalently to a `logging.Logger` object.\n\nThis is useful for ensuring proper diagnostics (e.g., to identify `logging` deprecations and other best-practices) for projects that re-export a `logging.Logger` object from a common module.\n\nFor example, if you have a module `logging_setup.py` with the following contents: ```python import logging\n\nlogger = logging.getLogger(__name__) ```\n\nAdding `\"logging_setup.logger\"` to `logger-objects` will ensure that `logging_setup.logger` is treated as a `logging.Logger` object when imported from other modules (e.g., `from logging_setup import logger`).", "type": [ @@ -1535,6 +1546,429 @@ "format": "uint16", "minimum": 1.0 }, + "LintOptions": { + "description": "Experimental section to configure Ruff's linting. This new section will eventually replace the top-level linting options.\n\nOptions specified in the `lint` section take precedence over the top-level settings.", + "type": "object", + "properties": { + "allowed-confusables": { + "description": "A list of allowed \"confusable\" Unicode characters to ignore when enforcing `RUF001`, `RUF002`, and `RUF003`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "maxLength": 1, + "minLength": 1 + } + }, + "dummy-variable-rgx": { + "description": "A regular expression used to identify \"dummy\" variables, or those which should be ignored when enforcing (e.g.) unused-variable rules. The default expression matches `_`, `__`, and `_var`, but not `_var_`.", + "type": [ + "string", + "null" + ] + }, + "extend-fixable": { + "description": "A list of rule codes or prefixes to consider autofixable, in addition to those specified by `fixable`.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + }, + "extend-per-file-ignores": { + "description": "A list of mappings from file pattern to rule codes or prefixes to exclude, in addition to any rules excluded by `per-file-ignores`.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/RuleSelector" + } + } + }, + "extend-select": { + "description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + }, + "external": { + "description": "A list of rule codes that are unsupported by Ruff, but should be preserved when (e.g.) validating `# noqa` directives. Useful for retaining `# noqa` directives that cover plugins not yet implemented by Ruff.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "fixable": { + "description": "A list of rule codes or prefixes to consider autofixable. By default, all rules are considered autofixable.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + }, + "flake8-annotations": { + "description": "Options for the `flake8-annotations` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8AnnotationsOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-bandit": { + "description": "Options for the `flake8-bandit` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8BanditOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-bugbear": { + "description": "Options for the `flake8-bugbear` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8BugbearOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-builtins": { + "description": "Options for the `flake8-builtins` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8BuiltinsOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-comprehensions": { + "description": "Options for the `flake8-comprehensions` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8ComprehensionsOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-copyright": { + "description": "Options for the `flake8-copyright` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8CopyrightOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-errmsg": { + "description": "Options for the `flake8-errmsg` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8ErrMsgOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-gettext": { + "description": "Options for the `flake8-gettext` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8GetTextOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-implicit-str-concat": { + "description": "Options for the `flake8-implicit-str-concat` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8ImplicitStrConcatOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-import-conventions": { + "description": "Options for the `flake8-import-conventions` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8ImportConventionsOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-pytest-style": { + "description": "Options for the `flake8-pytest-style` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8PytestStyleOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-quotes": { + "description": "Options for the `flake8-quotes` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8QuotesOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-self": { + "description": "Options for the `flake8_self` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8SelfOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-tidy-imports": { + "description": "Options for the `flake8-tidy-imports` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8TidyImportsOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-type-checking": { + "description": "Options for the `flake8-type-checking` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8TypeCheckingOptions" + }, + { + "type": "null" + } + ] + }, + "flake8-unused-arguments": { + "description": "Options for the `flake8-unused-arguments` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Flake8UnusedArgumentsOptions" + }, + { + "type": "null" + } + ] + }, + "ignore": { + "description": "A list of rule codes or prefixes to ignore. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + }, + "ignore-init-module-imports": { + "description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be flagged, but with a dedicated message suggesting that the import is either added to the module's `__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).", + "type": [ + "boolean", + "null" + ] + }, + "isort": { + "description": "Options for the `isort` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/IsortOptions" + }, + { + "type": "null" + } + ] + }, + "logger-objects": { + "description": "A list of objects that should be treated equivalently to a `logging.Logger` object.\n\nThis is useful for ensuring proper diagnostics (e.g., to identify `logging` deprecations and other best-practices) for projects that re-export a `logging.Logger` object from a common module.\n\nFor example, if you have a module `logging_setup.py` with the following contents: ```python import logging\n\nlogger = logging.getLogger(__name__) ```\n\nAdding `\"logging_setup.logger\"` to `logger-objects` will ensure that `logging_setup.logger` is treated as a `logging.Logger` object when imported from other modules (e.g., `from logging_setup import logger`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "mccabe": { + "description": "Options for the `mccabe` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/McCabeOptions" + }, + { + "type": "null" + } + ] + }, + "pep8-naming": { + "description": "Options for the `pep8-naming` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/Pep8NamingOptions" + }, + { + "type": "null" + } + ] + }, + "per-file-ignores": { + "description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/RuleSelector" + } + } + }, + "pycodestyle": { + "description": "Options for the `pycodestyle` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PycodestyleOptions" + }, + { + "type": "null" + } + ] + }, + "pydocstyle": { + "description": "Options for the `pydocstyle` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PydocstyleOptions" + }, + { + "type": "null" + } + ] + }, + "pyflakes": { + "description": "Options for the `pyflakes` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyflakesOptions" + }, + { + "type": "null" + } + ] + }, + "pylint": { + "description": "Options for the `pylint` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PylintOptions" + }, + { + "type": "null" + } + ] + }, + "pyupgrade": { + "description": "Options for the `pyupgrade` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyUpgradeOptions" + }, + { + "type": "null" + } + ] + }, + "select": { + "description": "A list of rule codes or prefixes to enable. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + }, + "task-tags": { + "description": "A list of task tags to recognize (e.g., \"TODO\", \"FIXME\", \"XXX\").\n\nComments starting with these tags will be ignored by commented-out code detection (`ERA`), and skipped by line-length rules (`E501`) if `ignore-overlong-task-comments` is set to `true`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "typing-modules": { + "description": "A list of modules whose exports should be treated equivalently to members of the `typing` module.\n\nThis is useful for ensuring proper type annotation inference for projects that re-export `typing` and `typing_extensions` members from a compatibility module. If omitted, any members imported from modules apart from `typing` and `typing_extensions` will be treated as ordinary Python objects.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "unfixable": { + "description": "A list of rule codes or prefixes to consider non-autofix-able.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/RuleSelector" + } + } + }, + "additionalProperties": false + }, "McCabeOptions": { "type": "object", "properties": {