#![allow(dead_code)]
use crate::config::{FacadeStyle, TargetModule};
use crate::domain_router;
use crate::file_analyzer::FileAnalyzer;
use crate::module_generator::{
deepen_super_in_use, deepen_super_in_use_tree, extract_test_module_path, generate_mod_rs_ext,
generate_tests_rs_full, item_defined_ident, item_visibility, Module, RefVisitor,
};
use anyhow::{Context, Result};
use proc_macro2::{Group, Ident, Punct, Spacing, TokenStream, TokenTree};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use syn::visit::Visit;
use syn::visit_mut::VisitMut;
pub struct NestedSplitOptions<'a> {
pub split_impl_blocks: bool,
pub max_impl_lines: usize,
pub max_lines: usize,
pub extract_tests: bool,
pub max_mod_depth: usize,
pub seeded_assignment: bool,
pub all_rules: &'a [TargetModule],
}
pub struct NestedModPlan {
pub name: String,
pub vis: syn::Visibility,
pub outer_attrs: Vec<syn::Attribute>,
pub inner_attrs: Vec<syn::Attribute>,
pub modules: Vec<Module>,
pub children: Vec<NestedModPlan>,
pub extracted_tests: Vec<syn::Item>,
pub synthetic_file: syn::File,
pub analyzer: FileAnalyzer,
pub needs_pub_super: HashSet<String>,
pub cross_module_imports: HashMap<String, HashMap<String, Vec<String>>>,
pub fields_need_pub_super: HashMap<String, HashSet<String>>,
pub scope_uses: Vec<syn::Item>,
}
impl NestedModPlan {
pub fn decl_item(&self) -> syn::ItemMod {
syn::ItemMod {
attrs: self.outer_attrs.clone(),
vis: self.vis.clone(),
unsafety: None,
mod_token: Default::default(),
ident: Ident::new(&self.name, proc_macro2::Span::call_site()),
content: None,
semi: Some(Default::default()),
}
}
fn collect_paths(&self, prefix: &str, out: &mut HashSet<String>) {
let path = if prefix.is_empty() {
self.name.clone()
} else {
format!("{}::{}", prefix, self.name)
};
for child in &self.children {
child.collect_paths(&path, out);
}
out.insert(path);
}
}
pub fn plan_nested_split(
mod_item: &syn::ItemMod,
original_source: &str,
opts: &NestedSplitOptions<'_>,
mod_path: &str,
depth: usize,
) -> Result<NestedModPlan> {
let Some((_, body_items)) = &mod_item.content else {
anyhow::bail!(
"internal error: nested module `{}` has no inline body",
mod_item.ident
);
};
let inner_all: Vec<syn::Attribute> = mod_item
.attrs
.iter()
.filter(|attr| matches!(attr.style, syn::AttrStyle::Inner(_)))
.cloned()
.collect();
let outer_attrs: Vec<syn::Attribute> = mod_item
.attrs
.iter()
.filter(|attr| matches!(attr.style, syn::AttrStyle::Outer))
.cloned()
.collect();
let inner_attrs: Vec<syn::Attribute> = inner_all
.iter()
.filter(|attr| !attr.path().is_ident("doc"))
.cloned()
.collect();
let synthetic_file = syn::File {
shebang: None,
attrs: inner_all,
items: body_items.clone(),
};
let mut analyzer = FileAnalyzer::new(opts.split_impl_blocks, opts.max_impl_lines);
analyzer.set_extract_tests(opts.extract_tests);
analyzer.set_seeded_assignment(opts.seeded_assignment);
let my_rules: Vec<TargetModule> = opts
.all_rules
.iter()
.filter(|rule| rule.parent.as_deref() == Some(mod_path))
.cloned()
.collect();
analyzer.set_target_modules(my_rules.clone());
analyzer.set_source(original_source);
if depth < opts.max_mod_depth {
analyzer.set_split_nested_mods(true, opts.max_lines);
}
analyzer.analyze(&synthetic_file);
domain_router::check_unmatched_patterns(&domain_router::routable_names(&analyzer), &my_rules)
.with_context(|| format!("in target-modules rules for parent `{}`", mod_path))?;
let child_mods = analyzer.take_nested_mods();
let mut children = Vec::new();
for child in &child_mods {
let child_path = format!("{}::{}", mod_path, child.ident);
children.push(plan_nested_split(
child,
original_source,
opts,
&child_path,
depth + 1,
)?);
}
let has_extracted_tests = !analyzer.extracted_tests.is_empty();
if has_extracted_tests && children.iter().any(|c| c.name == "tests") {
anyhow::bail!(
"nested module `{}::tests` conflicts with the tests.rs produced by --extract-tests; \
re-run without --extract-tests or rename the module",
mod_path
);
}
let mut modules = analyzer.group_by_module(opts.max_lines);
let mut reserved: HashSet<String> = children.iter().map(|c| c.name.clone()).collect();
if has_extracted_tests {
reserved.insert("tests".to_string());
}
rename_module_collisions(&mut modules, &reserved);
for module in &mut modules {
deepen_module_items(module);
}
let child_names: Vec<String> = children.iter().map(|c| c.name.clone()).collect();
add_child_mod_imports(&mut modules, &child_names);
for module in &modules {
for item in &module.standalone_items {
if let syn::Item::Trait(trait_item) = item {
let trait_name = trait_item.ident.to_string();
analyzer
.trait_tracker
.register_trait_module(&trait_name, &module.name);
}
}
}
let (mut needs_pub_super, cross_module_imports, fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
let extracted_tests = analyzer.take_extracted_tests();
let scope_uses = compute_parent_scope_items(
&child_mods,
&analyzer.use_statements,
&modules,
&mut needs_pub_super,
false,
);
Ok(NestedModPlan {
name: mod_item.ident.to_string(),
vis: mod_item.vis.clone(),
outer_attrs,
inner_attrs,
modules,
children,
extracted_tests,
synthetic_file,
analyzer,
needs_pub_super,
cross_module_imports,
fields_need_pub_super,
scope_uses,
})
}
pub fn write_plan(
plan: &NestedModPlan,
parent_dir: &Path,
facade: FacadeStyle,
) -> Result<Vec<PathBuf>> {
let dir = parent_dir.join(&plan.name);
fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create nested module directory {:?}", dir))?;
let mut created = Vec::new();
let mut type_to_module: HashMap<String, String> = HashMap::new();
for module in &plan.modules {
for exported in module.get_exported_types() {
type_to_module.insert(exported, module.name.clone());
}
}
for module in &plan.modules {
let content = module.generate_content(
&plan.synthetic_file,
&plan.analyzer.use_statements,
&type_to_module,
&plan.needs_pub_super,
plan.cross_module_imports.get(&module.name),
&plan.fields_need_pub_super,
Some(&plan.analyzer.trait_tracker),
);
let module_path = dir.join(format!("{}.rs", module.name));
fs::write(&module_path, &content)
.with_context(|| format!("Failed to write nested module file {:?}", module_path))?;
if let Err(e) = syn::parse_file(&content) {
eprintln!(
"Warning: generated nested module {:?} may contain syntax errors: {}",
module_path, e
);
}
created.push(module_path);
}
let has_extracted_tests = !plan.extracted_tests.is_empty();
if has_extracted_tests {
let empty_imports = HashMap::new();
let tests_sibling_imports = plan
.cross_module_imports
.get("tests")
.unwrap_or(&empty_imports);
let tests_parent_resolvable: HashSet<String> = type_to_module.keys().cloned().collect();
let tests_content = generate_tests_rs_full(
&plan.extracted_tests,
&plan.analyzer.use_statements,
tests_sibling_imports,
true,
&tests_parent_resolvable,
);
let tests_path = dir.join("tests.rs");
fs::write(&tests_path, &tests_content)
.with_context(|| format!("Failed to write nested tests file {:?}", tests_path))?;
if let Err(e) = syn::parse_file(&tests_content) {
eprintln!(
"Warning: generated nested tests.rs may contain syntax errors: {}",
e
);
}
created.push(tests_path);
}
let mut child_decls: Vec<syn::ItemMod> = Vec::new();
for child in &plan.children {
created.extend(write_plan(child, &dir, facade)?);
child_decls.push(child.decl_item());
}
let test_module_path = extract_test_module_path(&plan.synthetic_file);
let mod_content = generate_mod_rs_ext(
&plan.modules,
&dir,
test_module_path.as_deref(),
has_extracted_tests,
&plan.analyzer.file_inner_docs,
&plan.inner_attrs,
&child_decls,
facade,
&plan.scope_uses,
)?;
let mod_path = dir.join("mod.rs");
fs::write(&mod_path, &mod_content)
.with_context(|| format!("Failed to write nested mod.rs {:?}", mod_path))?;
if let Err(e) = syn::parse_file(&mod_content) {
eprintln!(
"Warning: generated nested mod.rs {:?} may contain syntax errors: {}",
mod_path, e
);
}
created.push(mod_path);
Ok(created)
}
pub fn validate_parent_rules(rules: &[TargetModule], plans: &[NestedModPlan]) -> Result<()> {
let mut known_paths: HashSet<String> = HashSet::new();
for plan in plans {
plan.collect_paths("", &mut known_paths);
}
for rule in rules {
let Some(parent) = &rule.parent else { continue };
if known_paths.contains(parent) {
continue;
}
let mut known: Vec<&String> = known_paths.iter().collect();
known.sort();
let listing = if known.is_empty() {
"none (no inline module exceeded the --max-lines budget)".to_string()
} else {
known
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
};
anyhow::bail!(
"target-modules rule `{}` declares parent = \"{}\", but no such nested module was \
descended. Descended module paths: {}. Check the path spelling, ensure the module \
body exceeds --max-lines, and that --split-nested-mods is enabled.",
rule.name,
parent,
listing
);
}
Ok(())
}
pub fn rename_module_collisions(modules: &mut [Module], reserved: &HashSet<String>) {
if reserved.is_empty() {
return;
}
let mut used: HashSet<String> = modules.iter().map(|m| m.name.clone()).collect();
used.extend(reserved.iter().cloned());
for module in modules.iter_mut() {
if !reserved.contains(&module.name) {
continue;
}
let mut suffix = 2usize;
loop {
let candidate = format!("{}_{}", module.name, suffix);
if !used.contains(&candidate) {
used.insert(candidate.clone());
module.name = candidate;
break;
}
suffix += 1;
}
}
}
pub fn add_child_mod_imports(modules: &mut [Module], child_names: &[String]) {
if child_names.is_empty() {
return;
}
for module in modules.iter_mut() {
let refs = module.analyze_references();
let local = module.local_item_names();
for child in child_names {
if refs.path_roots.contains(child) && !local.contains(child) {
module.sibling_mod_imports.push(child.clone());
}
}
}
}
pub fn compute_parent_scope_items(
nested_mods: &[syn::ItemMod],
use_statements: &[syn::Item],
modules: &[Module],
needs_pub_super: &mut HashSet<String>,
deepen: bool,
) -> Vec<syn::Item> {
if nested_mods.is_empty() {
return Vec::new();
}
let mut referenced: HashSet<String> = HashSet::new();
let mut method_calls: HashSet<String> = HashSet::new();
for mod_item in nested_mods {
let Some((_, body_items)) = &mod_item.content else {
continue;
};
let mut plain = RefVisitor::default();
let mut supers = SuperTargetCollector::default();
let mut defined: HashSet<String> = HashSet::new();
for item in body_items {
plain.visit_item(item);
supers.visit_item(item);
if let Some(name) = item_defined_ident(item) {
defined.insert(name);
}
}
method_calls.extend(plain.method_calls.iter().cloned());
let mut names: HashSet<String> = plain.path_roots;
names.extend(plain.attr_idents);
names.extend(supers.names);
for name in names {
if !defined.contains(&name) {
referenced.insert(name);
}
}
}
for keyword in ["super", "crate", "self", "Self"] {
referenced.remove(keyword);
}
let mut out: Vec<syn::Item> = Vec::new();
for use_item in use_statements {
let Some(pruned) = Module::prune_unused_use(use_item, &referenced, &method_calls) else {
continue;
};
if deepen {
out.push(deepen_super_in_use(&pruned));
} else {
out.push(pruned);
}
}
let mut bound: HashSet<String> = HashSet::new();
for module in modules {
let mut push_binding = |module_name: &str, name: &str, bound: &mut HashSet<String>| {
if !bound.insert(name.to_string()) {
return;
}
if let Ok(item) =
syn::parse_str::<syn::Item>(&format!("use self::{}::{};", module_name, name))
{
out.push(item);
}
};
for type_info in &module.types {
if !referenced.contains(&type_info.name) {
continue;
}
if matches!(
item_visibility(&type_info.item),
Some(syn::Visibility::Public(_))
) {
continue; }
push_binding(&module.name, &type_info.name, &mut bound);
}
for item in &module.standalone_items {
if matches!(item, syn::Item::Macro(_)) {
continue; }
let Some(name) = item_defined_ident(item) else {
continue;
};
if !referenced.contains(&name) {
continue;
}
if matches!(item_visibility(item), Some(syn::Visibility::Public(_))) {
continue;
}
if matches!(item, syn::Item::Fn(_)) {
needs_pub_super.insert(name.clone());
}
push_binding(&module.name, &name, &mut bound);
}
}
out
}
#[derive(Default)]
struct SuperTargetCollector {
names: HashSet<String>,
}
impl<'ast> Visit<'ast> for SuperTargetCollector {
fn visit_path(&mut self, path: &'ast syn::Path) {
if path.leading_colon.is_none() {
if let Some(first) = path.segments.first() {
if first.ident == "super" {
if let Some(target) = path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.find(|ident| ident != "super")
{
self.names.insert(target);
}
}
}
}
syn::visit::visit_path(self, path);
}
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
let tokens = mac.tokens.clone();
if let Ok(exprs) = syn::parse::Parser::parse2(
syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated,
tokens.clone(),
) {
for expr in &exprs {
self.visit_expr(expr);
}
} else if let Ok(expr) = syn::parse2::<syn::Expr>(tokens) {
self.visit_expr(&expr);
}
syn::visit::visit_macro(self, mac);
}
fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
fn walk(tree: &syn::UseTree, seen_super: bool, names: &mut HashSet<String>) {
match tree {
syn::UseTree::Path(path) => {
if path.ident == "super" {
walk(&path.tree, true, names);
} else if seen_super {
names.insert(path.ident.to_string());
}
}
syn::UseTree::Name(name) if seen_super => {
names.insert(name.ident.to_string());
}
syn::UseTree::Rename(rename) if seen_super => {
names.insert(rename.ident.to_string());
}
syn::UseTree::Group(group) => {
for tree in &group.items {
walk(tree, seen_super, names);
}
}
_ => {}
}
}
walk(&item.tree, false, &mut self.names);
syn::visit::visit_item_use(self, item);
}
}
pub fn dry_run_lines(plan: &NestedModPlan, indent: usize) -> Vec<String> {
let pad = " ".repeat(indent);
let mut lines = vec![format!("{}📁 {}/", pad, plan.name)];
for module in &plan.modules {
lines.push(format!(
"{} 📄 {}.rs ({} types, {} items, {} trait impls)",
pad,
module.name,
module.types.len(),
module.standalone_items.len(),
module.trait_impls.len()
));
}
if !plan.extracted_tests.is_empty() {
lines.push(format!("{} 📄 tests.rs", pad));
}
for child in &plan.children {
lines.extend(dry_run_lines(child, indent + 1));
}
lines.push(format!("{} 📄 mod.rs", pad));
lines
}
pub fn deepen_module_items(module: &mut Module) {
module.deepen_super = true;
let aligned = module.standalone_verbatim.len() == module.standalone_items.len();
for (idx, item) in module.standalone_items.iter_mut().enumerate() {
if deepen_super_in_item(item) && aligned {
module.standalone_verbatim[idx] = None;
}
}
for type_info in &mut module.types {
if deepen_super_in_item(&mut type_info.item) {
type_info.verbatim = None;
}
for impl_item in &mut type_info.impls {
deepen_super_in_item(impl_item);
}
for trait_impl in &mut type_info.trait_impls {
if deepen_super_in_item(&mut trait_impl.impl_item) {
trait_impl.verbatim = None;
}
}
}
for trait_impl in &mut module.trait_impls {
if deepen_super_in_item(&mut trait_impl.impl_item) {
trait_impl.verbatim = None;
}
}
if let Some(group) = &mut module.method_group {
for method in &mut group.methods {
let mut deepener = SuperDeepener { changed: false };
deepener.visit_impl_item_fn_mut(&mut method.item);
if deepener.changed {
method.verbatim = None;
}
}
}
if let Some(self_ty) = &mut module.impl_self_ty {
let mut deepener = SuperDeepener { changed: false };
deepener.visit_type_mut(self_ty);
if deepener.changed {
module.impl_header_verbatim = None;
}
}
if let Some(generics) = &mut module.impl_generics {
let mut deepener = SuperDeepener { changed: false };
deepener.visit_generics_mut(generics);
if deepener.changed {
module.impl_header_verbatim = None;
}
}
}
pub fn deepen_super_in_item(item: &mut syn::Item) -> bool {
let mut deepener = SuperDeepener { changed: false };
deepener.visit_item_mut(item);
deepener.changed
}
struct SuperDeepener {
changed: bool,
}
fn path_head_is_super(path: &syn::Path) -> bool {
path.leading_colon.is_none()
&& path
.segments
.first()
.is_some_and(|segment| segment.ident == "super")
}
fn deepen_path_head(path: &mut syn::Path) -> bool {
if !path_head_is_super(path) {
return false;
}
let span = path
.segments
.first()
.map(|segment| segment.ident.span())
.unwrap_or_else(proc_macro2::Span::call_site);
path.segments.insert(
0,
syn::PathSegment {
ident: Ident::new("super", span),
arguments: syn::PathArguments::None,
},
);
true
}
fn use_tree_head_is_super(tree: &syn::UseTree) -> bool {
matches!(tree, syn::UseTree::Path(p) if p.ident == "super")
}
impl VisitMut for SuperDeepener {
fn visit_path_mut(&mut self, path: &mut syn::Path) {
if deepen_path_head(path) {
self.changed = true;
}
syn::visit_mut::visit_path_mut(self, path);
}
fn visit_expr_path_mut(&mut self, node: &mut syn::ExprPath) {
let deepened = path_head_is_super(&node.path);
syn::visit_mut::visit_expr_path_mut(self, node);
if deepened {
if let Some(qself) = &mut node.qself {
qself.position += 1;
}
}
}
fn visit_type_path_mut(&mut self, node: &mut syn::TypePath) {
let deepened = path_head_is_super(&node.path);
syn::visit_mut::visit_type_path_mut(self, node);
if deepened {
if let Some(qself) = &mut node.qself {
qself.position += 1;
}
}
}
fn visit_vis_restricted_mut(&mut self, vis: &mut syn::VisRestricted) {
let first = vis
.path
.segments
.first()
.map(|segment| segment.ident.to_string());
match first.as_deref() {
Some("super") => {
let span = vis
.path
.segments
.first()
.map(|segment| segment.ident.span())
.unwrap_or_else(proc_macro2::Span::call_site);
vis.path.segments.insert(
0,
syn::PathSegment {
ident: Ident::new("super", span),
arguments: syn::PathArguments::None,
},
);
vis.in_token = Some(Default::default());
self.changed = true;
}
Some("self") if vis.path.segments.len() == 1 && vis.in_token.is_none() => {
if let Some(segment) = vis.path.segments.first_mut() {
segment.ident = Ident::new("super", segment.ident.span());
}
self.changed = true;
}
_ => {}
}
}
fn visit_item_use_mut(&mut self, item: &mut syn::ItemUse) {
if use_tree_head_is_super(&item.tree) {
deepen_super_in_use_tree(&mut item.tree);
self.changed = true;
}
}
fn visit_macro_mut(&mut self, mac: &mut syn::Macro) {
let (tokens, changed) = deepen_super_tokens(&mac.tokens);
if changed {
mac.tokens = tokens;
self.changed = true;
}
syn::visit_mut::visit_macro_mut(self, mac);
}
}
fn deepen_super_tokens(tokens: &TokenStream) -> (TokenStream, bool) {
let mut changed = false;
let items: Vec<TokenTree> = tokens.clone().into_iter().collect();
let mut out: Vec<TokenTree> = Vec::with_capacity(items.len());
for (i, tt) in items.iter().enumerate() {
match tt {
TokenTree::Group(group) => {
let (inner, inner_changed) = deepen_super_tokens(&group.stream());
changed |= inner_changed;
let mut new_group = Group::new(group.delimiter(), inner);
new_group.set_span(group.span());
out.push(TokenTree::Group(new_group));
}
TokenTree::Ident(ident) if ident == "super" => {
let followed_by_colons = matches!(
(items.get(i + 1), items.get(i + 2)),
(Some(TokenTree::Punct(a)), Some(TokenTree::Punct(b)))
if a.as_char() == ':' && b.as_char() == ':'
);
let preceded_by_colons = out.len() >= 2
&& matches!(
(&out[out.len() - 2], &out[out.len() - 1]),
(TokenTree::Punct(a), TokenTree::Punct(b))
if a.as_char() == ':' && b.as_char() == ':'
);
if followed_by_colons && !preceded_by_colons {
out.push(TokenTree::Ident(Ident::new("super", ident.span())));
let mut c1 = Punct::new(':', Spacing::Joint);
c1.set_span(ident.span());
let mut c2 = Punct::new(':', Spacing::Alone);
c2.set_span(ident.span());
out.push(TokenTree::Punct(c1));
out.push(TokenTree::Punct(c2));
changed = true;
}
out.push(tt.clone());
}
_ => out.push(tt.clone()),
}
}
(out.into_iter().collect(), changed)
}
#[cfg(test)]
mod tests {
use super::*;
fn render_item(item: &syn::Item) -> String {
prettyplease::unparse(&syn::File {
shebang: None,
attrs: Vec::new(),
items: vec![item.clone()],
})
}
#[test]
fn deepens_use_statement_head() {
let mut item: syn::Item = syn::parse_quote! { use super::helper::Foo; };
assert!(deepen_super_in_item(&mut item));
assert!(render_item(&item).contains("use super::super::helper::Foo;"));
}
#[test]
fn deepens_expression_and_type_paths() {
let mut item: syn::Item = syn::parse_quote! {
fn f(x: super::Foo) -> super::Bar {
super::helper(x)
}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(rendered.contains("super::super::Foo"), "{rendered}");
assert!(rendered.contains("super::super::Bar"), "{rendered}");
assert!(rendered.contains("super::super::helper(x)"), "{rendered}");
}
#[test]
fn deepens_qualified_and_nested_generic_paths_once_each() {
let mut item: syn::Item = syn::parse_quote! {
fn f() -> super::Wrapper<super::Inner> {
<super::T as super::Tr>::call()
}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(
rendered.contains("super::super::Wrapper<super::super::Inner>"),
"{rendered}"
);
assert!(
rendered.contains("<super::super::T as super::super::Tr>::call()"),
"{rendered}"
);
assert!(!rendered.contains("super::super::super"), "{rendered}");
}
#[test]
fn deepens_pub_super_visibility() {
let mut item: syn::Item = syn::parse_quote! {
pub(super) fn f() {}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(
rendered.contains("pub(in super::super) fn f()"),
"{rendered}"
);
}
#[test]
fn deepens_pub_in_super_path_visibility() {
let mut item: syn::Item = syn::parse_quote! {
pub(in super::x) fn f() {}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(
rendered.contains("pub(in super::super::x) fn f()"),
"{rendered}"
);
}
#[test]
fn leaves_crate_and_external_paths_untouched() {
let mut item: syn::Item = syn::parse_quote! {
pub(crate) fn f(x: crate::Foo) -> std::io::Result<()> {
crate::helper(x)
}
};
assert!(!deepen_super_in_item(&mut item));
}
#[test]
fn deepens_super_inside_macro_tokens() {
let mut item: syn::Item = syn::parse_quote! {
fn f() {
println!("{}", super::VALUE);
}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(
rendered.contains("super :: super :: VALUE")
|| rendered.contains("super::super::VALUE"),
"{rendered}"
);
}
#[test]
fn multi_super_paths_gain_exactly_one_level() {
let mut item: syn::Item = syn::parse_quote! {
fn f() {
super::super::helper();
}
};
assert!(deepen_super_in_item(&mut item));
let rendered = render_item(&item);
assert!(
rendered.contains("super::super::super::helper()"),
"{rendered}"
);
assert!(
!rendered.contains("super::super::super::super"),
"{rendered}"
);
}
#[test]
fn rename_module_collisions_yields_to_child_names() {
let mut modules = vec![
Module::new("types".to_string()),
Module::new("functions".to_string()),
];
let mut reserved = HashSet::new();
reserved.insert("types".to_string());
rename_module_collisions(&mut modules, &reserved);
assert_eq!(modules[0].name, "types_2");
assert_eq!(modules[1].name, "functions");
}
}