use syn::{Item, Path, spanned::Spanned};
use super::{path::QualifiedPath, scope::FileScope};
use crate::{hash::HashSet, options::Options};
#[derive(Debug, Clone)]
pub struct ModuleContext {
pub insert_pos: usize,
pub indent: String,
pub has_existing_use: bool,
pub scope: FileScope,
pub paths: Vec<QualifiedPath>,
}
pub struct PathCollector<'a> {
pub max_segments: usize,
pub keep_segments: usize,
pub allow_crates: &'a [String],
pub known_crates: &'a HashSet<String>,
pub source: &'a str,
pub line_offsets: &'a [usize],
pub use_depth: usize,
pub attr_depth: usize,
pub cfg_depth: usize,
pub local_idents: Vec<String>,
pub module_stack: Vec<ModuleContext>,
pub modules: Vec<ModuleContext>,
}
impl<'a> PathCollector<'a> {
pub fn new(
options: &'a Options,
known_crates: &'a HashSet<String>,
root_module: ModuleContext,
source: &'a str,
line_offsets: &'a [usize],
) -> Self {
Self {
max_segments: options.max_segments,
keep_segments: options.keep_segments,
allow_crates: &options.allow_crates,
known_crates,
source,
line_offsets,
use_depth: 0,
attr_depth: 0,
cfg_depth: 0,
local_idents: Vec::new(),
module_stack: vec![root_module],
modules: Vec::new(),
}
}
pub fn finish(mut self) -> Vec<ModuleContext> {
while let Some(m) = self.module_stack.pop() {
self.modules.push(m);
}
self.modules
}
pub(crate) fn inspect_path(&mut self, path: &Path) {
let segments = &path.segments;
let total_segments = segments.len();
if total_segments <= self.max_segments {
return;
}
let Some(first_seg) = segments.first() else {
return;
};
let first_ident = &first_seg.ident;
if first_ident == "self" || first_ident == "super" {
return;
}
let is_crate = first_ident == "crate";
if self.allow_crates.iter().any(|c| first_ident == c.as_str()) {
return;
}
if path.leading_colon.is_none()
&& !is_crate
&& !self.known_crates.is_empty()
&& !self.known_crates.iter().any(|c| first_ident == c.as_str())
{
return;
}
let mut seg_strings = Vec::with_capacity(total_segments);
seg_strings.extend(segments.iter().map(|s| s.ident.to_string()));
let last_ident = seg_strings.last().map_or("", String::as_str);
let current_mod = self
.module_stack
.last_mut()
.expect("module_stack should never be empty");
let mut matched_existing_prefix = None;
if self.cfg_depth > 0 {
for k in (1..=total_segments).rev() {
if let Some(prefix_segs) = seg_strings.get(..k) {
let prefix = prefix_segs.join("::");
if current_mod.scope.existing_use_paths.contains(&prefix) {
matched_existing_prefix = Some(k);
break;
}
}
}
if matched_existing_prefix.is_none() {
return;
}
}
let mut import_segments_count = total_segments;
let mut keep_count = self.keep_segments.max(1);
if let Some(k) = matched_existing_prefix {
import_segments_count = k;
keep_count = total_segments.saturating_sub(k - 1).max(1);
} else {
if total_segments >= 3 {
let prev_is_upper = seg_strings
.get(total_segments - 2)
.and_then(|s| s.as_bytes().first())
.is_some_and(u8::is_ascii_uppercase);
if prev_is_upper {
import_segments_count = total_segments - 1;
keep_count = 2;
}
}
if last_ident == "Result"
&& total_segments >= 2
&& seg_strings
.get(import_segments_count.saturating_sub(2))
.map(String::as_str)
!= Some("result")
{
import_segments_count = total_segments - 1;
keep_count = 2;
}
if total_segments >= 3 {
let is_std_or_core_fmt = seg_strings
.first()
.is_some_and(|s| s == "std" || s == "core")
&& seg_strings.get(1).is_some_and(|s| s == "fmt");
if is_std_or_core_fmt {
import_segments_count = 2;
keep_count = total_segments - 1;
} else if seg_strings
.get(total_segments - 2)
.is_some_and(|s| s == "fmt")
{
import_segments_count = total_segments - 1;
keep_count = 2;
}
}
}
let cap = seg_strings.iter().map(|s| s.len()).sum::<usize>()
+ (total_segments - 1) * 2
+ if path.leading_colon.is_some() { 2 } else { 0 };
let mut original = String::with_capacity(cap);
if path.leading_colon.is_some() {
original.push_str("::");
}
if let Some((first, rest)) = seg_strings.split_first() {
original.push_str(first);
for s in rest {
original.push_str("::");
original.push_str(s);
}
}
let full_import = original.strip_prefix("::").unwrap_or(&original);
let current_mod = self
.module_stack
.last_mut()
.expect("module_stack should never be empty");
let is_shadowed = current_mod.scope.in_scope_idents.contains(last_ident)
|| self.local_idents.iter().any(|ident| *ident == last_ident);
if keep_count == 1
&& !current_mod.scope.existing_use_paths.contains(full_import)
&& !current_mod.scope.renamed_uses.contains_key(full_import)
&& is_shadowed
{
if total_segments >= 3 {
import_segments_count = total_segments - 1;
keep_count = 2;
} else if seg_strings.first().is_some_and(|s| s == "crate") {
return;
}
}
let import = if import_segments_count == total_segments {
full_import.to_string()
} else {
seg_strings
.get(..import_segments_count)
.map_or_else(|| full_import.to_string(), |s| s.join("::"))
};
let replacement = if keep_count == 1 {
seg_strings.last().cloned().unwrap_or_default()
} else {
let keep_start = total_segments.saturating_sub(keep_count);
seg_strings
.get(keep_start..)
.map_or_else(String::new, |s| s.join("::"))
};
current_mod.paths.push(QualifiedPath {
original,
replacement,
import,
segments: seg_strings,
span: path.span(),
});
}
}
fn line_indent<'a>(source: &'a str, line_offsets: &[usize], line: usize) -> Option<&'a str> {
let line_start = *line_offsets.get(line.checked_sub(1)?)?;
let bytes = source.as_bytes().get(line_start..)?;
let len = bytes
.iter()
.take_while(|&&b| b == b' ' || b == b'\t')
.count();
source.get(line_start..line_start + len)
}
pub fn find_mod_insert_pos_and_indent(
item_mod: &syn::ItemMod,
items: &[syn::Item],
source: &str,
line_offsets: &[usize],
) -> (usize, String, bool) {
let indent = if let Some(first) = items.first() {
line_indent(source, line_offsets, first.span().start().line)
.unwrap_or(" ")
.to_string()
} else {
let mod_indent = line_indent(source, line_offsets, item_mod.span().start().line).unwrap_or("");
format!("{mod_indent} ")
};
let last_use_end_line = items
.iter()
.map_while(|item| match item {
Item::Use(u) => Some(u.span().end().line),
_ => None,
})
.last();
if let Some(end_line) = last_use_end_line {
let pos = line_offsets.get(end_line).copied().unwrap_or(source.len());
return (pos, indent, true);
}
if let Some(first) = items.first() {
let pos = first
.span()
.start()
.line
.checked_sub(1)
.and_then(|idx| line_offsets.get(idx).copied())
.unwrap_or(0);
return (pos, indent, false);
}
let mod_line = item_mod.span().start().line;
let pos = line_offsets.get(mod_line).copied().unwrap_or(source.len());
(pos, indent, false)
}