[go: up one dir, main page]

automod/
lib.rs

1//! [![github]](https://github.com/dtolnay/automod) [![crates-io]](https://crates.io/crates/automod) [![docs-rs]](https://docs.rs/automod)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! **Pull in every source file in a directory as a module.**
10//!
11//! # Syntax
12//!
13//! ```
14//! # const IGNORE: &str = stringify! {
15//! automod::dir!("path/to/directory");
16//! # };
17//! ```
18//!
19//! This macro expands to one or more `mod` items, one for each source file in
20//! the specified directory.
21//!
22//! The path is given relative to the directory containing Cargo.toml.
23//!
24//! It is an error if the given directory contains no source files.
25//!
26//! The macro takes an optional visibility to apply on the generated modules:
27//! `automod::dir!(pub "path/to/directory")`.
28//!
29//! # Example
30//!
31//! Suppose that we would like to keep a directory of regression tests for
32//! individual numbered issues:
33//!
34//! - tests/
35//!   - regression/
36//!     - issue1.rs
37//!     - issue2.rs
38//!     - ...
39//!     - issue128.rs
40//!
41//! We would like to be able to toss files in this directory and have them
42//! automatically tested, without listing them in some explicit list of modules.
43//! Automod solves this by adding *tests/regression.rs* containing:
44//!
45//! ```
46//! # const IGNORE: &str = stringify! {
47//! mod regression {
48//!     automod::dir!("tests/regression");
49//! }
50//! # };
51//! ```
52//!
53//! The macro invocation expands to:
54//!
55//! ```
56//! # const IGNORE: &str = stringify! {
57//! mod issue1;
58//! mod issue2;
59//! /* ... */
60//! mod issue128;
61//! # };
62//! ```
63
64#![doc(html_root_url = "https://docs.rs/automod/1.0.15")]
65#![allow(clippy::enum_glob_use, clippy::needless_pass_by_value)]
66
67extern crate proc_macro;
68
69mod error;
70
71use crate::error::{Error, Result};
72use proc_macro::TokenStream;
73use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
74use quote::quote;
75use std::env;
76use std::ffi::OsStr;
77use std::fs;
78use std::path::{Path, PathBuf};
79use syn::parse::{Parse, ParseStream};
80use syn::{parse_macro_input, LitStr, Visibility};
81
82struct Arg {
83    vis: Visibility,
84    path: LitStr,
85}
86
87impl Parse for Arg {
88    fn parse(input: ParseStream) -> syn::Result<Self> {
89        Ok(Arg {
90            vis: input.parse()?,
91            path: input.parse()?,
92        })
93    }
94}
95
96#[proc_macro]
97pub fn dir(input: TokenStream) -> TokenStream {
98    let input = parse_macro_input!(input as Arg);
99    let vis = &input.vis;
100    let rel_path = input.path.value();
101
102    let dir = match env::var_os("CARGO_MANIFEST_DIR") {
103        Some(manifest_dir) => PathBuf::from(manifest_dir).join(rel_path),
104        None => PathBuf::from(rel_path),
105    };
106
107    let expanded = match source_file_names(dir) {
108        Ok(names) => names.into_iter().map(|name| mod_item(vis, name)).collect(),
109        Err(err) => syn::Error::new(Span::call_site(), err).to_compile_error(),
110    };
111
112    TokenStream::from(expanded)
113}
114
115fn mod_item(vis: &Visibility, name: String) -> TokenStream2 {
116    let mut module_name = name.replace('-', "_");
117    if module_name.starts_with(|ch: char| ch.is_ascii_digit()) {
118        module_name.insert(0, '_');
119    }
120
121    let path = Option::into_iter(if name == module_name {
122        None
123    } else {
124        Some(format!("{}.rs", name))
125    });
126
127    let ident = Ident::new(&module_name, Span::call_site());
128
129    quote! {
130        #(#[path = #path])*
131        #vis mod #ident;
132    }
133}
134
135fn source_file_names<P: AsRef<Path>>(dir: P) -> Result<Vec<String>> {
136    let mut names = Vec::new();
137    let mut failures = Vec::new();
138
139    for entry in fs::read_dir(dir)? {
140        let entry = entry?;
141        if !entry.file_type()?.is_file() {
142            continue;
143        }
144
145        let file_name = entry.file_name();
146        if file_name == "mod.rs" || file_name == "lib.rs" || file_name == "main.rs" {
147            continue;
148        }
149
150        let path = Path::new(&file_name);
151        if path.extension() == Some(OsStr::new("rs")) {
152            match file_name.into_string() {
153                Ok(mut utf8) => {
154                    utf8.truncate(utf8.len() - ".rs".len());
155                    names.push(utf8);
156                }
157                Err(non_utf8) => {
158                    failures.push(non_utf8);
159                }
160            }
161        }
162    }
163
164    failures.sort();
165    if let Some(failure) = failures.into_iter().next() {
166        return Err(Error::Utf8(failure));
167    }
168
169    if names.is_empty() {
170        return Err(Error::Empty);
171    }
172
173    names.sort();
174    Ok(names)
175}