Repository: dtolnay/syn
Pull request #1768 (2024-10-23 19:43:30)

Parse safe and explicitly unsafe extern items
Closes https://github.com/dtolnay/syn/issues/1675.


Patch:
diff --git a/src/item.rs b/src/item.rs
index eb32d4f7..de83e28a 100644
--- a/src/item.rs
+++ b/src/item.rs
@@ -952,7 +952,8 @@ pub(crate) mod parsing {
         let vis: Visibility = ahead.parse()?;
 
         let lookahead = ahead.lookahead1();
-        let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead) {
+        let allow_safe = false;
+        let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead, allow_safe) {
             let vis: Visibility = input.parse()?;
             let sig: Signature = input.parse()?;
             if input.peek(Token![;]) {
@@ -1493,11 +1494,14 @@ pub(crate) mod parsing {
         }
     }
 
-    fn peek_signature(input: ParseStream) -> bool {
+    fn peek_signature(input: ParseStream, allow_safe: bool) -> bool {
         let fork = input.fork();
         fork.parse::<Option<Token![const]>>().is_ok()
             && fork.parse::<Option<Token![async]>>().is_ok()
-            && fork.parse::<Option<Token![unsafe]>>().is_ok()
+            && ((allow_safe
+                && token::parsing::peek_keyword(fork.cursor(), "safe")
+                && token::parsing::keyword(&fork, "safe").is_ok())
+                || fork.parse::<Option<Token![unsafe]>>().is_ok())
             && fork.parse::<Option<Abi>>().is_ok()
             && fork.peek(Token![fn])
     }
@@ -1505,22 +1509,37 @@ pub(crate) mod parsing {
     #[cfg_attr(docsrs, doc(cfg(feature = "parsing")))]
     impl Parse for Signature {
         fn parse(input: ParseStream) -> Result<Self> {
-            let constness: Option<Token![const]> = input.parse()?;
-            let asyncness: Option<Token![async]> = input.parse()?;
-            let unsafety: Option<Token![unsafe]> = input.parse()?;
-            let abi: Option<Abi> = input.parse()?;
-            let fn_token: Token![fn] = input.parse()?;
-            let ident: Ident = input.parse()?;
-            let mut generics: Generics = input.parse()?;
+            let allow_safe = false;
+            parse_signature(input, allow_safe).map(Option::unwrap)
+        }
+    }
 
-            let content;
-            let paren_token = parenthesized!(content in input);
-            let (inputs, variadic) = parse_fn_args(&content)?;
+    fn parse_signature(input: ParseStream, allow_safe: bool) -> Result<Option<Signature>> {
+        let constness: Option<Token![const]> = input.parse()?;
+        let asyncness: Option<Token![async]> = input.parse()?;
+        let unsafety: Option<Token![unsafe]> = input.parse()?;
+        let safe = allow_safe
+            && unsafety.is_none()
+            && token::parsing::peek_keyword(input.cursor(), "safe");
+        if safe {
+            token::parsing::keyword(input, "safe")?;
+        }
+        let abi: Option<Abi> = input.parse()?;
+        let fn_token: Token![fn] = input.parse()?;
+        let ident: Ident = input.parse()?;
+        let mut generics: Generics = input.parse()?;
 
-            let output: ReturnType = input.parse()?;
-            generics.where_clause = input.parse()?;
+        let content;
+        let paren_token = parenthesized!(content in input);
+        let (inputs, variadic) = parse_fn_args(&content)?;
 
-            Ok(Signature {
+        let output: ReturnType = input.parse()?;
+        generics.where_clause = input.parse()?;
+
+        Ok(if safe {
+            None
+        } else {
+            Some(Signature {
                 constness,
                 asyncness,
                 unsafety,
@@ -1533,7 +1552,7 @@ pub(crate) mod parsing {
                 variadic,
                 output,
             })
-        }
+        })
     }
 
     #[cfg_attr(docsrs, doc(cfg(feature = "parsing")))]
@@ -1829,35 +1848,55 @@ pub(crate) mod parsing {
             let vis: Visibility = ahead.parse()?;
 
             let lookahead = ahead.lookahead1();
-            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead) {
+            let allow_safe = true;
+            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead, allow_safe) {
                 let vis: Visibility = input.parse()?;
-                let sig: Signature = input.parse()?;
-                if input.peek(token::Brace) {
+                let sig = parse_signature(input, allow_safe)?;
+                let has_safe = sig.is_none();
+                let has_body = input.peek(token::Brace);
+                let semi_token: Option<Token![;]> = if has_body {
                     let content;
                     braced!(content in input);
                     content.call(Attribute::parse_inner)?;
                     content.call(Block::parse_within)?;
-
+                    None
+                } else {
+                    Some(input.parse()?)
+                };
+                if has_safe || has_body {
                     Ok(ForeignItem::Verbatim(verbatim::between(&begin, input)))
                 } else {
                     Ok(ForeignItem::Fn(ForeignItemFn {
                         attrs: Vec::new(),
                         vis,
-                        sig,
-                        semi_token: input.parse()?,
+                        sig: sig.unwrap(),
+                        semi_token: semi_token.unwrap(),
                     }))
                 }
-            } else if lookahead.peek(Token![static]) {
+            } else if lookahead.peek(Token![static])
+                || ((ahead.peek(Token![unsafe])
+                    || token::parsing::peek_keyword(ahead.cursor(), "safe"))
+                    && ahead.peek2(Token![static]))
+            {
                 let vis = input.parse()?;
+                let unsafety: Option<Token![unsafe]> = input.parse()?;
+                let safe =
+                    unsafety.is_none() && token::parsing::peek_keyword(input.cursor(), "safe");
+                if safe {
+                    token::parsing::keyword(input, "safe")?;
+                }
                 let static_token = input.parse()?;
                 let mutability = input.parse()?;
                 let ident = input.parse()?;
                 let colon_token = input.parse()?;
                 let ty = input.parse()?;
-                if input.peek(Token![=]) {
+                let has_value = input.peek(Token![=]);
+                if has_value {
                     input.parse::<Token![=]>()?;
                     input.parse::<Expr>()?;
-                    input.parse::<Token![;]>()?;
+                }
+                let semi_token: Token![;] = input.parse()?;
+                if safe || has_value {
                     Ok(ForeignItem::Verbatim(verbatim::between(&begin, input)))
                 } else {
                     Ok(ForeignItem::Static(ForeignItemStatic {
@@ -1868,7 +1907,7 @@ pub(crate) mod parsing {
                         ident,
                         colon_token,
                         ty,
-                        semi_token: input.parse()?,
+                        semi_token,
                     }))
                 }
             } else if lookahead.peek(Token![type]) {
@@ -2300,7 +2339,8 @@ pub(crate) mod parsing {
             let ahead = input.fork();
 
             let lookahead = ahead.lookahead1();
-            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead) {
+            let allow_safe = false;
+            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead, allow_safe) {
                 input.parse().map(TraitItem::Fn)
             } else if lookahead.peek(Token![const]) {
                 let const_token: Token![const] = ahead.parse()?;
@@ -2645,7 +2685,8 @@ pub(crate) mod parsing {
                 None
             };
 
-            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead) {
+            let allow_safe = false;
+            let mut item = if lookahead.peek(Token![fn]) || peek_signature(&ahead, allow_safe) {
                 let allow_omitted_body = true;
                 if let Some(item) = parse_impl_item_fn(input, allow_omitted_body)? {
                     Ok(ImplItem::Fn(item))


Test patch:
diff --git a/tests/repo/mod.rs b/tests/repo/mod.rs
index 9f8a418a..26da5b58 100644
--- a/tests/repo/mod.rs
+++ b/tests/repo/mod.rs
@@ -19,12 +19,6 @@ const REVISION: &str = "5069856495870486134dd2ca0b0e2516308c5c2a";
 
 #[rustfmt::skip]
 static EXCLUDE_FILES: &[&str] = &[
-    // TODO: `unsafe static`, `safe fn`
-    // https://github.com/dtolnay/syn/issues/1675
-    "src/tools/rustfmt/tests/target/unsafe_extern_blocks.rs",
-    "tests/rustdoc/unsafe-extern-blocks.rs",
-    "tests/ui/rust-2024/unsafe-extern-blocks/safe-items.rs",
-
     // TODO: non-lifetime binders: `where for<'a, T> &'a Struct<T>: Trait`
     // https://github.com/dtolnay/syn/issues/1435
     "src/tools/rustfmt/tests/source/issue_5721.rs",
@@ -301,6 +295,7 @@ static EXCLUDE_FILES: &[&str] = &[
     "src/tools/rustfmt/tests/target/configs/spaces_around_ranges/false.rs",
     "src/tools/rustfmt/tests/target/configs/spaces_around_ranges/true.rs",
     "src/tools/rustfmt/tests/target/type.rs",
+    "src/tools/rustfmt/tests/target/unsafe_extern_blocks.rs",
     "tests/run-make/translation/test.rs",
     "tests/ui/generics/issue-94432-garbage-ice.rs",