Skip to content

Commit a85ed30

Browse files
authored
Respect #(deprecated) attribute in configuration options (#8035)
1 parent 2e225d7 commit a85ed30

File tree

9 files changed

+269
-62
lines changed

9 files changed

+269
-62
lines changed

crates/ruff_dev/src/generate_docs.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use strum::IntoEnumIterator;
1111
use ruff_diagnostics::FixAvailability;
1212
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
1313
use ruff_workspace::options::Options;
14-
use ruff_workspace::options_base::OptionsMetadata;
14+
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
1515

1616
use crate::ROOT_DIR;
1717

@@ -55,7 +55,11 @@ pub(crate) fn main(args: &Args) -> Result<()> {
5555
output.push('\n');
5656
}
5757

58-
process_documentation(explanation.trim(), &mut output);
58+
process_documentation(
59+
explanation.trim(),
60+
&mut output,
61+
&rule.noqa_code().to_string(),
62+
);
5963

6064
let filename = PathBuf::from(ROOT_DIR)
6165
.join("docs")
@@ -74,7 +78,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
7478
Ok(())
7579
}
7680

77-
fn process_documentation(documentation: &str, out: &mut String) {
81+
fn process_documentation(documentation: &str, out: &mut String, rule_name: &str) {
7882
let mut in_options = false;
7983
let mut after = String::new();
8084

@@ -100,7 +104,17 @@ fn process_documentation(documentation: &str, out: &mut String) {
100104
if let Some(rest) = line.strip_prefix("- `") {
101105
let option = rest.trim_end().trim_end_matches('`');
102106

103-
assert!(Options::metadata().has(option), "unknown option {option}");
107+
match Options::metadata().find(option) {
108+
Some(OptionEntry::Field(field)) => {
109+
if field.deprecated.is_some() {
110+
eprintln!("Rule {rule_name} references deprecated option {option}.");
111+
}
112+
}
113+
Some(_) => {}
114+
None => {
115+
panic!("Unknown option {option} referenced by rule {rule_name}");
116+
}
117+
}
104118

105119
let anchor = option.replace('.', "-");
106120
out.push_str(&format!("- [`{option}`][{option}]\n"));
@@ -138,6 +152,7 @@ Something [`else`][other].
138152
139153
[other]: http://example.com.",
140154
&mut output,
155+
"example",
141156
);
142157
assert_eq!(
143158
output,

crates/ruff_dev/src/generate_options.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
101101
output.push_str(&format!("{header_level} [`{name}`](#{name})\n"));
102102
}
103103
output.push('\n');
104+
105+
if let Some(deprecated) = &field.deprecated {
106+
output.push_str("!!! warning \"Deprecated\"\n");
107+
output.push_str(" This option has been deprecated");
108+
109+
if let Some(since) = deprecated.since {
110+
write!(output, " in {since}").unwrap();
111+
}
112+
113+
output.push('.');
114+
115+
if let Some(message) = deprecated.message {
116+
writeln!(output, " {message}").unwrap();
117+
}
118+
119+
output.push('\n');
120+
}
121+
104122
output.push_str(field.doc);
105123
output.push_str("\n\n");
106124
output.push_str(&format!("**Default value**: `{}`\n", field.default));

crates/ruff_macros/src/combine_options.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
1818
Ok(quote! {
1919
impl crate::configuration::CombinePluginOptions for #ident {
2020
fn combine(self, other: Self) -> Self {
21+
#[allow(deprecated)]
2122
Self {
2223
#(
2324
#output

crates/ruff_macros/src/config.rs

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
use proc_macro2::TokenTree;
1+
use proc_macro2::{TokenStream, TokenTree};
22
use quote::{quote, quote_spanned};
3-
use syn::parse::{Parse, ParseStream};
3+
use syn::meta::ParseNestedMeta;
44
use syn::spanned::Spanned;
5-
use syn::token::Comma;
65
use syn::{
76
AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
8-
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
7+
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath,
98
};
109

1110
use ruff_python_trivia::textwrap::dedent;
1211

13-
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
12+
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
1413
let DeriveInput {
1514
ident,
1615
data,
@@ -190,16 +189,38 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
190189
default,
191190
value_type,
192191
example,
193-
} = attr.parse_args::<FieldAttributes>()?;
192+
} = parse_field_attributes(attr)?;
194193
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
195194

195+
let deprecated = if let Some(deprecated) = field
196+
.attrs
197+
.iter()
198+
.find(|attr| attr.path().is_ident("deprecated"))
199+
{
200+
fn quote_option(option: Option<String>) -> TokenStream {
201+
match option {
202+
None => quote!(None),
203+
Some(value) => quote!(Some(#value)),
204+
}
205+
}
206+
207+
let deprecated = parse_deprecated_attribute(deprecated)?;
208+
let note = quote_option(deprecated.note);
209+
let since = quote_option(deprecated.since);
210+
211+
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
212+
} else {
213+
quote!(None)
214+
};
215+
196216
Ok(quote_spanned!(
197217
ident.span() => {
198218
visit.record_field(#kebab_name, crate::options_base::OptionField{
199219
doc: &#doc,
200220
default: &#default,
201221
value_type: &#value_type,
202222
example: &#example,
223+
deprecated: #deprecated
203224
})
204225
}
205226
))
@@ -212,39 +233,109 @@ struct FieldAttributes {
212233
example: String,
213234
}
214235

215-
impl Parse for FieldAttributes {
216-
fn parse(input: ParseStream) -> syn::Result<Self> {
217-
let default = _parse_key_value(input, "default")?;
218-
input.parse::<Comma>()?;
219-
let value_type = _parse_key_value(input, "value_type")?;
220-
input.parse::<Comma>()?;
221-
let example = _parse_key_value(input, "example")?;
222-
if !input.is_empty() {
223-
input.parse::<Comma>()?;
236+
fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes> {
237+
let mut default = None;
238+
let mut value_type = None;
239+
let mut example = None;
240+
241+
attribute.parse_nested_meta(|meta| {
242+
if meta.path.is_ident("default") {
243+
default = Some(get_string_literal(&meta, "default", "option")?.value());
244+
} else if meta.path.is_ident("value_type") {
245+
value_type = Some(get_string_literal(&meta, "value_type", "option")?.value());
246+
} else if meta.path.is_ident("example") {
247+
let example_text = get_string_literal(&meta, "value_type", "option")?.value();
248+
example = Some(dedent(&example_text).trim_matches('\n').to_string());
249+
} else {
250+
return Err(syn::Error::new(
251+
meta.path.span(),
252+
format!(
253+
"Deprecated meta {:?} is not supported by ruff's option macro.",
254+
meta.path.get_ident()
255+
),
256+
));
224257
}
225258

226-
Ok(Self {
227-
default,
228-
value_type,
229-
example: dedent(&example).trim_matches('\n').to_string(),
230-
})
231-
}
259+
Ok(())
260+
})?;
261+
262+
let Some(default) = default else {
263+
return Err(syn::Error::new(attribute.span(), "Mandatory `default` field is missing in `#[option]` attribute. Specify the default using `#[option(default=\"..\")]`."));
264+
};
265+
266+
let Some(value_type) = value_type else {
267+
return Err(syn::Error::new(attribute.span(), "Mandatory `value_type` field is missing in `#[option]` attribute. Specify the value type using `#[option(value_type=\"..\")]`."));
268+
};
269+
270+
let Some(example) = example else {
271+
return Err(syn::Error::new(attribute.span(), "Mandatory `example` field is missing in `#[option]` attribute. Add an example using `#[option(example=\"..\")]`."));
272+
};
273+
274+
Ok(FieldAttributes {
275+
default,
276+
value_type,
277+
example,
278+
})
232279
}
233280

234-
fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
235-
let ident: proc_macro2::Ident = input.parse()?;
236-
if ident != name {
237-
return Err(syn::Error::new(
238-
ident.span(),
239-
format!("Expected `{name}` name"),
240-
));
281+
fn parse_deprecated_attribute(attribute: &Attribute) -> syn::Result<DeprecatedAttribute> {
282+
let mut deprecated = DeprecatedAttribute::default();
283+
attribute.parse_nested_meta(|meta| {
284+
if meta.path.is_ident("note") {
285+
deprecated.note = Some(get_string_literal(&meta, "note", "deprecated")?.value());
286+
} else if meta.path.is_ident("since") {
287+
deprecated.since = Some(get_string_literal(&meta, "since", "deprecated")?.value());
288+
} else {
289+
return Err(syn::Error::new(
290+
meta.path.span(),
291+
format!(
292+
"Deprecated meta {:?} is not supported by ruff's option macro.",
293+
meta.path.get_ident()
294+
),
295+
));
296+
}
297+
298+
Ok(())
299+
})?;
300+
301+
Ok(deprecated)
302+
}
303+
304+
fn get_string_literal(
305+
meta: &ParseNestedMeta,
306+
meta_name: &str,
307+
attribute_name: &str,
308+
) -> syn::Result<syn::LitStr> {
309+
let expr: syn::Expr = meta.value()?.parse()?;
310+
311+
let mut value = &expr;
312+
while let syn::Expr::Group(e) = value {
313+
value = &e.expr;
241314
}
242315

243-
input.parse::<Token![=]>()?;
244-
let value: Lit = input.parse()?;
316+
if let syn::Expr::Lit(ExprLit {
317+
lit: Lit::Str(lit), ..
318+
}) = value
319+
{
320+
let suffix = lit.suffix();
321+
if !suffix.is_empty() {
322+
return Err(syn::Error::new(
323+
lit.span(),
324+
format!("unexpected suffix `{suffix}` on string literal"),
325+
));
326+
}
245327

246-
match &value {
247-
Lit::Str(v) => Ok(v.value()),
248-
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
328+
Ok(lit.clone())
329+
} else {
330+
Err(syn::Error::new(
331+
expr.span(),
332+
format!("expected {attribute_name} attribute to be a string: `{meta_name} = \"...\"`"),
333+
))
249334
}
250335
}
336+
337+
#[derive(Default, Debug)]
338+
struct DeprecatedAttribute {
339+
since: Option<String>,
340+
note: Option<String>,
341+
}

crates/ruff_workspace/src/configuration.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,22 @@ pub struct LintConfiguration {
567567

568568
impl LintConfiguration {
569569
fn from_options(options: LintOptions, project_root: &Path) -> Result<Self> {
570+
#[allow(deprecated)]
571+
let ignore = options
572+
.common
573+
.ignore
574+
.into_iter()
575+
.flatten()
576+
.chain(options.common.extend_ignore.into_iter().flatten())
577+
.collect();
578+
#[allow(deprecated)]
579+
let unfixable = options
580+
.common
581+
.unfixable
582+
.into_iter()
583+
.flatten()
584+
.chain(options.common.extend_unfixable.into_iter().flatten())
585+
.collect();
570586
Ok(LintConfiguration {
571587
exclude: options.exclude.map(|paths| {
572588
paths
@@ -581,22 +597,10 @@ impl LintConfiguration {
581597

582598
rule_selections: vec![RuleSelection {
583599
select: options.common.select,
584-
ignore: options
585-
.common
586-
.ignore
587-
.into_iter()
588-
.flatten()
589-
.chain(options.common.extend_ignore.into_iter().flatten())
590-
.collect(),
600+
ignore,
591601
extend_select: options.common.extend_select.unwrap_or_default(),
592602
fixable: options.common.fixable,
593-
unfixable: options
594-
.common
595-
.unfixable
596-
.into_iter()
597-
.flatten()
598-
.chain(options.common.extend_unfixable.into_iter().flatten())
599-
.collect(),
603+
unfixable,
600604
extend_fixable: options.common.extend_fixable.unwrap_or_default(),
601605
}],
602606
extend_safe_fixes: options.common.extend_safe_fixes.unwrap_or_default(),

crates/ruff_workspace/src/options.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -471,9 +471,6 @@ pub struct LintCommonOptions {
471471

472472
/// A list of rule codes or prefixes to ignore, in addition to those
473473
/// specified by `ignore`.
474-
///
475-
/// This option has been **deprecated** in favor of `ignore`
476-
/// since its usage is now interchangeable with `ignore`.
477474
#[option(
478475
default = "[]",
479476
value_type = "list[RuleSelector]",
@@ -482,7 +479,9 @@ pub struct LintCommonOptions {
482479
extend-ignore = ["F841"]
483480
"#
484481
)]
485-
#[cfg_attr(feature = "schemars", schemars(skip))]
482+
#[deprecated(
483+
note = "The `extend-ignore` option is now interchangeable with `ignore`. Please update your configuration to use the `ignore` option instead."
484+
)]
486485
pub extend_ignore: Option<Vec<RuleSelector>>,
487486

488487
/// A list of rule codes or prefixes to enable, in addition to those
@@ -511,10 +510,9 @@ pub struct LintCommonOptions {
511510

512511
/// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those
513512
/// specified by `unfixable`.
514-
///
515-
/// This option has been **deprecated** in favor of `unfixable` since its usage is now
516-
/// interchangeable with `unfixable`.
517-
#[cfg_attr(feature = "schemars", schemars(skip))]
513+
#[deprecated(
514+
note = "The `extend-unfixable` option is now interchangeable with `unfixable`. Please update your configuration to use the `unfixable` option instead."
515+
)]
518516
pub extend_unfixable: Option<Vec<RuleSelector>>,
519517

520518
/// A list of rule codes that are unsupported by Ruff, but should be

0 commit comments

Comments
 (0)