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 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: Vec = CommandOutput::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: String| {
255 let path: &str = 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: Error| {
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: Document = open_cargo_toml(manifest_path)?;
282
283 let workspace_dependencies: BTreeMap = if manifest_path != workspace_manifest_path {
284 let workspace_manifest: Document = 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: BTreeMap = 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: &Document,
306) -> Result<BTreeMap<String, String>, Error> {
307 Ok(workspace_dep_tablesimpl Iterator(&workspace_toml)
308 .into_iter()
309 .flatten()
310 .map(move |(dep_name: &str, dep_value: &Item)| {
311 let pkg_name: &str = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(default: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: &Document) -> Option<&Table> {
320 cargo_tomlOption<&Item>
321 .get(key:"workspace")
322 .and_then(|w: &Item| w.as_table()?.get(key:"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(from:'-', to:"_")
328}
329
330/// Open the given `Cargo.toml` and parse it into a hashmap.
331fn open_cargo_toml(path: &Path) -> Result<Document, Error> {
332 let content: String = fs::read_to_string(path)
333 .map_err(|e: Error| Error::CouldNotRead { source: e, path: path.into() })?;
334 content.parse::<Document>().map_err(|e: TomlError| 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: &Document,
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: &Document) -> Option<&str> {
381 cargo_toml.get("package")?.get(index:"name")?.as_str()
382}
383
384fn target_dep_tables(cargo_toml: &Document) -> impl Iterator<Item = &Table> {
385 cargo_toml.get(key:"target").into_iter().filter_map(Item::as_table).flat_map(|t: &Table| {
386 t.iter().map(|(_, value: &Item)| value).filter_map(Item::as_table).flat_map(dep_tables)
387 })
388}
389
390fn dep_tables(table: &Table) -> impl Iterator<Item = &Table> {
391 tableimpl Iterator
392 .get(key:"dependencies")
393 .into_iter()
394 .chain(table.get(key:"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::<Document>()
412 .expect("Parses `Cargo.toml`");
413 let workspace_cargo_toml = $workspace_toml.parse::<Document>()
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}
556