use std::collections::{BTreeMap, BTreeSet};
use crate::idents::rust_path_to_tokens;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) struct ImportResolver;
impl ImportResolver {
pub fn new() -> Self {
Self
}
pub fn option_at(&self, ctx: &crate::context::CodeGenContext, nesting: usize) -> TokenStream {
ctx.root_runtime_path(OPTION_PATH, nesting)
}
pub fn string_at(&self, ctx: &crate::context::CodeGenContext, nesting: usize) -> TokenStream {
ctx.root_runtime_path(STRING_PATH, nesting)
}
pub fn vec_at(&self, ctx: &crate::context::CodeGenContext, nesting: usize) -> TokenStream {
ctx.root_runtime_path(VEC_PATH, nesting)
}
pub fn message_field_at(
&self,
ctx: &crate::context::CodeGenContext,
nesting: usize,
) -> TokenStream {
ctx.root_runtime_path(MESSAGE_FIELD_PATH, nesting)
}
pub fn enum_value_at(
&self,
ctx: &crate::context::CodeGenContext,
nesting: usize,
) -> TokenStream {
ctx.root_runtime_path(ENUM_VALUE_PATH, nesting)
}
pub fn hashmap_at(&self, ctx: &crate::context::CodeGenContext, nesting: usize) -> TokenStream {
ctx.root_runtime_path(HASHMAP_PATH, nesting)
}
}
const OPTION_PATH: &str = "::core::option::Option";
const STRING_PATH: &str = "::buffa::alloc::string::String";
const VEC_PATH: &str = "::buffa::alloc::vec::Vec";
const MESSAGE_FIELD_PATH: &str = "::buffa::MessageField";
const ENUM_VALUE_PATH: &str = "::buffa::EnumValue";
const HASHMAP_PATH: &str = "::buffa::__private::HashMap";
const RUNTIME_IMPORTS: &[(&str, &str)] = &[
(ENUM_VALUE_PATH, "EnumValue"),
(HASHMAP_PATH, "HashMap"),
(MESSAGE_FIELD_PATH, "MessageField"),
(OPTION_PATH, "Option"),
(STRING_PATH, "String"),
(VEC_PATH, "Vec"),
];
const ROOT_BARE_REFERENCED: &[&str] = &[
"Box",
"Clone",
"Copy",
"Debug",
"Default",
"Eq",
"From",
"Hash",
"Into",
"None",
"PartialEq",
"Result",
"Self",
"Send",
"Some",
"Sync",
"bool",
"f32",
"f64",
"i32",
"i64",
"serde",
"str",
"u8",
"u32",
"u64",
];
pub(crate) enum ImportsPhase {
Off,
Collecting(BTreeSet<String>),
Resolving(RootImports),
}
pub(crate) fn shortenable(path: &str) -> bool {
path.starts_with("::") || path.starts_with("crate::") || path.starts_with("super::")
}
pub(crate) struct RootImports {
resolved: BTreeMap<String, String>,
bindings: BTreeMap<String, String>,
}
impl RootImports {
pub fn assign(collected: &BTreeSet<String>, occupied: &BTreeSet<String>) -> Self {
let mut imports = RootImports {
resolved: BTreeMap::new(),
bindings: BTreeMap::new(),
};
for (path, short) in RUNTIME_IMPORTS {
if collected.contains(*path) && !occupied.contains(*short) {
imports
.bindings
.insert((*short).to_string(), (*path).to_string());
imports
.resolved
.insert((*path).to_string(), (*short).to_string());
}
}
let runtime_leaves: BTreeSet<&str> = RUNTIME_IMPORTS.iter().map(|(_, s)| *s).collect();
let proto_claimable = |name: &str| {
!occupied.contains(name)
&& !runtime_leaves.contains(name)
&& !ROOT_BARE_REFERENCED.contains(&name)
&& !matches!(name, "self" | "super" | "Self" | "crate")
&& !name.is_empty()
};
let runtime_paths: BTreeSet<&str> = RUNTIME_IMPORTS.iter().map(|(p, _)| *p).collect();
for path in collected {
if imports.resolved.contains_key(path)
|| runtime_paths.contains(path.as_str())
|| !shortenable(path)
{
continue;
}
let Some((parent, leaf)) = path.rsplit_once("::") else {
continue;
};
if proto_claimable(leaf) {
match imports.bindings.get(leaf) {
None => {
imports.bindings.insert(leaf.to_string(), path.clone());
imports.resolved.insert(path.clone(), leaf.to_string());
continue;
}
Some(existing) if existing == path => {
imports.resolved.insert(path.clone(), leaf.to_string());
continue;
}
Some(_) => {}
}
}
let parent_leaf = parent.rsplit("::").next().unwrap_or("");
if proto_claimable(parent_leaf) {
let claimed = match imports.bindings.get(parent_leaf) {
None => {
imports
.bindings
.insert(parent_leaf.to_string(), parent.to_string());
true
}
Some(existing) => existing == parent,
};
if claimed {
imports
.resolved
.insert(path.clone(), format!("{parent_leaf}::{leaf}"));
continue;
}
}
}
imports
}
pub fn resolve(&self, path: &str) -> Option<&str> {
self.resolved.get(path).map(String::as_str)
}
pub fn use_items(&self) -> TokenStream {
let mut out = TokenStream::new();
for target in self.bindings.values() {
let path = rust_path_to_tokens(target);
out.extend(quote! { use #path; });
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assign(paths: &[&str], occupied: &[&str]) -> RootImports {
let collected: BTreeSet<String> = paths.iter().map(|s| s.to_string()).collect();
let occupied: BTreeSet<String> = occupied.iter().map(|s| s.to_string()).collect();
RootImports::assign(&collected, &occupied)
}
#[test]
fn cross_package_binds_bare_leaf() {
let imports = assign(&["super::other::Msg"], &[]);
assert_eq!(imports.resolve("super::other::Msg"), Some("Msg"));
assert_eq!(
imports.use_items().to_string(),
"use super :: other :: Msg ;"
);
}
#[test]
fn extern_path_binds_bare_leaf() {
let imports = assign(&["::buffa_types::google::protobuf::Timestamp"], &[]);
assert_eq!(
imports.resolve("::buffa_types::google::protobuf::Timestamp"),
Some("Timestamp")
);
}
#[test]
fn same_package_relative_paths_pass_through() {
let imports = assign(&["outer::Inner", "Msg"], &[]);
assert_eq!(imports.resolve("outer::Inner"), None);
assert_eq!(imports.resolve("Msg"), None);
assert!(imports.use_items().is_empty());
}
#[test]
fn occupied_leaf_falls_to_parent_module() {
let imports = assign(&["super::other::Msg"], &["Msg"]);
assert_eq!(imports.resolve("super::other::Msg"), Some("other::Msg"));
assert_eq!(imports.use_items().to_string(), "use super :: other ;");
}
#[test]
fn occupied_leaf_and_parent_stays_qualified() {
let imports = assign(&["super::other::Msg"], &["Msg", "other"]);
assert_eq!(imports.resolve("super::other::Msg"), None);
assert!(imports.use_items().is_empty());
}
#[test]
fn duplicate_leaf_assignment_is_order_independent() {
let forward = assign(&["::a::Dup", "::b::Dup"], &[]);
let reverse = assign(&["::b::Dup", "::a::Dup"], &[]);
for imports in [forward, reverse] {
assert_eq!(imports.resolve("::a::Dup"), Some("Dup"));
assert_eq!(imports.resolve("::b::Dup"), Some("b::Dup"));
}
}
#[test]
fn runtime_types_claim_before_proto_types() {
let imports = assign(
&["::buffa::alloc::string::String", "super::other::String"],
&[],
);
assert_eq!(
imports.resolve("::buffa::alloc::string::String"),
Some("String")
);
assert_eq!(
imports.resolve("super::other::String"),
Some("other::String")
);
}
#[test]
fn proto_type_never_takes_runtime_leaf_even_when_unused() {
let imports = assign(&["super::other::String"], &[]);
assert_eq!(
imports.resolve("super::other::String"),
Some("other::String")
);
}
#[test]
fn runtime_type_blocked_by_package_item() {
let imports = assign(&["::buffa::alloc::string::String"], &["String"]);
assert_eq!(imports.resolve("::buffa::alloc::string::String"), None);
}
#[test]
fn bare_referenced_names_never_claimed() {
let imports = assign(&["super::other::From"], &[]);
assert_eq!(imports.resolve("super::other::From"), Some("other::From"));
let imports = assign(&["super::serde::Inner"], &["Inner"]);
assert_eq!(imports.resolve("super::serde::Inner"), None);
}
#[test]
fn shared_parent_module_binds_once() {
let imports = assign(&["super::pkg::A", "super::pkg::B"], &["A", "B"]);
assert_eq!(imports.resolve("super::pkg::A"), Some("pkg::A"));
assert_eq!(imports.resolve("super::pkg::B"), Some("pkg::B"));
assert_eq!(imports.use_items().to_string(), "use super :: pkg ;");
}
#[test]
fn keyword_segments_survive_in_use_targets() {
let imports = assign(&["super::type::LatLng"], &[]);
assert_eq!(imports.resolve("super::type::LatLng"), Some("LatLng"));
assert_eq!(
imports.use_items().to_string(),
"use super :: r#type :: LatLng ;"
);
}
#[test]
fn use_items_sorted_by_short_name() {
let imports = assign(&["::ext::Zebra", "::ext::Alpha"], &[]);
assert_eq!(
imports.use_items().to_string(),
"use :: ext :: Alpha ; use :: ext :: Zebra ;"
);
}
}