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## Edge cases
49
50There are multiple edge cases when it comes to determining the correct crate. If you for example
51import a crate as its own dependency, like this:
52
53```toml
54[package]
55name = "my_crate"
56
57[dev-dependencies]
58my_crate = { version = "0.1", features = [ "test-feature" ] }
59```
60
61The crate will return `FoundCrate::Itself` and you will not be able to find the other instance
62of your crate in `dev-dependencies`. Other similar cases are when one crate is imported multiple
63times:
64
65```toml
66[package]
67name = "my_crate"
68
69[dependencies]
70some-crate = { version = "0.5" }
71some-crate-old = { package = "some-crate", version = "0.1" }
72```
73
74When searching for `some-crate` in this `Cargo.toml` it will return `FoundCrate::Name("some_old_crate")`,
75aka the last definition of the crate in the `Cargo.toml`.
76
77## License
78
79Licensed under either of
80
81 * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
82
83 * [MIT license](https://opensource.org/licenses/MIT)
84
85at your option.
86*/
87
88use std::{
89    collections::btree_map::{self, BTreeMap},
90    env, fmt, fs, io,
91    path::{Path, PathBuf},
92    sync::Mutex,
93    time::SystemTime,
94};
95
96use once_cell::sync::Lazy;
97use toml_edit::{Document, Item, Table, TomlError};
98
99/// Error type used by this crate.
100#[derive(Debug)]
101pub enum Error {
102    NotFound(PathBuf),
103    CargoManifestDirNotSet,
104    CouldNotRead { path: PathBuf, source: io::Error },
105    InvalidToml { source: TomlError },
106    CrateNotFound { crate_name: String, path: PathBuf },
107}
108
109impl std::error::Error for Error {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Error::CouldNotRead { source, .. } => Some(source),
113            Error::InvalidToml { source } => Some(source),
114            _ => None,
115        }
116    }
117}
118
119impl fmt::Display for Error {
120    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121        match self {
122            Error::NotFound(path) => write!(
123                f,
124                "Could not find `Cargo.toml` in manifest dir: `{}`.",
125                path.display()
126            ),
127            Error::CargoManifestDirNotSet => {
128                f.write_str("`CARGO_MANIFEST_DIR` env variable not set.")
129            }
130            Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
131            Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
132            Error::CrateNotFound { crate_name, path } => write!(
133                f,
134                "Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
135                crate_name,
136                path.display(),
137            ),
138        }
139    }
140}
141
142/// The crate as found by [`crate_name`].
143#[derive(Debug, PartialEq, Clone, Eq)]
144pub enum FoundCrate {
145    /// The searched crate is this crate itself.
146    Itself,
147    /// The searched crate was found with this name.
148    Name(String),
149}
150
151// In a rustc invocation, there will only ever be one entry in this map, since every crate is
152// compiled with its own rustc process. However, the same is not (currently) the case for
153// rust-analyzer.
154type Cache = BTreeMap<String, CacheEntry>;
155
156struct CacheEntry {
157    manifest_ts: SystemTime,
158    crate_names: CrateNames,
159}
160
161type CrateNames = BTreeMap<String, FoundCrate>;
162
163/// Find the crate name for the given `orig_name` in the current `Cargo.toml`.
164///
165/// `orig_name` should be the original name of the searched crate.
166///
167/// The current `Cargo.toml` is determined by taking `CARGO_MANIFEST_DIR/Cargo.toml`.
168///
169/// # Returns
170///
171/// - `Ok(orig_name)` if the crate was found, but not renamed in the `Cargo.toml`.
172/// - `Ok(RENAMED)` if the crate was found, but is renamed in the `Cargo.toml`. `RENAMED` will be
173/// the renamed name.
174/// - `Err` if an error occurred.
175///
176/// The returned crate name is sanitized in such a way that it is a valid rust identifier. Thus,
177/// it is ready to be used in `extern crate` as identifier.
178pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
179    let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
180    let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
181    let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
182
183    // This `Lazy<Mutex<_>>` can just be a `Mutex<_>` starting in Rust 1.63:
184    // https://doc.rust-lang.org/beta/std/sync/struct.Mutex.html#method.new
185    static CACHE: Lazy<Mutex<Cache>> = Lazy::new(Mutex::default);
186    let mut cache = CACHE.lock().unwrap();
187
188    let crate_names = match cache.entry(manifest_dir) {
189        btree_map::Entry::Occupied(entry) => {
190            let cache_entry = entry.into_mut();
191
192            // Timestamp changed, rebuild this cache entry.
193            if manifest_ts != cache_entry.manifest_ts {
194                *cache_entry = read_cargo_toml(&manifest_path, manifest_ts)?;
195            }
196
197            &cache_entry.crate_names
198        }
199        btree_map::Entry::Vacant(entry) => {
200            let cache_entry = entry.insert(read_cargo_toml(&manifest_path, manifest_ts)?);
201            &cache_entry.crate_names
202        }
203    };
204
205    Ok(crate_names
206        .get(orig_name)
207        .ok_or_else(|| Error::CrateNotFound {
208            crate_name: orig_name.to_owned(),
209            path: manifest_path,
210        })?
211        .clone())
212}
213
214fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
215    fs::metadata(manifest_path)
216        .and_then(|meta| meta.modified())
217        .map_err(|source| {
218            if source.kind() == io::ErrorKind::NotFound {
219                Error::NotFound(manifest_path.to_owned())
220            } else {
221                Error::CouldNotRead {
222                    path: manifest_path.to_owned(),
223                    source,
224                }
225            }
226        })
227}
228
229fn read_cargo_toml(manifest_path: &Path, manifest_ts: SystemTime) -> Result<CacheEntry, Error> {
230    let manifest = open_cargo_toml(manifest_path)?;
231    let crate_names = extract_crate_names(&manifest)?;
232
233    Ok(CacheEntry {
234        manifest_ts,
235        crate_names,
236    })
237}
238
239/// Make sure that the given crate name is a valid rust identifier.
240fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
241    name.as_ref().replace('-', "_")
242}
243
244/// Open the given `Cargo.toml` and parse it into a hashmap.
245fn open_cargo_toml(path: &Path) -> Result<Document, Error> {
246    let content = fs::read_to_string(path).map_err(|e| Error::CouldNotRead {
247        source: e,
248        path: path.into(),
249    })?;
250    content
251        .parse::<Document>()
252        .map_err(|e| Error::InvalidToml { source: e })
253}
254
255/// Extract all crate names from the given `Cargo.toml` by checking the `dependencies` and
256/// `dev-dependencies`.
257fn extract_crate_names(cargo_toml: &Document) -> Result<CrateNames, Error> {
258    let package_name = extract_package_name(cargo_toml);
259    let root_pkg = package_name.as_ref().map(|name| {
260        let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
261            // We're running for a library/binary crate
262            None => FoundCrate::Itself,
263            // We're running for an integration test
264            Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
265        };
266
267        (name.to_string(), cr)
268    });
269
270    let dep_tables = dep_tables(cargo_toml).chain(target_dep_tables(cargo_toml));
271    let dep_pkgs = dep_tables.flatten().filter_map(move |(dep_name, dep_value)| {
272        let pkg_name = dep_value
273            .get("package")
274            .and_then(|i| i.as_str())
275            .unwrap_or(dep_name);
276
277        if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
278            return None;
279        }
280
281        let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
282
283        Some((pkg_name.to_owned(), cr))
284    });
285
286    Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
287}
288
289fn extract_package_name(cargo_toml: &Document) -> Option<&str> {
290    cargo_toml.get("package")?.get("name")?.as_str()
291}
292
293fn target_dep_tables(cargo_toml: &Document) -> impl Iterator<Item = &Table> {
294    cargo_toml
295        .get("target")
296        .into_iter()
297        .filter_map(Item::as_table)
298        .flat_map(|t| {
299            t.iter()
300                .map(|(_, value)| value)
301                .filter_map(Item::as_table)
302                .flat_map(dep_tables)
303        })
304}
305
306fn dep_tables(table: &Table) -> impl Iterator<Item = &Table> {
307    table
308        .get("dependencies")
309        .into_iter()
310        .chain(table.get("dev-dependencies"))
311        .filter_map(Item::as_table)
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    macro_rules! create_test {
319        (
320            $name:ident,
321            $cargo_toml:expr,
322            $( $result:tt )*
323        ) => {
324            #[test]
325            fn $name() {
326                let cargo_toml = $cargo_toml.parse::<Document>().expect("Parses `Cargo.toml`");
327
328               match extract_crate_names(&cargo_toml).map(|mut map| map.remove("my_crate")) {
329                    $( $result )* => (),
330                    o => panic!("Invalid result: {:?}", o),
331                }
332            }
333        };
334    }
335
336    create_test! {
337        deps_with_crate,
338        r#"
339            [dependencies]
340            my_crate = "0.1"
341        "#,
342        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
343    }
344
345    create_test! {
346        dev_deps_with_crate,
347        r#"
348            [dev-dependencies]
349            my_crate = "0.1"
350        "#,
351        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
352    }
353
354    create_test! {
355        deps_with_crate_renamed,
356        r#"
357            [dependencies]
358            cool = { package = "my_crate", version = "0.1" }
359        "#,
360        Ok(Some(FoundCrate::Name(name))) if name == "cool"
361    }
362
363    create_test! {
364        deps_with_crate_renamed_second,
365        r#"
366            [dependencies.cool]
367            package = "my_crate"
368            version = "0.1"
369        "#,
370        Ok(Some(FoundCrate::Name(name))) if name == "cool"
371    }
372
373    create_test! {
374        deps_empty,
375        r#"
376            [dependencies]
377        "#,
378        Ok(None)
379    }
380
381    create_test! {
382        crate_not_found,
383        r#"
384            [dependencies]
385            serde = "1.0"
386        "#,
387        Ok(None)
388    }
389
390    create_test! {
391        target_dependency,
392        r#"
393            [target.'cfg(target_os="android")'.dependencies]
394            my_crate = "0.1"
395        "#,
396        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
397    }
398
399    create_test! {
400        target_dependency2,
401        r#"
402            [target.x86_64-pc-windows-gnu.dependencies]
403            my_crate = "0.1"
404        "#,
405        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
406    }
407
408    create_test! {
409        own_crate,
410        r#"
411            [package]
412            name = "my_crate"
413        "#,
414        Ok(Some(FoundCrate::Itself))
415    }
416
417    create_test! {
418        own_crate_and_in_deps,
419        r#"
420            [package]
421            name = "my_crate"
422
423            [dev-dependencies]
424            my_crate = "0.1"
425        "#,
426        Ok(Some(FoundCrate::Itself))
427    }
428
429    create_test! {
430        multiple_times,
431        r#"
432            [dependencies]
433            my_crate = { version = "0.5" }
434            my-crate-old = { package = "my_crate", version = "0.1" }
435        "#,
436        Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
437    }
438}