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    process::Command,
93    sync::Mutex,
94    time::SystemTime,
95};
96
97use toml_edit::{DocumentMut, Item, Table, TomlError};
98
99/// Error type used by this crate.
100pub enum Error {
101    NotFound(PathBuf),
102    CargoManifestDirNotSet,
103    CargoEnvVariableNotSet,
104    FailedGettingWorkspaceManifestPath,
105    CouldNotRead { path: PathBuf, source: io::Error },
106    InvalidToml { source: TomlError },
107    CrateNotFound { crate_name: String, path: PathBuf },
108}
109
110impl std::error::Error for Error {
111    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
112        match self {
113            Error::CouldNotRead { source, .. } => Some(source),
114            Error::InvalidToml { source } => Some(source),
115            _ => None,
116        }
117    }
118}
119
120impl fmt::Debug for Error {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        fmt::Display::fmt(self, f)
123    }
124}
125
126impl fmt::Display for Error {
127    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128        match self {
129            Error::NotFound(path) =>
130                write!(f, "Could not find `Cargo.toml` in manifest dir: `{}`.", path.display()),
131            Error::CargoManifestDirNotSet =>
132                f.write_str("`CARGO_MANIFEST_DIR` env variable not set."),
133            Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
134            Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
135            Error::CrateNotFound { crate_name, path } => write!(
136                f,
137                "Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
138                crate_name,
139                path.display(),
140            ),
141            Error::CargoEnvVariableNotSet => f.write_str("`CARGO` env variable not set."),
142            Error::FailedGettingWorkspaceManifestPath =>
143                f.write_str("Failed to get the path of the workspace manifest path."),
144        }
145    }
146}
147
148/// The crate as found by [`crate_name`].
149#[derive(Debug, PartialEq, Clone, Eq)]
150pub enum FoundCrate {
151    /// The searched crate is this crate itself.
152    Itself,
153    /// The searched crate was found with this name.
154    Name(String),
155}
156
157// In a rustc invocation, there will only ever be one entry in this map, since every crate is
158// compiled with its own rustc process. However, the same is not (currently) the case for
159// rust-analyzer.
160type Cache = BTreeMap<String, CacheEntry>;
161
162struct CacheEntry {
163    manifest_ts: SystemTime,
164    workspace_manifest_ts: SystemTime,
165    workspace_manifest_path: PathBuf,
166    crate_names: CrateNames,
167}
168
169type CrateNames = BTreeMap<String, FoundCrate>;
170
171/// Find the crate name for the given `orig_name` in the current `Cargo.toml`.
172///
173/// `orig_name` should be the original name of the searched crate.
174///
175/// The current `Cargo.toml` is determined by taking `CARGO_MANIFEST_DIR/Cargo.toml`.
176///
177/// # Returns
178///
179/// - `Ok(orig_name)` if the crate was found, but not renamed in the `Cargo.toml`.
180/// - `Ok(RENAMED)` if the crate was found, but is renamed in the `Cargo.toml`. `RENAMED` will be
181/// the renamed name.
182/// - `Err` if an error occurred.
183///
184/// The returned crate name is sanitized in such a way that it is a valid rust identifier. Thus,
185/// it is ready to be used in `extern crate` as identifier.
186pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
187    let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
188    let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
189
190    let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
191
192    static CACHE: Mutex<Cache> = Mutex::new(BTreeMap::new());
193    let mut cache = CACHE.lock().unwrap();
194
195    let crate_names = match cache.entry(manifest_dir) {
196        btree_map::Entry::Occupied(entry) => {
197            let cache_entry = entry.into_mut();
198            let workspace_manifest_path = cache_entry.workspace_manifest_path.as_path();
199            let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
200
201            // Timestamp changed, rebuild this cache entry.
202            if manifest_ts != cache_entry.manifest_ts ||
203                workspace_manifest_ts != cache_entry.workspace_manifest_ts
204            {
205                *cache_entry = read_cargo_toml(
206                    &manifest_path,
207                    &workspace_manifest_path,
208                    manifest_ts,
209                    workspace_manifest_ts,
210                )?;
211            }
212
213            &cache_entry.crate_names
214        },
215        btree_map::Entry::Vacant(entry) => {
216            // If `workspace_manifest_path` returns `None`, we are probably in a vendored deps
217            // folder and cargo complaining that we have some package inside a workspace, that isn't
218            // part of the workspace. In this case we just use the `manifest_path` as the
219            // `workspace_manifest_path`.
220            let workspace_manifest_path =
221                workspace_manifest_path(&manifest_path)?.unwrap_or_else(|| manifest_path.clone());
222            let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
223
224            let cache_entry = entry.insert(read_cargo_toml(
225                &manifest_path,
226                &workspace_manifest_path,
227                manifest_ts,
228                workspace_manifest_ts,
229            )?);
230            &cache_entry.crate_names
231        },
232    };
233
234    Ok(crate_names
235        .get(orig_name)
236        .ok_or_else(|| Error::CrateNotFound {
237            crate_name: orig_name.to_owned(),
238            path: manifest_path,
239        })?
240        .clone())
241}
242
243fn workspace_manifest_path(cargo_toml_manifest: &Path) -> Result<Option<PathBuf>, Error> {
244    let stdout = Command::new(env::var("CARGO").map_err(|_| Error::CargoEnvVariableNotSet)?)
245        .arg("locate-project")
246        .args(&["--workspace", "--message-format=plain"])
247        .arg(format!("--manifest-path={}", cargo_toml_manifest.display()))
248        .output()
249        .map_err(|_| Error::FailedGettingWorkspaceManifestPath)?
250        .stdout;
251
252    String::from_utf8(stdout)
253        .map_err(|_| Error::FailedGettingWorkspaceManifestPath)
254        .map(|s| {
255            let path = s.trim();
256
257            if path.is_empty() {
258                None
259            } else {
260                Some(path.into())
261            }
262        })
263}
264
265fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
266    fs::metadata(manifest_path).and_then(|meta| meta.modified()).map_err(|source| {
267        if source.kind() == io::ErrorKind::NotFound {
268            Error::NotFound(manifest_path.to_owned())
269        } else {
270            Error::CouldNotRead { path: manifest_path.to_owned(), source }
271        }
272    })
273}
274
275fn read_cargo_toml(
276    manifest_path: &Path,
277    workspace_manifest_path: &Path,
278    manifest_ts: SystemTime,
279    workspace_manifest_ts: SystemTime,
280) -> Result<CacheEntry, Error> {
281    let manifest = open_cargo_toml(manifest_path)?;
282
283    let workspace_dependencies = if manifest_path != workspace_manifest_path {
284        let workspace_manifest = open_cargo_toml(workspace_manifest_path)?;
285        extract_workspace_dependencies(&workspace_manifest)?
286    } else {
287        extract_workspace_dependencies(&manifest)?
288    };
289
290    let crate_names = extract_crate_names(&manifest, workspace_dependencies)?;
291
292    Ok(CacheEntry {
293        manifest_ts,
294        workspace_manifest_ts,
295        crate_names,
296        workspace_manifest_path: workspace_manifest_path.to_path_buf(),
297    })
298}
299
300/// Extract all `[workspace.dependencies]`.
301///
302/// Returns a hash map that maps from dep name to the package name. Dep name
303/// and package name can be the same if there doesn't exist any rename.
304fn extract_workspace_dependencies(
305    workspace_toml: &DocumentMut,
306) -> Result<BTreeMap<String, String>, Error> {
307    Ok(workspace_dep_tables(&workspace_toml)
308        .into_iter()
309        .flatten()
310        .map(move |(dep_name, dep_value)| {
311            let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
312
313            (dep_name.to_owned(), pkg_name.to_owned())
314        })
315        .collect())
316}
317
318/// Return an iterator over all `[workspace.dependencies]`
319fn workspace_dep_tables(cargo_toml: &DocumentMut) -> Option<&Table> {
320    cargo_toml
321        .get("workspace")
322        .and_then(|w| w.as_table()?.get("dependencies")?.as_table())
323}
324
325/// Make sure that the given crate name is a valid rust identifier.
326fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
327    name.as_ref().replace('-', "_")
328}
329
330/// Open the given `Cargo.toml` and parse it into a hashmap.
331fn open_cargo_toml(path: &Path) -> Result<DocumentMut, Error> {
332    let content = fs::read_to_string(path)
333        .map_err(|e| Error::CouldNotRead { source: e, path: path.into() })?;
334    content.parse::<DocumentMut>().map_err(|e| Error::InvalidToml { source: e })
335}
336
337/// Extract all crate names from the given `Cargo.toml` by checking the `dependencies` and
338/// `dev-dependencies`.
339fn extract_crate_names(
340    cargo_toml: &DocumentMut,
341    workspace_dependencies: BTreeMap<String, String>,
342) -> Result<CrateNames, Error> {
343    let package_name = extract_package_name(cargo_toml);
344    let root_pkg = package_name.as_ref().map(|name| {
345        let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
346            // We're running for a library/binary crate
347            None => FoundCrate::Itself,
348            // We're running for an integration test
349            Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
350        };
351
352        (name.to_string(), cr)
353    });
354
355    let dep_tables = dep_tables(cargo_toml).chain(target_dep_tables(cargo_toml));
356    let dep_pkgs = dep_tables.flatten().filter_map(move |(dep_name, dep_value)| {
357        let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
358
359        // We already handle this via `root_pkg` above.
360        if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
361            return None
362        }
363
364        // Check if this is a workspace dependency.
365        let workspace = dep_value.get("workspace").and_then(|w| w.as_bool()).unwrap_or_default();
366
367        let pkg_name = workspace
368            .then(|| workspace_dependencies.get(pkg_name).map(|p| p.as_ref()))
369            .flatten()
370            .unwrap_or(pkg_name);
371
372        let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
373
374        Some((pkg_name.to_owned(), cr))
375    });
376
377    Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
378}
379
380fn extract_package_name(cargo_toml: &DocumentMut) -> Option<&str> {
381    cargo_toml.get("package")?.get("name")?.as_str()
382}
383
384fn target_dep_tables(cargo_toml: &DocumentMut) -> impl Iterator<Item = &Table> {
385    cargo_toml.get("target").into_iter().filter_map(Item::as_table).flat_map(|t| {
386        t.iter().map(|(_, value)| value).filter_map(Item::as_table).flat_map(dep_tables)
387    })
388}
389
390fn dep_tables(table: &Table) -> impl Iterator<Item = &Table> {
391    table
392        .get("dependencies")
393        .into_iter()
394        .chain(table.get("dev-dependencies"))
395        .filter_map(Item::as_table)
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    macro_rules! create_test {
403        (
404            $name:ident,
405            $cargo_toml:expr,
406            $workspace_toml:expr,
407            $( $result:tt )*
408        ) => {
409            #[test]
410            fn $name() {
411                let cargo_toml = $cargo_toml.parse::<DocumentMut>()
412                    .expect("Parses `Cargo.toml`");
413                let workspace_cargo_toml = $workspace_toml.parse::<DocumentMut>()
414                    .expect("Parses workspace `Cargo.toml`");
415
416                let workspace_deps = extract_workspace_dependencies(&workspace_cargo_toml)
417                    .expect("Extracts workspace dependencies");
418
419                match extract_crate_names(&cargo_toml, workspace_deps)
420                    .map(|mut map| map.remove("my_crate"))
421                {
422                   $( $result )* => (),
423                   o => panic!("Invalid result: {:?}", o),
424               }
425            }
426        };
427    }
428
429    create_test! {
430        deps_with_crate,
431        r#"
432            [dependencies]
433            my_crate = "0.1"
434        "#,
435        "",
436        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
437    }
438
439    create_test! {
440        dev_deps_with_crate,
441        r#"
442            [dev-dependencies]
443            my_crate = "0.1"
444        "#,
445        "",
446        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
447    }
448
449    create_test! {
450        deps_with_crate_renamed,
451        r#"
452            [dependencies]
453            cool = { package = "my_crate", version = "0.1" }
454        "#,
455        "",
456        Ok(Some(FoundCrate::Name(name))) if name == "cool"
457    }
458
459    create_test! {
460        deps_with_crate_renamed_second,
461        r#"
462            [dependencies.cool]
463            package = "my_crate"
464            version = "0.1"
465        "#,
466        "",
467        Ok(Some(FoundCrate::Name(name))) if name == "cool"
468    }
469
470    create_test! {
471        deps_empty,
472        r#"
473            [dependencies]
474        "#,
475        "",
476        Ok(None)
477    }
478
479    create_test! {
480        crate_not_found,
481        r#"
482            [dependencies]
483            serde = "1.0"
484        "#,
485        "",
486        Ok(None)
487    }
488
489    create_test! {
490        target_dependency,
491        r#"
492            [target.'cfg(target_os="android")'.dependencies]
493            my_crate = "0.1"
494        "#,
495        "",
496        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
497    }
498
499    create_test! {
500        target_dependency2,
501        r#"
502            [target.x86_64-pc-windows-gnu.dependencies]
503            my_crate = "0.1"
504        "#,
505        "",
506        Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
507    }
508
509    create_test! {
510        own_crate,
511        r#"
512            [package]
513            name = "my_crate"
514        "#,
515        "",
516        Ok(Some(FoundCrate::Itself))
517    }
518
519    create_test! {
520        own_crate_and_in_deps,
521        r#"
522            [package]
523            name = "my_crate"
524
525            [dev-dependencies]
526            my_crate = "0.1"
527        "#,
528        "",
529        Ok(Some(FoundCrate::Itself))
530    }
531
532    create_test! {
533        multiple_times,
534        r#"
535            [dependencies]
536            my_crate = { version = "0.5" }
537            my-crate-old = { package = "my_crate", version = "0.1" }
538        "#,
539        "",
540        Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
541    }
542
543    create_test! {
544        workspace_deps,
545        r#"
546            [dependencies]
547            my_crate_cool = { workspace = true }
548        "#,
549        r#"
550            [workspace.dependencies]
551            my_crate_cool = { package = "my_crate" }
552        "#,
553        Ok(Some(FoundCrate::Name(name))) if name == "my_crate_cool"
554    }
555}