Repository: dtolnay/syn
Pull request #1542 (2023-12-11 01:55:36)

Support interpolated $:path followed by macro call or struct literal
Example of an input that previously behaved incorrectly:

```rust
let nested = Group::new(Delimiter::None, quote!(a::b || true));
let tokens = quote!(if #nested && false {});
syn::parse2::<syn::Expr>(tokens).unwrap()
```

**Before:** this has the wrong precedence. `a::b || (true && false)`

```rust
Expr::If {
    cond: Expr::Binary {
        left: Expr::Path {
            path: Path {
                segments: [
                    PathSegment {
                        ident: "a",
                    },
                    PathSegment {
                        ident: "b",
                    },
                ],
            },
        },
        op: BinOp::Or,
        right: Expr::Binary {
            left: Expr::Lit {
                lit: Lit::Bool {
                    value: true,
                },
            },
            op: BinOp::And,
            right: Expr::Lit {
                lit: Lit::Bool {
                    value: false,
                },
            },
        },
    },
    then_branch: Block {
        stmts: [],
    },
}
```

**After:** correct precedence. `(a::b || true) && false`

```rust
Expr::If {
    cond: Expr::Binary {
        left: Expr::Group {
            expr: Expr::Binary {
                left: Expr::Path {
                    path: Path {
                        segments: [
                            PathSegment {
                                ident: "a",
                            },
                            PathSegment {
                                ident: "b",
                            },
                        ],
                    },
                },
                op: BinOp::Or,
                right: Expr::Lit {
                    lit: Lit::Bool {
                        value: true,
                    },
                },
            },
        },
        op: BinOp::And,
        right: Expr::Lit {
            lit: Lit::Bool {
                value: false,
            },
        },
    },
    then_branch: Block {
        stmts: [],
    },
}
```



