From 2afc1581466bad6b41b688f64693cef72c6030c7 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 8 Jun 2023 17:24:33 +0800 Subject: [PATCH 01/14] Support setting doc & attribute for each field separately - Tests are yet to be written --- src/args.rs | 35 ++++++++--- src/attrs/generator.rs | 75 ++++------------------- src/attrs/mod.rs | 68 ++++++++++++--------- src/fields/args.rs | 132 +++++++++++++++++++++++++++++++++++++++++ src/fields/attrs.rs | 116 ++++++++++++++++++++++++++++++++---- src/fields/mod.rs | 2 +- 6 files changed, 318 insertions(+), 110 deletions(-) create mode 100644 src/fields/args.rs diff --git a/src/args.rs b/src/args.rs index c91f9d1..2f669aa 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,7 +3,7 @@ use syn::parse::{Error, Parse, ParseStream, Result}; use syn::token::{Comma, Eq, Pub}; use syn::{parse2, Ident, LitStr, Meta, Visibility}; -mod kw { +pub mod kw { // NOTE: when adding new keywords update ArgList::next_is_kw syn::custom_keyword!(doc); syn::custom_keyword!(merge_fn); @@ -13,6 +13,10 @@ mod kw { syn::custom_keyword!(field_attrs); syn::custom_keyword!(from); + pub mod doc_sub { + syn::custom_keyword!(append); + } + pub mod attrs_sub { syn::custom_keyword!(add); } @@ -60,8 +64,12 @@ pub enum MergeFnName { #[cfg_attr(test, derive(Debug, PartialEq))] pub enum Doc { - Same, - Custom(String), + /// Keep the same documentation. + Keep, + /// Replace with custom documentation. + Replace(String), + /// Append additional documentation. + Append(String), } #[cfg_attr(test, derive(Debug, PartialEq))] @@ -327,11 +335,18 @@ impl Parse for Doc { if input.peek(Eq) { input.parse::()?; - let doc_text: LitStr = input.parse()?; + if input.peek(kw::doc_sub::append) { + input.parse::()?; - Ok(Doc::Custom(doc_text.value())) + let group: Group = input.parse()?; + let doc_text: LitStr = parse2(group.stream())?; + Ok(Doc::Append(doc_text.value())) + } else { + let doc_text: LitStr = input.parse()?; + Ok(Doc::Replace(doc_text.value())) + } } else { - Ok(Doc::Same) + Ok(Doc::Keep) } } } @@ -617,10 +632,14 @@ mod tests { #[test] fn parse_doc() { let cases = vec![ - (quote! {Opt, doc}, Doc::Same), + (quote! {Opt, doc}, Doc::Keep), ( quote! {Opt, doc = "custom docs"}, - Doc::Custom("custom docs".to_string()), + Doc::Replace("custom docs".to_string()), + ), + ( + quote! {Opt, doc = append("append docs")}, + Doc::Append("append docs".to_string()), ), ]; diff --git a/src/attrs/generator.rs b/src/attrs/generator.rs index 5f5dc50..1643376 100644 --- a/src/attrs/generator.rs +++ b/src/attrs/generator.rs @@ -1,80 +1,25 @@ use quote::quote; use syn::{parse::Parser, Attribute, Meta}; -use crate::args::Attrs; use crate::error; const DOC: &str = "doc"; -const OPT_ATTR: &str = "optfield"; +const OPTFIELD_ATTR_NAME: &str = "optfield"; pub trait AttrGenerator { - fn no_docs(&self) -> bool; - fn error_action_text(&self) -> String; - fn original_attrs(&self) -> &[Attribute]; - - fn attrs_arg(&self) -> &Option; - - fn custom_docs(&self) -> Option { - None - } - - fn keep_original_docs(&self) -> bool { - !self.no_docs() - } - - fn compute_capacity(&self) -> usize { - use Attrs::*; + fn new_attrs_except_docs(&self) -> Vec; - let orig_len = self.original_attrs().len(); - - match self.attrs_arg() { - Some(Replace(v)) | Some(Add(v)) => orig_len + v.len(), - _ => orig_len, - } - } + fn new_docs(&self) -> Vec; fn generate(&self) -> Vec { - use Attrs::*; - - let attrs_arg = self.attrs_arg(); - - // if no attrs and no docs should be set, remove all attrs - if self.no_docs() && attrs_arg.is_none() { - return Vec::new(); - } - - let mut new_attrs = Vec::with_capacity(self.compute_capacity()); - - if let Some(d) = self.custom_docs() { - new_attrs.push(d); - } - - for attr in self.original_attrs() { - let mut add_attr = self.keep_original_docs() && is_doc_attr(attr); - - if let Some(Keep) | Some(Add(_)) = attrs_arg { - if !is_doc_attr(attr) { - add_attr = true; - } - } - - if attr.path().is_ident(OPT_ATTR) { - add_attr = false - } - - if add_attr { - new_attrs.push(attr.meta.clone()); - } - } - - if let Some(Replace(v)) | Some(Add(v)) = attrs_arg { - new_attrs.extend(v.clone()); - } + let attrs_except_docs = self.new_attrs_except_docs(); + let docs = self.new_docs(); let attrs_tokens = quote! { - #(#[#new_attrs])* + #(#[#attrs_except_docs])* + #(#[#docs])* }; Attribute::parse_outer @@ -83,6 +28,12 @@ pub trait AttrGenerator { } } +/// Useful during attribute generation: it can prevent some unwanted recursive +/// behaviour. +pub fn is_optfield_attr(attr: &Attribute) -> bool { + attr.path().is_ident(OPTFIELD_ATTR_NAME) +} + pub fn is_doc_attr(attr: &Attribute) -> bool { attr.path().is_ident(DOC) } diff --git a/src/attrs/mod.rs b/src/attrs/mod.rs index 81d0fa2..42b7e4a 100644 --- a/src/attrs/mod.rs +++ b/src/attrs/mod.rs @@ -2,6 +2,7 @@ use quote::quote; use syn::{parse2, Attribute, ItemStruct, Meta}; use crate::args::{Args, Attrs, Doc}; +use crate::attrs::generator::{is_doc_attr, is_optfield_attr}; use crate::error::unexpected; pub mod generator; @@ -20,43 +21,54 @@ impl<'a> AttrGen<'a> { } impl<'a> AttrGenerator for AttrGen<'a> { - fn no_docs(&self) -> bool { - self.args.doc.is_none() - } - fn error_action_text(&self) -> String { format!("generating {} attrs", self.item.ident) } - fn original_attrs(&self) -> &[Attribute] { - &self.item.attrs - } - - fn attrs_arg(&self) -> &Option { - &self.args.attrs - } - - fn custom_docs(&self) -> Option { - if let Some(Doc::Custom(d)) = &self.args.doc { - let tokens = quote! { - doc = #d - }; - - Some( - parse2(tokens) - .unwrap_or_else(|e| panic!("{}", unexpected(self.error_action_text(), e))), - ) - } else { - None + fn new_attrs_except_docs(&self) -> Vec { + use Attrs::*; + + let original_attrs_it = self + .item + .attrs + .iter() + // this filter is required, otherwise multiple #[optfield] attributes + // on the same struct would error + .filter(|attr| !is_optfield_attr(attr)) + .filter_map(|attr| (!is_doc_attr(attr)).then(|| attr.meta.clone())); + + match &self.args.attrs { + None => vec![], + Some(Keep) => original_attrs_it.collect(), + Some(Replace(attrs)) => attrs.clone(), + Some(Add(attrs)) => original_attrs_it.chain(attrs.clone()).collect(), } } - fn keep_original_docs(&self) -> bool { + fn new_docs(&self) -> Vec { use Doc::*; - match self.args.doc { - None | Some(Custom(_)) => false, - Some(Same) => true, + let original_doc_it = self + .item + .attrs + .iter() + .filter_map(|attr| is_doc_attr(attr).then(|| attr.meta.clone())); + + match &self.args.doc { + None => vec![], + Some(Keep) => original_doc_it.collect(), + Some(Replace(doc_text)) => { + let tokens = quote! { doc = #doc_text }; + let doc_attr = parse2(tokens) + .unwrap_or_else(|e| panic!("{}", unexpected(self.error_action_text(), e))); + vec![doc_attr] + } + Some(Append(doc_text)) => { + let tokens = quote! { doc = #doc_text }; + let doc_attr = parse2(tokens) + .unwrap_or_else(|e| panic!("{}", unexpected(self.error_action_text(), e))); + original_doc_it.chain(std::iter::once(doc_attr)).collect() + } } } } diff --git a/src/fields/args.rs b/src/fields/args.rs new file mode 100644 index 0000000..546a479 --- /dev/null +++ b/src/fields/args.rs @@ -0,0 +1,132 @@ +use proc_macro2::Span; +use syn::{ + parse::{Parse, ParseStream, Result}, + token::Comma, + Error, +}; + +use crate::args::{kw, Attrs, Doc}; + +/// Args declared on a field. +pub struct FieldArgs { + pub doc: Option, + pub attrs: Option, +} +impl Parse for FieldArgs { + fn parse(input: ParseStream) -> Result { + let arg_list: FieldArgList = input.parse()?; + + Ok(arg_list.into()) + } +} +impl FieldArgs { + fn new() -> Self { + Self { + doc: None, + attrs: None, + } + } +} + +enum FieldArg { + Doc(Doc), + Attrs(Attrs), +} + +/// Parser for unordered args on a field. +struct FieldArgList { + doc: Option, + attrs: Option, + list: Vec, +} +impl Parse for FieldArgList { + fn parse(input: ParseStream) -> Result { + let mut arg_list = FieldArgList::new(); + + while !input.is_empty() { + input.parse::()?; + + // allow trailing commas + if input.is_empty() { + break; + } + + let lookahead = input.lookahead1(); + + if lookahead.peek(kw::doc) { + arg_list.parse_doc(input)?; + } else if lookahead.peek(kw::attrs) { + arg_list.parse_attrs(input)?; + } else { + return Err(lookahead.error()); + } + } + + Ok(arg_list) + } +} +impl From for FieldArgs { + fn from(arg_list: FieldArgList) -> Self { + use FieldArg::*; + + let mut args = FieldArgs::new(); + + for arg in arg_list.list { + match arg { + Doc(doc) => args.doc = Some(doc), + Attrs(attrs) => args.attrs = Some(attrs), + } + } + + args + } +} +impl FieldArgList { + fn new() -> Self { + Self { + doc: None, + attrs: None, + list: Vec::with_capacity(2), + } + } + + fn parse_doc(&mut self, input: ParseStream) -> Result<()> { + if let Some(doc_span) = self.doc { + return FieldArgList::already_defined_error(input, "doc", doc_span); + } + + let span = input.span(); + let doc: Doc = input.parse()?; + + self.doc = Some(span); + self.list.push(FieldArg::Doc(doc)); + + Ok(()) + } + + fn parse_attrs(&mut self, input: ParseStream) -> Result<()> { + if let Some(attrs_span) = self.attrs { + return FieldArgList::already_defined_error(input, "attrs", attrs_span); + } + + let span = input.span(); + + input.parse::()?; + let attrs: Attrs = input.parse()?; + + self.attrs = Some(span); + self.list.push(FieldArg::Attrs(attrs)); + + Ok(()) + } + + fn already_defined_error( + input: ParseStream, + arg_name: &'static str, + prev_span: Span, + ) -> Result<()> { + let mut e = input.error(format!("{} already defined", arg_name)); + e.combine(Error::new(prev_span, format!("{} defined here", arg_name))); + Err(e) + } +} diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index c087157..1385dde 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -1,7 +1,12 @@ -use syn::{Attribute, Field}; +use quote::quote; +use syn::{parse2, Attribute, Field, Meta}; -use crate::args::{Args, Attrs}; -use crate::attrs::generator::AttrGenerator; +use crate::args::{Args, Attrs, Doc}; +use crate::attrs::generator::{is_doc_attr, AttrGenerator}; +use crate::error::unexpected; +use crate::fields::args::FieldArgs; + +const OPTFIELD_FIELD_ATTR_NAME: &str = "optfield"; struct FieldAttrGen<'a> { field: &'a Field, @@ -12,13 +17,34 @@ impl<'a> FieldAttrGen<'a> { fn new(field: &'a Field, args: &'a Args) -> Self { Self { field, args } } -} -impl<'a> AttrGenerator for FieldAttrGen<'a> { - fn no_docs(&self) -> bool { - !self.args.field_doc + /// Get the #[optfield(...)] args on this field, if the attribute exists. + fn optfield_args(&self) -> Option { + let mut optfield_field_attrs_it = self + .field + .attrs + .iter() + .filter_map(|attr| is_optfield_field_attr(attr).then(|| &attr.meta)); + + let attr = match optfield_field_attrs_it.next() { + Some(attr) => attr, + None => return None, + }; + + if optfield_field_attrs_it.next().is_some() { + panic!("There can be at most 1 optfield attribute on each field."); + } + + let args: FieldArgs = match attr { + Meta::Path(_) | Meta::NameValue(_) => panic!("Expected parentheses."), + Meta::List(list) => list.parse_args().unwrap_or_else(|e| panic!("{}", e)), + }; + + Some(args) } +} +impl<'a> AttrGenerator for FieldAttrGen<'a> { fn error_action_text(&self) -> String { let field_name = match &self.field.ident { None => "".to_string(), @@ -28,12 +54,74 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { format!("generating {}field attrs", field_name) } - fn original_attrs(&self) -> &[Attribute] { - &self.field.attrs + fn new_attrs_except_docs(&self) -> Vec { + use Attrs::*; + + let struct_attrs_arg = &self.args.field_attrs; + + let original_attrs_it = self + .field + .attrs + .iter() + .filter_map(|attr| (!is_doc_attr(attr)).then(|| attr.meta.clone())); + + let attr_attrs_arg = self.optfield_args().and_then(|args| args.attrs); + + // field arg overrides struct arg + match (struct_attrs_arg, attr_attrs_arg) { + // no attributes + (None, None) => vec![], + // keep original + (Some(Keep), None) => original_attrs_it.collect(), + // replace with attributes defined on struct + (Some(Replace(attrs)), None) => attrs.clone(), + // add attributes defined on struct + (Some(Add(attrs)), None) => original_attrs_it.chain(attrs.iter().cloned()).collect(), + // keep original (field arg override) + (_, Some(Keep)) => original_attrs_it.collect(), + // replace with attributes defined on field (field arg override) + (_, Some(Replace(attrs))) => attrs, + // add attributes defined on field (field arg override) + (_, Some(Add(attrs))) => original_attrs_it.chain(attrs).collect(), + } } - fn attrs_arg(&self) -> &Option { - &self.args.field_attrs + fn new_docs(&self) -> Vec { + use Doc::*; + + let struct_doc_arg = self.args.field_doc; + + let original_doc_it = self + .field + .attrs + .iter() + .filter_map(|attr| is_doc_attr(attr).then(|| attr.meta.clone())); + + let attr_doc_arg = self.optfield_args().and_then(|args| args.doc); + + // field arg overrides struct arg + match (struct_doc_arg, attr_doc_arg) { + // no docs + (false, None) => vec![], + // keep original + (true, None) => original_doc_it.collect(), + // keep original (field arg override) + (_, Some(Keep)) => original_doc_it.collect(), + // replace with doc on field + (_, Some(Replace(doc_text))) => { + let tokens = quote! { doc = #doc_text }; + let doc_attr = parse2(tokens) + .unwrap_or_else(|e| panic!("{}", unexpected(self.error_action_text(), e))); + vec![doc_attr] + } + // append to original with doc on field + (_, Some(Append(doc_text))) => { + let tokens = quote! { doc = #doc_text }; + let doc_attr = parse2(tokens) + .unwrap_or_else(|e| panic!("{}", unexpected(self.error_action_text(), e))); + original_doc_it.chain(std::iter::once(doc_attr)).collect() + } + } } } @@ -41,6 +129,12 @@ pub fn generate(field: &Field, args: &Args) -> Vec { FieldAttrGen::new(field, args).generate() } +/// This currently has the same implementation as `is_optfield_attr`, but they +/// are semantically distinct, and therefore have separate functions. +pub fn is_optfield_field_attr(attr: &Attribute) -> bool { + attr.path().is_ident(OPTFIELD_FIELD_ATTR_NAME) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/fields/mod.rs b/src/fields/mod.rs index 2f638b0..225e9ac 100644 --- a/src/fields/mod.rs +++ b/src/fields/mod.rs @@ -4,6 +4,7 @@ use syn::{parse2, Field, Fields, ItemStruct, Path, Type, TypePath}; use crate::args::Args; use crate::error::unexpected; +mod args; mod attrs; const OPTION: &str = "Option"; @@ -16,7 +17,6 @@ pub fn generate(item: &ItemStruct, args: &Args) -> Fields { for field in fields.iter_mut() { field.attrs = attrs::generate(field, args); - attrs::generate(field, args); if is_option(field) && !args.rewrap { continue; From 6d3ba6ea4ef28979f50b72f568eaa548e5beb336 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 8 Jun 2023 21:51:02 +0800 Subject: [PATCH 02/14] Apply clippy lints --- src/fields/attrs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index 1385dde..75105a2 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -24,7 +24,7 @@ impl<'a> FieldAttrGen<'a> { .field .attrs .iter() - .filter_map(|attr| is_optfield_field_attr(attr).then(|| &attr.meta)); + .filter_map(|attr| is_optfield_field_attr(attr).then_some(&attr.meta)); let attr = match optfield_field_attrs_it.next() { Some(attr) => attr, From e17955a6ad3b91b7ce014912d593a41a1356d333 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 8 Jun 2023 21:58:33 +0800 Subject: [PATCH 03/14] Refrain from using `bool::then_some` - Not yet stabalised in Rust 1.56.0 --- src/fields/attrs.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index 75105a2..4064713 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -20,11 +20,13 @@ impl<'a> FieldAttrGen<'a> { /// Get the #[optfield(...)] args on this field, if the attribute exists. fn optfield_args(&self) -> Option { - let mut optfield_field_attrs_it = self - .field - .attrs - .iter() - .filter_map(|attr| is_optfield_field_attr(attr).then_some(&attr.meta)); + let mut optfield_field_attrs_it = self.field.attrs.iter().filter_map(|attr| { + if is_optfield_field_attr(attr) { + Some(&attr.meta) + } else { + None + } + }); let attr = match optfield_field_attrs_it.next() { Some(attr) => attr, From e12a1675e689cd64e68190745702783038a71d61 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:14:27 +0800 Subject: [PATCH 04/14] More accurate variable names --- src/fields/attrs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index 4064713..c33d89a 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -67,10 +67,10 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { .iter() .filter_map(|attr| (!is_doc_attr(attr)).then(|| attr.meta.clone())); - let attr_attrs_arg = self.optfield_args().and_then(|args| args.attrs); + let field_attrs_arg = self.optfield_args().and_then(|args| args.attrs); // field arg overrides struct arg - match (struct_attrs_arg, attr_attrs_arg) { + match (struct_attrs_arg, field_attrs_arg) { // no attributes (None, None) => vec![], // keep original @@ -99,10 +99,10 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { .iter() .filter_map(|attr| is_doc_attr(attr).then(|| attr.meta.clone())); - let attr_doc_arg = self.optfield_args().and_then(|args| args.doc); + let field_doc_arg = self.optfield_args().and_then(|args| args.doc); // field arg overrides struct arg - match (struct_doc_arg, attr_doc_arg) { + match (struct_doc_arg, field_doc_arg) { // no docs (false, None) => vec![], // keep original From ef4cb710340159a4342efb44e3084e34dee5f400 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:00:20 +0800 Subject: [PATCH 05/14] Rename trait methods to something more sensible --- src/attrs/generator.rs | 8 ++++---- src/attrs/mod.rs | 4 ++-- src/fields/attrs.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/attrs/generator.rs b/src/attrs/generator.rs index 1643376..8000a58 100644 --- a/src/attrs/generator.rs +++ b/src/attrs/generator.rs @@ -9,13 +9,13 @@ const OPTFIELD_ATTR_NAME: &str = "optfield"; pub trait AttrGenerator { fn error_action_text(&self) -> String; - fn new_attrs_except_docs(&self) -> Vec; + fn new_non_doc_attrs(&self) -> Vec; - fn new_docs(&self) -> Vec; + fn new_doc_attrs(&self) -> Vec; fn generate(&self) -> Vec { - let attrs_except_docs = self.new_attrs_except_docs(); - let docs = self.new_docs(); + let attrs_except_docs = self.new_non_doc_attrs(); + let docs = self.new_doc_attrs(); let attrs_tokens = quote! { #(#[#attrs_except_docs])* diff --git a/src/attrs/mod.rs b/src/attrs/mod.rs index 42b7e4a..ceaa999 100644 --- a/src/attrs/mod.rs +++ b/src/attrs/mod.rs @@ -25,7 +25,7 @@ impl<'a> AttrGenerator for AttrGen<'a> { format!("generating {} attrs", self.item.ident) } - fn new_attrs_except_docs(&self) -> Vec { + fn new_non_doc_attrs(&self) -> Vec { use Attrs::*; let original_attrs_it = self @@ -45,7 +45,7 @@ impl<'a> AttrGenerator for AttrGen<'a> { } } - fn new_docs(&self) -> Vec { + fn new_doc_attrs(&self) -> Vec { use Doc::*; let original_doc_it = self diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index c33d89a..a8eeacb 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -56,7 +56,7 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { format!("generating {}field attrs", field_name) } - fn new_attrs_except_docs(&self) -> Vec { + fn new_non_doc_attrs(&self) -> Vec { use Attrs::*; let struct_attrs_arg = &self.args.field_attrs; @@ -88,7 +88,7 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { } } - fn new_docs(&self) -> Vec { + fn new_doc_attrs(&self) -> Vec { use Doc::*; let struct_doc_arg = self.args.field_doc; From c42d3d569519a8c8a76984a9a1c554a8092bd5ca Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:11:57 +0800 Subject: [PATCH 06/14] Always derive - common traits used for debugging are no longer conditionally derived --- src/args.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/args.rs b/src/args.rs index 2f669aa..35c6ff9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -22,7 +22,7 @@ pub mod kw { } } -#[cfg_attr(test, derive(PartialEq))] +#[derive(PartialEq)] pub struct Args { pub item: GenItem, pub merge: Option, @@ -44,25 +44,25 @@ enum Arg { From(bool), } -#[cfg_attr(test, derive(PartialEq))] +#[derive(Debug, PartialEq)] pub struct GenItem { pub name: Ident, pub visibility: Option, } -#[cfg_attr(test, derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct MergeFn { pub visibility: Visibility, pub name: MergeFnName, } -#[cfg_attr(test, derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub enum MergeFnName { Default, Custom(Ident), } -#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub enum Doc { /// Keep the same documentation. Keep, @@ -72,7 +72,7 @@ pub enum Doc { Append(String), } -#[cfg_attr(test, derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub enum Attrs { /// Keep same attributes. Keep, From 270015007375e0b7fde5b1254d67c8f0b3d0f087 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:17:02 +0800 Subject: [PATCH 07/14] Derive `Debug` on everything --- src/args.rs | 4 +++- src/attrs/mod.rs | 1 + src/fields/args.rs | 3 +++ src/fields/attrs.rs | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/args.rs b/src/args.rs index 35c6ff9..6a7cf5d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -22,7 +22,7 @@ pub mod kw { } } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub struct Args { pub item: GenItem, pub merge: Option, @@ -34,6 +34,7 @@ pub struct Args { pub from: bool, } +#[derive(Debug)] enum Arg { Merge(MergeFn), Doc(Doc), @@ -86,6 +87,7 @@ pub enum Attrs { pub struct AttrList(Vec); /// Parser for unordered args. +#[derive(Debug)] struct ArgList { item: GenItem, merge: Option, diff --git a/src/attrs/mod.rs b/src/attrs/mod.rs index ceaa999..266f328 100644 --- a/src/attrs/mod.rs +++ b/src/attrs/mod.rs @@ -9,6 +9,7 @@ pub mod generator; use generator::AttrGenerator; +#[derive(Debug)] struct AttrGen<'a> { item: &'a ItemStruct, args: &'a Args, diff --git a/src/fields/args.rs b/src/fields/args.rs index 546a479..28a475f 100644 --- a/src/fields/args.rs +++ b/src/fields/args.rs @@ -8,6 +8,7 @@ use syn::{ use crate::args::{kw, Attrs, Doc}; /// Args declared on a field. +#[derive(Debug)] pub struct FieldArgs { pub doc: Option, pub attrs: Option, @@ -28,12 +29,14 @@ impl FieldArgs { } } +#[derive(Debug)] enum FieldArg { Doc(Doc), Attrs(Attrs), } /// Parser for unordered args on a field. +#[derive(Debug)] struct FieldArgList { doc: Option, attrs: Option, diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index a8eeacb..7cd2ac6 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -8,6 +8,7 @@ use crate::fields::args::FieldArgs; const OPTFIELD_FIELD_ATTR_NAME: &str = "optfield"; +#[derive(Debug)] struct FieldAttrGen<'a> { field: &'a Field, args: &'a Args, From e2ac4cdbc4095ee8341baeab668fcd8603b012ac Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:39:57 +0800 Subject: [PATCH 08/14] Rename helper function to distinguish between `Args` and `FieldArgs` --- src/args.rs | 22 +++++++++++----------- src/attrs/mod.rs | 8 ++++---- src/fields/attrs.rs | 4 ++-- src/generate.rs | 2 +- src/lib.rs | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/args.rs b/src/args.rs index 6a7cf5d..5c7f2db 100644 --- a/src/args.rs +++ b/src/args.rs @@ -476,7 +476,7 @@ mod tests { #[test] #[should_panic(expected = $expected)] fn []() { - parse_args(quote! { + parse_struct_args(quote! { Opt, $attr, $dup @@ -500,7 +500,7 @@ mod tests { #[test] #[should_panic(expected = "first argument must be opt struct name")] fn [<$attr _first_panics>]() { - parse_args(quote! { + parse_struct_args(quote! { $attr, Opt }); @@ -520,7 +520,7 @@ mod tests { #[test] #[should_panic(expected = "expected opt struct name")] fn empty_args_panics() { - parse_args(TokenStream::new()); + parse_struct_args(TokenStream::new()); } #[test] @@ -541,7 +541,7 @@ mod tests { ]; for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); assert_eq!(args.item.name, "OptionalFields"); } @@ -549,7 +549,7 @@ mod tests { #[test] fn parse_no_optional_args() { - let args = parse_args(quote! { + let args = parse_struct_args(quote! { Opt }); @@ -614,7 +614,7 @@ mod tests { ]; for (args_tokens, fn_name, vis) in cases { - let args = parse_args(args_tokens); + let args = parse_struct_args(args_tokens); assert_eq!(args.merge.clone().unwrap().name, fn_name); assert_eq!(args.merge.unwrap().visibility, vis); @@ -623,7 +623,7 @@ mod tests { #[test] fn parse_rewrap() { - let args = parse_args(quote! { + let args = parse_struct_args(quote! { Opt, rewrap }); @@ -700,7 +700,7 @@ mod tests { ]; for (args_tokens, attrs) in cases { - let args = parse_args(args_tokens); + let args = parse_struct_args(args_tokens); assert_eq!(args.attrs, Some(attrs)); } @@ -708,7 +708,7 @@ mod tests { #[test] fn parse_field_doc() { - let args = parse_args(quote! { + let args = parse_struct_args(quote! { Opt, field_doc }); @@ -741,7 +741,7 @@ mod tests { ]; for (args_tokens, attrs) in cases { - let args = parse_args(args_tokens); + let args = parse_struct_args(args_tokens); assert_eq!(args.field_attrs, Some(attrs)); } @@ -749,7 +749,7 @@ mod tests { #[test] fn parse_from() { - let args = parse_args(quote! { + let args = parse_struct_args(quote! { Opt, from }); diff --git a/src/attrs/mod.rs b/src/attrs/mod.rs index 266f328..24f8f86 100644 --- a/src/attrs/mod.rs +++ b/src/attrs/mod.rs @@ -118,7 +118,7 @@ mod tests { let item_docs = doc_attrs(&item.attrs); for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&item, &args); @@ -156,7 +156,7 @@ mod tests { let item_docs = doc_attrs(&item.attrs); for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&item, &args); @@ -208,7 +208,7 @@ mod tests { let item_docs = doc_attrs(&item.attrs); for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&item, &args); @@ -339,7 +339,7 @@ mod tests { ]; for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&item, &args); diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index 7cd2ac6..d772def 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -178,7 +178,7 @@ mod tests { let item_docs = doc_attrs(&field.attrs); for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&field, &args); @@ -221,7 +221,7 @@ mod tests { let item_docs = doc_attrs(&field.attrs); for case in cases { - let args = parse_args(case); + let args = parse_struct_args(case); let generated = generate(&field, &args); diff --git a/src/generate.rs b/src/generate.rs index 9bc3287..0a99020 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -102,7 +102,7 @@ mod tests { ]; for (args_tokens, vis_tokens) in cases { - let args = parse_args(args_tokens); + let args = parse_struct_args(args_tokens); let vis = parse_visibility(vis_tokens); let generated = parse_item(generate(&item, args)); diff --git a/src/lib.rs b/src/lib.rs index 5e90f1c..e7d2025 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -497,14 +497,14 @@ mod test_util { item_tokens: TokenStream, args_tokens: TokenStream, ) -> (ItemStruct, Args) { - (parse_item(item_tokens), parse_args(args_tokens)) + (parse_item(item_tokens), parse_struct_args(args_tokens)) } pub fn parse_field_and_args( field_tokens: TokenStream, args_tokens: TokenStream, ) -> (Field, Args) { - (parse_field(field_tokens), parse_args(args_tokens)) + (parse_field(field_tokens), parse_struct_args(args_tokens)) } pub fn parse_item(tokens: TokenStream) -> ItemStruct { @@ -515,7 +515,7 @@ mod test_util { Field::parse_named.parse2(tokens).unwrap() } - pub fn parse_args(tokens: TokenStream) -> Args { + pub fn parse_struct_args(tokens: TokenStream) -> Args { parse2(tokens).unwrap() } From 8dc68ad4d600a512f0bfba0aab602897b93aebfe Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:42:53 +0800 Subject: [PATCH 09/14] Add/overhaul field attribute tests --- Cargo.toml | 1 + src/fields/args.rs | 12 +- src/fields/attrs.rs | 328 ++++++++++++++++++++++---------------------- src/fields/mod.rs | 4 +- src/lib.rs | 13 +- 5 files changed, 176 insertions(+), 182 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b853435..f54e69d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ features = ["full", "extra-traits"] [dev-dependencies] paste = "1.0.12" +rstest = "0.17.0" [dev-dependencies.serde] version = "1.0.163" diff --git a/src/fields/args.rs b/src/fields/args.rs index 28a475f..8fd3e50 100644 --- a/src/fields/args.rs +++ b/src/fields/args.rs @@ -47,13 +47,6 @@ impl Parse for FieldArgList { let mut arg_list = FieldArgList::new(); while !input.is_empty() { - input.parse::()?; - - // allow trailing commas - if input.is_empty() { - break; - } - let lookahead = input.lookahead1(); if lookahead.peek(kw::doc) { @@ -63,6 +56,11 @@ impl Parse for FieldArgList { } else { return Err(lookahead.error()); } + + // allow trailing commas + if !input.is_empty() { + input.parse::()?; + } } Ok(arg_list) diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index d772def..f49af8f 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -66,6 +66,8 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { .field .attrs .iter() + // #[optfield(..)] attributes do not need to be regenerated + .filter(|attr| !is_optfield_field_attr(attr)) .filter_map(|attr| (!is_doc_attr(attr)).then(|| attr.meta.clone())); let field_attrs_arg = self.optfield_args().and_then(|args| args.attrs); @@ -140,189 +142,193 @@ pub fn is_optfield_field_attr(attr: &Attribute) -> bool { #[cfg(test)] mod tests { + use std::collections::HashSet; + use super::*; + use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; + use rstest::{fixture, rstest}; use crate::test_util::*; - #[test] - fn remove_docs() { - let field = parse_field(quote! { + #[fixture] + fn field() -> Field { + parse_field(quote! { /// some /// doc /// lines #[attr] field: i32 - }); - - let cases = vec![ - quote! { - Opt - - }, - quote! { - Opt, - field_attrs - }, - quote! { - Opt, - field_attrs = (new_attr) - }, - quote! { - Opt, - field_attrs = add(new_attr) - }, - ]; - - let item_docs = doc_attrs(&field.attrs); - - for case in cases { - let args = parse_struct_args(case); - - let generated = generate(&field, &args); - - assert!(!attrs_contain_any(&generated, &item_docs)); - } + }) } - #[test] - fn keep_docs() { - let field = parse_field(quote! { - /// field - /// with - /// docs - #[attr] - field: String - }); + /// Shared impl for expected results. + trait ExpectedAttrs { + fn into_expected_attrs(self) -> HashSet; + /// Chain multiple possibly-indeterminate expected results. + /// + /// Similar to [`Option::or`]. + fn or(self, other: Self) -> Self; + } - let cases = vec![ - quote! { - Opt, - field_doc - }, - quote! { - Opt, - field_doc, - field_attrs - }, - quote! { - Opt, - field_doc, - field_attrs = (new_attr) - }, - quote! { - Opt, - field_doc, - field_attrs = add(new_attr) - }, - ]; - - let item_docs = doc_attrs(&field.attrs); - - for case in cases { - let args = parse_struct_args(case); - - let generated = generate(&field, &args); - - assert!(attrs_contain_all(&generated, &item_docs)); + /// The possible expected generated attributes for [`test_field_doc_attrs_only`]. + #[derive(Debug, Copy, Clone)] + enum ExpectedDocAttrs { + Unknown, + Discarded, + Kept, + Replaced, + Appended, + } + impl ExpectedAttrs for ExpectedDocAttrs { + fn into_expected_attrs(self) -> HashSet { + use ExpectedDocAttrs::*; + + // we are only testing for doc attributes + let base_it = field().attrs.into_iter().filter(|a| is_doc_attr(a)); + + match self { + Unknown => unreachable!("The expected doc attributes depend on other factors"), + Discarded => HashSet::new(), + Kept => base_it.collect(), + Replaced => std::iter::once(parse_attr(quote!(#[doc = "replaced"]))).collect(), + Appended => base_it + .chain(std::iter::once(parse_attr(quote!(#[doc = "appended"])))) + .collect(), + } + } + fn or(self, other: Self) -> Self { + use ExpectedDocAttrs::*; + match self { + Unknown => other, + Discarded | Kept | Replaced | Appended => self, + } } } - #[test] - fn remove_attrs() { - let (field, args) = parse_field_and_args( - quote! { - #[some] - #[attrs] - field: String - }, - quote! { - Opt - }, - ); - - let generated = generate(&field, &args); - - assert!(!attrs_contain_any(&generated, &field.attrs)); + /// The possible expected generated attributes for [`test_field_non_doc_attrs_only`]. + #[derive(Debug, Copy, Clone)] + enum ExpectedNonDocAttrs { + Unknown, + Discarded, + KeptByStruct, + KeptByField, + ReplacedByStruct, + ReplacedByField, + AddedByStruct, + AddedByField, } - - #[test] - fn keep_attrs() { - let (field, args) = parse_field_and_args( - quote! { - #[some] - #[attrs] - field: String - }, - quote! { - Opt, - field_attrs - }, - ); - - let generated = generate(&field, &args); - - assert!(attrs_contain_all(&generated, &field.attrs)); + impl ExpectedAttrs for ExpectedNonDocAttrs { + fn into_expected_attrs(self) -> HashSet { + use ExpectedNonDocAttrs::*; + + // we are only testing for non-doc attributes + let base_it = field().attrs.into_iter().filter(|a| !is_doc_attr(a)); + + match self { + Unknown => unreachable!("The expected non-doc attributes depend on other factors"), + Discarded => HashSet::new(), + KeptByStruct | KeptByField => base_it.collect(), + ReplacedByStruct => parse_attrs(quote! { + #[replaced_by_struct_0] + #[replaced_by_struct_1] + }) + .into_iter() + .collect(), + ReplacedByField => parse_attrs(quote! { + #[replaced_by_field_0] + #[replaced_by_field_1] + }) + .into_iter() + .collect(), + AddedByStruct => base_it + .chain(parse_attrs(quote! { + #[added_by_struct_0] + #[added_by_struct_1] + })) + .collect(), + AddedByField => base_it + .chain(parse_attrs(quote! { + #[added_by_field_0] + #[added_by_field_1] + })) + .collect(), + } + } + fn or(self, other: Self) -> Self { + use ExpectedNonDocAttrs::*; + match self { + Unknown => other, + Discarded | KeptByStruct | KeptByField | ReplacedByStruct | ReplacedByField + | AddedByStruct | AddedByField => self, + } + } } - #[test] - fn replace_attrs() { - let (field, args) = parse_field_and_args( - quote! { - #[some] - #[attrs] - field: String - }, - quote! { - Opt, - field_attrs = ( - other, - new, - attribute - ) - }, - ); - - let new_attrs = parse_attrs(quote! { - #[other] - #[new] - #[attribute] - }); - - let generated = generate(&field, &args); - - assert!(!attrs_contain_any(&generated, &field.attrs)); - assert!(attrs_contain_all(&generated, &new_attrs)); + fn test_field_attrs( + mut field: Field, + (struct_args, struct_expected): (TokenStream, T), + (field_args, field_expected): (TokenStream, T), + ) where + T: ExpectedAttrs, + { + // setup: insert field args into base field + let optfield_field_attr_ident = Ident::new(OPTFIELD_FIELD_ATTR_NAME, Span::call_site()); + let optfield_field_attr = parse_attr(quote!(#[#optfield_field_attr_ident(#field_args)])); + field.attrs.push(optfield_field_attr); + + let generated: HashSet<_> = generate(&field, &parse_struct_args(struct_args)) + .into_iter() + .collect(); + // field args override struct args + let expected = field_expected.or(struct_expected).into_expected_attrs(); + dbg!(&generated, &expected); + + assert_eq!(generated, expected); } - #[test] - fn add_attrs() { - let (field, args) = parse_field_and_args( - quote! { - #[old] - #[attrs] - field: u8 - }, - quote! { - Opt, - field_attrs = add( - new, - field, - attributes - ) - }, - ); - - let new_attrs = parse_attrs(quote! { - #[new] - #[field] - #[attributes] - }); - - let generated = generate(&field, &args); + #[rstest] + fn test_field_doc_attrs_only( + field: Field, + // .0 are the args on struct; .1 are the expected docs (unless overridden) + #[values( + (quote!(Opt), ExpectedDocAttrs::Discarded), + (quote!(Opt, field_doc), ExpectedDocAttrs::Kept), + )] + struct_args_pair: (TokenStream, ExpectedDocAttrs), + // .0 are the args on field; .1 are the expected docs (overriding) + #[values( + (quote!(), ExpectedDocAttrs::Unknown), + (quote!(doc), ExpectedDocAttrs::Kept), + (quote!(doc = "replaced"), ExpectedDocAttrs::Replaced), + (quote!(doc = append("appended")), ExpectedDocAttrs::Appended), + )] + field_args_pair: (TokenStream, ExpectedDocAttrs), + ) { + test_field_attrs(field, struct_args_pair, field_args_pair) + } - assert!(attrs_contain_all(&generated, &field.attrs)); - assert!(attrs_contain_all(&generated, &new_attrs)); + #[rstest] + fn test_field_non_doc_attrs_only( + field: Field, + // .0 are the args on struct; .1 are the expected attrs (unless overridden) + #[values( + (quote!(Opt), ExpectedNonDocAttrs::Discarded), + (quote!(Opt, field_attrs), ExpectedNonDocAttrs::KeptByStruct), + (quote!(Opt, field_attrs = (replaced_by_struct_0, replaced_by_struct_1)), ExpectedNonDocAttrs::ReplacedByStruct), + (quote!(Opt, field_attrs = add(added_by_struct_0, added_by_struct_1)), ExpectedNonDocAttrs::AddedByStruct), + )] + struct_args_pair: (TokenStream, ExpectedNonDocAttrs), + // .0 are the args on field; .1 are the expected attrs (overriding) + #[values( + (quote!(), ExpectedNonDocAttrs::Unknown), + (quote!(attrs), ExpectedNonDocAttrs::KeptByField), + (quote!(attrs = (replaced_by_field_0, replaced_by_field_1)), ExpectedNonDocAttrs::ReplacedByField), + (quote!(attrs = add(added_by_field_0, added_by_field_1)), ExpectedNonDocAttrs::AddedByField), + )] + field_args_pair: (TokenStream, ExpectedNonDocAttrs), + ) { + test_field_attrs(field, struct_args_pair, field_args_pair) } } diff --git a/src/fields/mod.rs b/src/fields/mod.rs index 225e9ac..0cc50e3 100644 --- a/src/fields/mod.rs +++ b/src/fields/mod.rs @@ -4,8 +4,8 @@ use syn::{parse2, Field, Fields, ItemStruct, Path, Type, TypePath}; use crate::args::Args; use crate::error::unexpected; -mod args; -mod attrs; +pub mod args; +pub mod attrs; const OPTION: &str = "Option"; diff --git a/src/lib.rs b/src/lib.rs index e7d2025..72748c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -500,13 +500,6 @@ mod test_util { (parse_item(item_tokens), parse_struct_args(args_tokens)) } - pub fn parse_field_and_args( - field_tokens: TokenStream, - args_tokens: TokenStream, - ) -> (Field, Args) { - (parse_field(field_tokens), parse_struct_args(args_tokens)) - } - pub fn parse_item(tokens: TokenStream) -> ItemStruct { parse2(tokens).unwrap() } @@ -564,10 +557,6 @@ mod test_util { } pub fn doc_attrs(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter(|a| is_doc_attr(a)) - .map(|a| a.clone()) - .collect() + attrs.iter().filter(|a| is_doc_attr(a)).cloned().collect() } } From 051a9170e921d1dbb9b8ecf0124a9398308c8fb3 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 9 Jun 2023 18:28:42 +0800 Subject: [PATCH 10/14] Downgrade `rstest` to satisfy MSRV --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f54e69d..a2ccfe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ features = ["full", "extra-traits"] [dev-dependencies] paste = "1.0.12" -rstest = "0.17.0" +rstest = "0.12.0" [dev-dependencies.serde] version = "1.0.163" From e91537959b7238a9a1e2c50d00f25ed7a05b4d61 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:30:57 +0800 Subject: [PATCH 11/14] Documentation for new `append` mode in `doc` arg --- src/lib.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 72748c1..b1b4d3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,8 +110,10 @@ //! ``` //! //! # Documentation -//! To document the opt struct, either duplicate the same documentation as the -//! original using the `doc` argument by itself: +//! To document the opt struct, you can: +//! +//! 1. duplicate the same documentation as the original using the `doc` argument +//! by itself: //! ``` //! # use optfield::*; //! /// My struct documentation @@ -130,7 +132,7 @@ //! text: Option //! } //! ``` -//! Or write custom documentation by giving `doc` a value: +//! 2. specify custom documentation by giving `doc` a value: //! ``` //! # use optfield::*; //! #[optfield( @@ -152,6 +154,27 @@ //! text: Option //! } //! ``` +//! 3. append additional documentation using the `append` syntax: +//! ``` +//! /// Original documentation +//! # use optfield::*; +//! #[optfield( +//! Opt, +//! doc = append("Additional documentation") +//! )] +//! struct MyStruct { +//! text: String +//! } +//! ``` +//! Will generate: +//! ``` +//! /// Original documentation +//! /// +//! /// Additional documentation +//! struct Opt { +//! text: Option +//! } +//! ``` //! //! # Attributes //! The `attrs` argument alone makes optfield insert the same attributes as the From d677d17b2af724fb47fd61201198a13e8dccf793 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Sun, 25 Jun 2023 14:57:06 +0800 Subject: [PATCH 12/14] Impl workaround to deal with proc_macro_attribute inadequacy --- src/fields/attrs.rs | 13 +++++---- src/generate.rs | 71 ++++++++++++++++++++++++++++++++++++--------- src/lib.rs | 4 +-- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/fields/attrs.rs b/src/fields/attrs.rs index f49af8f..5319c22 100644 --- a/src/fields/attrs.rs +++ b/src/fields/attrs.rs @@ -6,7 +6,10 @@ use crate::attrs::generator::{is_doc_attr, AttrGenerator}; use crate::error::unexpected; use crate::fields::args::FieldArgs; -const OPTFIELD_FIELD_ATTR_NAME: &str = "optfield"; +/// Without helper attribute support on `proc_macro_attribute`, this attribute +/// name needs to be different from the name of the proc macro (`optfield`) to +/// avoid namespace conflicts. +const OPTFIELD_FIELD_ATTR_NAME: &str = "optfield_field"; #[derive(Debug)] struct FieldAttrGen<'a> { @@ -19,8 +22,8 @@ impl<'a> FieldAttrGen<'a> { Self { field, args } } - /// Get the #[optfield(...)] args on this field, if the attribute exists. - fn optfield_args(&self) -> Option { + /// Get the #[optfield_field(...)] args on this field, if the attribute exists. + fn optfield_field_args(&self) -> Option { let mut optfield_field_attrs_it = self.field.attrs.iter().filter_map(|attr| { if is_optfield_field_attr(attr) { Some(&attr.meta) @@ -70,7 +73,7 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { .filter(|attr| !is_optfield_field_attr(attr)) .filter_map(|attr| (!is_doc_attr(attr)).then(|| attr.meta.clone())); - let field_attrs_arg = self.optfield_args().and_then(|args| args.attrs); + let field_attrs_arg = self.optfield_field_args().and_then(|args| args.attrs); // field arg overrides struct arg match (struct_attrs_arg, field_attrs_arg) { @@ -102,7 +105,7 @@ impl<'a> AttrGenerator for FieldAttrGen<'a> { .iter() .filter_map(|attr| is_doc_attr(attr).then(|| attr.meta.clone())); - let field_doc_arg = self.optfield_args().and_then(|args| args.doc); + let field_doc_arg = self.optfield_field_args().and_then(|args| args.doc); // field arg overrides struct arg match (struct_doc_arg, field_doc_arg) { diff --git a/src/generate.rs b/src/generate.rs index 0a99020..b6e91e3 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,22 +1,57 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::ItemStruct; +use syn::{Field, ItemStruct}; use crate::args::Args; +use crate::fields::attrs::is_optfield_field_attr; use crate::{attrs, fields, from, merge}; -pub fn generate(original: &ItemStruct, args: Args) -> TokenStream { +pub fn generate(original: &mut ItemStruct, args: Args) -> TokenStream { let mut opt_struct = original.clone(); opt_struct.ident = args.item.name.clone(); opt_struct.vis = args.item.final_visibility(); - opt_struct.attrs = attrs::generate(original, &args); - opt_struct.fields = fields::generate(original, &args); - - let merge_impl = merge::generate(original, &opt_struct, &args); - - let from_impl = from::generate(original, &opt_struct, &args); + // the original struct def with at most one `optfield_field` attribute on each + // field + // + // we do this here so that each `generate` sub-step need not worry about + // having potentially multiple `optfield_field` attributes + let original_sanitised = { + let mut o = original.clone(); + for field in o.fields.iter_mut() { + if let Some(idx) = index_of_first_optfield_field_attr(field) { + let attrs_remaining: Vec<_> = field + .attrs + .drain(idx + 1..) + .filter(|attr| !is_optfield_field_attr(attr)) + .collect(); + field.attrs.extend(attrs_remaining); + } + } + o + }; + + opt_struct.attrs = attrs::generate(&original_sanitised, &args); + opt_struct.fields = fields::generate(&original_sanitised, &args); + + let merge_impl = merge::generate(&original_sanitised, &opt_struct, &args); + + let from_impl = from::generate(&original_sanitised, &opt_struct, &args); + + // manually remove the first `optfield_field` attribute from each field of the + // original struct + // + // we only remove the first because each `optfield` struct attribute only + // consumes at most one `optfield_field` attribute on each field + // + // this manual handling may become unnecessary in the future + // see https://github.com/rust-lang/rust/issues/65823 + for field in original.fields.iter_mut() { + if let Some(idx) = index_of_first_optfield_field_attr(field) { + field.attrs.remove(idx); + } + } quote! { #opt_struct @@ -27,6 +62,14 @@ pub fn generate(original: &ItemStruct, args: Args) -> TokenStream { } } +fn index_of_first_optfield_field_attr(field: &Field) -> Option { + field + .attrs + .iter() + .enumerate() + .find_map(|(idx, attr)| is_optfield_field_attr(attr).then(|| idx)) +} + #[cfg(test)] mod tests { use super::*; @@ -35,7 +78,7 @@ mod tests { #[test] fn sets_name() { - let (item, args) = parse_item_and_args( + let (mut item, args) = parse_item_and_args( quote! { struct S; }, @@ -44,14 +87,14 @@ mod tests { }, ); - let generated = parse_item(generate(&item, args)); + let generated = parse_item(generate(&mut item, args)); assert_eq!(generated.ident, "Opt"); } #[test] fn sets_generics() { - let (item, args) = parse_item_and_args( + let (mut item, args) = parse_item_and_args( quote! { struct S<'a, 'b, T, G> { t: &'a T, @@ -63,14 +106,14 @@ mod tests { }, ); - let generated = parse_item(generate(&item, args)); + let generated = parse_item(generate(&mut item, args)); assert_eq!(item.generics, generated.generics); } #[test] fn sets_visibility() { - let item = parse_item(quote! { + let mut item = parse_item(quote! { struct S; }); @@ -105,7 +148,7 @@ mod tests { let args = parse_struct_args(args_tokens); let vis = parse_visibility(vis_tokens); - let generated = parse_item(generate(&item, args)); + let generated = parse_item(generate(&mut item, args)); assert_eq!(generated.vis, vis); } diff --git a/src/lib.rs b/src/lib.rs index b1b4d3b..f77ff6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -494,10 +494,10 @@ use generate::generate; /// [crate documentation]: ./index.html #[proc_macro_attribute] pub fn optfield(attr: TokenStream, item: TokenStream) -> TokenStream { - let item: ItemStruct = parse_macro_input!(item); + let mut item: ItemStruct = parse_macro_input!(item); let args: Args = parse_macro_input!(attr); - let opt_item = generate(&item, args); + let opt_item = generate(&mut item, args); let out = quote! { #item From 0d3744b91a155f3d095c7dcd8324f0c2a5d2aa04 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Sun, 25 Jun 2023 15:16:23 +0800 Subject: [PATCH 13/14] Documentation for per-field attributes & docs --- CHANGELOG.md | 4 +++ src/lib.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5314e20..a1e1e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased +* add ability to specify attributes and docs on each field individually by [cyqsimon](https://github.com/cyqsimon) +* add an `append` mode to `doc` argument by [cyqsimon](https://github.com/cyqsimon) + ## 0.3.0 * update to syn v2.0, bumping minimum rustc version to 1.56.0 * add feature list and attribute order note to documentation diff --git a/src/lib.rs b/src/lib.rs index f77ff6a..f1c9c37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -294,6 +294,32 @@ //! text: Option //! } //! ``` +//! You can also set field documentation for each field individually by using +//! the `optfield_field` attribute: +//! ``` +//! # use optfield::*; +//! #[optfield(Opt)] +//! struct MyStruct { +//! /// Text field +//! #[optfield_field(doc = "Replaced docs for text field")] +//! text: String, +//! /// Number field +//! #[optfield_field(doc = append("Appended docs for number field"))] +//! number: i32, +//! } +//! ``` +//! Will generate: +//! ``` +//! struct Opt { +//! /// Replaced docs for text field +//! text: Option, +//! /// Number field +//! /// Appended docs for number field +//! number: Option, +//! } +//! ``` +//! Note that the `doc` argument on each field ***always*** overrides the +//! `field_doc` argument on the struct. //! //! # Field attributes //! Field attributes can be handled using the `field_attrs` argument which works @@ -402,6 +428,63 @@ //! my_number: Option //! } //! ``` +//! You can also set field attributes for each field individually by using +//! the `optfield_field` attribute: +//! ``` +//! # use optfield::*; +//! # use serde::Deserialize; +//! #[optfield(Opt, attrs)] +//! #[derive(Deserialize)] +//! struct MyStruct { +//! #[optfield_field(attrs)] +//! #[serde(rename = "text")] +//! my_text: String, +//! } +//! ``` +//! Will generate: +//! ``` +//! # use serde::Deserialize; +//! #[derive(Deserialize)] +//! struct Opt { +//! #[serde(rename = "text")] +//! my_text: Option, +//! } +//! ``` +//! Add and replace syntax is supported too: +//! ``` +//! # use optfield::*; +//! # use serde::Deserialize; +//! #[optfield(Opt, attrs)] +//! #[derive(Deserialize)] +//! struct MyStruct { +//! #[optfield_field(attrs = add( +//! serde(default) +//! ))] +//! #[serde(rename = "text")] +//! my_text: String, +//! +//! #[optfield_field(attrs = ( +//! serde(default) +//! ))] +//! #[serde(rename = "number")] +//! my_number: i32 +//! } +//! ``` +//! Will generate: +//! ``` +//! # use serde::Deserialize; +//! #[derive(Deserialize)] +//! struct Opt { +//! #[serde(rename = "text")] +//! #[serde(default)] +//! my_text: Option, +//! +//! #[serde(default)] +//! my_number: Option +//! } +//! ``` +//! Note that the `attrs` argument on each field ***always*** overrides the +//! `field_attrs` argument on the struct. //! //! # Merging //! When the `merge_fn` argument is used `optfield` will add a method to the From 5ae059fbfd2d06b519159efa1e35d61551d5d992 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Sun, 25 Jun 2023 15:22:35 +0800 Subject: [PATCH 14/14] Suppress a clippy lint due to MSRV restrictions --- src/generate.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/generate.rs b/src/generate.rs index b6e91e3..439e4af 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -62,6 +62,9 @@ pub fn generate(original: &mut ItemStruct, args: Args) -> TokenStream { } } +// `bool::then_some` was stabilised in 1.62.0 but MSRV is 1.56.0 +// TODO: re-enable this lint after MSRV bump +#[allow(clippy::unnecessary_lazy_evaluations)] fn index_of_first_optfield_field_attr(field: &Field) -> Option { field .attrs