From b55e4108e4f5065cd6fb52948e30d753dcebc393 Mon Sep 17 00:00:00 2001 From: usmanovbf Date: Sun, 22 Mar 2026 09:45:28 +0300 Subject: [PATCH] feat: add support for `impl const Trait` syntax (issue #1972) --- src/gen/clone.rs | 1 + src/gen/debug.rs | 1 + src/gen/eq.rs | 6 +- src/gen/fold.rs | 1 + src/gen/hash.rs | 1 + src/gen/visit.rs | 1 + src/gen/visit_mut.rs | 1 + src/item.rs | 104 +++++++++++++++++----------- syn.json | 5 ++ tests/debug/gen.rs | 6 ++ tests/test_item.rs | 157 +++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 240 insertions(+), 44 deletions(-) diff --git a/src/gen/clone.rs b/src/gen/clone.rs index be2b698422..bc227e1cce 100644 --- a/src/gen/clone.rs +++ b/src/gen/clone.rs @@ -1187,6 +1187,7 @@ impl Clone for crate::ItemImpl { attrs: self.attrs.clone(), defaultness: self.defaultness.clone(), unsafety: self.unsafety.clone(), + constness: self.constness.clone(), impl_token: self.impl_token.clone(), generics: self.generics.clone(), trait_: self.trait_.clone(), diff --git a/src/gen/debug.rs b/src/gen/debug.rs index bf0784d1c4..3a264692e2 100644 --- a/src/gen/debug.rs +++ b/src/gen/debug.rs @@ -1751,6 +1751,7 @@ impl crate::ItemImpl { formatter.field("attrs", &self.attrs); formatter.field("defaultness", &self.defaultness); formatter.field("unsafety", &self.unsafety); + formatter.field("constness", &self.constness); formatter.field("impl_token", &self.impl_token); formatter.field("generics", &self.generics); formatter.field("trait_", &self.trait_); diff --git a/src/gen/eq.rs b/src/gen/eq.rs index 5ba15722b7..4d84d1936c 100644 --- a/src/gen/eq.rs +++ b/src/gen/eq.rs @@ -1196,9 +1196,9 @@ impl Eq for crate::ItemImpl {} impl PartialEq for crate::ItemImpl { fn eq(&self, other: &Self) -> bool { self.attrs == other.attrs && self.defaultness == other.defaultness - && self.unsafety == other.unsafety && self.generics == other.generics - && self.trait_ == other.trait_ && self.self_ty == other.self_ty - && self.items == other.items + && self.unsafety == other.unsafety && self.constness == other.constness + && self.generics == other.generics && self.trait_ == other.trait_ + && self.self_ty == other.self_ty && self.items == other.items } } #[cfg(feature = "full")] diff --git a/src/gen/fold.rs b/src/gen/fold.rs index b71421f47a..ca2b7c0332 100644 --- a/src/gen/fold.rs +++ b/src/gen/fold.rs @@ -2502,6 +2502,7 @@ where attrs: f.fold_attributes(node.attrs), defaultness: node.defaultness, unsafety: node.unsafety, + constness: node.constness, impl_token: node.impl_token, generics: f.fold_generics(node.generics), trait_: (node.trait_).map(|it| ((it).0, f.fold_path((it).1), (it).2)), diff --git a/src/gen/hash.rs b/src/gen/hash.rs index 508af526c1..b29001d8da 100644 --- a/src/gen/hash.rs +++ b/src/gen/hash.rs @@ -1543,6 +1543,7 @@ impl Hash for crate::ItemImpl { self.attrs.hash(state); self.defaultness.hash(state); self.unsafety.hash(state); + self.constness.hash(state); self.generics.hash(state); self.trait_.hash(state); self.self_ty.hash(state); diff --git a/src/gen/visit.rs b/src/gen/visit.rs index cd258fcde1..d483b8342e 100644 --- a/src/gen/visit.rs +++ b/src/gen/visit.rs @@ -2526,6 +2526,7 @@ where } skip!(node.defaultness); skip!(node.unsafety); + skip!(node.constness); skip!(node.impl_token); v.visit_generics(&node.generics); if let Some(it) = &node.trait_ { diff --git a/src/gen/visit_mut.rs b/src/gen/visit_mut.rs index 6588182c4f..9c2045b57f 100644 --- a/src/gen/visit_mut.rs +++ b/src/gen/visit_mut.rs @@ -2411,6 +2411,7 @@ where v.visit_attributes_mut(&mut node.attrs); skip!(node.defaultness); skip!(node.unsafety); + skip!(node.constness); skip!(node.impl_token); v.visit_generics_mut(&mut node.generics); if let Some(it) = &mut node.trait_ { diff --git a/src/item.rs b/src/item.rs index 272e2d26e9..3b5a7e6441 100644 --- a/src/item.rs +++ b/src/item.rs @@ -176,6 +176,8 @@ ast_struct! { pub attrs: Vec, pub defaultness: Option, pub unsafety: Option, + /// The `const` keyword in `const impl`, used for const trait impls. + pub constness: Option, pub impl_token: Token![impl], pub generics: Generics, /// Trait this impl implements. @@ -1022,43 +1024,55 @@ pub(crate) mod parsing { } } } else if lookahead.peek(Token![const]) { - let vis = input.parse()?; - let const_token: Token![const] = input.parse()?; - let lookahead = input.lookahead1(); - let ident = if lookahead.peek(Ident) || lookahead.peek(Token![_]) { - input.call(Ident::parse_any)? - } else { - return Err(lookahead.error()); - }; - let mut generics: Generics = input.parse()?; - let colon_token = input.parse()?; - let ty = input.parse()?; - let value = if let Some(eq_token) = input.parse::>()? { - let expr: Expr = input.parse()?; - Some((eq_token, expr)) + // Check for "const impl" (issue #1972) + ahead.parse::()?; + if ahead.peek(Token![impl]) { + let allow_verbatim_impl = true; + if let Some(item) = parse_impl(input, allow_verbatim_impl)? { + Ok(Item::Impl(item)) + } else { + Ok(Item::Verbatim(verbatim::between(&begin, input))) + } } else { - None - }; - generics.where_clause = input.parse()?; - let semi_token: Token![;] = input.parse()?; - match value { - Some((eq_token, expr)) - if generics.lt_token.is_none() && generics.where_clause.is_none() => - { - Ok(Item::Const(ItemConst { - attrs: Vec::new(), - vis, - const_token, - ident, - generics, - colon_token, - ty, - eq_token, - expr: Box::new(expr), - semi_token, - })) + // Parse as const item + let vis = input.parse()?; + let const_token: Token![const] = input.parse()?; + let lookahead = input.lookahead1(); + let ident = if lookahead.peek(Ident) || lookahead.peek(Token![_]) { + input.call(Ident::parse_any)? + } else { + return Err(lookahead.error()); + }; + let mut generics: Generics = input.parse()?; + let colon_token = input.parse()?; + let ty = input.parse()?; + let value = if let Some(eq_token) = input.parse::>()? { + let expr: Expr = input.parse()?; + Some((eq_token, expr)) + } else { + None + }; + generics.where_clause = input.parse()?; + let semi_token: Token![;] = input.parse()?; + match value { + Some((eq_token, expr)) + if generics.lt_token.is_none() && generics.where_clause.is_none() => + { + Ok(Item::Const(ItemConst { + attrs: Vec::new(), + vis, + const_token, + ident, + generics, + colon_token, + ty, + eq_token, + expr: Box::new(expr), + semi_token, + })) + } + _ => Ok(Item::Verbatim(verbatim::between(&begin, input))), } - _ => Ok(Item::Verbatim(verbatim::between(&begin, input))), } } else if lookahead.peek(Token![unsafe]) { ahead.parse::()?; @@ -2593,6 +2607,13 @@ pub(crate) mod parsing { let has_visibility = allow_verbatim_impl && input.parse::()?.is_some(); let defaultness: Option = input.parse()?; let unsafety: Option = input.parse()?; + // Parse `const` before `impl` keyword (e.g., `const impl Trait for Type {}`). + // Only parse if followed by `impl` to distinguish from `*const` pointer types. + let mut constness: Option = if input.peek(Token![const]) && input.peek2(Token![impl]) { + Some(input.parse()?) + } else { + None + }; let impl_token: Token![impl] = input.parse()?; let has_generics = generics::parsing::choose_generics_over_qpath(input); @@ -2602,11 +2623,10 @@ pub(crate) mod parsing { Generics::default() }; - let is_const_impl = allow_verbatim_impl - && (input.peek(Token![const]) || input.peek(Token![?]) && input.peek2(Token![const])); - if is_const_impl { - input.parse::>()?; - input.parse::()?; + // Parse `const` after `impl` keyword for `impl const Trait for Type {}` syntax. + // Only parse if `const` wasn't already parsed before `impl`. + if constness.is_none() && input.peek(Token![const]) { + constness = Some(input.parse()?); } let polarity = if input.peek(Token![!]) && !input.peek2(token::Brace) { @@ -2667,13 +2687,14 @@ pub(crate) mod parsing { items.push(content.parse()?); } - if has_visibility || is_const_impl || is_impl_for && trait_.is_none() { + if has_visibility || is_impl_for && trait_.is_none() { Ok(None) } else { Ok(Some(ItemImpl { attrs, defaultness, unsafety, + constness, impl_token, generics, trait_, @@ -3192,6 +3213,7 @@ mod printing { tokens.append_all(self.attrs.outer()); self.defaultness.to_tokens(tokens); self.unsafety.to_tokens(tokens); + self.constness.to_tokens(tokens); self.impl_token.to_tokens(tokens); self.generics.to_tokens(tokens); if let Some((polarity, path, for_token)) = &self.trait_ { diff --git a/syn.json b/syn.json index d67cef6bc8..7adc0f1210 100644 --- a/syn.json +++ b/syn.json @@ -2945,6 +2945,11 @@ "token": "Unsafe" } }, + "constness": { + "option": { + "token": "Const" + } + }, "impl_token": { "token": "Impl" }, diff --git a/tests/debug/gen.rs b/tests/debug/gen.rs index c0d37b791d..0591d4300f 100644 --- a/tests/debug/gen.rs +++ b/tests/debug/gen.rs @@ -2346,6 +2346,9 @@ impl Debug for Lite { if _val.unsafety.is_some() { formatter.field("unsafety", &Present); } + if _val.constness.is_some() { + formatter.field("constness", &Present); + } formatter.field("generics", Lite(&_val.generics)); if let Some(val) = &_val.trait_ { #[derive(RefCast)] @@ -2651,6 +2654,9 @@ impl Debug for Lite { if self.value.unsafety.is_some() { formatter.field("unsafety", &Present); } + if self.value.constness.is_some() { + formatter.field("constness", &Present); + } formatter.field("generics", Lite(&self.value.generics)); if let Some(val) = &self.value.trait_ { #[derive(RefCast)] diff --git a/tests/test_item.rs b/tests/test_item.rs index 2840f91af0..f170f11e76 100644 --- a/tests/test_item.rs +++ b/tests/test_item.rs @@ -368,3 +368,160 @@ fn test_nested_receiver_classification() { let _ = syn::parse2::(tokens); } + +// Test for issue https://github.com/dtolnay/syn/issues/1972 +#[test] +fn test_const_impl() { + // Test const impl trait for type + let tokens = quote! { + const impl Trait for Type {} + }; + let parsed: syn::ItemImpl = syn::parse2(tokens).expect("failed to parse const impl"); + + // Verify const token is captured + assert!(parsed.constness.is_some(), "const token should be present in const impl"); + assert!(parsed.trait_.is_some(), "trait_ should be Some for trait impl"); + assert!(parsed.defaultness.is_none(), "default should be None"); + assert!(parsed.unsafety.is_none(), "unsafe should be None"); +} + +#[test] +fn test_const_impl_round_trip() { + // Test that const impl round-trips correctly through ToTokens + let tokens = quote! { + const impl MyTrait for MyStruct { + fn foo() -> i32 { 42 } + } + }; + + let parsed: syn::ItemImpl = syn::parse2(tokens.clone()).expect("failed to parse const impl"); + assert!(parsed.constness.is_some(), "const token should be present"); + + // Verify ToTokens produces the same input + let regenerated = quote!(#parsed); + assert_eq!( + tokens.to_string(), + regenerated.to_string(), + "const impl should round-trip correctly" + ); +} + +#[test] +fn test_const_impl_with_modifiers() { + // Test const impl combined with other modifiers + let tokens = quote! { + default unsafe const impl Trait for Type {} + }; + snapshot!(tokens as Item, @r#" + Item::Impl { + defaultness: Some, + unsafety: Some, + constness: Some, + generics: Generics, + trait_: Some(( + None, + Path { + segments: [ + PathSegment { + ident: "Trait", + }, + ], + }, + )), + self_ty: Type::Path { + path: Path { + segments: [ + PathSegment { + ident: "Type", + }, + ], + }, + }, + } + "#); +} + +#[test] +fn test_regular_impl_without_const() { + // Verify regular impl still works without const + let tokens = quote! { + impl Trait for Type {} + }; + snapshot!(tokens as Item, @r#" + Item::Impl { + generics: Generics, + trait_: Some(( + None, + Path { + segments: [ + PathSegment { + ident: "Trait", + }, + ], + }, + )), + self_ty: Type::Path { + path: Path { + segments: [ + PathSegment { + ident: "Type", + }, + ], + }, + }, + } + "#); +} + +// Test for `impl const Trait for Type {}` syntax (alternative const impl syntax) +#[test] +fn test_impl_const_trait() { + // Test impl const trait for type (const after impl) + let tokens = quote! { + impl const Trait for Type {} + }; + let parsed: syn::ItemImpl = syn::parse2(tokens).expect("failed to parse impl const"); + + // Verify const token is captured + assert!(parsed.constness.is_some(), "const token should be present in impl const"); + assert!(parsed.trait_.is_some(), "trait_ should be Some for trait impl"); +} + +#[test] +fn test_impl_const_with_generics() { + // Test impl const with generic parameters + let tokens = quote! { + impl const Trait for Type {} + }; + let parsed: syn::ItemImpl = syn::parse2(tokens).expect("failed to parse impl const with generics"); + + assert!(parsed.constness.is_some(), "const token should be present"); + assert!(parsed.generics.lt_token.is_some(), "generics should be present"); +} + +#[test] +fn test_impl_const_round_trip() { + // Test that impl const round-trips correctly through ToTokens + // Note: ToTokens normalizes to `const impl` syntax regardless of input order + let tokens = quote! { + impl const Default for String { + fn default() -> Self { String::new() } + } + }; + + let parsed: syn::ItemImpl = syn::parse2(tokens).expect("failed to parse impl const"); + assert!(parsed.constness.is_some(), "const token should be present"); + + // Verify ToTokens produces valid output (normalized to `const impl`) + let regenerated = quote!(#parsed); + let regenerated_str = regenerated.to_string(); + assert!( + regenerated_str.contains("const impl"), + "ToTokens should produce `const impl` syntax, got: {}", + regenerated_str + ); + + // Verify the regenerated code can be parsed again + let reparsed: syn::ItemImpl = syn::parse2(regenerated).expect("failed to re-parse"); + assert!(reparsed.constness.is_some(), "const token should be preserved after round-trip"); +}