Options: Support `#[serde(alias = name)]`

Signed-off-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Micha Reiser 2023-10-18 08:44:45 +09:00
parent f1b00cafd4
commit 6ca7407868
No known key found for this signature in database
6 changed files with 81 additions and 29 deletions

22
Cargo.lock generated
View File

@ -2259,6 +2259,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"serde_derive_internals 0.29.0",
"syn 2.0.38",
]
@ -2624,7 +2625,7 @@ checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"serde_derive_internals 0.26.0",
"syn 1.0.109",
]
@ -2664,9 +2665,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.188"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
dependencies = [
"serde_derive",
]
@ -2684,9 +2685,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.188"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [
"proc-macro2",
"quote",
@ -2704,6 +2705,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "serde_json"
version = "1.0.107"

View File

@ -34,7 +34,7 @@ quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.152", features = ["derive"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.3.0", features = ["inline"] }

View File

@ -1,6 +1,7 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use itertools::Itertools;
use std::fmt::Write;
use ruff_workspace::options::Options;
@ -107,6 +108,24 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
if !field.aliases.is_empty() {
let title = if field.aliases.len() == 1 {
"Alias"
} else {
"Aliases"
};
output.push_str(&format!(
"**{title}**: {}\n",
field
.aliases
.iter()
.map(|alias| format!("`{alias}`"))
.join(", ")
));
output.push('\n');
}
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if let Some(set_name) = parent_set.name() {

View File

@ -16,6 +16,7 @@ doctest = false
[dependencies]
ruff_python_trivia = { path = "../ruff_python_trivia" }
serde_derive_internals = "0.29.0"
proc-macro2 = { workspace = true }
quote = { workspace = true }

View File

@ -1,11 +1,11 @@
use proc_macro2::TokenTree;
use quote::{quote, quote_spanned};
use serde_derive_internals::Ctxt;
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, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath,
};
use ruff_python_trivia::textwrap::dedent;
@ -38,25 +38,14 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
.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"))
{
} else if let Type::Path(ty) = &field.ty {
let serde_field = serde_field_metadata(field)?;
// 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;
}
}
}
if serde_field.flatten() {
let ty_name = ty.path.require_ident()?;
output.push(quote_spanned!(ident.span() => (#ty_name::record(visit))));
}
}
}
@ -193,6 +182,10 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
} = attr.parse_args::<FieldAttributes>()?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let serde_field = serde_field_metadata(field)?;
let attributed_aliases = serde_field.aliases();
let aliases = quote!(BTreeSet::from_iter([#(#attributed_aliases),*]));
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
@ -200,6 +193,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
default: &#default,
value_type: &#value_type,
example: &#example,
aliases: #aliases
})
}
))
@ -248,3 +242,17 @@ fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
}
}
fn serde_field_metadata(field: &Field) -> syn::Result<serde_derive_internals::attr::Field> {
let context = Ctxt::new();
let field = serde_derive_internals::attr::Field::from_ast(
&context,
0,
field,
None,
&serde_derive_internals::attr::Default::Default,
);
context.check()?;
Ok(field)
}

View File

@ -1,3 +1,4 @@
use std::collections::BTreeSet;
use std::fmt::{Debug, Display, Formatter};
/// Visits [`OptionsMetadata`].
@ -89,7 +90,8 @@ impl OptionSet {
/// ### Test for the existence of a child option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
/// # use std::collections::BTreeSet;
/// use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
///
/// struct WithOptions;
///
@ -100,6 +102,7 @@ impl OptionSet {
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// });
/// }
/// }
@ -110,7 +113,8 @@ impl OptionSet {
/// ### Test for the existence of a nested option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
/// # use std::collections::BTreeSet;
/// use ruff_workspace::options_base::{OptionField, OptionsMetadata, Visit};
///
/// struct Root;
///
@ -121,6 +125,7 @@ impl OptionSet {
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// });
///
/// visit.record_set("format", Nested::metadata());
@ -136,6 +141,7 @@ impl OptionSet {
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// });
/// }
/// }
@ -157,7 +163,8 @@ impl OptionSet {
/// ### Find a child option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
/// # use std::collections::BTreeSet;
/// use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
///
/// struct WithOptions;
///
@ -166,6 +173,7 @@ impl OptionSet {
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// };
///
/// impl OptionsMetadata for WithOptions {
@ -180,13 +188,15 @@ impl OptionSet {
/// ### Find a nested option
///
/// ```rust
/// # use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
/// # use std::collections::BTreeSet;
/// use ruff_workspace::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit};
///
/// static HARD_TABS: OptionField = OptionField {
/// doc: "Use hard tabs for indentation and spaces for alignment.",
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// };
///
/// struct Root;
@ -198,6 +208,7 @@ impl OptionSet {
/// default: "false",
/// value_type: "bool",
/// example: "",
/// aliases: BTreeSet::new()
/// });
///
/// visit.record_set("format", Nested::metadata());
@ -307,6 +318,7 @@ pub struct OptionField {
pub doc: &'static str,
pub default: &'static str,
pub value_type: &'static str,
pub aliases: BTreeSet<&'static str>,
pub example: &'static str,
}