use crate::scope_analyzer;
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use syn::visit::Visit;
use syn::{File, Item};
use super::types::{Module, RefVisitor};
pub(super) fn use_tree_is_pure_glob(use_item: &Item) -> bool {
let Item::Use(u) = use_item else {
return false;
};
fn tail_is_glob(tree: &syn::UseTree) -> bool {
match tree {
syn::UseTree::Glob(_) => true,
syn::UseTree::Path(p) => tail_is_glob(&p.tree),
_ => false,
}
}
tail_is_glob(&u.tree)
}
pub(crate) fn collect_use_bound_names(use_item: &Item, out: &mut HashSet<String>) {
let Item::Use(u) = use_item else {
return;
};
fn walk(tree: &syn::UseTree, out: &mut HashSet<String>) {
match tree {
syn::UseTree::Name(n) => {
out.insert(n.ident.to_string());
}
syn::UseTree::Rename(r) => {
out.insert(r.rename.to_string());
}
syn::UseTree::Path(p) => walk(&p.tree, out),
syn::UseTree::Group(g) => {
for item in &g.items {
walk(item, out);
}
}
syn::UseTree::Glob(_) => {}
}
}
walk(&u.tree, out);
}
pub(super) fn std_prelude_names() -> &'static [&'static str] {
&[
"Option",
"Some",
"None",
"Result",
"Ok",
"Err",
"Box",
"Vec",
"String",
"ToString",
"ToOwned",
"Cow",
"Copy",
"Clone",
"Drop",
"Send",
"Sync",
"Sized",
"Unpin",
"Default",
"Eq",
"PartialEq",
"Ord",
"PartialOrd",
"Hash",
"Debug",
"Display",
"From",
"Into",
"TryFrom",
"TryInto",
"AsRef",
"AsMut",
"Fn",
"FnMut",
"FnOnce",
"Iterator",
"IntoIterator",
"DoubleEndedIterator",
"ExactSizeIterator",
"Extend",
]
}
pub(crate) fn item_defined_ident(item: &Item) -> Option<String> {
let ident = match item {
Item::Struct(i) => &i.ident,
Item::Enum(i) => &i.ident,
Item::Fn(i) => &i.sig.ident,
Item::Const(i) => &i.ident,
Item::Static(i) => &i.ident,
Item::Type(i) => &i.ident,
Item::Trait(i) => &i.ident,
Item::TraitAlias(i) => &i.ident,
Item::Union(i) => &i.ident,
Item::Mod(i) => &i.ident,
Item::Macro(i) => return i.ident.as_ref().map(|id| id.to_string()),
_ => return None,
};
Some(ident.to_string())
}
pub(crate) fn deepen_super_in_use(use_item: &Item) -> Item {
let Item::Use(mut u) = use_item.clone() else {
return use_item.clone();
};
deepen_super_in_use_tree(&mut u.tree);
Item::Use(u)
}
pub(crate) fn deepen_super_in_use_tree(tree: &mut syn::UseTree) {
use syn::UseTree;
if let UseTree::Path(p) = tree {
if p.ident == "super" {
let inner = (*p.tree).clone();
let extra = syn::UsePath {
ident: syn::Ident::new("super", p.ident.span()),
colon2_token: p.colon2_token,
tree: Box::new(UseTree::Path(syn::UsePath {
ident: p.ident.clone(),
colon2_token: p.colon2_token,
tree: Box::new(inner),
})),
};
*tree = UseTree::Path(extra);
}
}
}
pub(crate) fn rewrite_pinned_mod_refs_in_use(
use_item: &Item,
pinned_mods: &HashSet<String>,
) -> Item {
if pinned_mods.is_empty() {
return use_item.clone();
}
let Item::Use(mut u) = use_item.clone() else {
return use_item.clone();
};
rewrite_pinned_mod_refs_in_use_tree(&mut u.tree, pinned_mods);
Item::Use(u)
}
fn rewrite_pinned_mod_refs_in_use_tree(tree: &mut syn::UseTree, pinned_mods: &HashSet<String>) {
use syn::UseTree;
if let UseTree::Path(p) = tree {
if pinned_mods.contains(&p.ident.to_string()) {
let inner = (*p.tree).clone();
let wrapped = syn::UsePath {
ident: syn::Ident::new("super", p.ident.span()),
colon2_token: p.colon2_token,
tree: Box::new(UseTree::Path(syn::UsePath {
ident: p.ident.clone(),
colon2_token: p.colon2_token,
tree: Box::new(inner),
})),
};
*tree = UseTree::Path(wrapped);
}
}
}
pub(crate) fn bitflags_defined_idents(mac: &syn::ItemMacro) -> Vec<String> {
let is_bitflags = mac
.mac
.path
.segments
.last()
.is_some_and(|seg| seg.ident == "bitflags");
if !is_bitflags {
return Vec::new();
}
let mut names = Vec::new();
let mut saw_struct = false;
for tt in mac.mac.tokens.clone() {
match tt {
proc_macro2::TokenTree::Ident(ident) if saw_struct => {
names.push(ident.to_string());
saw_struct = false;
}
proc_macro2::TokenTree::Ident(ident) => {
saw_struct = ident == "struct";
}
_ => saw_struct = false,
}
}
names
}
pub(crate) fn bitflags_defines_pub_type(mac: &syn::ItemMacro) -> bool {
let is_bitflags = mac
.mac
.path
.segments
.last()
.is_some_and(|seg| seg.ident == "bitflags");
if !is_bitflags {
return false;
}
let mut saw_pub = false;
for tt in mac.mac.tokens.clone() {
match tt {
proc_macro2::TokenTree::Ident(ident) if ident == "struct" && saw_pub => {
return true;
}
proc_macro2::TokenTree::Ident(ident) => {
saw_pub = ident == "pub";
}
proc_macro2::TokenTree::Group(_) if saw_pub => {}
_ => saw_pub = false,
}
}
false
}
pub(super) fn extract_type_names(ty: &syn::Type, types: &mut HashSet<String>) {
match ty {
syn::Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
types.insert(type_name);
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
for arg in &args.args {
if let syn::GenericArgument::Type(inner_ty) = arg {
extract_type_names(inner_ty, types);
}
}
}
}
}
syn::Type::Reference(type_ref) => {
extract_type_names(&type_ref.elem, types);
}
syn::Type::Slice(type_slice) => {
extract_type_names(&type_slice.elem, types);
}
syn::Type::Array(type_array) => {
extract_type_names(&type_array.elem, types);
}
syn::Type::Ptr(type_ptr) => {
extract_type_names(&type_ptr.elem, types);
}
syn::Type::Tuple(type_tuple) => {
for elem in &type_tuple.elems {
extract_type_names(elem, types);
}
}
_ => {}
}
}
#[allow(dead_code)]
fn is_pascal_case(name: &str) -> bool {
name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
}
pub(crate) fn item_visibility(item: &Item) -> Option<&syn::Visibility> {
match item {
Item::Fn(f) => Some(&f.vis),
Item::Const(c) => Some(&c.vis),
Item::Static(s) => Some(&s.vis),
Item::Type(t) => Some(&t.vis),
Item::Trait(t) => Some(&t.vis),
Item::TraitAlias(t) => Some(&t.vis),
Item::Enum(e) => Some(&e.vis),
Item::Struct(s) => Some(&s.vis),
Item::Union(u) => Some(&u.vis),
Item::Mod(m) => Some(&m.vis),
Item::ExternCrate(e) => Some(&e.vis),
_ => None,
}
}
pub(super) fn apply_field_visibility(
item: Item,
visibility: &scope_analyzer::FieldVisibility,
) -> Item {
match item {
Item::Struct(mut s) => {
match visibility {
scope_analyzer::FieldVisibility::PubSuper => {
for field in &mut s.fields {
if matches!(field.vis, syn::Visibility::Inherited) {
field.vis = syn::parse_quote!(pub (super));
}
}
}
scope_analyzer::FieldVisibility::PubCrate => {
for field in &mut s.fields {
if matches!(field.vis, syn::Visibility::Inherited) {
field.vis = syn::parse_quote!(pub (crate));
}
}
}
scope_analyzer::FieldVisibility::Pub => {
for field in &mut s.fields {
if matches!(field.vis, syn::Visibility::Inherited) {
field.vis = syn::parse_quote!(pub);
}
}
}
scope_analyzer::FieldVisibility::Private => {}
}
Item::Struct(s)
}
Item::Enum(e) => Item::Enum(e),
other => other,
}
}
pub(super) fn upgrade_function_visibility(item: Item, needs_pub_super: &HashSet<String>) -> Item {
match item {
Item::Fn(mut f) => {
let fn_name = f.sig.ident.to_string();
if needs_pub_super.contains(&fn_name) && matches!(f.vis, syn::Visibility::Inherited) {
f.vis = syn::parse_quote!(pub (super));
}
Item::Fn(f)
}
other => other,
}
}
pub(super) fn upgrade_type_visibility(item: Item) -> Item {
fn bump(vis: &mut syn::Visibility) {
if matches!(vis, syn::Visibility::Inherited) {
*vis = syn::parse_quote!(pub (super));
}
}
match item {
Item::Struct(mut s) => {
bump(&mut s.vis);
Item::Struct(s)
}
Item::Enum(mut e) => {
bump(&mut e.vis);
Item::Enum(e)
}
Item::Type(mut t) => {
bump(&mut t.vis);
Item::Type(t)
}
Item::Union(mut u) => {
bump(&mut u.vis);
Item::Union(u)
}
Item::Trait(mut t) => {
bump(&mut t.vis);
Item::Trait(t)
}
Item::Const(mut c) => {
bump(&mut c.vis);
Item::Const(c)
}
Item::Static(mut s) => {
bump(&mut s.vis);
Item::Static(s)
}
Item::Mod(mut m) => {
bump(&mut m.vis);
Item::Mod(m)
}
other => other,
}
}
pub(super) fn upgrade_inherent_impl_methods_visibility(item: Item) -> Item {
match item {
Item::Impl(mut imp) if imp.trait_.is_none() => {
for impl_item in &mut imp.items {
match impl_item {
syn::ImplItem::Fn(m) if matches!(m.vis, syn::Visibility::Inherited) => {
m.vis = syn::parse_quote!(pub (super));
}
syn::ImplItem::Const(c) if matches!(c.vis, syn::Visibility::Inherited) => {
c.vis = syn::parse_quote!(pub (super));
}
syn::ImplItem::Type(t) if matches!(t.vis, syn::Visibility::Inherited) => {
t.vis = syn::parse_quote!(pub (super));
}
_ => {}
}
}
Item::Impl(imp)
}
other => other,
}
}
pub(super) fn apply_specific_field_visibility(
item: Item,
struct_name: &str,
fields_to_upgrade: &HashSet<String>,
) -> Item {
match item {
Item::Struct(mut s) => {
if s.ident == struct_name {
for field in &mut s.fields {
if let Some(ident) = &field.ident {
let field_name = ident.to_string();
if fields_to_upgrade.contains(&field_name)
&& matches!(field.vis, syn::Visibility::Inherited)
{
field.vis = syn::parse_quote!(pub (super));
}
}
}
}
Item::Struct(s)
}
other => other,
}
}
#[allow(dead_code)]
pub fn generate_mod_rs(
modules: &[Module],
output_dir: &Path,
test_module_path: Option<&str>,
has_extracted_tests: bool,
file_inner_docs: &[syn::Attribute],
) -> Result<String> {
generate_mod_rs_ext(
modules,
output_dir,
test_module_path,
has_extracted_tests,
file_inner_docs,
&[],
&[],
crate::config::FacadeStyle::Glob,
&[],
)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_mod_rs_ext(
modules: &[Module],
_output_dir: &Path,
test_module_path: Option<&str>,
has_extracted_tests: bool,
file_inner_docs: &[syn::Attribute],
file_inner_attrs: &[syn::Attribute],
child_mods: &[syn::ItemMod],
facade: crate::config::FacadeStyle,
scope_uses: &[Item],
) -> Result<String> {
use crate::config::FacadeStyle;
let mut content = String::new();
for attr in file_inner_docs {
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident("doc") {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
let doc_text = s.value();
if doc_text.trim().is_empty() {
content.push_str("//!\n");
} else {
content.push_str(&format!("//!{}\n", doc_text));
}
}
}
}
}
if file_inner_docs.is_empty() {
content.push_str("//! Auto-generated module structure\n\n");
} else {
content.push('\n');
}
if !file_inner_attrs.is_empty() {
let rendered = prettyplease::unparse(&File {
shebang: None,
attrs: file_inner_attrs.to_vec(),
items: Vec::new(),
});
content.push_str(&rendered);
content.push('\n');
}
if !scope_uses.is_empty() {
let rendered = prettyplease::unparse(&File {
shebang: None,
attrs: Vec::new(),
items: scope_uses.to_vec(),
});
content.push_str(&rendered);
content.push('\n');
}
let defines_macro = |module: &Module| {
module
.standalone_items
.iter()
.any(|item| matches!(item, Item::Macro(m) if m.ident.is_some()))
};
for module in modules.iter().filter(|m| defines_macro(m)) {
content.push_str(&format!("#[macro_use]\npub mod {};\n", module.name));
}
for module in modules.iter().filter(|m| !defines_macro(m)) {
content.push_str(&format!("pub mod {};\n", module.name));
}
for child in child_mods {
let rendered = prettyplease::unparse(&File {
shebang: None,
attrs: Vec::new(),
items: vec![Item::Mod(child.clone())],
});
content.push_str(&rendered);
}
match facade {
FacadeStyle::Glob => {
content.push_str("\n// Re-export all types\n");
for module in modules {
if module.has_public_reexport() {
content.push_str(&format!("pub use {}::*;\n", module.name));
}
}
}
FacadeStyle::Named => {
content.push_str("\n// Re-export all types\n");
for module in modules {
let names = module.public_export_names();
if names.is_empty() {
continue;
}
if names.len() == 1 {
content.push_str(&format!("pub use {}::{};\n", module.name, names[0]));
} else {
content.push_str(&format!(
"pub use {}::{{{}}};\n",
module.name,
names.join(", ")
));
}
}
}
FacadeStyle::None => {}
}
if let Some(test_path) = test_module_path {
content.push_str("\n#[cfg(test)]\n");
content.push_str(&format!("#[path = \"{}\"]\n", test_path));
content.push_str("mod tests;\n");
}
if has_extracted_tests {
content.push_str("\n#[cfg(test)]\nmod tests;\n");
}
Ok(content)
}
#[allow(dead_code)]
pub fn generate_tests_rs(extracted_tests: &[Item]) -> String {
generate_tests_rs_with_uses(extracted_tests, &[])
}
pub fn generate_tests_rs_with_uses(extracted_tests: &[Item], file_uses: &[Item]) -> String {
generate_tests_rs_with_imports(extracted_tests, file_uses, &HashMap::new())
}
pub fn generate_tests_rs_with_imports(
extracted_tests: &[Item],
file_uses: &[Item],
sibling_imports: &HashMap<String, Vec<String>>,
) -> String {
generate_tests_rs_with_imports_deep(extracted_tests, file_uses, sibling_imports, false)
}
pub fn generate_tests_rs_with_imports_deep(
extracted_tests: &[Item],
file_uses: &[Item],
sibling_imports: &HashMap<String, Vec<String>>,
deepen_super: bool,
) -> String {
generate_tests_rs_full(
extracted_tests,
file_uses,
sibling_imports,
deepen_super,
&HashSet::new(),
None,
)
}
pub fn generate_tests_rs_full(
extracted_tests: &[Item],
file_uses: &[Item],
sibling_imports: &HashMap<String, Vec<String>>,
deepen_super: bool,
parent_resolvable: &HashSet<String>,
original_source: Option<&str>,
) -> String {
let mut content = String::from(
"//! Auto-generated test module (consolidated from inline `#[cfg(test)] mod` blocks)\n\n",
);
let mut refs = RefVisitor::default();
for it in extracted_tests {
refs.visit_item(it);
}
let mut test_used: HashSet<String> = refs.path_roots.clone();
test_used.extend(refs.attr_idents.iter().cloned());
let test_called_methods: HashSet<String> = refs.method_calls.clone();
let mut explicit_uses: Vec<Item> = Vec::new();
let mut glob_uses: Vec<Item> = Vec::new();
let mut explicit_bound: HashSet<String> = HashSet::new();
for it in file_uses.iter().filter(|it| matches!(it, Item::Use(_))) {
let Some(pruned) = Module::prune_unused_use(it, &test_used, &test_called_methods) else {
continue;
};
if use_tree_is_pure_glob(&pruned) {
glob_uses.push(pruned);
} else {
collect_use_bound_names(&pruned, &mut explicit_bound);
explicit_uses.push(pruned);
}
}
let mut resolved: HashSet<String> = explicit_bound;
resolved.extend(parent_resolvable.iter().cloned());
for kw in ["Self", "self", "super", "crate", "std", "core", "alloc"] {
resolved.insert(kw.to_string());
}
for name in std_prelude_names() {
resolved.insert(name.to_string());
}
let needs_glob = refs
.path_roots
.iter()
.chain(refs.attr_idents.iter())
.any(|name| {
name.chars().next().is_some_and(|c| c.is_uppercase()) && !resolved.contains(name)
});
let mut kept: Vec<Item> = explicit_uses;
if needs_glob {
kept.append(&mut glob_uses);
}
let use_items: Vec<Item> = if deepen_super {
kept.iter().map(deepen_super_in_use).collect()
} else {
kept
};
if !use_items.is_empty() {
let formatted = prettyplease::unparse(&syn::File {
shebang: None,
attrs: Vec::new(),
items: use_items,
});
content.push_str(&formatted);
content.push('\n');
}
content.push_str("use super::*;\n");
{
let mut modules: Vec<&String> = sibling_imports.keys().collect();
modules.sort();
for module_name in modules {
let mut fns = sibling_imports[module_name].clone();
fns.sort();
fns.dedup();
if fns.is_empty() {
continue;
}
if fns.len() == 1 {
content.push_str(&format!("use super::{}::{};\n", module_name, fns[0]));
} else {
content.push_str(&format!(
"use super::{}::{{{}}};\n",
module_name,
fns.join(", ")
));
}
}
}
content.push('\n');
let mut used_names: std::collections::HashSet<String> = std::collections::HashSet::new();
used_names.insert("tests".to_string());
for item in extracted_tests {
let Item::Mod(mod_item) = item else { continue };
let original_name = mod_item.ident.to_string();
let unique_name = pick_unique_mod_name(&original_name, &used_names);
used_names.insert(unique_name.clone());
use syn::spanned::Spanned;
let verbatim = original_source.and_then(|src| {
let sm = crate::source_map::SourceMap::new(src);
sm.item_verbatim_with_indent(mod_item.span(), &mod_item.attrs)
});
let formatted = match verbatim {
Some(verbatim_text) => {
let mut s = if unique_name != original_name {
rename_mod_ident_verbatim(verbatim_text, &original_name, &unique_name)
} else {
verbatim_text.to_string()
};
if !s.ends_with('\n') {
s.push('\n');
}
s
}
None => {
let mut renamed = mod_item.clone();
if unique_name != original_name {
renamed.ident = syn::Ident::new(&unique_name, mod_item.ident.span());
}
prettyplease::unparse(&syn::File {
shebang: None,
attrs: Vec::new(),
items: vec![Item::Mod(renamed)],
})
}
};
content.push_str(&formatted);
if !formatted.ends_with('\n') {
content.push('\n');
}
}
content
}
fn rename_mod_ident_verbatim(verbatim: &str, original: &str, new_name: &str) -> String {
let bytes = verbatim.as_bytes();
let is_ident_byte = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let mut i = 0usize;
while let Some(rel) = verbatim[i..].find("mod") {
let start = i + rel;
let before_ok = start == 0 || !is_ident_byte(bytes[start - 1]);
let after = start + 3;
let after_ok = after >= bytes.len() || !is_ident_byte(bytes[after]);
if before_ok && after_ok {
let mut j = after;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if verbatim[j..].starts_with(original) {
let end = j + original.len();
let ident_after_ok = end >= bytes.len() || !is_ident_byte(bytes[end]);
if ident_after_ok {
let mut out = String::with_capacity(verbatim.len());
out.push_str(&verbatim[..j]);
out.push_str(new_name);
out.push_str(&verbatim[end..]);
return out;
}
}
}
i = start + 3;
}
verbatim.to_string()
}
fn pick_unique_mod_name(original: &str, used_names: &std::collections::HashSet<String>) -> String {
if !used_names.contains(original) {
return original.to_string();
}
let mut suffix = 2usize;
loop {
let candidate = format!("{}_{}", original, suffix);
if !used_names.contains(&candidate) {
return candidate;
}
suffix += 1;
}
}
pub(crate) fn extract_test_module_path(file: &File) -> Option<String> {
for item in &file.items {
if let Item::Mod(mod_item) = item {
let mut path_attr: Option<String> = None;
let mut is_test = false;
for attr in &mod_item.attrs {
let meta_path = attr.path();
if let Some(ident) = meta_path.get_ident() {
if ident == "cfg" {
if let syn::Meta::List(meta_list) = &attr.meta {
let tokens = meta_list.tokens.to_string();
if tokens.contains("test") {
is_test = true;
}
}
} else if ident == "path" {
if let syn::Meta::NameValue(nv) = &attr.meta {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &nv.value
{
path_attr = Some(lit_str.value());
}
}
}
}
}
if is_test && path_attr.is_some() {
return path_attr;
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn render(item: &Item) -> String {
prettyplease::unparse(&syn::File {
shebang: None,
attrs: Vec::new(),
items: vec![item.clone()],
})
.trim()
.to_string()
}
#[test]
fn strip_string_literals_removes_string_contents() {
let code = r#"# [doc = " Write stats for export"] pub struct PlyWriteStats { n : usize }"#;
let stripped = Module::strip_string_literals(code);
assert!(!stripped.contains("Write stats"));
assert!(stripped.contains("PlyWriteStats"));
assert!(stripped.contains("struct"));
}
#[test]
fn strip_string_literals_preserves_lifetimes() {
let code = "fn f < 'a > (x : & 'a str) -> & 'a str { x }";
let stripped = Module::strip_string_literals(code);
assert!(stripped.contains("'a"));
}
#[test]
fn doc_comment_word_does_not_count_as_used_ident() {
let item: Item = syn::parse_quote! {
#[doc = " Write stats describing a PLY export."] pub struct PlyWriteStats {
pub n : usize, }
};
let mut idents = HashSet::new();
Module::extract_idents_from_item(&item, &mut idents);
assert!(idents.contains("PlyWriteStats"));
assert!(
!idents.contains("Write"),
"doc-comment word leaked into used idents: {idents:?}"
);
}
#[test]
fn tests_rs_emits_sibling_private_imports() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { assert!(logit(0.5_f32)
.is_finite()); } }
};
let mut sibling_imports = HashMap::new();
sibling_imports.insert("functions".to_string(), vec!["logit".to_string()]);
let out = generate_tests_rs_with_imports(&[test_mod], &[], &sibling_imports);
assert!(
out.contains("use super::functions::logit;"),
"missing sibling private import in tests.rs:\n{out}"
);
assert!(out.contains("use super::*;"));
}
#[test]
fn macro_body_paths_are_captured_as_references() {
let item: Item = syn::parse_quote! {
impl Default for EvalConfig { fn default() -> Self { Self { metrics :
vec![EvalMetricKind::Psnr, EvalMetricKind::Ssim], } } }
};
let mut v = RefVisitor::default();
v.visit_item(&item);
assert!(
v.path_roots.contains("EvalMetricKind"),
"macro-body type not captured: {:?}",
v.path_roots
);
assert!(v.path_roots.contains("vec"));
}
#[test]
fn real_code_ident_still_detected() {
let item: Item = syn::parse_quote! {
pub fn writer < W : Write > (w : & mut W) { let _ = w; }
};
let mut idents = HashSet::new();
Module::extract_idents_from_item(&item, &mut idents);
assert!(idents.contains("Write"));
assert!(idents.contains("writer"));
}
#[test]
fn deepen_super_single_segment() {
let item: Item = syn::parse_quote! {
use super::catalog::Atom;
};
let out = deepen_super_in_use(&item);
assert_eq!(render(&out), "use super::super::catalog::Atom;");
}
#[test]
fn deepen_super_group() {
let item: Item = syn::parse_quote! {
use super::types:: { A, B };
};
let out = deepen_super_in_use(&item);
assert_eq!(render(&out), "use super::super::types::{A, B};");
}
#[test]
fn deepen_super_leaves_crate_and_external_untouched() {
let crate_use: Item = syn::parse_quote! {
use crate ::foo::Bar;
};
assert_eq!(
render(&deepen_super_in_use(&crate_use)),
"use crate::foo::Bar;"
);
let ext_use: Item = syn::parse_quote! {
use std::collections::HashMap;
};
assert_eq!(
render(&deepen_super_in_use(&ext_use)),
"use std::collections::HashMap;"
);
let self_use: Item = syn::parse_quote! {
use self::inner::Thing;
};
assert_eq!(
render(&deepen_super_in_use(&self_use)),
"use self::inner::Thing;"
);
}
#[test]
fn tests_rs_deepens_forwarded_super_imports() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { let _ = Sensitivity::High;
assert!(helper().is_some()); } }
};
let file_use: Item = syn::parse_quote! {
use super::config_types::Sensitivity;
};
let mut sibling_imports = HashMap::new();
sibling_imports.insert("functions".to_string(), vec!["helper".to_string()]);
let out = generate_tests_rs_with_imports_deep(
&[test_mod],
std::slice::from_ref(&file_use),
&sibling_imports,
true,
);
assert!(
out.contains("use super::super::config_types::Sensitivity;"),
"forwarded file-level super import was not deepened:\n{out}"
);
assert!(out.contains("use super::*;"), "missing parent glob:\n{out}");
assert!(
out.contains("use super::functions::helper;"),
"sibling import wrongly deepened or missing:\n{out}"
);
assert!(
!out.contains("super::super::functions"),
"sibling import must not be deepened:\n{out}"
);
}
#[test]
fn rename_mod_ident_verbatim_only_touches_declaration() {
let verbatim =
"#[cfg(test)]\nmod tests {\n // mod tests is mentioned here too\n fn t() {}\n}\n";
let renamed = rename_mod_ident_verbatim(verbatim, "tests", "tests_2");
assert!(
renamed.starts_with("#[cfg(test)]\nmod tests_2 {"),
"declaration must be renamed:\n{renamed}"
);
assert!(
renamed.contains("// mod tests is mentioned here too"),
"unrelated `mod tests` inside a comment must be left untouched:\n{renamed}"
);
}
#[test]
fn rename_mod_ident_verbatim_noop_when_pattern_missing() {
let verbatim = "not a mod item";
let renamed = rename_mod_ident_verbatim(verbatim, "tests", "tests_2");
assert_eq!(
renamed, verbatim,
"unmatched input must pass through unchanged"
);
}
#[test]
fn tests_rs_full_preserves_inline_comments_verbatim() {
let source = "#[cfg(test)]\n\
mod tests {\n \
#[test]\n \
fn t() {\n \
// Explains why 2 is expected here.\n \
assert_eq!(1 + 1, 2);\n \
}\n\
}\n";
let syntax_tree: syn::File = syn::parse_file(source).expect("parse fixture");
let test_mod = syntax_tree
.items
.into_iter()
.find(|it| matches!(it, Item::Mod(_)))
.expect("fixture must contain a mod item");
let out = generate_tests_rs_full(
std::slice::from_ref(&test_mod),
&[],
&HashMap::new(),
false,
&HashSet::new(),
Some(source),
);
assert!(
out.contains("// Explains why 2 is expected here."),
"inline `//` comment was dropped from verbatim-emitted test mod:\n{out}"
);
assert!(
out.contains("mod tests_2 {"),
"expected the sole `mod tests` to be renamed to `mod tests_2`:\n{out}"
);
}
#[test]
fn tests_rs_full_falls_back_to_prettyplease_without_source() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { assert_eq!(1 + 1, 2); } }
};
let out = generate_tests_rs_full(
&[test_mod],
&[],
&HashMap::new(),
false,
&HashSet::new(),
None,
);
assert!(out.contains("mod tests_2"), "fallback rename lost:\n{out}");
assert!(out.contains("fn t"), "fallback body lost:\n{out}");
}
#[test]
fn tests_rs_drops_unused_inherited_glob() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { let _ = Foo::default(); } }
};
let glob_use: Item = syn::parse_quote! {
use super::cfg::*;
};
let mut parent = HashSet::new();
parent.insert("Foo".to_string());
let out = generate_tests_rs_full(
&[test_mod],
std::slice::from_ref(&glob_use),
&HashMap::new(),
true,
&parent,
None,
);
assert!(
!out.contains("super::cfg") && !out.contains("super::super::cfg"),
"dead inherited glob was not dropped from tests.rs:\n{out}"
);
assert!(out.contains("use super::*;"));
}
#[test]
fn tests_rs_keeps_needed_inherited_glob() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { let _ = Bar::new(); } }
};
let glob_use: Item = syn::parse_quote! {
use super::cfg::*;
};
let parent = HashSet::new();
let out = generate_tests_rs_full(
&[test_mod],
std::slice::from_ref(&glob_use),
&HashMap::new(),
true,
&parent,
None,
);
assert!(
out.contains("use super::super::cfg::*;"),
"needed inherited glob was wrongly dropped or not deepened:\n{out}"
);
}
#[test]
fn tests_rs_without_deepen_keeps_original_depth() {
let test_mod: Item = syn::parse_quote! {
#[cfg(test)] mod tests { #[test] fn t() { let _ = Sensitivity::High; } }
};
let file_use: Item = syn::parse_quote! {
use super::config_types::Sensitivity;
};
let out = generate_tests_rs_with_imports_deep(
&[test_mod],
std::slice::from_ref(&file_use),
&HashMap::new(),
false,
);
assert!(
out.contains("use super::config_types::Sensitivity;"),
"non-deepen path must keep the original single super:\n{out}"
);
assert!(!out.contains("super::super::config_types"));
}
#[test]
fn pure_glob_detection() {
let glob: Item = syn::parse_quote! {
use super::super::config_types::*;
};
assert!(use_tree_is_pure_glob(&glob));
let named: Item = syn::parse_quote! {
use super::types::Foo;
};
assert!(!use_tree_is_pure_glob(&named));
let mixed: Item = syn::parse_quote! {
use a:: { b::*, C };
};
assert!(!use_tree_is_pure_glob(&mixed));
}
#[test]
fn collect_use_bound_names_handles_groups_and_renames() {
let mut names = HashSet::new();
let grouped: Item = syn::parse_quote! {
use a:: { B, C as D, e::F };
};
collect_use_bound_names(&grouped, &mut names);
assert!(names.contains("B"));
assert!(names.contains("D"));
assert!(!names.contains("C"));
assert!(names.contains("F"));
let mut g = HashSet::new();
let glob: Item = syn::parse_quote! {
use a::*;
};
collect_use_bound_names(&glob, &mut g);
assert!(g.is_empty());
}
#[test]
fn item_defined_ident_covers_common_kinds() {
let s: Item = syn::parse_quote! {
pub struct Foo { x : u8 }
};
assert_eq!(item_defined_ident(&s).as_deref(), Some("Foo"));
let f: Item = syn::parse_quote! {
fn bar() {}
};
assert_eq!(item_defined_ident(&f).as_deref(), Some("bar"));
let c: Item = syn::parse_quote! {
const BAZ : u8 = 0;
};
assert_eq!(item_defined_ident(&c).as_deref(), Some("BAZ"));
let imp: Item = syn::parse_quote! {
impl Foo {}
};
assert_eq!(item_defined_ident(&imp), None);
let u: Item = syn::parse_quote! {
use a::B;
};
assert_eq!(item_defined_ident(&u), None);
}
}