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