blob: c873c917b76e2c33c2fa68d4f9770d708a9a0e5d [file] [log] [blame] [edit]
//! [![github]](https://github.com/dtolnay/automod) [![crates-io]](https://crates.io/crates/automod) [![docs-rs]](https://docs.rs/automod)
//!
//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K
//!
//! <br>
//!
//! **Pull in every source file in a directory as a module.**
//!
//! # Syntax
//!
//! ```
//! # const IGNORE: &str = stringify! {
//! automod::dir!("path/to/directory");
//! # };
//! ```
//!
//! This macro expands to one or more `mod` items, one for each source file in
//! the specified directory.
//!
//! The path is given relative to the directory containing Cargo.toml.
//!
//! It is an error if the given directory contains no source files.
//!
//! # Example
//!
//! Suppose that we would like to keep a directory of regression tests for
//! individual numbered issues:
//!
//! - tests/
//! - regression/
//! - issue1.rs
//! - issue2.rs
//! - ...
//! - issue128.rs
//!
//! We would like to be able to toss files in this directory and have them
//! automatically tested, without listing them in some explicit list of modules.
//! Automod solves this by adding *tests/regression.rs* containing:
//!
//! ```
//! # const IGNORE: &str = stringify! {
//! mod regression {
//! automod::dir!("tests/regression");
//! }
//! # };
//! ```
//!
//! The macro invocation expands to:
//!
//! ```
//! # const IGNORE: &str = stringify! {
//! mod issue1;
//! mod issue2;
//! /* ... */
//! mod issue128;
//! # };
//! ```
#![allow(clippy::enum_glob_use, clippy::needless_pass_by_value)]
extern crate proc_macro;
mod error;
use crate::error::{Error, Result};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::quote;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, LitStr, Visibility};
struct Arg {
vis: Visibility,
path: LitStr,
}
impl Parse for Arg {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Arg {
vis: input.parse()?,
path: input.parse()?,
})
}
}
#[proc_macro]
pub fn dir(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as Arg);
let vis = &input.vis;
let rel_path = input.path.value();
let dir = match env::var_os("CARGO_MANIFEST_DIR") {
Some(manifest_dir) => PathBuf::from(manifest_dir).join(rel_path),
None => PathBuf::from(rel_path),
};
let expanded = match source_file_names(dir) {
Ok(names) => names.into_iter().map(|name| mod_item(vis, name)).collect(),
Err(err) => syn::Error::new(Span::call_site(), err).to_compile_error(),
};
TokenStream::from(expanded)
}
fn mod_item(vis: &Visibility, name: String) -> TokenStream2 {
if name.contains('-') {
let path = format!("{}.rs", name);
let ident = Ident::new(&name.replace('-', "_"), Span::call_site());
quote! {
#[path = #path]
#vis mod #ident;
}
} else {
let ident = Ident::new(&name, Span::call_site());
quote! {
#vis mod #ident;
}
}
}
fn source_file_names<P: AsRef<Path>>(dir: P) -> Result<Vec<String>> {
let mut names = Vec::new();
let mut failures = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let file_name = entry.file_name();
if file_name == "mod.rs" || file_name == "lib.rs" || file_name == "main.rs" {
continue;
}
let path = Path::new(&file_name);
if path.extension() == Some(OsStr::new("rs")) {
match file_name.into_string() {
Ok(mut utf8) => {
utf8.truncate(utf8.len() - ".rs".len());
names.push(utf8);
}
Err(non_utf8) => {
failures.push(non_utf8);
}
}
}
}
failures.sort();
if let Some(failure) = failures.into_iter().next() {
return Err(Error::Utf8(failure));
}
if names.is_empty() {
return Err(Error::Empty);
}
names.sort();
Ok(names)
}