fixrs 0.1.2

Blazing-fast CLI to replace Rust qualified paths with use statements, auto-fixing clippy::absolute_paths
Documentation
use syn::{Item, Path, spanned::Spanned};

use super::{path::QualifiedPath, scope::FileScope};
use crate::{hash::HashSet, options::Options};

/// 单个模块(顶层文件或内嵌 mod)的作用域与替换上下文
#[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>,
}

/// AST 绝对路径收集器
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();

    // 若非全局 :: 绝对路径,检查是否属于已知 extern crate(已知集合非空时生效)
    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();

    // 启发式判断:
    // 1. 若倒数第二项为大写(类型),最后一项为方法或枚举变体,保留末尾两段(如 Error::new)
    // 2. 若末尾项为 Result 且非标准库 result::Result,保留末尾两段(如 fmt::Result, io::Result)
    // 3. 若末尾项与当前作用域符号冲突(且非同一条 use 路径),提升保留两段(如 io::Error)
    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;
      }
    }

    // 保护 Prelude 中的核心类型(尤其是 Result):
    // 若末尾是 "Result",且导入不是标准库泛型 Result(如 core::result::Result 或 std::result::Result),
    // 强制保留末尾两段(如 fmt::Result, io::Result),避免覆盖全局 Prelude 的 Result<T, E>
    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;
    }

    // 保护 fmt 模块类型与 Trait(避免 Display/Debug 与标准库冲突导致 .fmt() 多义性调用错误,以及 Formatter/Result 冲突):
    // 凡是 (core|std)::fmt 下的路径,统一保留 fmt 命名空间(如 fmt::Display, fmt::Debug::fmt, fmt::Formatter, fmt::Result),头部仅导入 fmt
    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 replacement = if keep_count == 1 {
      seg_strings.last().expect("segments not empty").clone()
    } else {
      let keep_start = total_segments.saturating_sub(keep_count);
      seg_strings[keep_start..].join("::")
    };

    current_mod.paths.push(QualifiedPath {
      original,
      replacement,
      import,
      segments: seg_strings,
      span: path.span(),
    });
  }
}

/// 计算内嵌模块(ItemMod)内部的 use 注入字节位置、行首缩进与是否存在现有 use
pub fn find_mod_insert_pos_and_indent(
  item_mod: &syn::ItemMod,
  items: &[syn::Item],
  source: &str,
  line_offsets: &[usize],
) -> (usize, String, bool) {
  // 1. 推导模块内缩进
  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 bytes = source[line_start..].as_bytes();
      let len = bytes
        .iter()
        .take_while(|&&b| b == b' ' || b == b'\t')
        .count();
      source[line_start..line_start + len].to_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 bytes = source[line_start..].as_bytes();
      let len = bytes
        .iter()
        .take_while(|&&b| b == b' ' || b == b'\t')
        .count();
      let mod_indent = &source[line_start..line_start + len];
      format!("{mod_indent}  ")
    } else {
      "  ".to_string()
    }
  };

  // 2. 推导插入点
  let mut last_use_end_line = None;
  for item in items {
    match item {
      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)
}