fixrs 0.1.0

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

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

/// 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 scope: &'a FileScope,
  pub use_depth: usize,
  pub attr_depth: usize,
  pub mod_depth: usize,
  pub cfg_depth: usize,
  pub local_idents: Vec<String>,
  pub paths: Vec<QualifiedPath>,
}

impl<'a> PathCollector<'a> {
  pub fn new(
    max_segments: usize,
    keep_segments: usize,
    allow_crates: &'a [String],
    known_crates: &'a HashSet<String>,
    scope: &'a FileScope,
  ) -> Self {
    Self {
      max_segments,
      keep_segments,
      allow_crates,
      known_crates,
      scope,
      use_depth: 0,
      attr_depth: 0,
      mod_depth: 0,
      cfg_depth: 0,
      local_idents: Vec::new(),
      paths: Vec::new(),
    }
  }

  /// 检查并提取合格的长路径
  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 is_shadowed = self.scope.in_scope_idents.contains(last_ident)
      || self.local_idents.iter().any(|ident| *ident == last_ident);

    if keep_count == 1
      && !self.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("::");

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