use proc_macro2::{Delimiter, Group, 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,
pub generic: bool,
}
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));
}
}
let generic = is_punct(it.peek(), '<');
Ok(Head {
kind,
ident,
generic,
})
}
#[derive(Clone, Debug)]
pub(crate) struct OuterAttr {
pub at: usize,
pub leading_colon: bool,
pub segments: Vec<Ident>,
inner: Vec<TokenTree>,
path_len: usize,
pub args: Option<Group>,
}
impl OuterAttr {
pub fn is_bare(&self, name: &str) -> bool {
!self.leading_colon && self.segments.len() == 1 && self.segments[0] == name
}
pub fn is_owned_by(&self, owned: &crate::helper::OwnedAttrs) -> bool {
!self.leading_colon
&& self.segments.first().map_or(false, |s| {
owned.contains(s.to_string().trim_start_matches("r#"))
})
}
pub fn with_args(&self, args: TokenStream) -> TokenStream {
let mut out: TokenStream = self.inner[..self.path_len].iter().cloned().collect();
if !args.is_empty() {
out.extend([TokenTree::Group(Group::new(Delimiter::Parenthesis, args))]);
}
out
}
}
pub(crate) struct Outer {
pub tts: Vec<TokenTree>,
pub attrs: Vec<OuterAttr>,
}
impl Outer {
pub fn attrs_end(&self) -> usize {
self.attrs.last().map_or(0, |a| a.at + 2)
}
}
pub(crate) fn starts_with_attr(tokens: &TokenStream) -> bool {
is_punct(Flat::new(tokens.clone()).next().as_ref(), '#')
}
fn parse_attr(at: usize, group: &Group) -> OuterAttr {
let inner: Vec<TokenTree> = group.stream().into_iter().collect();
let colons = |i: usize| is_punct(inner.get(i), ':') && is_punct(inner.get(i + 1), ':');
let leading_colon = colons(0);
let mut i = if leading_colon { 2 } else { 0 };
let mut segments = Vec::new();
let mut dirty = false;
while let Some(TokenTree::Ident(id)) = inner.get(i) {
segments.push(id.clone());
i += 1;
if colons(i) {
i += 2;
dirty = !matches!(inner.get(i), Some(TokenTree::Ident(_)));
} else {
break;
}
}
let path_len = i;
let mut args = None;
match inner.get(path_len) {
None => {}
Some(TokenTree::Punct(p)) if p.as_char() == '=' => {}
Some(TokenTree::Group(g)) if inner.len() == path_len + 1 => args = Some(g.clone()),
_ => dirty = true,
}
if dirty || segments.is_empty() {
segments.clear();
args = None;
}
OuterAttr {
at,
leading_colon,
segments,
inner,
path_len,
args,
}
}
pub(crate) fn outer(tokens: &TokenStream) -> Outer {
let tts: Vec<TokenTree> = Flat::new(tokens.clone()).collect();
let mut attrs = Vec::new();
let mut at = 0;
while is_punct(tts.get(at), '#') {
match tts.get(at + 1) {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {
attrs.push(parse_attr(at, g));
at += 2;
}
_ => break,
}
}
Outer { tts, attrs }
}
#[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 generics_are_told_by_the_token_after_the_name() {
for (src, generic) in [
("struct S;", false),
("pub struct S { a: u8 }", false),
("struct S(u8);", false),
("struct S where u8: Copy;", false),
("type S = Vec<u8>;", false),
("struct S<T>(T);", true),
("pub enum S<'a> { A(&'a u8) }", true),
("union S<T: Copy> { a: T }", true),
("trait S<T> {}", true),
("#[a] type S<T> = Vec<T>;", true),
] {
assert_eq!(head(&ts(src)).unwrap().generic, generic, "{}", src);
}
}
#[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());
}
}