From e5af4f8efe20280d782e941aa6371a9dddc5800c Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 3 Nov 2021 11:46:33 +0100 Subject: [PATCH] Replace some manual parsing code with `darling` helper crate --- macro/Cargo.toml | 7 +- macro/src/ir.rs | 265 +-------------------------------------------- macro/src/lib.rs | 11 +- macro/src/parse.rs | 187 ++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 270 deletions(-) create mode 100644 macro/src/parse.rs diff --git a/macro/Cargo.toml b/macro/Cargo.toml index 020c08e..51dd2b2 100644 --- a/macro/Cargo.toml +++ b/macro/Cargo.toml @@ -12,7 +12,8 @@ license = "MIT/Apache-2.0" proc-macro = true [dependencies] -syn = "1.0" -quote = "1.0" -proc-macro2 = "1.0" +darling = "0.13.0" heck = "0.3.2" +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" diff --git a/macro/src/ir.rs b/macro/src/ir.rs index e6ee088..a0c5d64 100644 --- a/macro/src/ir.rs +++ b/macro/src/ir.rs @@ -1,9 +1,5 @@ //! Definition of the intermediate representation. -use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned}; - -use crate::util::{is_option, unwrap_option}; - /// The parsed input to the `gen_config` macro. pub(crate) struct Input { @@ -57,6 +53,7 @@ impl LeafKind { /// The kinds of expressions (just literals) we allow for default or example /// values. +#[derive(Debug)] pub(crate) enum Expr { Str(syn::LitStr), Int(syn::LitInt), @@ -64,263 +61,3 @@ pub(crate) enum Expr { Bool(syn::LitBool), // TODO: arrays? } - -impl Input { - pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result { - let fields = match input.data { - syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f, - _ => return Err(Error::new( - input.span(), - "`confique::Config` can only be derive for structs with named fields", - )), - }; - - let doc = extract_doc(&mut input.attrs); - let fields = fields.named.into_iter() - .map(Field::from_ast) - .collect::, _>>()?; - - - Ok(Self { - doc, - visibility: input.vis, - name: input.ident, - fields, - }) - } -} - -impl Field { - fn from_ast(mut field: syn::Field) -> Result { - let doc = extract_doc(&mut field.attrs); - let attrs = extract_internal_attrs(&mut field.attrs)?; - - // TODO: check no other attributes are here - let kind = if attrs.nested { - if is_option(&field.ty) { - return Err(Error::new( - field.ident.span(), - "nested configurations cannot be optional (type `Option<_>`)", - )); - } - if attrs.default.is_some() { - return Err(Error::new( - field.ident.span(), - "cannot specify `nested` and `default` attributes at the same time", - )); - } - if attrs.env.is_some() { - return Err(Error::new( - field.ident.span(), - "cannot specify `nested` and `env` attributes at the same time", - )); - } - - FieldKind::Nested { ty: field.ty } - } else { - match unwrap_option(&field.ty) { - None => FieldKind::Leaf { - env: attrs.env, - kind: LeafKind::Required { - default: attrs.default, - ty: field.ty, - }, - }, - Some(inner) => { - if attrs.default.is_some() { - return Err(Error::new( - field.ident.span(), - "optional fields (type `Option<_>`) cannot have default \ - values (`#[config(default = ...)]`)", - )); - } - - FieldKind::Leaf { - env: attrs.env, - kind: LeafKind::Optional { - inner_ty: inner.clone(), - }, - } - } - } - }; - - Ok(Self { - doc, - name: field.ident.expect("bug: expected named field"), - kind, - }) - } - - pub(crate) fn is_leaf(&self) -> bool { - matches!(self.kind, FieldKind::Leaf { .. }) - } -} - -impl Expr { - fn from_lit(lit: syn::Lit) -> Result { - match lit { - syn::Lit::Str(l) => Ok(Self::Str(l)), - syn::Lit::Int(l) => Ok(Self::Int(l)), - syn::Lit::Float(l) => Ok(Self::Float(l)), - syn::Lit::Bool(l) => Ok(Self::Bool(l)), - - _ => { - let msg = "only string, integer, float and bool literals are allowed here"; - Err(Error::new(lit.span(), msg)) - } - } - } -} - -/// Extracts all doc string attributes from the list and return them as list of -/// strings (in order). -fn extract_doc(attrs: &mut Vec) -> Vec { - extract_attrs(attrs, |attr| { - match attr.parse_meta().ok()? { - syn::Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Str(s), - path, - .. - }) if path.is_ident("doc") => Some(s.value()), - _ => None, - } - }) -} - -fn extract_attrs(attrs: &mut Vec, mut pred: P) -> Vec -where - P: FnMut(&syn::Attribute) -> Option, -{ - // TODO: use `Vec::drain_filter` once stabilized. The current impl is O(n²). - let mut i = 0; - let mut out = Vec::new(); - while i < attrs.len() { - match pred(&attrs[i]) { - Some(v) => { - out.push(v); - attrs.remove(i); - } - None => i += 1, - } - } - - out -} - -fn extract_internal_attrs( - attrs: &mut Vec, -) -> Result { - let internal_attrs = extract_attrs(attrs, |attr| { - if attr.path.is_ident("config") { - // TODO: clone not necessary once we use drain_filter - Some(attr.clone()) - } else { - None - } - }); - - - let mut out = InternalAttrs::default(); - for attr in internal_attrs { - type AttrList = syn::punctuated::Punctuated; - let parsed_list = attr.parse_args_with(AttrList::parse_terminated)?; - - for parsed in parsed_list { - let keyword = parsed.keyword(); - - macro_rules! duplicate_if { - ($cond:expr) => { - if $cond { - let msg = format!("duplicate '{}' confique attribute", keyword); - return Err(Error::new(attr.tokens.span(), msg)); - } - }; - } - - match parsed { - InternalAttr::Default(expr) => { - duplicate_if!(out.default.is_some()); - out.default = Some(expr); - } - InternalAttr::Nested => { - duplicate_if!(out.nested); - out.nested = true; - } - InternalAttr::Env(key) => { - duplicate_if!(out.env.is_some()); - out.env = Some(key); - } - } - } - } - - Ok(out) -} - -#[derive(Default)] -struct InternalAttrs { - nested: bool, - default: Option, - env: Option -} - -enum InternalAttr { - Nested, - Default(Expr), - Env(String), -} - -impl InternalAttr { - fn keyword(&self) -> &'static str { - match self { - Self::Nested => "nested", - Self::Default(_) => "default", - Self::Env(_) => "env", - } - } -} - -impl Parse for InternalAttr { - fn parse(input: ParseStream) -> Result { - let ident: syn::Ident = input.parse()?; - match &*ident.to_string() { - "nested" => { - assert_empty_or_comma(input)?; - Ok(Self::Nested) - } - - "default" => { - let _: Token![=] = input.parse()?; - let expr = Expr::from_lit(input.parse()?)?; - assert_empty_or_comma(input)?; - Ok(Self::Default(expr)) - } - - "env" => { - let _: Token![=] = input.parse()?; - let key: syn::LitStr = input.parse()?; - assert_empty_or_comma(input)?; - let value = key.value(); - if value.contains('=') || value.contains('\0') { - Err(syn::Error::new( - key.span(), - "environment variable key must not contain '=' or null bytes", - )) - } else { - Ok(Self::Env(value)) - } - } - - _ => Err(syn::Error::new(ident.span(), "unknown confique attribute")), - } - } -} - -fn assert_empty_or_comma(input: ParseStream) -> Result<(), Error> { - if input.is_empty() || input.peek(Token![,]) { - Ok(()) - } else { - Err(Error::new(input.span(), "unexpected tokens, expected no more tokens in this context")) - } -} diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 51eaba7..bc1c79f 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -3,14 +3,19 @@ use proc_macro::TokenStream as TokenStream1; mod gen; mod ir; +mod parse; mod util; #[proc_macro_derive(Config, attributes(config))] pub fn config(input: TokenStream1) -> TokenStream1 { - syn::parse2::(input.into()) - .and_then(ir::Input::from_ast) + let input = match syn::parse2::(input.into()) { + Err(e) => return e.to_compile_error().into(), + Ok(i) => i, + }; + + ir::Input::from_ast(input) .map(gen::gen) - .unwrap_or_else(|e| e.to_compile_error()) + .unwrap_or_else(|e| e.write_errors()) .into() } diff --git a/macro/src/parse.rs b/macro/src/parse.rs new file mode 100644 index 0000000..b85906e --- /dev/null +++ b/macro/src/parse.rs @@ -0,0 +1,187 @@ +use darling::Error; +use syn::spanned::Spanned; + +use crate::{ir, util::{is_option, unwrap_option}}; + + +macro_rules! bail { + ($span:expr, $msg:expr $(,)?) => { + return Err(Error::custom($msg).with_span(&$span)) + }; +} + +impl ir::Input { + pub(crate) fn from_ast(mut input: syn::DeriveInput) -> Result { + let struct_fields = match input.data { + syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(f), .. }) => f, + _ => bail!( + input.span(), + "`confique::Config` can only be derive for structs with named fields", + ), + }; + + let doc = extract_doc(&mut input.attrs); + + let mut errors = Vec::new(); + let mut fields = Vec::new(); + for field in struct_fields.named { + match ir::Field::from_ast(field) { + Ok(f) => fields.push(f), + Err(e) => errors.push(e), + } + } + + if !errors.is_empty() { + return Err(Error::multiple(errors)); + } + + Ok(Self { + doc, + visibility: input.vis, + name: input.ident, + fields, + }) + } +} + +impl ir::Field { + fn from_ast(field: syn::Field) -> Result { + use darling::FromField; + let mut field = Field::from_field(&field)?; + let doc = extract_doc(&mut field.attrs); + + let kind = if field.nested { + // Nested field. + + if is_option(&field.ty) { + bail!( + field.ident.span(), + "nested configurations cannot be optional (type `Option<_>`)", + ); + } + if field.default.is_some() { + bail!( + field.ident.span(), + "cannot specify `nested` and `default` attributes at the same time", + ); + } + if field.env.is_some() { + bail!( + field.ident.span(), + "cannot specify `nested` and `env` attributes at the same time", + ); + } + + ir::FieldKind::Nested { ty: field.ty } + } else { + // Leaf field. + + match unwrap_option(&field.ty) { + None => ir::FieldKind::Leaf { + env: field.env, + kind: ir::LeafKind::Required { + default: field.default, + ty: field.ty, + }, + }, + Some(inner) => { + if field.default.is_some() { + bail!( + field.ident.span(), + "optional fields (type `Option<_>`) cannot have default \ + values (`#[config(default = ...)]`)", + ); + } + + ir::FieldKind::Leaf { + env: field.env, + kind: ir::LeafKind::Optional { + inner_ty: inner.clone(), + }, + } + } + } + }; + + Ok(Self { + doc, + name: field.ident.expect("bug: expected named field"), + kind, + }) + } + + pub(crate) fn is_leaf(&self) -> bool { + matches!(self.kind, ir::FieldKind::Leaf { .. }) + } +} + +impl darling::FromMeta for ir::Expr { + fn from_value(lit: &syn::Lit) -> Result { + match lit { + syn::Lit::Str(l) => Ok(Self::Str(l.clone())), + syn::Lit::Int(l) => Ok(Self::Int(l.clone())), + syn::Lit::Float(l) => Ok(Self::Float(l.clone())), + syn::Lit::Bool(l) => Ok(Self::Bool(l.clone())), + + _ => { + // let msg = "only string, integer, float and bool literals are allowed here"; + // Err(Error::new(lit.span(), msg)) + Err(darling::Error::unexpected_lit_type(lit)) + } + } + } +} + + + +#[derive(Debug, darling::FromField)] +#[darling(attributes(config), forward_attrs(doc))] +struct Field { + ident: Option, + ty: syn::Type, + attrs: Vec, + + #[darling(default)] + nested: bool, + + #[darling(default)] + env: Option, + + #[darling(default)] + default: Option, +} + +/// Extracts all doc string attributes from the list and return them as list of +/// strings (in order). +fn extract_doc(attrs: &mut Vec) -> Vec { + extract_attrs(attrs, |attr| { + match attr.parse_meta().ok()? { + syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(s), + path, + .. + }) if path.is_ident("doc") => Some(s.value()), + _ => None, + } + }) +} + +fn extract_attrs(attrs: &mut Vec, mut pred: P) -> Vec +where + P: FnMut(&syn::Attribute) -> Option, +{ + // TODO: use `Vec::drain_filter` once stabilized. The current impl is O(n²). + let mut i = 0; + let mut out = Vec::new(); + while i < attrs.len() { + match pred(&attrs[i]) { + Some(v) => { + out.push(v); + attrs.remove(i); + } + None => i += 1, + } + } + + out +}