Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump to 2018, add optional 2nd arg, use spans and quote, add wrap macro for stable rust #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
[package]
authors = ["Dana Jansens <danakj@orodu.net>"]
authors = [
"Dana Jansens <danakj@orodu.net>",
"Ryan Butler <thebutlah@gmail.com>",
]
description = "An attribute macro to specify a path to a module dynamically."
license = "MIT/Apache-2.0"
name = "dynpath"
version = "0.1.4"
version = "0.1.5"
repository = "https://github.com/danakj/dynpath"
homepage = "https://github.com/danakj/dynpath"
documentation = "https://docs.rs/dynpath"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
syn = {version = "1", features = ["full"]}
syn = { version = "1", features = ["full"] }
31 changes: 31 additions & 0 deletions src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use syn::spanned::Spanned;

pub enum Suffix<'a> {
Mod(&'a syn::ItemMod),
Literal(&'a syn::LitStr),
}

pub fn get_modpath(env_var: &syn::NestedMeta, suffix: Suffix) -> syn::Result<String> {
let env_var = match env_var {
syn::NestedMeta::Lit(syn::Lit::Str(lit)) => lit.value(),
_ => {
return Err(syn::Error::new(
env_var.span(),
"Argument should be the name of an environment variable, e.g. `\"OUT_DIR\"`",
));
}
};
let prefix = std::env::var(&env_var)
.unwrap_or_else(|_| panic!("The \"{}\" environment variable is not set", &env_var));

let modpath = std::path::PathBuf::from(prefix);
let modpath = match suffix {
Suffix::Mod(m) => {
let modname = m.ident.to_string();
modpath.join(format!("{}.rs", modname))
}
Suffix::Literal(l) => modpath.join(l.value()),
};

Ok(format!("{}", modpath.display()))
}
153 changes: 99 additions & 54 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,122 @@
//! The primary purpose of this crate is to include bindgen-generated bindings
//! without an `include!()` statement. This allows for code completion and
//! cross-references.
//!
//! The macro takes a single parameter which is the name of an environment
//! variable to read the path from, and it appends the module name and `.rs`
//! extension onto the contents of the variable.
//!
//! # Example
//! ```
//! // Turns into `#[path = "whatever/is/in/OUT_DIR/bindings.rs"]`.
//! #[dynpath("OUT_DIR")]
//! mod bindings;
//! ```

#![deny(clippy::all)]

extern crate proc_macro;
extern crate proc_macro2;
extern crate quote;
extern crate syn;
mod helpers;
mod parse;

use proc_macro::*;
use quote::{quote, ToTokens};
use syn::parse_macro_input;
use parse::WrapArgs;
use proc_macro2::Span;
use quote::quote;
use syn::{parse_macro_input, parse_quote, spanned::Spanned};

macro_rules! tokens {
($($expr:expr),* $(,)?) => {
vec![$($expr,)*].into_iter().collect::<TokenStream>()
}
}
use crate::helpers::{get_modpath, Suffix};

/// See the crate documentation for how to use the `#[dynpath()]` macro.
const CRATE_NAME: &str = env!("CARGO_PKG_NAME");

/// Attaches `#[path = ..]` to an existing mod dynamically.
///
/// NOTE: This macro requires you to use the nightly
/// [`proc_macro_hygiene`](https://github.com/rust-lang/rust/issues/54727) feature.
///
/// # Arguments
/// * First argument: The name of an environment variable to read the path from
/// * Second argument: The string to be concatenated to the first argument. If not
/// provided, it defaults to the name of the module the attribute is on.
///
/// # Example
/// ```ignore
/// #![feature(proc_macro_hygiene)]
/// // Turns into `#[path = "whatever/is/in/OUT_DIR/bindings.rs"]`.
/// #[dynpath("OUT_DIR")]
/// mod bindings;
/// ```
///
/// ```ignore
/// #![feature(proc_macro_hygiene)]
/// // Turns into `#[path = "whatever/is/in/OUT_DIR/generated/mod.rs"]`.
/// #[dynpath("OUT_DIR", "generated/mod.rs")]
/// mod bindings;
/// ```
#[proc_macro_attribute]
pub fn dynpath(attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn dynpath(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let attr = parse_macro_input!(attr as syn::AttributeArgs);
if attr.len() != 1 {
return quote! {
compile_error!("Expected one argument.")
}
.into();
let item = parse_macro_input!(item as syn::ItemMod);

if !(attr.len() == 1 || attr.len() == 2) {
return syn::Error::new(Span::call_site(), "Expected one or two arguments")
.into_compile_error()
.into();
}

let option = match &attr[0] {
syn::NestedMeta::Lit(syn::Lit::Str(lit)) => lit.value(),
let suffix = match attr.get(1) {
None => Suffix::Mod(&item),
Some(syn::NestedMeta::Lit(syn::Lit::Str(lit))) => Suffix::Literal(lit),
_ => {
return quote! {
compile_error!("Argument should be the name of an environment variable, e.g. `\"OUT_DIR\"`")
}
.into();
return syn::Error::new(attr[1].span(), "Expected a string literal")
.into_compile_error()
.into();
}
};

let dir = std::env::var(&option)
.unwrap_or_else(|_| panic!("The \"{}\" environment variable is not set", option));
let modpath = match get_modpath(&attr[0], suffix) {
Ok(s) => s,
Err(e) => return e.into_compile_error().into(),
};

let item = parse_macro_input!(item as syn::ItemMod);
let modname = item.ident.to_string();
quote! {
#[path = #modpath]
#item
}
.into()
}

let modpath = std::path::PathBuf::from(dir).join(format!("{}.rs", modname));
/// Wraps a dynpath statement such that it can be expanded without any nightly features.
///
/// # Example
/// ```ignore
/// // No nightly rust needed!
/// wrap! {
/// // Turns into `#[path = "whatever/is/in/OUT_DIR/bindings.rs"]`.
/// #[dynpath("OUT_DIR")]
/// mod bindings;
/// }
/// ``
#[proc_macro]
pub fn wrap(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
let args = parse_macro_input!(args as WrapArgs);

let stream = vec![
TokenTree::Punct(Punct::new('#', Spacing::Alone)),
TokenTree::Group(Group::new(
Delimiter::Bracket,
tokens![
TokenTree::Ident(Ident::new("path", Span::call_site())),
TokenTree::Punct(Punct::new('=', Spacing::Alone)),
TokenTree::Literal(Literal::string(&modpath.to_string_lossy())),
],
)),
];
// Create a new module
let mod_ident = args.mod_ident;
let vis = args.vis;
let item_mod: syn::ItemMod = parse_quote! {
#vis mod #mod_ident;
};

let item_stream: TokenStream = item.to_token_stream().into();
// Process optional suffix argument
let suffix = if let Some(ref l) = args.dynpath_args.suffix_lit {
Suffix::Literal(l)
} else {
Suffix::Mod(&item_mod)
};

// Compute modpath
let modpath = match get_modpath(&args.dynpath_args.env_var, suffix) {
Ok(p) => p,
Err(e) => return e.into_compile_error().into(),
};

stream.into_iter().chain(item_stream.into_iter()).collect()
let attrs = args.attrs;
// Tokenify it
quote! {
#(#attrs)*
#[path = #modpath]
#item_mod
}
.into()
}
67 changes: 67 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use proc_macro2::Span;
use syn::Token;

pub struct DynpathArgs {
pub env_var: syn::NestedMeta,
pub suffix_lit: Option<syn::LitStr>,
}
impl syn::parse::Parse for DynpathArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let content;
syn::parenthesized!(content in input);
let env_var = content.parse()?;
content.parse::<Token![,]>()?;
let suffix_lit = content.parse()?;
Ok(Self {
env_var,
suffix_lit,
})
}
}

pub struct WrapArgs {
pub vis: syn::Visibility,
pub mod_ident: syn::Ident,
pub dynpath_args: DynpathArgs,
pub attrs: Vec<syn::Attribute>,
}
impl syn::parse::Parse for WrapArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
// Parse and validate that attr statement is correct
let (attr, attrs) = {
let mut attrs = syn::Attribute::parse_outer(input)?;
let idx = if let Some(idx) = attrs.iter().position(|a| {
if let Some(ident) = a.path.get_ident() {
ident.to_string() == crate::CRATE_NAME
} else {
false
}
}) {
idx
} else {
return Err(syn::Error::new(
Span::call_site(),
"Expected a `dynpath` attribute",
));
};
let attr = attrs.remove(idx);
(attr, attrs)
};

// Parse mod statement
let vis: syn::Visibility = input.parse()?;
input.parse::<Token![mod]>()?;
let mod_ident: syn::Ident = input.parse()?;
input.parse::<Token![;]>()?;

// Parse arguments to dynpath
let dynpath_args: DynpathArgs = syn::parse2(attr.tokens)?;

Ok(Self {
vis,
mod_ident,
dynpath_args,
attrs,
})
}
}