use proc_macro2::{Delimiter, Ident, TokenStream, TokenTree};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Kind {
Struct,
Enum,
Union,
Trait,
Type,
Mod,
}
#[derive(Clone, Debug)]
pub(crate) struct Head {
pub kind: Kind,
pub ident: Ident,
}
const UNSUPPORTED: &str =
"#[announce] supports struct, enum, union, trait, type alias and inline mod items";
const MOD_FILE: &str = "#[announce] supports only inline modules (`mod m { .. }`), not `mod m;`";
struct Flat {
stack: Vec<proc_macro2::token_stream::IntoIter>,
}
impl Flat {
fn new(ts: TokenStream) -> Self {
Flat {
stack: vec![ts.into_iter()],
}
}
}
impl Iterator for Flat {
type Item = TokenTree;
fn next(&mut self) -> Option<TokenTree> {
loop {
match self.stack.last_mut()?.next() {
None => {
self.stack.pop();
}
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::None => {
self.stack.push(g.stream().into_iter())
}
Some(t) => return Some(t),
}
}
}
}
fn is_ident(t: Option<&TokenTree>, word: &str) -> bool {
matches!(t, Some(TokenTree::Ident(i)) if i == word)
}
fn is_punct(t: Option<&TokenTree>, c: char) -> bool {
matches!(t, Some(TokenTree::Punct(p)) if p.as_char() == c)
}
fn is_restriction(inside: TokenStream) -> bool {
match inside.into_iter().next() {
Some(TokenTree::Ident(i)) => i == "crate" || i == "self" || i == "super" || i == "in",
_ => false,
}
}
pub(crate) fn head(tokens: &TokenStream) -> syn::Result<Head> {
let unsupported = || syn::Error::new_spanned(tokens, UNSUPPORTED);
let mut it = Flat::new(tokens.clone()).peekable();
while matches!(it.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '#') {
it.next();
match it.next() {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {}
_ => return Err(unsupported()),
}
}
if is_ident(it.peek(), "pub") {
it.next();
let restricted = matches!(
it.peek(),
Some(TokenTree::Group(g))
if g.delimiter() == Delimiter::Parenthesis && is_restriction(g.stream())
);
if restricted {
it.next();
}
}
let mut word = it.next();
let mut qualified = false;
if is_ident(word.as_ref(), "unsafe") {
qualified = true;
word = it.next();
}
if is_ident(word.as_ref(), "auto") {
qualified = true;
word = it.next();
if !is_ident(word.as_ref(), "trait") {
return Err(unsupported());
}
}
let kind = match &word {
Some(TokenTree::Ident(i)) => match i.to_string().as_str() {
"struct" if !qualified => Kind::Struct,
"enum" if !qualified => Kind::Enum,
"trait" => Kind::Trait,
"type" if !qualified => Kind::Type,
"mod" => Kind::Mod,
"union" if !qualified && matches!(it.peek(), Some(TokenTree::Ident(_))) => Kind::Union,
_ => return Err(unsupported()),
},
_ => return Err(unsupported()),
};
let ident = match it.next() {
Some(TokenTree::Ident(i)) => i,
_ => return Err(unsupported()),
};
if kind == Kind::Mod
&& !matches!(it.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace)
{
if is_punct(it.peek(), ';') || it.peek().is_none() {
return Err(syn::Error::new_spanned(tokens, MOD_FILE));
}
}
Ok(Head { kind, ident })
}
#[cfg(test)]
mod tests {
use super::*;
fn ts(s: &str) -> TokenStream {
s.parse().unwrap()
}
fn ok(s: &str) -> (Kind, String) {
let h = head(&ts(s)).unwrap_or_else(|e| panic!("{}: {}", s, e));
(h.kind, h.ident.to_string())
}
fn err(s: &str) -> String {
head(&ts(s))
.err()
.unwrap_or_else(|| panic!("{}", s))
.to_string()
}
#[test]
fn kinds_and_names() {
assert_eq!(ok("struct S;"), (Kind::Struct, "S".into()));
assert_eq!(ok("pub struct S { a: u8 }"), (Kind::Struct, "S".into()));
assert_eq!(ok("pub(crate) struct S<T>(T);"), (Kind::Struct, "S".into()));
assert_eq!(ok("pub(in a::b) enum E {}"), (Kind::Enum, "E".into()));
assert_eq!(
ok("pub(super) union U { a: u8 }"),
(Kind::Union, "U".into())
);
assert_eq!(ok("pub(self) trait T {}"), (Kind::Trait, "T".into()));
assert_eq!(ok("unsafe trait T {}"), (Kind::Trait, "T".into()));
assert_eq!(ok("pub unsafe auto trait T {}"), (Kind::Trait, "T".into()));
assert_eq!(ok("auto trait T {}"), (Kind::Trait, "T".into()));
assert_eq!(ok("type A = u8;"), (Kind::Type, "A".into()));
assert_eq!(ok("pub type A<T> = Vec<T>;"), (Kind::Type, "A".into()));
assert_eq!(ok("pub mod m { }"), (Kind::Mod, "m".into()));
assert_eq!(ok("mod m { fn f() {} }"), (Kind::Mod, "m".into()));
}
#[test]
fn attributes_come_first() {
assert_eq!(
ok("#[derive(Clone)] #[doc = \"x\"] /// y\n pub struct S;"),
(Kind::Struct, "S".into())
);
assert_eq!(ok("#[a] #[b(c)] trait T {}"), (Kind::Trait, "T".into()));
}
#[test]
fn union_is_contextual() {
assert_eq!(ok("union U { a: u8 }"), (Kind::Union, "U".into()));
assert!(err("union! { }").contains("supports struct"));
assert!(err("union::f();").contains("supports struct"));
}
#[test]
fn raw_identifiers_and_keywords_as_names() {
assert_eq!(ok("struct r#match;"), (Kind::Struct, "r#match".into()));
}
#[test]
fn other_items_are_unsupported() {
for src in [
"fn f() {}",
"pub fn f() {}",
"const C: u8 = 0;",
"static S: u8 = 0;",
"impl S {}",
"use a::b;",
"extern crate a;",
"macro_rules! m { () => {} }",
"m!();",
"unsafe impl T for S {}",
"unsafe fn f() {}",
"unsafe struct S;",
"auto struct S;",
"async fn f() {}",
"union",
"",
"struct",
"#[a] ",
] {
let e = err(src);
assert!(
e.contains("supports struct, enum, union, trait, type alias and inline mod"),
"{}: {}",
src,
e
);
}
}
#[test]
fn a_module_must_be_inline() {
for src in ["mod m;", "pub mod m;", "#[a] mod m;"] {
assert!(err(src).contains("only inline modules"), "{}", src);
}
assert!(head(&ts("mod m { }")).is_ok());
}
#[test]
fn groups_without_delimiters_are_read_in_place() {
use proc_macro2::Group;
let vis = TokenTree::Group(Group::new(Delimiter::None, ts("pub")));
let mut tokens = TokenStream::new();
tokens.extend([vis]);
tokens.extend(ts("struct S;"));
let h = head(&tokens).unwrap();
assert_eq!((h.kind, h.ident.to_string()), (Kind::Struct, "S".into()));
let all = TokenTree::Group(Group::new(Delimiter::None, ts("pub trait T {}")));
let mut tokens = TokenStream::new();
tokens.extend([all]);
assert_eq!(head(&tokens).unwrap().kind, Kind::Trait);
}
#[test]
fn the_error_spans_the_whole_item() {
let e = head(&ts("fn f() {}")).err().unwrap();
let again = syn::Error::new_spanned(ts("fn f() {}"), UNSUPPORTED);
assert_eq!(e.to_string(), again.to_string());
}
}