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