proc_macro_crate/
lib.rs

1/*!
2
3[![](https://docs.rs/proc-macro-crate/badge.svg)](https://docs.rs/proc-macro-crate/) [![](https://img.shields.io/crates/v/proc-macro-crate.svg)](https://crates.io/crates/proc-macro-crate) [![](https://img.shields.io/crates/d/proc-macro-crate.png)](https://crates.io/crates/proc-macro-crate) [![Build Status](https://travis-ci.org/bkchr/proc-macro-crate.png?branch=master)](https://travis-ci.org/bkchr/proc-macro-crate)
4
5Providing support for `$crate` in procedural macros.
6
7* [Introduction](#introduction)
8* [Example](#example)
9* [License](#license)
10
11## Introduction
12
13In `macro_rules!` `$crate` is used to get the path of the crate where a macro is declared in. In
14procedural macros there is currently no easy way to get this path. A common hack is to import the
15desired crate with a know name and use this. However, with rust edition 2018 and dropping
16`extern crate` declarations from `lib.rs`, people start to rename crates in `Cargo.toml` directly.
17However, this breaks importing the crate, as the proc-macro developer does not know the renamed
18name of the crate that should be imported.
19
20This crate provides a way to get the name of a crate, even if it renamed in `Cargo.toml`. For this
21purpose a single function `crate_name` is provided. This function needs to be called in the context
22of a proc-macro with the name of the desired crate. `CARGO_MANIFEST_DIR` will be used to find the
23current active `Cargo.toml` and this `Cargo.toml` is searched for the desired crate.
24
25## Example
26
27```
28use quote::quote;
29use syn::Ident;
30use proc_macro2::Span;
31use proc_macro_crate::{crate_name, FoundCrate};
32
33fn import_my_crate() {
34    let found_crate = crate_name("my-crate").expect("my-crate is present in `Cargo.toml`");
35
36    match found_crate {
37        FoundCrate::Itself => quote!( crate::Something ),
38        FoundCrate::Name(name) => {
39            let ident = Ident::new(&name, Span::call_site());
40            quote!( #ident::Something )
41        }
42    };
43}
44
45# fn main() {}
46```
47
48## License
49
50Licensed under either of
51
52 * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
53
54 * [MIT license](http://opensource.org/licenses/MIT)
55
56at your option.
57*/
58
59use std::{
60    collections::HashMap,
61    env,
62    fs::File,
63    io::{self, Read},
64    path::{Path, PathBuf},
65};
66
67use toml::{self, value::Table};
68
69type CargoToml = HashMap<String, toml::Value>;
70
71/// Error type used by this crate.
72#[derive(Debug, thiserror::Error)]
73pub enum Error {
74    #[error("Could not find `Cargo.toml` in manifest dir: `{0}`.")]
75    NotFound(PathBuf),
76    #[error("`CARGO_MANIFEST_DIR` env variable not set.")]
77    CargoManifestDirNotSet,
78    #[error("Could not read `{path}`.")]
79    CouldNotRead { path: PathBuf, source: io::Error },
80    #[error("Invalid toml file.")]
81    InvalidToml { source: toml::de::Error },
82    #[error("Could not find `{crate_name}` in `dependencies` or `dev-dependencies` in `{path}`!")]
83    CrateNotFound { crate_name: String, path: PathBuf },
84}
85
86/// The crate as found by [`crate_name`].
87#[derive(Debug, PartialEq, Clone, Eq)]
88pub enum FoundCrate {
89    /// The searched crate is this crate itself.
90    Itself,
91    /// The searched crate was found with this name.
92    Name(String),
93}
94
95/// Find the crate name for the given `orig_name` in the current `Cargo.toml`.
96///
97/// `orig_name` should be the original name of the searched crate.
98///
99/// The current `Cargo.toml` is determined by taking `CARGO_MANIFEST_DIR/Cargo.toml`.
100///
101/// # Returns
102///
103/// - `Ok(orig_name)` if the crate was found, but not renamed in the `Cargo.toml`.
104/// - `Ok(RENAMED)` if the crate was found, but is renamed in the `Cargo.toml`. `RENAMED` will be
105/// the renamed name.
106/// - `Err` if an error occurred.
107///
108/// The returned crate name is sanitized in such a way that it is a valid rust identifier. Thus,
109/// it is ready to be used in `extern crate` as identifier.
110pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
111    let manifest_dir =
112        PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?);
113
114    let cargo_toml_path = manifest_dir.join("Cargo.toml");
115
116    if !cargo_toml_path.exists() {
117        return Err(Error::NotFound(manifest_dir.into()));
118    }
119
120    let cargo_toml = open_cargo_toml(&cargo_toml_path)?;
121
122    extract_crate_name(orig_name, cargo_toml, &cargo_toml_path)
123}
124
125/// Make sure that the given crate name is a valid rust identifier.
126fn sanitize_crate_name(name: String) -> String {
127    name.replace("-", "_")
128}
129
130/// Open the given `Cargo.toml` and parse it into a hashmap.
131fn open_cargo_toml(path: &Path) -> Result<CargoToml, Error> {
132    let mut content = String::new();
133    File::open(path)
134        .map_err(|e| Error::CouldNotRead {
135            source: e,
136            path: path.into(),
137        })?
138        .read_to_string(&mut content)
139        .map_err(|e| Error::CouldNotRead {
140            source: e,
141            path: path.into(),
142        })?;
143    toml::from_str(&content).map_err(|e| Error::InvalidToml { source: e })
144}
145
146/// Extract the crate name for the given `orig_name` from the given `Cargo.toml` by checking the
147/// `dependencies` and `dev-dependencies`.
148///
149/// Returns `Ok(orig_name)` if the crate is not renamed in the `Cargo.toml` or otherwise
150/// the renamed identifier.
151fn extract_crate_name(
152    orig_name: &str,
153    mut cargo_toml: CargoToml,
154    cargo_toml_path: &Path,
155) -> Result<FoundCrate, Error> {
156    if let Some(toml::Value::Table(t)) = cargo_toml.get("package") {
157        if let Some(toml::Value::String(s)) = t.get("name") {
158            if s == orig_name {
159                return Ok(FoundCrate::Itself);
160            }
161        }
162    }
163
164    if let Some(name) = ["dependencies", "dev-dependencies"]
165        .iter()
166        .find_map(|k| search_crate_at_key(k, orig_name, &mut cargo_toml))
167    {
168        return Ok(FoundCrate::Name(sanitize_crate_name(name)));
169    }
170
171    // Start searching `target.xy.dependencies`
172    if let Some(name) = cargo_toml
173        .remove("target")
174        .and_then(|t| t.try_into::<Table>().ok())
175        .and_then(|t| {
176            t.values()
177                .filter_map(|v| v.as_table())
178                .filter_map(|t| t.get("dependencies").and_then(|t| t.as_table()))
179                .find_map(|t| extract_crate_name_from_deps(orig_name, t.clone()))
180        })
181    {
182        return Ok(FoundCrate::Name(sanitize_crate_name(name)));
183    }
184
185    Err(Error::CrateNotFound {
186        crate_name: orig_name.into(),
187        path: cargo_toml_path.into(),
188    })
189}
190
191/// Search the `orig_name` crate at the given `key` in `cargo_toml`.
192fn search_crate_at_key(key: &str, orig_name: &str, cargo_toml: &mut CargoToml) -> Option<String> {
193    cargo_toml
194        .remove(key)
195        .and_then(|v| v.try_into::<Table>().ok())
196        .and_then(|t| extract_crate_name_from_deps(orig_name, t))
197}
198
199/// Extract the crate name from the given dependencies.
200///
201/// Returns `Some(orig_name)` if the crate is not renamed in the `Cargo.toml` or otherwise
202/// the renamed identifier.
203fn extract_crate_name_from_deps(orig_name: &str, deps: Table) -> Option<String> {
204    for (key, value) in deps.into_iter() {
205        let renamed = value
206            .try_into::<Table>()
207            .ok()
208            .and_then(|t| t.get("package").cloned())
209            .map(|t| t.as_str() == Some(orig_name))
210            .unwrap_or(false);
211
212        if key == orig_name || renamed {
213            return Some(key.clone());
214        }
215    }
216
217    None
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    macro_rules! create_test {
225        (
226            $name:ident,
227            $cargo_toml:expr,
228            $( $result:tt )*
229        ) => {
230            #[test]
231            fn $name() {
232                let cargo_toml = toml::from_str($cargo_toml).expect("Parses `Cargo.toml`");
233                let path = PathBuf::from("test-path");
234
235                match extract_crate_name("my_crate", cargo_toml, &path) {
236                    $( $result )* => (),
237                    o => panic!("Invalid result: {:?}", o),
238                }
239            }
240        };
241    }
242
243    create_test! {
244        deps_with_crate,
245        r#"
246            [dependencies]
247            my_crate = "0.1"
248        "#,
249        Ok(FoundCrate::Name(name)) if name == "my_crate"
250    }
251
252    create_test! {
253        dev_deps_with_crate,
254        r#"
255            [dev-dependencies]
256            my_crate = "0.1"
257        "#,
258        Ok(FoundCrate::Name(name)) if name == "my_crate"
259    }
260
261    create_test! {
262        deps_with_crate_renamed,
263        r#"
264            [dependencies]
265            cool = { package = "my_crate", version = "0.1" }
266        "#,
267        Ok(FoundCrate::Name(name)) if name == "cool"
268    }
269
270    create_test! {
271        deps_with_crate_renamed_second,
272        r#"
273            [dependencies.cool]
274            package = "my_crate"
275            version = "0.1"
276        "#,
277        Ok(FoundCrate::Name(name)) if name == "cool"
278    }
279
280    create_test! {
281        deps_empty,
282        r#"
283            [dependencies]
284        "#,
285        Err(Error::CrateNotFound {
286            crate_name,
287            path,
288        }) if crate_name == "my_crate" && path.display().to_string() == "test-path"
289    }
290
291    create_test! {
292        crate_not_found,
293        r#"
294            [dependencies]
295            serde = "1.0"
296        "#,
297        Err(Error::CrateNotFound {
298            crate_name,
299            path,
300        }) if crate_name == "my_crate" && path.display().to_string() == "test-path"
301    }
302
303    create_test! {
304        target_dependency,
305        r#"
306            [target.'cfg(target_os="android")'.dependencies]
307            my_crate = "0.1"
308        "#,
309        Ok(FoundCrate::Name(name)) if name == "my_crate"
310    }
311
312    create_test! {
313        target_dependency2,
314        r#"
315            [target.x86_64-pc-windows-gnu.dependencies]
316            my_crate = "0.1"
317        "#,
318        Ok(FoundCrate::Name(name)) if name == "my_crate"
319    }
320
321    create_test! {
322        own_crate,
323        r#"
324            [package]
325            name = "my_crate"
326        "#,
327        Ok(FoundCrate::Itself)
328    }
329}