578 lines
18 KiB
Rust
578 lines
18 KiB
Rust
/*!
|
|
|
|
[](https://docs.rs/proc-macro-crate/) [](https://crates.io/crates/proc-macro-crate) [](https://crates.io/crates/proc-macro-crate) [](https://travis-ci.org/bkchr/proc-macro-crate)
|
|
|
|
Providing support for `$crate` in procedural macros.
|
|
|
|
* [Introduction](#introduction)
|
|
* [Example](#example)
|
|
* [License](#license)
|
|
|
|
## Introduction
|
|
|
|
In `macro_rules!` `$crate` is used to get the path of the crate where a macro is declared in. In
|
|
procedural macros there is currently no easy way to get this path. A common hack is to import the
|
|
desired crate with a know name and use this. However, with rust edition 2018 and dropping
|
|
`extern crate` declarations from `lib.rs`, people start to rename crates in `Cargo.toml` directly.
|
|
However, this breaks importing the crate, as the proc-macro developer does not know the renamed
|
|
name of the crate that should be imported.
|
|
|
|
This crate provides a way to get the name of a crate, even if it renamed in `Cargo.toml`. For this
|
|
purpose a single function `crate_name` is provided. This function needs to be called in the context
|
|
of a proc-macro with the name of the desired crate. `CARGO_MANIFEST_DIR` will be used to find the
|
|
current active `Cargo.toml` and this `Cargo.toml` is searched for the desired crate.
|
|
|
|
## Example
|
|
|
|
```
|
|
use quote::quote;
|
|
use syn::Ident;
|
|
use proc_macro2::Span;
|
|
use proc_macro_crate::{crate_name, FoundCrate};
|
|
|
|
fn import_my_crate() {
|
|
let found_crate = crate_name("my-crate").expect("my-crate is present in `Cargo.toml`");
|
|
|
|
match found_crate {
|
|
FoundCrate::Itself => quote!( crate::Something ),
|
|
FoundCrate::Name(name) => {
|
|
let ident = Ident::new(&name, Span::call_site());
|
|
quote!( #ident::Something )
|
|
}
|
|
};
|
|
}
|
|
|
|
# fn main() {}
|
|
```
|
|
|
|
## Edge cases
|
|
|
|
There are multiple edge cases when it comes to determining the correct crate. If you for example
|
|
import a crate as its own dependency, like this:
|
|
|
|
```toml
|
|
[package]
|
|
name = "my_crate"
|
|
|
|
[dev-dependencies]
|
|
my_crate = { version = "0.1", features = [ "test-feature" ] }
|
|
```
|
|
|
|
The crate will return `FoundCrate::Itself` and you will not be able to find the other instance
|
|
of your crate in `dev-dependencies`. Other similar cases are when one crate is imported multiple
|
|
times:
|
|
|
|
```toml
|
|
[package]
|
|
name = "my_crate"
|
|
|
|
[dependencies]
|
|
some-crate = { version = "0.5" }
|
|
some-crate-old = { package = "some-crate", version = "0.1" }
|
|
```
|
|
|
|
When searching for `some-crate` in this `Cargo.toml` it will return `FoundCrate::Name("some_old_crate")`,
|
|
aka the last definition of the crate in the `Cargo.toml`.
|
|
|
|
## License
|
|
|
|
Licensed under either of
|
|
|
|
* [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
|
|
|
* [MIT license](https://opensource.org/licenses/MIT)
|
|
|
|
at your option.
|
|
*/
|
|
|
|
use std::{
|
|
collections::btree_map::{self, BTreeMap},
|
|
env, fmt, fs, io,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
sync::Mutex,
|
|
time::SystemTime,
|
|
};
|
|
|
|
use toml_edit::{DocumentMut, Item, TableLike, TomlError};
|
|
|
|
/// Error type used by this crate.
|
|
pub enum Error {
|
|
NotFound(PathBuf),
|
|
CargoManifestDirNotSet,
|
|
FailedGettingWorkspaceManifestPath,
|
|
CouldNotRead { path: PathBuf, source: io::Error },
|
|
InvalidToml { source: TomlError },
|
|
CrateNotFound { crate_name: String, path: PathBuf },
|
|
}
|
|
|
|
impl std::error::Error for Error {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Error::CouldNotRead { source, .. } => Some(source),
|
|
Error::InvalidToml { source } => Some(source),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt::Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Error {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
Error::NotFound(path) =>
|
|
write!(f, "Could not find `Cargo.toml` in manifest dir: `{}`.", path.display()),
|
|
Error::CargoManifestDirNotSet =>
|
|
f.write_str("`CARGO_MANIFEST_DIR` env variable not set."),
|
|
Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
|
|
Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
|
|
Error::CrateNotFound { crate_name, path } => write!(
|
|
f,
|
|
"Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
|
|
crate_name,
|
|
path.display(),
|
|
),
|
|
Error::FailedGettingWorkspaceManifestPath =>
|
|
f.write_str("Failed to get the path of the workspace manifest path."),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The crate as found by [`crate_name`].
|
|
#[derive(Debug, PartialEq, Clone, Eq)]
|
|
pub enum FoundCrate {
|
|
/// The searched crate is this crate itself.
|
|
Itself,
|
|
/// The searched crate was found with this name.
|
|
Name(String),
|
|
}
|
|
|
|
// In a rustc invocation, there will only ever be one entry in this map, since every crate is
|
|
// compiled with its own rustc process. However, the same is not (currently) the case for
|
|
// rust-analyzer.
|
|
type Cache = BTreeMap<String, CacheEntry>;
|
|
|
|
struct CacheEntry {
|
|
manifest_ts: SystemTime,
|
|
workspace_manifest_ts: SystemTime,
|
|
workspace_manifest_path: PathBuf,
|
|
crate_names: CrateNames,
|
|
}
|
|
|
|
type CrateNames = BTreeMap<String, FoundCrate>;
|
|
|
|
/// Find the crate name for the given `orig_name` in the current `Cargo.toml`.
|
|
///
|
|
/// `orig_name` should be the original name of the searched crate.
|
|
///
|
|
/// The current `Cargo.toml` is determined by taking `CARGO_MANIFEST_DIR/Cargo.toml`.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// - `Ok(FoundCrate::Itself)` the searched crate is the current crate being compiled.
|
|
/// - `Ok(FoundCrate::Name(new_name))` the searched create was found with the given name in the
|
|
/// `Cargo.toml`.
|
|
/// - `Err` if an error occurred. See [`Error`].
|
|
///
|
|
/// The returned crate name is sanitized in such a way that it is a valid rust identifier. Thus,
|
|
/// it is ready to be used in `extern crate` as identifier.
|
|
pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
|
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
|
|
let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
|
|
|
|
let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
|
|
|
|
static CACHE: Mutex<Cache> = Mutex::new(BTreeMap::new());
|
|
let mut cache = CACHE.lock().unwrap();
|
|
|
|
let crate_names = match cache.entry(manifest_dir) {
|
|
btree_map::Entry::Occupied(entry) => {
|
|
let cache_entry = entry.into_mut();
|
|
let workspace_manifest_path = cache_entry.workspace_manifest_path.as_path();
|
|
let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
|
|
|
|
// Timestamp changed, rebuild this cache entry.
|
|
if manifest_ts != cache_entry.manifest_ts ||
|
|
workspace_manifest_ts != cache_entry.workspace_manifest_ts
|
|
{
|
|
*cache_entry = read_cargo_toml(
|
|
&manifest_path,
|
|
&workspace_manifest_path,
|
|
manifest_ts,
|
|
workspace_manifest_ts,
|
|
)?;
|
|
}
|
|
|
|
&cache_entry.crate_names
|
|
},
|
|
btree_map::Entry::Vacant(entry) => {
|
|
// If `workspace_manifest_path` returns `None`, we are probably in a vendored deps
|
|
// folder and cargo complaining that we have some package inside a workspace, that isn't
|
|
// part of the workspace. In this case we just use the `manifest_path` as the
|
|
// `workspace_manifest_path`.
|
|
let workspace_manifest_path =
|
|
workspace_manifest_path(&manifest_path)?.unwrap_or_else(|| manifest_path.clone());
|
|
let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
|
|
|
|
let cache_entry = entry.insert(read_cargo_toml(
|
|
&manifest_path,
|
|
&workspace_manifest_path,
|
|
manifest_ts,
|
|
workspace_manifest_ts,
|
|
)?);
|
|
&cache_entry.crate_names
|
|
},
|
|
};
|
|
|
|
Ok(crate_names
|
|
.get(orig_name)
|
|
.ok_or_else(|| Error::CrateNotFound {
|
|
crate_name: orig_name.to_owned(),
|
|
path: manifest_path,
|
|
})?
|
|
.clone())
|
|
}
|
|
|
|
fn workspace_manifest_path(cargo_toml_manifest: &Path) -> Result<Option<PathBuf>, Error> {
|
|
let Ok(cargo) = env::var("CARGO") else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let stdout = Command::new(cargo)
|
|
.arg("locate-project")
|
|
.args(&["--workspace", "--message-format=plain"])
|
|
.arg(format!("--manifest-path={}", cargo_toml_manifest.display()))
|
|
.output()
|
|
.map_err(|_| Error::FailedGettingWorkspaceManifestPath)?
|
|
.stdout;
|
|
|
|
String::from_utf8(stdout)
|
|
.map_err(|_| Error::FailedGettingWorkspaceManifestPath)
|
|
.map(|s| {
|
|
let path = s.trim();
|
|
|
|
if path.is_empty() {
|
|
None
|
|
} else {
|
|
Some(path.into())
|
|
}
|
|
})
|
|
}
|
|
|
|
fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
|
|
fs::metadata(manifest_path).and_then(|meta| meta.modified()).map_err(|source| {
|
|
if source.kind() == io::ErrorKind::NotFound {
|
|
Error::NotFound(manifest_path.to_owned())
|
|
} else {
|
|
Error::CouldNotRead { path: manifest_path.to_owned(), source }
|
|
}
|
|
})
|
|
}
|
|
|
|
fn read_cargo_toml(
|
|
manifest_path: &Path,
|
|
workspace_manifest_path: &Path,
|
|
manifest_ts: SystemTime,
|
|
workspace_manifest_ts: SystemTime,
|
|
) -> Result<CacheEntry, Error> {
|
|
let manifest = open_cargo_toml(manifest_path)?;
|
|
|
|
let workspace_dependencies = if manifest_path != workspace_manifest_path {
|
|
let workspace_manifest = open_cargo_toml(workspace_manifest_path)?;
|
|
extract_workspace_dependencies(&workspace_manifest)?
|
|
} else {
|
|
extract_workspace_dependencies(&manifest)?
|
|
};
|
|
|
|
let crate_names = extract_crate_names(&manifest, workspace_dependencies)?;
|
|
|
|
Ok(CacheEntry {
|
|
manifest_ts,
|
|
workspace_manifest_ts,
|
|
crate_names,
|
|
workspace_manifest_path: workspace_manifest_path.to_path_buf(),
|
|
})
|
|
}
|
|
|
|
/// Extract all `[workspace.dependencies]`.
|
|
///
|
|
/// Returns a hash map that maps from dep name to the package name. Dep name
|
|
/// and package name can be the same if there doesn't exist any rename.
|
|
fn extract_workspace_dependencies(
|
|
workspace_toml: &DocumentMut,
|
|
) -> Result<BTreeMap<String, String>, Error> {
|
|
Ok(workspace_dep_tables(&workspace_toml)
|
|
.into_iter()
|
|
.map(|t| t.iter())
|
|
.flatten()
|
|
.map(move |(dep_name, dep_value)| {
|
|
let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
|
|
|
|
(dep_name.to_owned(), pkg_name.to_owned())
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// Return an iterator over all `[workspace.dependencies]`
|
|
fn workspace_dep_tables(cargo_toml: &DocumentMut) -> Option<&dyn TableLike> {
|
|
cargo_toml
|
|
.get("workspace")
|
|
.and_then(|w| w.as_table_like()?.get("dependencies")?.as_table_like())
|
|
}
|
|
|
|
/// Make sure that the given crate name is a valid rust identifier.
|
|
fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
|
|
name.as_ref().replace('-', "_")
|
|
}
|
|
|
|
/// Open the given `Cargo.toml` and parse it into a hashmap.
|
|
fn open_cargo_toml(path: &Path) -> Result<DocumentMut, Error> {
|
|
let content = fs::read_to_string(path)
|
|
.map_err(|e| Error::CouldNotRead { source: e, path: path.into() })?;
|
|
content.parse::<DocumentMut>().map_err(|e| Error::InvalidToml { source: e })
|
|
}
|
|
|
|
/// Extract all crate names from the given `Cargo.toml` by checking the `dependencies` and
|
|
/// `dev-dependencies`.
|
|
fn extract_crate_names(
|
|
cargo_toml: &DocumentMut,
|
|
workspace_dependencies: BTreeMap<String, String>,
|
|
) -> Result<CrateNames, Error> {
|
|
let package_name = extract_package_name(cargo_toml);
|
|
let root_pkg = package_name.as_ref().map(|name| {
|
|
let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
|
|
// We're running for a library/binary crate
|
|
None => FoundCrate::Itself,
|
|
// We're running for an integration test
|
|
Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
|
|
};
|
|
|
|
(name.to_string(), cr)
|
|
});
|
|
|
|
let dep_tables = dep_tables(cargo_toml.as_table()).chain(target_dep_tables(cargo_toml));
|
|
let dep_pkgs =
|
|
dep_tables.map(|t| t.iter()).flatten().filter_map(move |(dep_name, dep_value)| {
|
|
let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
|
|
|
|
// We already handle this via `root_pkg` above.
|
|
if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
|
|
return None
|
|
}
|
|
|
|
// Check if this is a workspace dependency.
|
|
let workspace =
|
|
dep_value.get("workspace").and_then(|w| w.as_bool()).unwrap_or_default();
|
|
|
|
let pkg_name = workspace
|
|
.then(|| workspace_dependencies.get(pkg_name).map(|p| p.as_ref()))
|
|
.flatten()
|
|
.unwrap_or(pkg_name);
|
|
|
|
let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
|
|
|
|
Some((pkg_name.to_owned(), cr))
|
|
});
|
|
|
|
Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
|
|
}
|
|
|
|
fn extract_package_name(cargo_toml: &DocumentMut) -> Option<&str> {
|
|
cargo_toml.get("package")?.get("name")?.as_str()
|
|
}
|
|
|
|
fn target_dep_tables(cargo_toml: &DocumentMut) -> impl Iterator<Item = &dyn TableLike> {
|
|
cargo_toml
|
|
.get("target")
|
|
.into_iter()
|
|
.filter_map(Item::as_table_like)
|
|
.flat_map(|t| {
|
|
t.iter()
|
|
.map(|(_, value)| value)
|
|
.filter_map(Item::as_table_like)
|
|
.flat_map(dep_tables)
|
|
})
|
|
}
|
|
|
|
fn dep_tables(table: &dyn TableLike) -> impl Iterator<Item = &dyn TableLike> {
|
|
table
|
|
.get("dependencies")
|
|
.into_iter()
|
|
.chain(table.get("dev-dependencies"))
|
|
.filter_map(Item::as_table_like)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
macro_rules! create_test {
|
|
(
|
|
$name:ident,
|
|
$cargo_toml:expr,
|
|
$workspace_toml:expr,
|
|
$( $result:tt )*
|
|
) => {
|
|
#[test]
|
|
fn $name() {
|
|
let cargo_toml = $cargo_toml.parse::<DocumentMut>()
|
|
.expect("Parses `Cargo.toml`");
|
|
let workspace_cargo_toml = $workspace_toml.parse::<DocumentMut>()
|
|
.expect("Parses workspace `Cargo.toml`");
|
|
|
|
let workspace_deps = extract_workspace_dependencies(&workspace_cargo_toml)
|
|
.expect("Extracts workspace dependencies");
|
|
|
|
match extract_crate_names(&cargo_toml, workspace_deps)
|
|
.map(|mut map| map.remove("my_crate"))
|
|
{
|
|
$( $result )* => (),
|
|
o => panic!("Invalid result: {:?}", o),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
create_test! {
|
|
deps_with_crate,
|
|
r#"
|
|
[dependencies]
|
|
my_crate = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
|
|
}
|
|
|
|
// forbidding toml_edit::Item::as_table ought to mean this is OK, but let's have a test too
|
|
create_test! {
|
|
deps_with_crate_inline_table,
|
|
r#"
|
|
dependencies = { my_crate = "0.1" }
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
|
|
}
|
|
|
|
create_test! {
|
|
dev_deps_with_crate,
|
|
r#"
|
|
[dev-dependencies]
|
|
my_crate = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
|
|
}
|
|
|
|
create_test! {
|
|
deps_with_crate_renamed,
|
|
r#"
|
|
[dependencies]
|
|
cool = { package = "my_crate", version = "0.1" }
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "cool"
|
|
}
|
|
|
|
create_test! {
|
|
deps_with_crate_renamed_second,
|
|
r#"
|
|
[dependencies.cool]
|
|
package = "my_crate"
|
|
version = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "cool"
|
|
}
|
|
|
|
create_test! {
|
|
deps_empty,
|
|
r#"
|
|
[dependencies]
|
|
"#,
|
|
"",
|
|
Ok(None)
|
|
}
|
|
|
|
create_test! {
|
|
crate_not_found,
|
|
r#"
|
|
[dependencies]
|
|
serde = "1.0"
|
|
"#,
|
|
"",
|
|
Ok(None)
|
|
}
|
|
|
|
create_test! {
|
|
target_dependency,
|
|
r#"
|
|
[target.'cfg(target_os="android")'.dependencies]
|
|
my_crate = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
|
|
}
|
|
|
|
create_test! {
|
|
target_dependency2,
|
|
r#"
|
|
[target.x86_64-pc-windows-gnu.dependencies]
|
|
my_crate = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
|
|
}
|
|
|
|
create_test! {
|
|
own_crate,
|
|
r#"
|
|
[package]
|
|
name = "my_crate"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Itself))
|
|
}
|
|
|
|
create_test! {
|
|
own_crate_and_in_deps,
|
|
r#"
|
|
[package]
|
|
name = "my_crate"
|
|
|
|
[dev-dependencies]
|
|
my_crate = "0.1"
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Itself))
|
|
}
|
|
|
|
create_test! {
|
|
multiple_times,
|
|
r#"
|
|
[dependencies]
|
|
my_crate = { version = "0.5" }
|
|
my-crate-old = { package = "my_crate", version = "0.1" }
|
|
"#,
|
|
"",
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
|
|
}
|
|
|
|
create_test! {
|
|
workspace_deps,
|
|
r#"
|
|
[dependencies]
|
|
my_crate_cool = { workspace = true }
|
|
"#,
|
|
r#"
|
|
[workspace.dependencies]
|
|
my_crate_cool = { package = "my_crate" }
|
|
"#,
|
|
Ok(Some(FoundCrate::Name(name))) if name == "my_crate_cool"
|
|
}
|
|
}
|