1#![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}