flaggy_codegen/
lib.rs

1// Copyright 2015 Axel Rasmussen
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15extern crate proc_macro;
16
17use proc_macro2::TokenStream;
18use quote::quote;
19use syn::punctuated::Punctuated;
20use syn::token::Comma;
21use syn::{
22    parse_macro_input, Expr, FnArg, GenericArgument, Ident, ItemFn, Pat, PathArguments, Type,
23    TypeSlice,
24};
25
26// A special case of `handle_argument`, for command callback arguments which are
27// guaranteed to have exactly one value (this includes required and boolean
28// flags).
29fn handle_required_argument(name: &Ident, ty: &Type) -> (TokenStream, Expr) {
30    (
31        TokenStream::from(quote! {
32            let #name: #ty = take_required(values.get_as(stringify!(#name))?)?;
33        }),
34        Expr::Verbatim(TokenStream::from(quote! { #name })),
35    )
36}
37
38// A special case of `handle_argument`, for command callback arguments which are
39// optional (i.e., which will have exactly 0 or 1 values). This refers mainly to
40// optional flags which can be omitted entirely.
41fn handle_optional_argument(name: &Ident, inner_ty: &Type) -> (TokenStream, Expr) {
42    (
43        TokenStream::from(quote! {
44            let #name: Option<#inner_ty> = take_optional(values.get_as(stringify!(#name))?)?;
45        }),
46        Expr::Verbatim(TokenStream::from(quote! { #name })),
47    )
48}
49
50// A special case of `handle_argument`, for command callback arguments of the
51// form `&[T]`. These should be positional arguments which can have zero or more
52// values.
53fn handle_slice_argument(name: &Ident, slice: &TypeSlice) -> (TokenStream, Expr) {
54    let elem = &slice.elem;
55    (
56        TokenStream::from(quote! {
57            let #name: Vec<#elem> = values.get_as(stringify!(#name))?;
58        }),
59        Expr::Verbatim(TokenStream::from(quote! { #name.as_slice() })),
60    )
61}
62
63// Handles a single callback function argument by generating the code for two
64// pieces (returned separately in a tuple):
65//
66// - The code which extracts the argument from the monolithic `Values` struct.
67// - The `Expr` which we'll pass in to the real command callback with the value.
68fn handle_argument(arg: &FnArg) -> (TokenStream, Expr) {
69    match arg {
70        FnArg::Receiver(_) => panic!("Command callbacks cannot be member functions."),
71        FnArg::Typed(pattern_type) => {
72            match &*pattern_type.pat {
73                Pat::Ident(ident) => {
74                    let ty = &pattern_type.ty;
75                    let name = &ident.ident;
76
77                    match &**ty {
78                        Type::Reference(r) => match r.elem.as_ref() {
79                            Type::Slice(s) => handle_slice_argument(name, s),
80                            _ => panic!("Command callbacks only accept references of slices."),
81                        },
82                        Type::Path(p) => {
83                            if let Some(last) = p.path.segments.last() {
84                                // This is gross, but since we're dealing with an
85                                // AST there isn't another way to differentiate
86                                // between Option and other types...
87                                if last.ident.to_string() == "Option" {
88                                    match &last.arguments {
89                                        PathArguments::AngleBracketed(inner_ty) => {
90                                            if inner_ty.args.len() != 1 {
91                                                panic!("Found unrecognized Option type with multiple generic type arguments");
92                                            }
93                                            match inner_ty.args.last().unwrap() {
94                                            GenericArgument::Type(inner_ty) => return handle_optional_argument(name, &inner_ty),
95                                            _ => panic!("Found unrecognized Option generic type parameter"),
96                                        }
97                                        }
98                                        _ => panic!("Found unrecognized, non-generic Option type"),
99                                    }
100                                }
101                            }
102
103                            handle_required_argument(name, &ty)
104                        }
105                        _ => panic!("Invalid command callback parameter type"),
106                    }
107                }
108                _ => panic!("Command callback function takes an unhandled argument type."),
109            }
110        }
111    }
112}
113
114#[proc_macro_attribute]
115pub fn command_callback(
116    args: proc_macro::TokenStream,
117    input: proc_macro::TokenStream,
118) -> proc_macro::TokenStream {
119    // This attribute macro accepts no arguments.
120    assert!(args.is_empty());
121
122    let input = parse_macro_input!(input as ItemFn);
123    let visibility = input.vis;
124    let constness = input.sig.constness;
125    let unsafety = input.sig.unsafety;
126    let asyncness = input.sig.asyncness;
127    let abi = input.sig.abi;
128    let name = input.sig.ident;
129
130    let args = input.sig.inputs;
131    let output = input.sig.output;
132
133    let block = input.block;
134
135    let mut arg_parsing = TokenStream::new();
136    let mut real_args: Punctuated<Expr, Comma> = Punctuated::new();
137
138    for arg in args.iter() {
139        let (parse, arg) = handle_argument(arg);
140        arg_parsing.extend(parse);
141        real_args.push(arg);
142    }
143
144    TokenStream::from(quote! {
145        #visibility #constness #unsafety #asyncness #abi fn #name(values: Values) #output {
146            #arg_parsing
147            #constness #unsafety #asyncness #abi fn inner(#args) #output #block
148            inner(#real_args)
149        }
150    })
151    .into()
152}