summaryrefslogtreecommitdiff
path: root/vendor/syn/tests/test_expr.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/syn/tests/test_expr.rs')
-rw-r--r--vendor/syn/tests/test_expr.rs1699
1 files changed, 1699 insertions, 0 deletions
diff --git a/vendor/syn/tests/test_expr.rs b/vendor/syn/tests/test_expr.rs
new file mode 100644
index 00000000..e23d0bd9
--- /dev/null
+++ b/vendor/syn/tests/test_expr.rs
@@ -0,0 +1,1699 @@
+#![cfg(not(miri))]
+#![recursion_limit = "1024"]
+#![feature(rustc_private)]
+#![allow(
+ clippy::elidable_lifetime_names,
+ clippy::match_like_matches_macro,
+ clippy::needless_lifetimes,
+ clippy::single_element_loop,
+ clippy::too_many_lines,
+ clippy::uninlined_format_args,
+ clippy::unreadable_literal
+)]
+
+#[macro_use]
+mod macros;
+
+mod common;
+
+use crate::common::visit::{AsIfPrinted, FlattenParens};
+use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream};
+use quote::{quote, ToTokens as _};
+use std::process::ExitCode;
+use syn::punctuated::Punctuated;
+use syn::visit_mut::VisitMut as _;
+use syn::{
+ parse_quote, token, AngleBracketedGenericArguments, Arm, BinOp, Block, Expr, ExprArray,
+ ExprAssign, ExprAsync, ExprAwait, ExprBinary, ExprBlock, ExprBreak, ExprCall, ExprCast,
+ ExprClosure, ExprConst, ExprContinue, ExprField, ExprForLoop, ExprIf, ExprIndex, ExprLet,
+ ExprLit, ExprLoop, ExprMacro, ExprMatch, ExprMethodCall, ExprPath, ExprRange, ExprRawAddr,
+ ExprReference, ExprReturn, ExprStruct, ExprTry, ExprTryBlock, ExprTuple, ExprUnary, ExprUnsafe,
+ ExprWhile, ExprYield, GenericArgument, Label, Lifetime, Lit, LitInt, Macro, MacroDelimiter,
+ Member, Pat, PatWild, Path, PathArguments, PathSegment, PointerMutability, QSelf, RangeLimits,
+ ReturnType, Stmt, Token, Type, TypePath, UnOp,
+};
+
+#[test]
+fn test_expr_parse() {
+ let tokens = quote!(..100u32);
+ snapshot!(tokens as Expr, @r#"
+ Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Lit {
+ lit: 100u32,
+ }),
+ }
+ "#);
+
+ let tokens = quote!(..100u32);
+ snapshot!(tokens as ExprRange, @r#"
+ ExprRange {
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Lit {
+ lit: 100u32,
+ }),
+ }
+ "#);
+}
+
+#[test]
+fn test_await() {
+ // Must not parse as Expr::Field.
+ let tokens = quote!(fut.await);
+
+ snapshot!(tokens as Expr, @r#"
+ Expr::Await {
+ base: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "fut",
+ },
+ ],
+ },
+ },
+ }
+ "#);
+}
+
+#[rustfmt::skip]
+#[test]
+fn test_tuple_multi_index() {
+ let expected = snapshot!("tuple.0.0" as Expr, @r#"
+ Expr::Field {
+ base: Expr::Field {
+ base: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "tuple",
+ },
+ ],
+ },
+ },
+ member: Member::Unnamed(Index {
+ index: 0,
+ }),
+ },
+ member: Member::Unnamed(Index {
+ index: 0,
+ }),
+ }
+ "#);
+
+ for &input in &[
+ "tuple .0.0",
+ "tuple. 0.0",
+ "tuple.0 .0",
+ "tuple.0. 0",
+ "tuple . 0 . 0",
+ ] {
+ assert_eq!(expected, syn::parse_str(input).unwrap());
+ }
+
+ for tokens in [
+ quote!(tuple.0.0),
+ quote!(tuple .0.0),
+ quote!(tuple. 0.0),
+ quote!(tuple.0 .0),
+ quote!(tuple.0. 0),
+ quote!(tuple . 0 . 0),
+ ] {
+ assert_eq!(expected, syn::parse2(tokens).unwrap());
+ }
+}
+
+#[test]
+fn test_macro_variable_func() {
+ // mimics the token stream corresponding to `$fn()`
+ let path = Group::new(Delimiter::None, quote!(f));
+ let tokens = quote!(#path());
+
+ snapshot!(tokens as Expr, @r#"
+ Expr::Call {
+ func: Expr::Group {
+ expr: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "f",
+ },
+ ],
+ },
+ },
+ },
+ }
+ "#);
+
+ let path = Group::new(Delimiter::None, quote! { #[inside] f });
+ let tokens = quote!(#[outside] #path());
+
+ snapshot!(tokens as Expr, @r#"
+ Expr::Call {
+ attrs: [
+ Attribute {
+ style: AttrStyle::Outer,
+ meta: Meta::Path {
+ segments: [
+ PathSegment {
+ ident: "outside",
+ },
+ ],
+ },
+ },
+ ],
+ func: Expr::Group {
+ expr: Expr::Path {
+ attrs: [
+ Attribute {
+ style: AttrStyle::Outer,
+ meta: Meta::Path {
+ segments: [
+ PathSegment {
+ ident: "inside",
+ },
+ ],
+ },
+ },
+ ],
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "f",
+ },
+ ],
+ },
+ },
+ },
+ }
+ "#);
+}
+
+#[test]
+fn test_macro_variable_macro() {
+ // mimics the token stream corresponding to `$macro!()`
+ let mac = Group::new(Delimiter::None, quote!(m));
+ let tokens = quote!(#mac!());
+
+ snapshot!(tokens as Expr, @r#"
+ Expr::Macro {
+ mac: Macro {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "m",
+ },
+ ],
+ },
+ delimiter: MacroDelimiter::Paren,
+ tokens: TokenStream(``),
+ },
+ }
+ "#);
+}
+
+#[test]
+fn test_macro_variable_struct() {
+ // mimics the token stream corresponding to `$struct {}`
+ let s = Group::new(Delimiter::None, quote! { S });
+ let tokens = quote!(#s {});
+
+ snapshot!(tokens as Expr, @r#"
+ Expr::Struct {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "S",
+ },
+ ],
+ },
+ }
+ "#);
+}
+
+#[test]
+fn test_macro_variable_unary() {
+ // mimics the token stream corresponding to `$expr.method()` where expr is `&self`
+ let inner = Group::new(Delimiter::None, quote!(&self));
+ let tokens = quote!(#inner.method());
+ snapshot!(tokens as Expr, @r#"
+ Expr::MethodCall {
+ receiver: Expr::Group {
+ expr: Expr::Reference {
+ expr: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "self",
+ },
+ ],
+ },
+ },
+ },
+ },
+ method: "method",
+ }
+ "#);
+}
+
+#[test]
+fn test_macro_variable_match_arm() {
+ // mimics the token stream corresponding to `match v { _ => $expr }`
+ let expr = Group::new(Delimiter::None, quote! { #[a] () });
+ let tokens = quote!(match v { _ => #expr });
+ snapshot!(tokens as Expr, @r#"
+ Expr::Match {
+ expr: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "v",
+ },
+ ],
+ },
+ },
+ arms: [
+ Arm {
+ pat: Pat::Wild,
+ body: Expr::Group {
+ expr: Expr::Tuple {
+ attrs: [
+ Attribute {
+ style: AttrStyle::Outer,
+ meta: Meta::Path {
+ segments: [
+ PathSegment {
+ ident: "a",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ }
+ "#);
+
+ let expr = Group::new(Delimiter::None, quote!(loop {} + 1));
+ let tokens = quote!(match v { _ => #expr });
+ snapshot!(tokens as Expr, @r#"
+ Expr::Match {
+ expr: Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "v",
+ },
+ ],
+ },
+ },
+ arms: [
+ Arm {
+ pat: Pat::Wild,
+ body: Expr::Group {
+ expr: Expr::Binary {
+ left: Expr::Loop {
+ body: Block {
+ stmts: [],
+ },
+ },
+ op: BinOp::Add,
+ right: Expr::Lit {
+ lit: 1,
+ },
+ },
+ },
+ },
+ ],
+ }
+ "#);
+}
+
+// https://github.com/dtolnay/syn/issues/1019
+#[test]
+fn test_closure_vs_rangefull() {
+ #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/4808
+ let tokens = quote!(|| .. .method());
+ snapshot!(tokens as Expr, @r#"
+ Expr::MethodCall {
+ receiver: Expr::Closure {
+ output: ReturnType::Default,
+ body: Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ },
+ },
+ method: "method",
+ }
+ "#);
+}
+
+#[test]
+fn test_postfix_operator_after_cast() {
+ syn::parse_str::<Expr>("|| &x as T[0]").unwrap_err();
+ syn::parse_str::<Expr>("|| () as ()()").unwrap_err();
+}
+
+#[test]
+fn test_range_kinds() {
+ syn::parse_str::<Expr>("..").unwrap();
+ syn::parse_str::<Expr>("..hi").unwrap();
+ syn::parse_str::<Expr>("lo..").unwrap();
+ syn::parse_str::<Expr>("lo..hi").unwrap();
+
+ syn::parse_str::<Expr>("..=").unwrap_err();
+ syn::parse_str::<Expr>("..=hi").unwrap();
+ syn::parse_str::<Expr>("lo..=").unwrap_err();
+ syn::parse_str::<Expr>("lo..=hi").unwrap();
+
+ syn::parse_str::<Expr>("...").unwrap_err();
+ syn::parse_str::<Expr>("...hi").unwrap_err();
+ syn::parse_str::<Expr>("lo...").unwrap_err();
+ syn::parse_str::<Expr>("lo...hi").unwrap_err();
+}
+
+#[test]
+fn test_range_precedence() {
+ snapshot!(".. .." as Expr, @r#"
+ Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ }
+ "#);
+
+ snapshot!(".. .. ()" as Expr, @r#"
+ Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Tuple),
+ }),
+ }
+ "#);
+
+ snapshot!("() .. .." as Expr, @r#"
+ Expr::Range {
+ start: Some(Expr::Tuple),
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ }
+ "#);
+
+ snapshot!("() = .. + ()" as Expr, @r"
+ Expr::Binary {
+ left: Expr::Assign {
+ left: Expr::Tuple,
+ right: Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ },
+ },
+ op: BinOp::Add,
+ right: Expr::Tuple,
+ }
+ ");
+
+ // A range with a lower bound cannot be the upper bound of another range,
+ // and a range with an upper bound cannot be the lower bound of another
+ // range.
+ syn::parse_str::<Expr>(".. x ..").unwrap_err();
+ syn::parse_str::<Expr>("x .. x ..").unwrap_err();
+}
+
+#[test]
+fn test_range_attrs() {
+ // Attributes are not allowed on range expressions starting with `..`
+ syn::parse_str::<Expr>("#[allow()] ..").unwrap_err();
+ syn::parse_str::<Expr>("#[allow()] .. hi").unwrap_err();
+
+ snapshot!("#[allow()] lo .. hi" as Expr, @r#"
+ Expr::Range {
+ start: Some(Expr::Path {
+ attrs: [
+ Attribute {
+ style: AttrStyle::Outer,
+ meta: Meta::List {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "allow",
+ },
+ ],
+ },
+ delimiter: MacroDelimiter::Paren,
+ tokens: TokenStream(``),
+ },
+ },
+ ],
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "lo",
+ },
+ ],
+ },
+ }),
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "hi",
+ },
+ ],
+ },
+ }),
+ }
+ "#);
+}
+
+#[test]
+fn test_ranges_bailout() {
+ syn::parse_str::<Expr>(".. ?").unwrap_err();
+ syn::parse_str::<Expr>(".. .field").unwrap_err();
+
+ snapshot!("return .. ?" as Expr, @r"
+ Expr::Try {
+ expr: Expr::Return {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ }
+ ");
+
+ snapshot!("break .. ?" as Expr, @r"
+ Expr::Try {
+ expr: Expr::Break {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ }
+ ");
+
+ snapshot!("|| .. ?" as Expr, @r"
+ Expr::Try {
+ expr: Expr::Closure {
+ output: ReturnType::Default,
+ body: Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ },
+ },
+ }
+ ");
+
+ snapshot!("return .. .field" as Expr, @r#"
+ Expr::Field {
+ base: Expr::Return {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ member: Member::Named("field"),
+ }
+ "#);
+
+ snapshot!("break .. .field" as Expr, @r#"
+ Expr::Field {
+ base: Expr::Break {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ member: Member::Named("field"),
+ }
+ "#);
+
+ snapshot!("|| .. .field" as Expr, @r#"
+ Expr::Field {
+ base: Expr::Closure {
+ output: ReturnType::Default,
+ body: Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ },
+ },
+ member: Member::Named("field"),
+ }
+ "#);
+
+ snapshot!("return .. = ()" as Expr, @r"
+ Expr::Assign {
+ left: Expr::Return {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ right: Expr::Tuple,
+ }
+ ");
+
+ snapshot!("return .. += ()" as Expr, @r"
+ Expr::Binary {
+ left: Expr::Return {
+ expr: Some(Expr::Range {
+ limits: RangeLimits::HalfOpen,
+ }),
+ },
+ op: BinOp::AddAssign,
+ right: Expr::Tuple,
+ }
+ ");
+}
+
+#[test]
+fn test_ambiguous_label() {
+ for stmt in [
+ quote! {
+ return 'label: loop { break 'label 42; };
+ },
+ quote! {
+ break ('label: loop { break 'label 42; });
+ },
+ quote! {
+ break 1 + 'label: loop { break 'label 42; };
+ },
+ quote! {
+ break 'outer 'inner: loop { break 'inner 42; };
+ },
+ ] {
+ syn::parse2::<Stmt>(stmt).unwrap();
+ }
+
+ for stmt in [
+ // Parentheses required. See https://github.com/rust-lang/rust/pull/87026.
+ quote! {
+ break 'label: loop { break 'label 42; };
+ },
+ ] {
+ 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",
+ },
+ Token![::],
+ PathSegment {
+ ident: "b",
+ },
+ ],
+ },
+ },
+ },
+ then_branch: Block {
+ stmts: [],
+ },
+ }
+ "#);
+
+ let tokens = quote!(#path {});
+ snapshot!(tokens as Expr, @r#"
+ Expr::Struct {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "a",
+ },
+ Token![::],
+ PathSegment {
+ ident: "b",
+ },
+ ],
+ },
+ }
+ "#);
+
+ let tokens = quote!(#path :: c);
+ snapshot!(tokens as Expr, @r#"
+ Expr::Path {
+ path: Path {
+ segments: [
+ PathSegment {
+ ident: "a",
+ },
+ Token![::],
+ PathSegment {
+ ident: "b",
+ },
+ Token![::],
+ 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",
+ },
+ Token![::],
+ 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: [],
+ },
+ }
+ "#);
+}
+
+#[test]
+fn test_tuple_comma() {
+ let mut expr = ExprTuple {
+ attrs: Vec::new(),
+ paren_token: token::Paren::default(),
+ elems: Punctuated::new(),
+ };
+ snapshot!(expr.to_token_stream() as Expr, @"Expr::Tuple");
+
+ expr.elems.push_value(parse_quote!(continue));
+ // Must not parse to Expr::Paren
+ snapshot!(expr.to_token_stream() as Expr, @r#"
+ Expr::Tuple {
+ elems: [
+ Expr::Continue,
+ Token![,],
+ ],
+ }
+ "#);
+
+ expr.elems.push_punct(<Token![,]>::default());
+ snapshot!(expr.to_token_stream() as Expr, @r#"
+ Expr::Tuple {
+ elems: [
+ Expr::Continue,
+ Token![,],
+ ],
+ }
+ "#);
+
+ expr.elems.push_value(parse_quote!(continue));
+ snapshot!(expr.to_token_stream() as Expr, @r#"
+ Expr::Tuple {
+ elems: [
+ Expr::Continue,
+ Token![,],
+ Expr::Continue,
+ ],
+ }
+ "#);
+
+ expr.elems.push_punct(<Token![,]>::default());
+ snapshot!(expr.to_token_stream() as Expr, @r#"
+ Expr::Tuple {
+ elems: [
+ Expr::Continue,
+ Token![,],
+ Expr::Continue,
+ Token![,],
+ ],
+ }
+ "#);
+}
+
+#[test]
+fn test_binop_associativity() {
+ // Left to right.
+ snapshot!("() + () + ()" as Expr, @r#"
+ Expr::Binary {
+ left: Expr::Binary {
+ left: Expr::Tuple,
+ op: BinOp::Add,
+ right: Expr::Tuple,
+ },
+ op: BinOp::Add,
+ right: Expr::Tuple,
+ }
+ "#);
+
+ // Right to left.
+ snapshot!("() += () += ()" as Expr, @r#"
+ Expr::Binary {
+ left: Expr::Tuple,
+ op: BinOp::AddAssign,
+ right: Expr::Binary {
+ left: Expr::Tuple,
+ op: BinOp::AddAssign,
+ right: Expr::Tuple,
+ },
+ }
+ "#);
+
+ // Parenthesization is required.
+ syn::parse_str::<Expr>("() == () == ()").unwrap_err();
+}
+
+#[test]
+fn test_assign_range_precedence() {
+ // Range has higher precedence as the right-hand of an assignment, but
+ // ambiguous precedence as the left-hand of an assignment.
+ snapshot!("() = () .. ()" as Expr, @r#"
+ Expr::Assign {
+ left: Expr::Tuple,
+ right: Expr::Range {
+ start: Some(Expr::Tuple),
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Tuple),
+ },
+ }
+ "#);
+
+ snapshot!("() += () .. ()" as Expr, @r#"
+ Expr::Binary {
+ left: Expr::Tuple,
+ op: BinOp::AddAssign,
+ right: Expr::Range {
+ start: Some(Expr::Tuple),
+ limits: RangeLimits::HalfOpen,
+ end: Some(Expr::Tuple),
+ },
+ }
+ "#);
+
+ syn::parse_str::<Expr>("() .. () = ()").unwrap_err();
+ syn::parse_str::<Expr>("() .. () += ()").unwrap_err();
+}
+
+#[test]
+fn test_chained_comparison() {
+ // https://github.com/dtolnay/syn/issues/1738
+ let _ = syn::parse_str::<Expr>("a = a < a <");
+ let _ = syn::parse_str::<Expr>("a = a .. a ..");
+ let _ = syn::parse_str::<Expr>("a = a .. a +=");
+
+ let err = syn::parse_str::<Expr>("a < a < a").unwrap_err();
+ assert_eq!("comparison operators cannot be chained", err.to_string());
+
+ let err = syn::parse_str::<Expr>("a .. a .. a").unwrap_err();
+ assert_eq!("unexpected token", err.to_string());
+
+ let err = syn::parse_str::<Expr>("a .. a += a").unwrap_err();
+ assert_eq!("unexpected token", err.to_string());
+}
+
+#[test]
+fn test_fixup() {
+ for tokens in [
+ quote! { 2 * (1 + 1) },
+ quote! { 0 + (0 + 0) },
+ quote! { (a = b) = c },
+ quote! { (x as i32) < 0 },
+ quote! { 1 + (x as i32) < 0 },
+ quote! { (1 + 1).abs() },
+ quote! { (lo..hi)[..] },
+ quote! { (a..b)..(c..d) },
+ quote! { (x > ..) > x },
+ quote! { (&mut fut).await },
+ quote! { &mut (x as i32) },
+ quote! { -(x as i32) },
+ quote! { if (S {}) == 1 {} },
+ quote! { { (m! {}) - 1 } },
+ quote! { match m { _ => ({}) - 1 } },
+ quote! { if let _ = (a && b) && c {} },
+ quote! { if let _ = (S {}) {} },
+ quote! { if (S {}) == 0 && let Some(_) = x {} },
+ quote! { break ('a: loop { break 'a 1 } + 1) },
+ quote! { a + (|| b) + c },
+ quote! { if let _ = ((break) - 1 || true) {} },
+ quote! { if let _ = (break + 1 || true) {} },
+ quote! { if break (break) {} },
+ quote! { if break break {} {} },
+ quote! { if return (..) {} },
+ quote! { if return .. {} {} },
+ quote! { if || (Struct {}) {} },
+ quote! { if || (Struct {}).await {} },
+ quote! { if break || Struct {}.await {} },
+ quote! { if break 'outer 'block: {} {} },
+ quote! { if ..'block: {} {} },
+ quote! { if break ({}).await {} },
+ quote! { (break)() },
+ quote! { (..) = () },
+ quote! { (..) += () },
+ quote! { (1 < 2) == (3 < 4) },
+ quote! { { (let _ = ()) } },
+ quote! { (#[attr] thing).field },
+ quote! { #[attr] (1 + 1) },
+ quote! { #[attr] (x = 1) },
+ quote! { #[attr] (x += 1) },
+ quote! { #[attr] (1 as T) },
+ quote! { (return #[attr] (x + ..)).field },
+ quote! { (self.f)() },
+ quote! { (return)..=return },
+ quote! { 1 + (return)..=1 + return },
+ quote! { .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. },
+ ] {
+ let original: Expr = syn::parse2(tokens).unwrap();
+
+ let mut flat = original.clone();
+ FlattenParens::combine_attrs().visit_expr_mut(&mut flat);
+ let reconstructed: Expr = match syn::parse2(flat.to_token_stream()) {
+ Ok(reconstructed) => reconstructed,
+ Err(err) => panic!("failed to parse `{}`: {}", flat.to_token_stream(), err),
+ };
+
+ assert!(
+ original == reconstructed,
+ "original: {}\n{:#?}\nreconstructed: {}\n{:#?}",
+ original.to_token_stream(),
+ crate::macros::debug::Lite(&original),
+ reconstructed.to_token_stream(),
+ crate::macros::debug::Lite(&reconstructed),
+ );
+ }
+}
+
+#[test]
+fn test_permutations() -> ExitCode {
+ fn iter(depth: usize, f: &mut dyn FnMut(Expr)) {
+ let span = Span::call_site();
+
+ // Expr::Path
+ f(Expr::Path(ExprPath {
+ // `x`
+ attrs: Vec::new(),
+ qself: None,
+ path: Path::from(Ident::new("x", span)),
+ }));
+ if false {
+ f(Expr::Path(ExprPath {
+ // `x::<T>`
+ attrs: Vec::new(),
+ qself: None,
+ path: Path {
+ leading_colon: None,
+ segments: Punctuated::from_iter([PathSegment {
+ ident: Ident::new("x", span),
+ arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+ colon2_token: Some(Token![::](span)),
+ lt_token: Token![<](span),
+ args: Punctuated::from_iter([GenericArgument::Type(Type::Path(
+ TypePath {
+ qself: None,
+ path: Path::from(Ident::new("T", span)),
+ },
+ ))]),
+ gt_token: Token![>](span),
+ }),
+ }]),
+ },
+ }));
+ f(Expr::Path(ExprPath {
+ // `<T as Trait>::CONST`
+ attrs: Vec::new(),
+ qself: Some(QSelf {
+ lt_token: Token![<](span),
+ ty: Box::new(Type::Path(TypePath {
+ qself: None,
+ path: Path::from(Ident::new("T", span)),
+ })),
+ position: 1,
+ as_token: Some(Token![as](span)),
+ gt_token: Token![>](span),
+ }),
+ path: Path {
+ leading_colon: None,
+ segments: Punctuated::from_iter([
+ PathSegment::from(Ident::new("Trait", span)),
+ PathSegment::from(Ident::new("CONST", span)),
+ ]),
+ },
+ }));
+ }
+
+ let Some(depth) = depth.checked_sub(1) else {
+ return;
+ };
+
+ // Expr::Assign
+ iter(depth, &mut |expr| {
+ iter(0, &mut |simple| {
+ f(Expr::Assign(ExprAssign {
+ // `x = $expr`
+ attrs: Vec::new(),
+ left: Box::new(simple.clone()),
+ eq_token: Token![=](span),
+ right: Box::new(expr.clone()),
+ }));
+ f(Expr::Assign(ExprAssign {
+ // `$expr = x`
+ attrs: Vec::new(),
+ left: Box::new(expr.clone()),
+ eq_token: Token![=](span),
+ right: Box::new(simple),
+ }));
+ });
+ });
+
+ // Expr::Binary
+ iter(depth, &mut |expr| {
+ iter(0, &mut |simple| {
+ for op in [
+ BinOp::Add(Token![+](span)),
+ //BinOp::Sub(Token![-](span)),
+ //BinOp::Mul(Token![*](span)),
+ //BinOp::Div(Token![/](span)),
+ //BinOp::Rem(Token![%](span)),
+ //BinOp::And(Token![&&](span)),
+ //BinOp::Or(Token![||](span)),
+ //BinOp::BitXor(Token![^](span)),
+ //BinOp::BitAnd(Token![&](span)),
+ //BinOp::BitOr(Token![|](span)),
+ //BinOp::Shl(Token![<<](span)),
+ //BinOp::Shr(Token![>>](span)),
+ //BinOp::Eq(Token![==](span)),
+ BinOp::Lt(Token![<](span)),
+ //BinOp::Le(Token![<=](span)),
+ //BinOp::Ne(Token![!=](span)),
+ //BinOp::Ge(Token![>=](span)),
+ //BinOp::Gt(Token![>](span)),
+ BinOp::ShlAssign(Token![<<=](span)),
+ ] {
+ f(Expr::Binary(ExprBinary {
+ // `x + $expr`
+ attrs: Vec::new(),
+ left: Box::new(simple.clone()),
+ op,
+ right: Box::new(expr.clone()),
+ }));
+ f(Expr::Binary(ExprBinary {
+ // `$expr + x`
+ attrs: Vec::new(),
+ left: Box::new(expr.clone()),
+ op,
+ right: Box::new(simple.clone()),
+ }));
+ }
+ });
+ });
+
+ // Expr::Block
+ f(Expr::Block(ExprBlock {
+ // `{}`
+ attrs: Vec::new(),
+ label: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::Break
+ f(Expr::Break(ExprBreak {
+ // `break`
+ attrs: Vec::new(),
+ break_token: Token![break](span),
+ label: None,
+ expr: None,
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Break(ExprBreak {
+ // `break $expr`
+ attrs: Vec::new(),
+ break_token: Token![break](span),
+ label: None,
+ expr: Some(Box::new(expr)),
+ }));
+ });
+
+ // Expr::Call
+ iter(depth, &mut |expr| {
+ f(Expr::Call(ExprCall {
+ // `$expr()`
+ attrs: Vec::new(),
+ func: Box::new(expr),
+ paren_token: token::Paren(span),
+ args: Punctuated::new(),
+ }));
+ });
+
+ // Expr::Cast
+ iter(depth, &mut |expr| {
+ f(Expr::Cast(ExprCast {
+ // `$expr as T`
+ attrs: Vec::new(),
+ expr: Box::new(expr),
+ as_token: Token![as](span),
+ ty: Box::new(Type::Path(TypePath {
+ qself: None,
+ path: Path::from(Ident::new("T", span)),
+ })),
+ }));
+ });
+
+ // Expr::Closure
+ iter(depth, &mut |expr| {
+ f(Expr::Closure(ExprClosure {
+ // `|| $expr`
+ attrs: Vec::new(),
+ lifetimes: None,
+ constness: None,
+ movability: None,
+ asyncness: None,
+ capture: None,
+ or1_token: Token![|](span),
+ inputs: Punctuated::new(),
+ or2_token: Token![|](span),
+ output: ReturnType::Default,
+ body: Box::new(expr),
+ }));
+ });
+
+ // Expr::Field
+ iter(depth, &mut |expr| {
+ f(Expr::Field(ExprField {
+ // `$expr.field`
+ attrs: Vec::new(),
+ base: Box::new(expr),
+ dot_token: Token![.](span),
+ member: Member::Named(Ident::new("field", span)),
+ }));
+ });
+
+ // Expr::If
+ iter(depth, &mut |expr| {
+ f(Expr::If(ExprIf {
+ // `if $expr {}`
+ attrs: Vec::new(),
+ if_token: Token![if](span),
+ cond: Box::new(expr),
+ then_branch: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ else_branch: None,
+ }));
+ });
+
+ // Expr::Let
+ iter(depth, &mut |expr| {
+ f(Expr::Let(ExprLet {
+ attrs: Vec::new(),
+ let_token: Token![let](span),
+ pat: Box::new(Pat::Wild(PatWild {
+ attrs: Vec::new(),
+ underscore_token: Token![_](span),
+ })),
+ eq_token: Token![=](span),
+ expr: Box::new(expr),
+ }));
+ });
+
+ // Expr::Range
+ f(Expr::Range(ExprRange {
+ // `..`
+ attrs: Vec::new(),
+ start: None,
+ limits: RangeLimits::HalfOpen(Token![..](span)),
+ end: None,
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Range(ExprRange {
+ // `..$expr`
+ attrs: Vec::new(),
+ start: None,
+ limits: RangeLimits::HalfOpen(Token![..](span)),
+ end: Some(Box::new(expr.clone())),
+ }));
+ f(Expr::Range(ExprRange {
+ // `$expr..`
+ attrs: Vec::new(),
+ start: Some(Box::new(expr)),
+ limits: RangeLimits::HalfOpen(Token![..](span)),
+ end: None,
+ }));
+ });
+
+ // Expr::Reference
+ iter(depth, &mut |expr| {
+ f(Expr::Reference(ExprReference {
+ // `&$expr`
+ attrs: Vec::new(),
+ and_token: Token![&](span),
+ mutability: None,
+ expr: Box::new(expr),
+ }));
+ });
+
+ // Expr::Return
+ f(Expr::Return(ExprReturn {
+ // `return`
+ attrs: Vec::new(),
+ return_token: Token![return](span),
+ expr: None,
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Return(ExprReturn {
+ // `return $expr`
+ attrs: Vec::new(),
+ return_token: Token![return](span),
+ expr: Some(Box::new(expr)),
+ }));
+ });
+
+ // Expr::Try
+ iter(depth, &mut |expr| {
+ f(Expr::Try(ExprTry {
+ // `$expr?`
+ attrs: Vec::new(),
+ expr: Box::new(expr),
+ question_token: Token![?](span),
+ }));
+ });
+
+ // Expr::Unary
+ iter(depth, &mut |expr| {
+ for op in [
+ UnOp::Deref(Token![*](span)),
+ //UnOp::Not(Token![!](span)),
+ //UnOp::Neg(Token![-](span)),
+ ] {
+ f(Expr::Unary(ExprUnary {
+ // `*$expr`
+ attrs: Vec::new(),
+ op,
+ expr: Box::new(expr.clone()),
+ }));
+ }
+ });
+
+ if false {
+ // Expr::Array
+ f(Expr::Array(ExprArray {
+ // `[]`
+ attrs: Vec::new(),
+ bracket_token: token::Bracket(span),
+ elems: Punctuated::new(),
+ }));
+
+ // Expr::Async
+ f(Expr::Async(ExprAsync {
+ // `async {}`
+ attrs: Vec::new(),
+ async_token: Token![async](span),
+ capture: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::Await
+ iter(depth, &mut |expr| {
+ f(Expr::Await(ExprAwait {
+ // `$expr.await`
+ attrs: Vec::new(),
+ base: Box::new(expr),
+ dot_token: Token![.](span),
+ await_token: Token![await](span),
+ }));
+ });
+
+ // Expr::Block
+ f(Expr::Block(ExprBlock {
+ // `'a: {}`
+ attrs: Vec::new(),
+ label: Some(Label {
+ name: Lifetime::new("'a", span),
+ colon_token: Token![:](span),
+ }),
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Block(ExprBlock {
+ // `{ $expr }`
+ attrs: Vec::new(),
+ label: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::from([Stmt::Expr(expr.clone(), None)]),
+ },
+ }));
+ f(Expr::Block(ExprBlock {
+ // `{ $expr; }`
+ attrs: Vec::new(),
+ label: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::from([Stmt::Expr(expr, Some(Token![;](span)))]),
+ },
+ }));
+ });
+
+ // Expr::Break
+ f(Expr::Break(ExprBreak {
+ // `break 'a`
+ attrs: Vec::new(),
+ break_token: Token![break](span),
+ label: Some(Lifetime::new("'a", span)),
+ expr: None,
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Break(ExprBreak {
+ // `break 'a $expr`
+ attrs: Vec::new(),
+ break_token: Token![break](span),
+ label: Some(Lifetime::new("'a", span)),
+ expr: Some(Box::new(expr)),
+ }));
+ });
+
+ // Expr::Closure
+ f(Expr::Closure(ExprClosure {
+ // `|| -> T {}`
+ attrs: Vec::new(),
+ lifetimes: None,
+ constness: None,
+ movability: None,
+ asyncness: None,
+ capture: None,
+ or1_token: Token![|](span),
+ inputs: Punctuated::new(),
+ or2_token: Token![|](span),
+ output: ReturnType::Type(
+ Token![->](span),
+ Box::new(Type::Path(TypePath {
+ qself: None,
+ path: Path::from(Ident::new("T", span)),
+ })),
+ ),
+ body: Box::new(Expr::Block(ExprBlock {
+ attrs: Vec::new(),
+ label: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ })),
+ }));
+
+ // Expr::Const
+ f(Expr::Const(ExprConst {
+ // `const {}`
+ attrs: Vec::new(),
+ const_token: Token![const](span),
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::Continue
+ f(Expr::Continue(ExprContinue {
+ // `continue`
+ attrs: Vec::new(),
+ continue_token: Token![continue](span),
+ label: None,
+ }));
+ f(Expr::Continue(ExprContinue {
+ // `continue 'a`
+ attrs: Vec::new(),
+ continue_token: Token![continue](span),
+ label: Some(Lifetime::new("'a", span)),
+ }));
+
+ // Expr::ForLoop
+ iter(depth, &mut |expr| {
+ f(Expr::ForLoop(ExprForLoop {
+ // `for _ in $expr {}`
+ attrs: Vec::new(),
+ label: None,
+ for_token: Token![for](span),
+ pat: Box::new(Pat::Wild(PatWild {
+ attrs: Vec::new(),
+ underscore_token: Token![_](span),
+ })),
+ in_token: Token![in](span),
+ expr: Box::new(expr.clone()),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ f(Expr::ForLoop(ExprForLoop {
+ // `'a: for _ in $expr {}`
+ attrs: Vec::new(),
+ label: Some(Label {
+ name: Lifetime::new("'a", span),
+ colon_token: Token![:](span),
+ }),
+ for_token: Token![for](span),
+ pat: Box::new(Pat::Wild(PatWild {
+ attrs: Vec::new(),
+ underscore_token: Token![_](span),
+ })),
+ in_token: Token![in](span),
+ expr: Box::new(expr),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ });
+
+ // Expr::Index
+ iter(depth, &mut |expr| {
+ f(Expr::Index(ExprIndex {
+ // `$expr[0]`
+ attrs: Vec::new(),
+ expr: Box::new(expr),
+ bracket_token: token::Bracket(span),
+ index: Box::new(Expr::Lit(ExprLit {
+ attrs: Vec::new(),
+ lit: Lit::Int(LitInt::new("0", span)),
+ })),
+ }));
+ });
+
+ // Expr::Loop
+ f(Expr::Loop(ExprLoop {
+ // `loop {}`
+ attrs: Vec::new(),
+ label: None,
+ loop_token: Token![loop](span),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ f(Expr::Loop(ExprLoop {
+ // `'a: loop {}`
+ attrs: Vec::new(),
+ label: Some(Label {
+ name: Lifetime::new("'a", span),
+ colon_token: Token![:](span),
+ }),
+ loop_token: Token![loop](span),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::Macro
+ f(Expr::Macro(ExprMacro {
+ // `m!()`
+ attrs: Vec::new(),
+ mac: Macro {
+ path: Path::from(Ident::new("m", span)),
+ bang_token: Token![!](span),
+ delimiter: MacroDelimiter::Paren(token::Paren(span)),
+ tokens: TokenStream::new(),
+ },
+ }));
+ f(Expr::Macro(ExprMacro {
+ // `m! {}`
+ attrs: Vec::new(),
+ mac: Macro {
+ path: Path::from(Ident::new("m", span)),
+ bang_token: Token![!](span),
+ delimiter: MacroDelimiter::Brace(token::Brace(span)),
+ tokens: TokenStream::new(),
+ },
+ }));
+
+ // Expr::Match
+ iter(depth, &mut |expr| {
+ f(Expr::Match(ExprMatch {
+ // `match $expr {}`
+ attrs: Vec::new(),
+ match_token: Token![match](span),
+ expr: Box::new(expr.clone()),
+ brace_token: token::Brace(span),
+ arms: Vec::new(),
+ }));
+ f(Expr::Match(ExprMatch {
+ // `match x { _ => $expr }`
+ attrs: Vec::new(),
+ match_token: Token![match](span),
+ expr: Box::new(Expr::Path(ExprPath {
+ attrs: Vec::new(),
+ qself: None,
+ path: Path::from(Ident::new("x", span)),
+ })),
+ brace_token: token::Brace(span),
+ arms: Vec::from([Arm {
+ attrs: Vec::new(),
+ pat: Pat::Wild(PatWild {
+ attrs: Vec::new(),
+ underscore_token: Token![_](span),
+ }),
+ guard: None,
+ fat_arrow_token: Token![=>](span),
+ body: Box::new(expr.clone()),
+ comma: None,
+ }]),
+ }));
+ f(Expr::Match(ExprMatch {
+ // `match x { _ if $expr => {} }`
+ attrs: Vec::new(),
+ match_token: Token![match](span),
+ expr: Box::new(Expr::Path(ExprPath {
+ attrs: Vec::new(),
+ qself: None,
+ path: Path::from(Ident::new("x", span)),
+ })),
+ brace_token: token::Brace(span),
+ arms: Vec::from([Arm {
+ attrs: Vec::new(),
+ pat: Pat::Wild(PatWild {
+ attrs: Vec::new(),
+ underscore_token: Token![_](span),
+ }),
+ guard: Some((Token![if](span), Box::new(expr))),
+ fat_arrow_token: Token![=>](span),
+ body: Box::new(Expr::Block(ExprBlock {
+ attrs: Vec::new(),
+ label: None,
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ })),
+ comma: None,
+ }]),
+ }));
+ });
+
+ // Expr::MethodCall
+ iter(depth, &mut |expr| {
+ f(Expr::MethodCall(ExprMethodCall {
+ // `$expr.method()`
+ attrs: Vec::new(),
+ receiver: Box::new(expr.clone()),
+ dot_token: Token![.](span),
+ method: Ident::new("method", span),
+ turbofish: None,
+ paren_token: token::Paren(span),
+ args: Punctuated::new(),
+ }));
+ f(Expr::MethodCall(ExprMethodCall {
+ // `$expr.method::<T>()`
+ attrs: Vec::new(),
+ receiver: Box::new(expr),
+ dot_token: Token![.](span),
+ method: Ident::new("method", span),
+ turbofish: Some(AngleBracketedGenericArguments {
+ colon2_token: Some(Token![::](span)),
+ lt_token: Token![<](span),
+ args: Punctuated::from_iter([GenericArgument::Type(Type::Path(
+ TypePath {
+ qself: None,
+ path: Path::from(Ident::new("T", span)),
+ },
+ ))]),
+ gt_token: Token![>](span),
+ }),
+ paren_token: token::Paren(span),
+ args: Punctuated::new(),
+ }));
+ });
+
+ // Expr::RawAddr
+ iter(depth, &mut |expr| {
+ f(Expr::RawAddr(ExprRawAddr {
+ // `&raw const $expr`
+ attrs: Vec::new(),
+ and_token: Token![&](span),
+ raw: Token![raw](span),
+ mutability: PointerMutability::Const(Token![const](span)),
+ expr: Box::new(expr),
+ }));
+ });
+
+ // Expr::Struct
+ f(Expr::Struct(ExprStruct {
+ // `Struct {}`
+ attrs: Vec::new(),
+ qself: None,
+ path: Path::from(Ident::new("Struct", span)),
+ brace_token: token::Brace(span),
+ fields: Punctuated::new(),
+ dot2_token: None,
+ rest: None,
+ }));
+
+ // Expr::TryBlock
+ f(Expr::TryBlock(ExprTryBlock {
+ // `try {}`
+ attrs: Vec::new(),
+ try_token: Token![try](span),
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::Unsafe
+ f(Expr::Unsafe(ExprUnsafe {
+ // `unsafe {}`
+ attrs: Vec::new(),
+ unsafe_token: Token![unsafe](span),
+ block: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+
+ // Expr::While
+ iter(depth, &mut |expr| {
+ f(Expr::While(ExprWhile {
+ // `while $expr {}`
+ attrs: Vec::new(),
+ label: None,
+ while_token: Token![while](span),
+ cond: Box::new(expr.clone()),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ f(Expr::While(ExprWhile {
+ // `'a: while $expr {}`
+ attrs: Vec::new(),
+ label: Some(Label {
+ name: Lifetime::new("'a", span),
+ colon_token: Token![:](span),
+ }),
+ while_token: Token![while](span),
+ cond: Box::new(expr),
+ body: Block {
+ brace_token: token::Brace(span),
+ stmts: Vec::new(),
+ },
+ }));
+ });
+
+ // Expr::Yield
+ f(Expr::Yield(ExprYield {
+ // `yield`
+ attrs: Vec::new(),
+ yield_token: Token![yield](span),
+ expr: None,
+ }));
+ iter(depth, &mut |expr| {
+ f(Expr::Yield(ExprYield {
+ // `yield $expr`
+ attrs: Vec::new(),
+ yield_token: Token![yield](span),
+ expr: Some(Box::new(expr)),
+ }));
+ });
+ }
+ }
+
+ let mut failures = 0;
+ macro_rules! fail {
+ ($($message:tt)*) => {{
+ eprintln!($($message)*);
+ failures += 1;
+ return;
+ }};
+ }
+ let mut assert = |mut original: Expr| {
+ let tokens = original.to_token_stream();
+ let Ok(mut parsed) = syn::parse2::<Expr>(tokens.clone()) else {
+ fail!(
+ "failed to parse: {}\n{:#?}",
+ tokens,
+ crate::macros::debug::Lite(&original),
+ );
+ };
+ AsIfPrinted.visit_expr_mut(&mut original);
+ FlattenParens::combine_attrs().visit_expr_mut(&mut parsed);
+ if original != parsed {
+ fail!(
+ "before: {}\n{:#?}\nafter: {}\n{:#?}",
+ tokens,
+ crate::macros::debug::Lite(&original),
+ parsed.to_token_stream(),
+ crate::macros::debug::Lite(&parsed),
+ );
+ }
+ let mut tokens_no_paren = tokens.clone();
+ FlattenParens::visit_token_stream_mut(&mut tokens_no_paren);
+ if tokens.to_string() != tokens_no_paren.to_string() {
+ if let Ok(mut parsed2) = syn::parse2::<Expr>(tokens_no_paren) {
+ FlattenParens::combine_attrs().visit_expr_mut(&mut parsed2);
+ if original == parsed2 {
+ fail!("redundant parens: {}", tokens);
+ }
+ }
+ }
+ };
+
+ iter(4, &mut assert);
+ if failures > 0 {
+ eprintln!("FAILURES: {failures}");
+ ExitCode::FAILURE
+ } else {
+ ExitCode::SUCCESS
+ }
+}