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