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: &Error, .. } => Some(source),
113 Error::InvalidToml { source: &TomlError } => 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: &PathBuf) =>
129 write!(f, "Could not find `Cargo.toml` in manifest dir: `{}`.", path.display()),
130 Error::CargoManifestDirNotSet =>
131 f.write_str(data:"`CARGO_MANIFEST_DIR` env variable not set."),
132 Error::CouldNotRead { path: &PathBuf, .. } => write!(f, "Could not read `{}`.", path.display()),
133 Error::InvalidToml { .. } => f.write_str(data:"Invalid toml file."),
134 Error::CrateNotFound { crate_name: &String, path: &PathBuf } => 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(data:"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: Error| {
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: DocumentMut = open_cargo_toml(manifest_path)?;
284
285 let workspace_dependencies: BTreeMap = if manifest_path != workspace_manifest_path {
286 let workspace_manifest: DocumentMut = 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: BTreeMap = 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_tablesimpl Iterator(&workspace_toml)
310 .into_iter()
311 .map(|t: &dyn TableLike| t.iter())
312 .flatten()
313 .map(move |(dep_name: &str, dep_value: &Item)| {
314 let pkg_name: &str = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(default: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_tomlOption<&Item>
324 .get(key:"workspace")
325 .and_then(|w: &Item| w.as_table_like()?.get(key:"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(from:'-', to:"_")
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: String = fs::read_to_string(path)
336 .map_err(|e: Error| Error::CouldNotRead { source: e, path: path.into() })?;
337 content.parse::<DocumentMut>().map_err(|e: TomlError| 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(index:"name")?.as_str()
387}
388
389fn target_dep_tables(cargo_toml: &DocumentMut) -> impl Iterator<Item = &dyn TableLike> {
390 cargo_tomlimpl Iterator
391 .get(key:"target")
392 .into_iter()
393 .filter_map(Item::as_table_like)
394 .flat_map(|t: &dyn TableLike| {
395 timpl Iterator.iter()
396 .map(|(_, value: &Item)| 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 tableimpl Iterator
404 .get(key:"dependencies")
405 .into_iter()
406 .chain(table.get(key:"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}
578