diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 2a7302a07ad08..8e3f21f72985f 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -129,12 +129,12 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set: output.push_str("**Example usage**:\n\n"); output.push_str(&format_tab( "pyproject.toml", - &format_header(parent_set, ConfigurationFile::PyprojectToml), + &format_header(field.scope, parent_set, ConfigurationFile::PyprojectToml), field.example, )); output.push_str(&format_tab( "ruff.toml", - &format_header(parent_set, ConfigurationFile::RuffToml), + &format_header(field.scope, parent_set, ConfigurationFile::RuffToml), field.example, )); output.push('\n'); @@ -149,23 +149,53 @@ fn format_tab(tab_name: &str, header: &str, content: &str) -> String { ) } -fn format_header(parent_set: &Set, configuration: ConfigurationFile) -> String { - let fmt = if let Some(set_name) = parent_set.name() { - if set_name == "format" { - String::from(".format") - } else { - format!(".lint.{set_name}") - } - } else { - String::new() - }; +/// Format the TOML header for the example usage for a given option. +/// +/// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`. +fn format_header( + scope: Option<&str>, + parent_set: &Set, + configuration: ConfigurationFile, +) -> String { match configuration { - ConfigurationFile::PyprojectToml => format!("[tool.ruff{fmt}]"), + ConfigurationFile::PyprojectToml => { + let mut header = if let Some(set_name) = parent_set.name() { + if set_name == "format" { + String::from("tool.ruff.format") + } else { + format!("tool.ruff.lint.{set_name}") + } + } else { + "tool.ruff".to_string() + }; + if let Some(scope) = scope { + if !header.is_empty() { + header.push('.'); + } + header.push_str(scope); + } + format!("[{header}]") + } ConfigurationFile::RuffToml => { - if fmt.is_empty() { + let mut header = if let Some(set_name) = parent_set.name() { + if set_name == "format" { + String::from("format") + } else { + format!("lint.{set_name}") + } + } else { + String::new() + }; + if let Some(scope) = scope { + if !header.is_empty() { + header.push('.'); + } + header.push_str(scope); + } + if header.is_empty() { String::new() } else { - format!("[{}]", fmt.strip_prefix('.').unwrap()) + format!("[{header}]") } } } diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index b069dfc063c96..1a902498b16eb 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -190,9 +190,16 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result syn::Result, } fn parse_field_attributes(attribute: &Attribute) -> syn::Result { let mut default = None; let mut value_type = None; let mut example = None; + let mut scope = None; attribute.parse_nested_meta(|meta| { if meta.path.is_ident("default") { default = Some(get_string_literal(&meta, "default", "option")?.value()); } else if meta.path.is_ident("value_type") { value_type = Some(get_string_literal(&meta, "value_type", "option")?.value()); + } else if meta.path.is_ident("scope") { + scope = Some(get_string_literal(&meta, "scope", "option")?.value()); } else if meta.path.is_ident("example") { let example_text = get_string_literal(&meta, "value_type", "option")?.value(); example = Some(dedent(&example_text).trim_matches('\n').to_string()); @@ -276,6 +288,7 @@ fn parse_field_attributes(attribute: &Attribute) -> syn::Result default, value_type, example, + scope, }) } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 25a76de206c98..9d91977fa1ce6 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -834,9 +834,9 @@ pub struct LintCommonOptions { #[option( default = "{}", value_type = "dict[str, list[RuleSelector]]", + scope = "per-file-ignores", example = r#" # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. - [tool.ruff.lint.per-file-ignores] "__init__.py" = ["E402"] "path/to/file.py" = ["E402"] "# @@ -848,9 +848,9 @@ pub struct LintCommonOptions { #[option( default = "{}", value_type = "dict[str, list[RuleSelector]]", + scope = "extend-per-file-ignores", example = r#" # Also ignore `E402` in all `__init__.py` files. - [tool.ruff.lint.extend-per-file-ignores] "__init__.py" = ["E402"] "# )] @@ -1205,8 +1205,8 @@ pub struct Flake8ImportConventionsOptions { #[option( default = r#"{"altair": "alt", "matplotlib": "mpl", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "tkinter": "tk", "holoviews": "hv", "panel": "pn", "plotly.express": "px", "polars": "pl", "pyarrow": "pa"}"#, value_type = "dict[str, str]", + scope = "aliases", example = r#" - [tool.ruff.lint.flake8-import-conventions.aliases] # Declare the default aliases. altair = "alt" "matplotlib.pyplot" = "plt" @@ -1223,8 +1223,8 @@ pub struct Flake8ImportConventionsOptions { #[option( default = r#"{}"#, value_type = "dict[str, str]", + scope = "extend-aliases", example = r#" - [tool.ruff.lint.flake8-import-conventions.extend-aliases] # Declare a custom alias for the `matplotlib` module. "dask.dataframe" = "dd" "# @@ -1235,8 +1235,8 @@ pub struct Flake8ImportConventionsOptions { #[option( default = r#"{}"#, value_type = "dict[str, list[str]]", + scope = "banned-aliases", example = r#" - [tool.ruff.lint.flake8-import-conventions.banned-aliases] # Declare the banned aliases. "tensorflow.keras.backend" = ["K"] "# @@ -1548,8 +1548,8 @@ pub struct Flake8TidyImportsOptions { #[option( default = r#"{}"#, value_type = r#"dict[str, { "msg": str }]"#, + scope = "banned-api", example = r#" - [tool.ruff.lint.flake8-tidy-imports.banned-api] "cgi".msg = "The cgi module is deprecated, see https://peps.python.org/pep-0594/#cgi." "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." "# @@ -2000,9 +2000,9 @@ pub struct IsortOptions { #[option( default = "{}", value_type = "dict[str, list[str]]", + scope = "sections", example = r#" # Group all Django imports into a separate section. - [tool.ruff.lint.isort.sections] "django" = ["django"] "# )] diff --git a/crates/ruff_workspace/src/options_base.rs b/crates/ruff_workspace/src/options_base.rs index 60cbbe6facf24..8ac16d0a3b695 100644 --- a/crates/ruff_workspace/src/options_base.rs +++ b/crates/ruff_workspace/src/options_base.rs @@ -100,6 +100,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None, /// }); /// } @@ -122,6 +123,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None /// }); /// @@ -138,6 +140,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None /// }); /// } @@ -169,6 +172,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None /// }; /// @@ -191,6 +195,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None /// }; /// @@ -203,6 +208,7 @@ impl OptionSet { /// default: "false", /// value_type: "bool", /// example: "", + /// scope: None, /// deprecated: None /// }); /// @@ -319,8 +325,12 @@ impl Debug for OptionSet { #[derive(Debug, Eq, PartialEq, Clone)] pub struct OptionField { pub doc: &'static str, + /// Ex) `"false"` pub default: &'static str, + /// Ex) `"bool"` pub value_type: &'static str, + /// Ex) `"per-file-ignores"` + pub scope: Option<&'static str>, pub example: &'static str, pub deprecated: Option, }