Repository: TedDriggs/darling (rust)

Issue:
#[darling(with = "func")] string paths no longer work
In previous versions of darling (e.g. 0.20.10 and earlier), the following syntax was supported:
```
#[darling(with = "maybe_float_or_int")]
```
This worked by allowing string literals to represent function paths, which were parsed and treated as equivalent to:
```
#[darling(with = maybe_float_or_int)]
```
However, in darling 0.20.11, this syntax now fails with a type mismatch error:
```
> cargo c
    Checking playground v0.1.0 (/home/suika/code/suika/playground)
error: Unexpected type `lit`
 --> src/darling.rs:7:47
  |
7 |     #[darling(default, rename = "min", with = "maybe_float_or_int")]
  |                                               ^^^^^^^^^^^^^^^^^^^^

error: could not compile `playground` (bin "playground") due to 1 previous error
```

In PR [#310](https://github.com/TedDriggs/darling/pull/310), support for closures in #[darling(with = ...)] was added, but as part of the change, the with attribute was converted from syn::Path to syn::Expr. As a result, the string literal form is now interpreted as a literal string (Expr::Lit(LitStr)), which breaks existing uses.


Pull request:
fix support string literal paths in #[darling(with = ...)]
fix #338 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d6150a..dcaa7a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Unreleased
+
+-  Potentially breaking: Emit error when an attribute path is present in both `attributes` and `forward_attrs`. [#336](https://github.com/TedDriggs/darling/issues/336)
+
 ## v0.20.11 (March 28, 2025)
 
 -   Support `#[darling(with = ...)]` on the `data` field when deriving `FromDeriveInput`. This allows the use of simpler receiver types, such as a `Vec` of enum variants.
diff --git a/core/src/codegen/field.rs b/core/src/codegen/field.rs
index 3ada543..2c11cd8 100644
--- a/core/src/codegen/field.rs
+++ b/core/src/codegen/field.rs
@@ -183,7 +183,7 @@ impl ToTokens for MatchArm<'_> {
         // even-more-specific span, our attempt here will not overwrite that and will only cost
         // us one `if` check.
         let extractor = quote_spanned!(with_callable.span()=>
-        ::darling::export::identity::<fn(&::syn::Meta) -> ::darling::Result<_>>(#with_callable)(__inner)
+        ::darling::export::identity::<fn(&::darling::export::syn::Meta) -> ::darling::Result<_>>(#with_callable)(__inner)
             #post_transform
             .map_err(|e| e.with_span(&__inner).at(#location))
         );
diff --git a/core/src/options/outer_from.rs b/core/src/options/outer_from.rs
index 5a02a1a..bcdf65c 100644
--- a/core/src/options/outer_from.rs
+++ b/core/src/options/outer_from.rs
@@ -1,3 +1,4 @@
+use quote::ToTokens;
 use syn::spanned::Spanned;
 use syn::{Field, Ident, Meta};
 
@@ -107,5 +108,17 @@ impl ParseData for OuterFrom {
                 );
             }
         }
+
+        if let Some(ForwardAttrsFilter::Only(fwd)) = &self.forward_attrs {
+            for path in fwd.intersection(&self.attr_names) {
+                errors.push(
+                    Error::custom(format!(
+                        "attribute path `{}` will not be forwarded because it is also listed in `attributes`",
+                        path.to_token_stream()
+                    ))
+                    .with_span(path),
+                );
+            }
+        };
     }
 }
diff --git a/core/src/util/callable.rs b/core/src/util/callable.rs
index c726679..50c4101 100644
--- a/core/src/util/callable.rs
+++ b/core/src/util/callable.rs
@@ -24,6 +24,16 @@ impl AsRef<syn::Expr> for Callable {
     }
 }
 
+impl From<syn::Path> for Callable {
+    fn from(path: syn::Path) -> Self {
+        Self::from(syn::ExprPath {
+            attrs: vec![],
+            qself: None,
+            path,
+        })
+    }
+}
+
 impl From<syn::ExprPath> for Callable {
     fn from(value: syn::ExprPath) -> Self {
         Self {
@@ -50,6 +60,15 @@ impl FromMeta for Callable {
     fn from_expr(expr: &syn::Expr) -> Result<Self> {
         match expr {
             syn::Expr::Path(_) | syn::Expr::Closure(_) => Ok(Self { call: expr.clone() }),
+            syn::Expr::Lit(syn::ExprLit {
+                lit: syn::Lit::Str(s),
+                ..
+            }) => syn::parse_str::<syn::Path>(&s.value())
+                .map_err(|e| {
+                    Error::custom(format!("`with` must be a path if it's a string: {}", e))
+                        .with_span(s)
+                })
+                .map(Self::from),
             _ => Err(Error::unexpected_expr_type(expr)),
         }
     }
diff --git a/core/src/util/path_list.rs b/core/src/util/path_list.rs
index bea25fd..0dee093 100644
--- a/core/src/util/path_list.rs
+++ b/core/src/util/path_list.rs
@@ -31,6 +31,20 @@ impl PathList {
     pub fn to_strings(&self) -> Vec<String> {
         self.0.iter().map(path_to_string).collect()
     }
+
+    /// Visits the values representing the intersection, i.e., the values that are both in `self` and `other`.
+    ///
+    /// Values will be returned in the order they appear in `self`.
+    ///
+    /// Values that are present multiple times in `self` and present at least once in `other` will be returned multiple times.
+    ///
+    /// # Performance
+    /// This function runs in `O(n * m)` time.
+    /// It is believed that path lists are usually short enough that this is better than allocating a set containing the values
+    /// of `other` or adding a conditional solution.
+    pub fn intersection<'a>(&'a self, other: &'a PathList) -> impl Iterator<Item = &'a Path> {
+        self.0.iter().filter(|path| other.0.contains(path))
+    }
 }
 
 impl Deref for PathList {
@@ -102,4 +116,14 @@ mod tests {
         let err = input.unwrap_err();
         assert!(err.has_span());
     }
+
+    #[test]
+    fn intersection() {
+        let left = fm::<PathList>(quote!(ignore(Debug, Clone, Eq)));
+        let right = fm::<PathList>(quote!(ignore(Clone, Eq, Clone)));
+        assert_eq!(
+            left.intersection(&right).cloned().collect::<Vec<_>>(),
+            vec![parse_quote!(Clone), parse_quote!(Eq)],
+        );
+    }
 }


Test patch:
diff --git a/tests/compile-fail/attr_forwarded_and_parsed.rs b/tests/compile-fail/attr_forwarded_and_parsed.rs
new file mode 100644
index 0000000..a118a36
--- /dev/null
+++ b/tests/compile-fail/attr_forwarded_and_parsed.rs
@@ -0,0 +1,13 @@
+#[derive(darling::FromDeriveInput)]
+#[darling(attributes(example), forward_attrs(example))]
+pub struct Example {
+    ignored: String,
+}
+
+#[derive(darling::FromField)]
+#[darling(attributes(example), forward_attrs(example))]
+pub struct ExampleField {
+    ignored: String,
+}
+
+fn main() {}
diff --git a/tests/compile-fail/attr_forwarded_and_parsed.stderr b/tests/compile-fail/attr_forwarded_and_parsed.stderr
new file mode 100644
index 0000000..c958442
--- /dev/null
+++ b/tests/compile-fail/attr_forwarded_and_parsed.stderr
@@ -0,0 +1,11 @@
+error: attribute path `example` will not be forwarded because it is also listed in `attributes`
+ --> tests/compile-fail/attr_forwarded_and_parsed.rs:2:46
+  |
+2 | #[darling(attributes(example), forward_attrs(example))]
+  |                                              ^^^^^^^
+
+error: attribute path `example` will not be forwarded because it is also listed in `attributes`
+ --> tests/compile-fail/attr_forwarded_and_parsed.rs:8:46
+  |
+8 | #[darling(attributes(example), forward_attrs(example))]
+  |                                              ^^^^^^^
diff --git a/tests/compile-fail/with_closure_capture.stderr b/tests/compile-fail/with_closure_capture.stderr
index fc36752..47ba732 100644
--- a/tests/compile-fail/with_closure_capture.stderr
+++ b/tests/compile-fail/with_closure_capture.stderr
@@ -10,7 +10,7 @@ error[E0308]: mismatched types
 15 | |         ),
    | |_________^ expected fn pointer, found closure
    |
-   = note: expected fn pointer `for<'a> fn(&'a syn::Meta) -> Result<String, darling::Error>`
+   = note: expected fn pointer `for<'a> fn(&'a syn::attr::Meta) -> Result<String, darling::Error>`
                  found closure `{closure@$DIR/tests/compile-fail/with_closure_capture.rs:12:16: 12:19}`
 note: closures can only be coerced to `fn` types if they do not capture any variables
   --> tests/compile-fail/with_closure_capture.rs:14:15
diff --git a/tests/meta_with.rs b/tests/meta_with.rs
index d4f2821..812568e 100644
--- a/tests/meta_with.rs
+++ b/tests/meta_with.rs
@@ -17,12 +17,28 @@ pub struct Receiver {
     // inferred from usage within the closure.
     #[darling(with = |m| Ok(m.path().clone()))]
     example3: syn::Path,
+    #[darling(with = "example4_parser")]
+    example4: String,
+}
+
+// the custom parser function referred to by string
+fn example4_parser(meta: &syn::Meta) -> darling::Result<String> {
+    match meta {
+        syn::Meta::NameValue(nv) => match &nv.value {
+            syn::Expr::Lit(syn::ExprLit {
+                lit: syn::Lit::Str(s),
+                ..
+            }) => Ok(s.value()),
+            other => Err(darling::Error::unexpected_expr_type(other)),
+        },
+        _ => Err(darling::Error::unexpected_type("name-value")),
+    }
 }
 
 #[test]
 fn handles_all_cases() {
     let input = Receiver::from_derive_input(&parse_quote! {
-        #[demo(example1 = test::path, example2 = "hello", example3)]
+        #[demo(example1 = test::path, example2 = "hello", example3, example4 = "world")]
         struct Example;
     })
     .unwrap();
@@ -30,4 +46,5 @@ fn handles_all_cases() {
     assert_eq!(input.example1, Some(parse_quote!(test::path)));
     assert_eq!(input.example2, Some("HELLO".to_string()));
     assert_eq!(input.example3, parse_quote!(example3));
+    assert_eq!(input.example4, "world".to_string());
 }