diff --git a/crates/ruff_macros/src/combine_options.rs b/crates/ruff_macros/src/combine_options.rs index 05b2395e072bf2..0da1ee1f93f733 100644 --- a/crates/ruff_macros/src/combine_options.rs +++ b/crates/ruff_macros/src/combine_options.rs @@ -48,13 +48,56 @@ fn handle_field(field: &Field) -> syn::Result { .. }) => match segments.first() { Some(PathSegment { - ident: type_ident, .. - }) if type_ident == "Option" => Ok(quote_spanned!( - ident.span() => #ident: self.#ident.or(other.#ident) - )), + ident: type_ident, + arguments, + }) if type_ident == "Option" => { + // Given `Option>`, combine the maps by merging. In TOML, a hash map is + // represented as a table, so merging the maps is the correct behavior. + if let syn::PathArguments::AngleBracketed(args) = arguments { + let inner_type_ident = args + .args + .first() + .and_then(|arg| match arg { + syn::GenericArgument::Type(ty) => match ty { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => segments.first().map(|seg| &seg.ident), + _ => None, + }, + _ => None, + }) + .ok_or_else(|| { + syn::Error::new( + ident.span(), + "Expected `Option<_>` with a single type argument.", + ) + })?; + if inner_type_ident == "HashMap" + || inner_type_ident == "BTreeMap" + || inner_type_ident == "FxHashMap" + { + return Ok(quote_spanned!( + ident.span() => #ident: match (self.#ident, other.#ident) { + (Some(mut m1), Some(m2)) => { + m1.extend(m2); + Some(m1) + }, + (None, Some(m)) | (Some(m), None) => Some(m), + (None, None) => None, + } + )); + } + } + + Ok(quote_spanned!( + ident.span() => #ident: self.#ident.or(other.#ident) + )) + } + _ => Err(syn::Error::new( ident.span(), - "Expected `Option<_>` or `Vec<_>` as type.", + "Expected `Option<_>` as type.", )), }, _ => Err(syn::Error::new(ident.span(), "Expected type.")),