Patch:
diff --git a/src/expr.rs b/src/expr.rs
index 59c0a086..f3e37d49 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -1577,12 +1577,8 @@ pub(crate) mod parsing {
     // interactions, as they are fully contained.
     #[cfg(feature = "full")]
     fn atom_expr(input: ParseStream, allow_struct: AllowStruct) -> Result<Expr> {
-        if input.peek(token::Group)
-            && !input.peek2(Token![::])
-            && !input.peek2(Token![!])
-            && !input.peek2(token::Brace)
-        {
-            input.call(expr_group).map(Expr::Group)
+        if input.peek(token::Group) {
+            expr_group(input, allow_struct)
         } else if input.peek(Lit) {
             input.parse().map(Expr::Lit)
         } else if input.peek(Token![async])
@@ -1680,12 +1676,8 @@ pub(crate) mod parsing {
 
     #[cfg(not(feature = "full"))]
     fn atom_expr(input: ParseStream) -> Result<Expr> {
-        if input.peek(token::Group)
-            && !input.peek2(Token![::])
-            && !input.peek2(Token![!])
-            && !input.peek2(token::Brace)
-        {
-            input.call(expr_group).map(Expr::Group)
+        if input.peek(token::Group) {
+            expr_group(input)
         } else if input.peek(Lit) {
             input.parse().map(Expr::Lit)
         } else if input.peek(token::Paren) {
@@ -1736,7 +1728,21 @@ pub(crate) mod parsing {
         #[cfg(feature = "full")] allow_struct: AllowStruct,
     ) -> Result<Expr> {
         let (qself, path) = path::parsing::qpath(input, true)?;
+        rest_of_path_or_macro_or_struct(
+            qself,
+            path,
+            input,
+            #[cfg(feature = "full")]
+            allow_struct,
+        )
+    }
 
+    fn rest_of_path_or_macro_or_struct(
+        qself: Option<QSelf>,
+        path: Path,
+        input: ParseStream,
+        #[cfg(feature = "full")] allow_struct: AllowStruct,
+    ) -> Result<Expr> {
         if qself.is_none()
             && input.peek(Token![!])
             && !input.peek(Token![!=])
@@ -1962,13 +1968,38 @@ pub(crate) mod parsing {
         }
     }
 
-    fn expr_group(input: ParseStream) -> Result<ExprGroup> {
+    fn expr_group(
+        input: ParseStream,
+        #[cfg(feature = "full")] allow_struct: AllowStruct,
+    ) -> Result<Expr> {
         let group = crate::group::parse_group(input)?;
-        Ok(ExprGroup {
+        let mut inner: Expr = group.content.parse()?;
+
+        match inner {
+            Expr::Path(mut expr) if expr.attrs.is_empty() => {
+                let grouped_len = expr.path.segments.len();
+                Path::parse_rest(input, &mut expr.path, true)?;
+                match rest_of_path_or_macro_or_struct(
+                    expr.qself,
+                    expr.path,
+                    input,
+                    #[cfg(feature = "full")]
+                    allow_struct,
+                )? {
+                    Expr::Path(expr) if expr.path.segments.len() == grouped_len => {
+                        inner = Expr::Path(expr);
+                    }
+                    extended => return Ok(extended),
+                }
+            }
+            _ => {}
+        }
+
+        Ok(Expr::Group(ExprGroup {
             attrs: Vec::new(),
             group_token: group.token,
-            expr: group.content.parse()?,
-        })
+            expr: Box::new(inner),
+        }))
     }
 
     #[cfg(feature = "full")]


Test patch:
diff --git a/tests/test_expr.rs b/tests/test_expr.rs
index 2574ea54..9d0b44f9 100644
--- a/tests/test_expr.rs
+++ b/tests/test_expr.rs
@@ -339,3 +339,106 @@ fn test_ambiguous_label() {
         syn::parse2::<Stmt>(stmt).unwrap_err();
     }
 }
+
+#[test]
+fn test_extended_interpolated_path() {
+    let path = Group::new(Delimiter::None, quote!(a::b));
+
+    let tokens = quote!(if #path {});
+    snapshot!(tokens as Expr, @r###"
+    Expr::If {
+        cond: Expr::Group {
+            expr: Expr::Path {
+                path: Path {
+                    segments: [
+                        PathSegment {
+                            ident: "a",
+                        },
+                        PathSegment {
+                            ident: "b",
+                        },
+                    ],
+                },
+            },
+        },
+        then_branch: Block {
+            stmts: [],
+        },
+    }
+    "###);
+
+    let tokens = quote!(#path {});
+    snapshot!(tokens as Expr, @r###"
+    Expr::Struct {
+        path: Path {
+            segments: [
+                PathSegment {
+                    ident: "a",
+                },
+                PathSegment {
+                    ident: "b",
+                },
+            ],
+        },
+    }
+    "###);
+
+    let tokens = quote!(#path :: c);
+    snapshot!(tokens as Expr, @r###"
+    Expr::Path {
+        path: Path {
+            segments: [
+                PathSegment {
+                    ident: "a",
+                },
+                PathSegment {
+                    ident: "b",
+                },
+                PathSegment {
+                    ident: "c",
+                },
+            ],
+        },
+    }
+    "###);
+
+    let nested = Group::new(Delimiter::None, quote!(a::b || true));
+    let tokens = quote!(if #nested && false {});
+    snapshot!(tokens as Expr, @r###"
+    Expr::If {
+        cond: Expr::Binary {
+            left: Expr::Group {
+                expr: Expr::Binary {
+                    left: Expr::Path {
+                        path: Path {
+                            segments: [
+                                PathSegment {
+                                    ident: "a",
+                                },
+                                PathSegment {
+                                    ident: "b",
+                                },
+                            ],
+                        },
+                    },
+                    op: BinOp::Or,
+                    right: Expr::Lit {
+                        lit: Lit::Bool {
+                            value: true,
+                        },
+                    },
+                },
+            },
+            op: BinOp::And,
+            right: Expr::Lit {
+                lit: Lit::Bool {
+                    value: false,
+                },
+            },
+        },
+        then_branch: Block {
+            stmts: [],
+        },
+    }
+    "###);
+}