use syn::{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) {
if self.cfg_depth > 0 {
return;
}
let segments = &path.segments;
let total_segments = segments.len();
if total_segments <= self.max_segments {
return;
}
let first_ident = &segments[0].ident;
if first_ident == "crate" || first_ident == "self" || first_ident == "super" {
return;
}
if self.allow_crates.iter().any(|c| first_ident == c.as_str()) {
return;
}
let first_str = first_ident.to_string();
if path.leading_colon.is_none()
&& !self.known_crates.is_empty()
&& !self.known_crates.contains(&first_str)
{
return;
}
let mut seg_strings = Vec::with_capacity(total_segments);
seg_strings.push(first_str);
for s in segments.iter().skip(1) {
seg_strings.push(s.ident.to_string());
}
let last_ident = seg_strings[total_segments - 1].as_str();
let mut import_segments_count = total_segments;
let mut keep_count = self.keep_segments.max(1);
if total_segments >= 3 {
let prev_is_upper = seg_strings[total_segments - 2]
.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(|s| s.as_str())
!= Some("result")
{
import_segments_count = total_segments - 1;
keep_count = 2;
}
if total_segments >= 3
&& (seg_strings[0] == "std" || seg_strings[0] == "core")
&& seg_strings[1] == "fmt"
{
import_segments_count = 2;
keep_count = total_segments - 1;
} else if total_segments >= 3 && seg_strings[total_segments - 2] == "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("::");
}
for (idx, s) in seg_strings.iter().enumerate() {
if idx > 0 {
original.push_str("::");
}
original.push_str(s);
}
let full_import = if path.leading_colon.is_some() {
&original[2..]
} else {
&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)
&& is_shadowed
&& total_segments >= 2
{
import_segments_count = total_segments - 1;
keep_count = 2;
}
let import = if import_segments_count == total_segments {
full_import.to_string()
} else {
seg_strings[..import_segments_count].join("::")
};
let keep_start = total_segments.saturating_sub(keep_count);
let replacement = seg_strings[keep_start..].join("::");
current_mod.paths.push(QualifiedPath {
original,
replacement,
import,
segments: seg_strings,
span: path.span(),
});
}
}
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() {
let line = first.span().start().line;
if line > 0 && line - 1 < line_offsets.len() {
let line_start = line_offsets[line - 1];
let line_text = &source[line_start..];
line_text
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect::<String>()
} else {
" ".to_string()
}
} else {
let line = item_mod.span().start().line;
if line > 0 && line - 1 < line_offsets.len() {
let line_start = line_offsets[line - 1];
let line_text = &source[line_start..];
let mod_indent: String = line_text
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
format!("{mod_indent} ")
} else {
" ".to_string()
}
};
let mut last_use_end_line = None;
for item in items {
match item {
syn::Item::Use(u) => {
last_use_end_line = Some(u.span().end().line);
}
_ => break,
}
}
if let Some(end_line) = last_use_end_line {
let pos = if end_line < line_offsets.len() {
line_offsets[end_line]
} else {
source.len()
};
return (pos, indent, true);
}
if let Some(first) = items.first() {
let line = first.span().start().line;
let pos = if line > 0 && line - 1 < line_offsets.len() {
line_offsets[line - 1]
} else {
0
};
return (pos, indent, false);
}
let mod_line = item_mod.span().start().line;
let pos = if mod_line < line_offsets.len() {
line_offsets[mod_line]
} else {
source.len()
};
(pos, indent, false)
}